diff --git a/.eslintignore b/.eslintignore index 57fedb0da..e657b6260 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,4 @@ apps/animclk/V29.LBM.js apps/banglerun/rollup.config.js +apps/schoolCalendar/fullcalendar/main.js +apps/authentiwatch/qr_packed.js diff --git a/.gitignore b/.gitignore index 2d418b7c8..47233d1f5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ package-lock.json appdates.csv .vscode .idea/ -_config.yml +_config.yml +tests/Layout/bin/tmp.* +tests/Layout/testresult.bmp diff --git a/README.md b/README.md index e71d7eee0..8e186cf79 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ Bangle.js App Loader (and Apps) ================================ -[![Build Status](https://travis-ci.org/espruino/BangleApps.svg?branch=master)](https://travis-ci.org/espruino/BangleApps) +[![Build Status](https://app.travis-ci.com/espruino/BangleApps.svg?branch=master)](https://app.travis-ci.com/github/espruino/BangleApps) * Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps) -* Try the **development version** at [github.io](https://espruino.github.io/BangleApps/) +* Try the **development version** at [espruino.github.io](https://espruino.github.io/BangleApps/) **All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By submitting code to this repository you confirm that you are happy with it being MIT licensed, @@ -49,25 +49,25 @@ easily distinguish between file types, we use the following: ## Adding your app to the menu -* Come up with a unique (all lowercase, nu spaces) name, we'll assume `7chname`. Bangle.js +* Come up with a unique (all lowercase, no spaces) name, we'll assume `myappid`. Bangle.js is limited to 28 char filenames and appends a file extension (eg `.js`) so please try and keep filenames short to avoid overflowing the buffer. -* Create a folder called `apps/`, lets assume `apps/7chname` +* Create a folder called `apps/`, lets assume `apps/myappid` * 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" +* `apps/myappid/app.png` should be a 48px icon +* Use http://www.espruino.com/Image+Converter to create `apps/myappid/app-icon.js`, using a 1 bit, 4 bit or 8 bit Web Palette "Image String" * Create an entry in `apps.json` as follows: ``` -{ "id": "7chname", +{ "id": "myappid", "name": "My app's human readable name", "shortName" : "Short Name", "icon": "app.png", "description": "A detailed description of my great app", "tags": "", "storage": [ - {"name":"7chname.app.js","url":"app.js"}, - {"name":"7chname.img","url":"app-icon.js","evaluate":true} + {"name":"myappid.app.js","url":"app.js"}, + {"name":"myappid.img","url":"app-icon.js","evaluate":true} ], }, ``` @@ -95,12 +95,12 @@ Be aware of the delay between commits and updates on github.io - it can take a f Using the 'Storage' icon in [the Web IDE](https://www.espruino.com/ide/) (4 discs), upload your files into the places described in your JSON: -* `app-icon.js` -> `7chname.img` +* `app-icon.js` -> `myappid.img` Now load `app.js` up in the editor, and click the down-arrow to the bottom right of the `Send to Espruino` icon. Click `Storage` and then either choose -`7chname.app.js` (if you'd uploaded your app previously), or `New File` -and then enter `7chname.app.js` as the name. +`myappid.app.js` (if you'd uploaded your app previously), or `New File` +and then enter `myappid.app.js` as the name. Now, clicking the `Send to Espruino` icon will load the app directly into Espruino **and** will automatically run it. @@ -115,10 +115,13 @@ and set it to `Load default application`. ## 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 +when creating your own. Just come up with a unique name (ideally lowercase, under 20 chars), copy `apps/_example_app` +or `apps/_example_widget` to `apps/myappid`, and add `apps/_example_X/add_to_apps.json` to `apps.json`. +**Note:** the max filename length is 28 chars, so we suggest an app ID of under +20 so that when `.app.js`/etc gets added to the end the filename isn't cropped. + **If you're making a widget** please start the name with `wid` to make it easy to find! @@ -192,8 +195,8 @@ and which gives information about the app for the Launcher. ``` { "name":"Short Name", // for Bangle.js menu - "icon":"*7chname", // for Bangle.js menu - "src":"-7chname", // source file + "icon":"*myappid", // for Bangle.js menu + "src":"-myappid", // source file "type":"widget/clock/app/bootloader", // optional, default "app" // if this is 'widget' then it's not displayed in the menu // if it's 'clock' then it'll be loaded by default at boot time @@ -217,8 +220,10 @@ and which gives information about the app for the Launcher. { "id": "appid", // 7 character app id "name": "Readable name", // readable name "shortName": "Short name", // short name for launcher - "icon": "icon.png", // icon in apps/ + "version": "0v01", // the version of this app "description": "...", // long description (can contain markdown) + "icon": "icon.png", // icon in apps/ + "screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app "type":"...", // optional(if app) - // 'app' - an application // 'widget' - a widget @@ -226,7 +231,9 @@ and which gives information about the app for the Launcher. // 'bootloader' - code that runs at startup only // 'RAM' - code that runs and doesn't upload anything to storage "tags": "", // comma separated tag list for searching + "supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2 "dependencies" : { "notify":"type" } // optional, app 'types' we depend on + "dependencies" : { "messages":"app" } // optional, depend on a specific app ID // for instance this will use notify/notifyfs is they exist, or will pull in 'notify' "readme": "README.md", // if supplied, a link to a markdown-style text file // that contains more information about this app (usage, etc) @@ -237,6 +244,11 @@ and which gives information about the app for the Launcher. // like this one with 'storage','name' and 'id' set up // see below for more info + "customConnect": true, // if supplied, ensure we are connected to a device + // before the "custom.html" iframe is loaded. An + // onInit function in "custom.html" is then called + // with info on the currently connected device. + "interface": "interface.html", // if supplied, apps/interface.html is loaded in an // iframe, and it may interact with the connected Bangle // to retrieve information from it @@ -254,6 +266,9 @@ and which gives information about the app for the Launcher. // (eg it's evaluated as JS) "noOverwrite":true // if supplied, this file will not be overwritten if it // already exists + "supports": ["BANGLEJS2"]// if supplied, this file will ONLY be uploaded to the device + // types named in the array. This allows different versions of + // the app to be uploaded for different platforms }, ] "data": [ // list of files the app writes to @@ -301,10 +316,10 @@ version of what's in `apps.json`: + + + + + + + +

Authentiwatch

+
+

No watch comms.

+
+
+ + + +
+
+
+
+
+
+ +
+
+ + + + diff --git a/apps/authentiwatch/qr_packed.js b/apps/authentiwatch/qr_packed.js new file mode 100644 index 000000000..28b1bddd0 --- /dev/null +++ b/apps/authentiwatch/qr_packed.js @@ -0,0 +1,107 @@ +/* Packed with Google Closure +* +* Ported to JavaScript by Lazar Laszlo 2011 +* lazarsoft@gmail.com, www.lazarsoft.info +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +var qrcode=function(){"use strict";function a(h,b){this.count=h;this.dataCodewords=b;this.__defineGetter__("Count",function(){return this.count});this.__defineGetter__("DataCodewords",function(){return this.dataCodewords})}function f(h,b,e){this.ecCodewordsPerBlock=h;this.ecBlocks=e?[b,e]:Array(b);this.__defineGetter__("ECCodewordsPerBlock",function(){return this.ecCodewordsPerBlock});this.__defineGetter__("TotalECCodewords",function(){return this.ecCodewordsPerBlock*this.NumBlocks});this.__defineGetter__("NumBlocks", +function(){for(var d=0,c=0;cMath.abs(d-b);if(h){var a=b;b=e;e=a;a=d;d=c;c=a}for(var m=Math.abs(d-b),f=Math.abs(c-e),q=-m>>1,k=ed?(h=b/(b-d),d=0):d>=g.width&&(h=(g.width-1-b)/(d-b),d=g.width-1);c=Math.floor(e-(c-e)*h);h=1;0>c?(h=e/(e-c),c=0):c>=g.height&&(h=(g.height-1-e)/(c-e),c=g.height-1);d=Math.floor(b+(d-b)*h);a+=this.sizeOfBlackWhiteBlackRun(b,e,d,c);return a-1};this.calculateModuleSizeOneWay=function(b,e){var d=this.sizeOfBlackWhiteBlackRunBothWays(Math.floor(b.X), +Math.floor(b.Y),Math.floor(e.X),Math.floor(e.Y)),c=this.sizeOfBlackWhiteBlackRunBothWays(Math.floor(e.X),Math.floor(e.Y),Math.floor(b.X),Math.floor(b.Y));return isNaN(d)?c/7:isNaN(c)?d/7:(d+c)/14};this.calculateModuleSize=function(b,e,d){return(this.calculateModuleSizeOneWay(b,e)+this.calculateModuleSizeOneWay(b,d))/2};this.distance=function(b,e){var d=b.X-e.X,c=b.Y-e.Y;return Math.sqrt(d*d+c*c)};this.computeDimension=function(b,e,d,c){e=Math.round(this.distance(b,e)/c);b=Math.round(this.distance(b, +d)/c);b=(e+b>>1)+7;switch(b&3){case 0:b++;break;case 2:b--;break;case 3:throw"Error";}return b};this.findAlignmentInRegion=function(b,e,d,c){c=Math.floor(c*b);var h=Math.max(0,e-c);e=Math.min(g.width-1,e+c);if(e-h<3*b)throw"Error";var a=Math.max(0,d-c);return(new R(this.image,h,a,e-h,Math.min(g.height-1,d+c)-a,b,this.resultPointCallback)).find()};this.createTransform=function(b,e,d,c,h){h-=3.5;var a;if(null!=c){var p=c.X;c=c.Y;var f=a=h-3}else p=e.X-b.X+d.X,c=e.Y-b.Y+d.Y,f=a=h;return z.quadrilateralToQuadrilateral(3.5, +3.5,h,3.5,f,a,3.5,h,b.X,b.Y,e.X,e.Y,p,c,d.X,d.Y)};this.sampleGrid=function(b,e,d){return F.sampleGrid3(b,d,e)};this.processFinderPatternInfo=function(b){var e=b.TopLeft,d=b.TopRight;b=b.BottomLeft;var c=this.calculateModuleSize(e,d,b);if(1>c)throw"Error";var h=this.computeDimension(e,d,b,c),a=k.getProvisionalVersionForDimension(h),m=a.DimensionForVersion-7,f=null;if(0>3&3);this.dataMask=h&7;this.__defineGetter__("ErrorCorrectionLevel",function(){return this.errorCorrectionLevel});this.__defineGetter__("DataMask",function(){return this.dataMask});this.GetHashCode=function(){return this.errorCorrectionLevel.ordinal()<< +3|this.dataMask};this.Equals=function(b){return this.errorCorrectionLevel==b.errorCorrectionLevel&&this.dataMask==b.dataMask}}function C(h,b,e){this.ordinal_Renamed_Field=h;this.bits=b;this.name=e;this.__defineGetter__("Bits",function(){return this.bits});this.__defineGetter__("Name",function(){return this.name});this.ordinal=function(){return this.ordinal_Renamed_Field}}function I(h,b){b||(b=h);if(1>h||1>b)throw"Both dimensions must be greater than 0";this.width=h;this.height=b;var e=h>>5;0!=(h& +31)&&e++;this.rowSize=e;this.bits=Array(e*b);for(e=0;e>5)],d&31)&1)};this.set_Renamed=function(d,c){this.bits[c*this.rowSize+ +(d>>5)]|=1<<(d&31)};this.flip=function(d,c){this.bits[c*this.rowSize+(d>>5)]^=1<<(d&31)};this.clear=function(){for(var d=this.bits.length,c=0;cc||0>d)throw"Left and top must be nonnegative";if(1>b||1>e)throw"Height and width must be at least 1";e=d+e;b=c+b;if(b>this.height||e>this.width)throw"The region must fit inside the matrix";for(;c>5)]|=1<<(a&31)}}function G(a,b){this.numDataCodewords= +a;this.codewords=b;this.__defineGetter__("NumDataCodewords",function(){return this.numDataCodewords});this.__defineGetter__("Codewords",function(){return this.codewords})}function T(a){var b=a.Dimension;if(21>b||1!=(b&3))throw"Error BitMatrixParser";this.bitMatrix=a;this.parsedFormatInfo=this.parsedVersion=null;this.copyBit=function(e,d,c){return this.bitMatrix.get_Renamed(e,d)?c<<1|1:c<<1};this.readFormatInformation=function(){if(null!=this.parsedFormatInfo)return this.parsedFormatInfo;for(var e= +0,d=0;6>d;d++)e=this.copyBit(d,8,e);e=this.copyBit(7,8,e);e=this.copyBit(8,8,e);e=this.copyBit(8,7,e);for(d=5;0<=d;d--)e=this.copyBit(8,d,e);this.parsedFormatInfo=r.decodeFormatInformation(e);if(null!=this.parsedFormatInfo)return this.parsedFormatInfo;for(var c=this.bitMatrix.Dimension,e=0,b=c-8,d=c-1;d>=b;d--)e=this.copyBit(d,8,e);for(d=c-7;d>2;if(6>=d)return k.getVersionForNumber(d);for(var d=0,c=e-11,b=5;0<=b;b--)for(var a=e-9;a>=c;a--)d=this.copyBit(a,b,d);this.parsedVersion=k.decodeVersionInformation(d);if(null!=this.parsedVersion&&this.parsedVersion.DimensionForVersion==e)return this.parsedVersion;d=0;for(a=5;0<=a;a--)for(b=e-9;b>=c;b--)d=this.copyBit(a,b,d);this.parsedVersion=k.decodeVersionInformation(d);if(null!= +this.parsedVersion&&this.parsedVersion.DimensionForVersion==e)return this.parsedVersion;throw"Error readVersion";};this.readCodewords=function(){var b=this.readFormatInformation(),d=this.readVersion(),c=H.forReference(b.DataMask),b=this.bitMatrix.Dimension;c.unmaskBitMatrix(this.bitMatrix,b);for(var c=d.buildFunctionPattern(),a=!0,h=Array(d.TotalCodewords),m=0,f=0,g=0,k=b-1;0t;t++)c.get_Renamed(k-t,v)||(g++,f<<=1,this.bitMatrix.get_Renamed(k- +t,v)&&(f|=1),8==g&&(h[m++]=f,f=g=0));a^=1}if(m!=d.TotalCodewords)throw"Error readCodewords";return h}}function w(a,b){if(null==b||0==b.length)throw"System.ArgumentException";this.field=a;var e=b.length;if(1c.length){var b=d,d=c;c=b}for(var b=Array(c.length),e=c.length-d.length,h=0;hc)throw"System.ArgumentException";if(0==d)return this.field.Zero;for(var b=this.coefficients.length,e=Array(b+c),a=0;a=c.Degree&&!b.Zero;)var a=b.Degree-c.Degree, +h=this.field.multiply(b.getCoefficient(b.Degree),e),f=c.multiplyByMonomial(a,h),a=this.field.buildMonomial(a,h),d=d.addOrSubtract(a),b=b.addOrSubtract(f);return[d,b]}}function n(a){this.expTable=Array(256);this.logTable=Array(256);for(var b=1,e=0;256>e;e++)this.expTable[e]=b,b<<=1,256<=b&&(b^=a);for(e=0;255>e;e++)this.logTable[this.expTable[e]]=e;a=Array(1);a[0]=0;this.zero=new w(this,Array(a));a=Array(1);a[0]=1;this.one=new w(this,Array(a));this.__defineGetter__("Zero",function(){return this.zero}); +this.__defineGetter__("One",function(){return this.one});this.buildMonomial=function(d,c){if(0>d)throw"System.ArgumentException";if(0==c)return this.zero;for(var b=Array(d+1),e=0;e>b:(a>>b)+(2<<~b)}function U(a,b,e){this.x=a;this.y=b;this.count=1;this.estimatedModuleSize=e;this.__defineGetter__("EstimatedModuleSize",function(){return this.estimatedModuleSize});this.__defineGetter__("Count",function(){return this.count});this.__defineGetter__("X",function(){return this.x});this.__defineGetter__("Y",function(){return this.y});this.incrementCount=function(){this.count++}; +this.aboutEquals=function(d,c,b){return Math.abs(c-this.y)<=d&&Math.abs(b-this.x)<=d?(d=Math.abs(d-this.estimatedModuleSize),1>=d||1>=d/this.estimatedModuleSize):!1}}function V(a){this.bottomLeft=a[0];this.topLeft=a[1];this.topRight=a[2];this.__defineGetter__("BottomLeft",function(){return this.bottomLeft});this.__defineGetter__("TopLeft",function(){return this.topLeft});this.__defineGetter__("TopRight",function(){return this.topRight})}function S(){this.image=null;this.possibleCenters=[];this.hasSkipped= +!1;this.crossCheckStateCount=[0,0,0,0,0];this.resultPointCallback=null;this.__defineGetter__("CrossCheckStateCount",function(){this.crossCheckStateCount[0]=0;this.crossCheckStateCount[1]=0;this.crossCheckStateCount[2]=0;this.crossCheckStateCount[3]=0;this.crossCheckStateCount[4]=0;return this.crossCheckStateCount});this.foundPatternCross=function(a){for(var b=0,e=0;5>e;e++){var d=a[e];if(0==d)return!1;b+=d}if(7>b)return!1;b=Math.floor((b<m)return NaN;for(;0<=m&&!c[b+m*g.width]&&l[1]<=e;)l[1]++,m--;if(0>m||l[1]>e)return NaN;for(;0<=m&&c[b+m*g.width]&&l[0]<=e;)l[0]++,m--;if(l[0]>e)return NaN;for(m=a+1;m=e)return NaN;for(;m=e||5*Math.abs(l[0]+l[1]+l[2]+l[3]+l[4]-d)>=2*d?NaN:this.foundPatternCross(l)?this.centerFromEnd(l,m):NaN};this.crossCheckHorizontal=function(a,b,e,d){for(var c=this.image,h=g.width,l=this.CrossCheckStateCount,m=a;0<=m&&c[m+b*g.width];)l[2]++,m--;if(0>m)return NaN;for(;0<=m&&!c[m+b*g.width]&&l[1]<=e;)l[1]++,m--;if(0>m||l[1]>e)return NaN;for(;0<=m&&c[m+b*g.width]&&l[0]<= +e;)l[0]++,m--;if(l[0]>e)return NaN;for(m=a+1;m=e)return NaN;for(;m=e||5*Math.abs(l[0]+l[1]+l[2]+l[3]+l[4]-d)>=d?NaN:this.foundPatternCross(l)?this.centerFromEnd(l,m):NaN};this.handlePossibleCenter=function(a,b,e){var d=a[0]+a[1]+a[2]+a[3]+a[4];e=this.centerFromEnd(a,e);b=this.crossCheckVertical(b,Math.floor(e),a[2],d);if(!isNaN(b)&&(e=this.crossCheckHorizontal(Math.floor(e), +Math.floor(b),a[2],d),!isNaN(e))){a=d/7;for(var d=!1,c=this.possibleCenters.length,h=0;ha)throw"Couldn't find enough finder patterns (found "+a+")";if(3a&&this.possibleCenters.splice(d,1)}3d.count?-1:c.count=a)return 0;for(var b=null,e=0;e=K)if(null==b)b=d;else return this.hasSkipped=!0,Math.floor((Math.abs(b.X-d.X)-Math.abs(b.Y-d.Y))/2)}return 0};this.haveMultiplyConfirmedCenters=function(){for(var a,b=0,e=0,d=this.possibleCenters.length,c=0;c=K&&(b++,e+=a.EstimatedModuleSize); +if(3>b)return!1;for(var b=e/d,p=0,c=0;cl[2]&&(m+=b-l[2]-c,f=d-1));else{do f++;while(f=d||1>=d/this.estimatedModuleSize):!1}}function R(a,b,e,d,c,f,l){this.image=a;this.possibleCenters= +[];this.startX=b;this.startY=e;this.width=d;this.height=c;this.moduleSize=f;this.crossCheckStateCount=[0,0,0];this.resultPointCallback=l;this.centerFromEnd=function(c,d){return d-c[2]-c[1]/2};this.foundPatternCross=function(c){for(var d=this.moduleSize,b=d/2,a=0;3>a;a++)if(Math.abs(d-c[a])>=b)return!1;return!0};this.crossCheckVertical=function(c,d,b,a){var e=this.image,h=g.height,f=this.crossCheckStateCount;f[0]=0;f[1]=0;f[2]=0;for(var l=c;0<=l&&e[d+l*g.width]&&f[1]<=b;)f[1]++,l--;if(0>l||f[1]>b)return NaN; +for(;0<=l&&!e[d+l*g.width]&&f[0]<=b;)f[0]++,l--;if(f[0]>b)return NaN;for(l=c+1;lb)return NaN;for(;lb||5*Math.abs(f[0]+f[1]+f[2]-a)>=2*a?NaN:this.foundPatternCross(f)?this.centerFromEnd(f,l):NaN};this.handlePossibleCenter=function(c,d,b){var a=c[0]+c[1]+c[2];b=this.centerFromEnd(c,b);d=this.crossCheckVertical(d,Math.floor(b),2*c[1],a);if(!isNaN(d)){c=(c[0]+c[1]+c[2])/3;for(var a=this.possibleCenters.length, +e=0;e>1),p=[0,0,0],k=0;k>1:-(k+1>>1));p[0]=0;p[1]=0;p[2]=0;for(var A=b;A=b?this.dataLengthMode=0:10<=b&&26>=b?this.dataLengthMode=1:27<= +b&&40>=b&&(this.dataLengthMode=2);this.getNextBits=function(b){var c,d;if(b>this.bitPointer-b+1;this.bitPointer-=b;return d}if(b>8-(b-(this.bitPointer+1));this.bitPointer-=b%8;0>this.bitPointer&&(this.bitPointer= +8+this.bitPointer);return d}if(b>8-(b-(this.bitPointer+1+8));this.bitPointer-=(b-8)%8;0>this.bitPointer&&(this.bitPointer=8+this.bitPointer);return c+e+d}return 0}; +this.NextMode=function(){return this.blockPointer>this.blocks.length-this.numErrorCorrectionCode-2?0:this.getNextBits(4)};this.getDataLength=function(b){for(var c=0;1!=b>>c;)c++;return this.getNextBits(g.sizeOfDataLengthInfo[this.dataLengthMode][c])};this.getRomanAndFigureString=function(b){var c="",d="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:".split("");do if(1c&&(d+="0"),10>c&&(d+="0"),b-=3):2==b?(c=this.getNextBits(7),10>c&&(d+="0"),b-=2):1==b&&(c=this.getNextBits(4),--b),d+=c;while(0=d+33088?d+33088:d+49472);b--}while(0< +b);return c};this.parseECIValue=function(){var b=0,c=this.getNextBits(8);0==(c&128)&&(b=c&127);128==(c&192)&&(b=this.getNextBits(8),b|=(c&63)<<8);192==(c&224)&&(b=this.getNextBits(8),b|=(c&31)<<16);return b};this.__defineGetter__("DataByte",function(){var b=[];do{var c=this.NextMode();if(0==c)if(0a)throw"Invalid data length: "+a;switch(c){case 1:c=this.getFigureString(a);for(var a=Array(c.length),e=0;ed||d>c||-1>e||e>h)throw"Error.checkAndNudgePoints ";f=!1;-1==d?(b[m]=0,f=!0):d==c&&(b[m]=c-1,f=!0);-1==e?(b[m+1]=0,f=!0):e==h&&(b[m+1]=h-1,f=!0)}f=!0;for(m=b.length-2;0<=m&&f;m-=2){d=Math.floor(b[m]);e=Math.floor(b[m+1]);if(-1>d||d>c||-1>e||e>h)throw"Error.checkAndNudgePoints ";f=!1;-1==d?(b[m]=0,f=!0):d==c&&(b[m]=c-1,f=!0);-1==e?(b[m+1]=0,f=!0):e==h&&(b[m+1]=h-1,f=!0)}},sampleGrid3:function(a,b,e){for(var d=new I(b),c=Array(b<<1),h=0;h>1)+.5,c[k+1]=m;e.transformPoints1(c);F.checkAndNudgePoints(a,c);try{for(k=0;k>1,h)}catch(q){throw"Error.checkAndNudgePoints";}}return d},sampleGridx:function(a,b,e,d,c,f,l,g,k,q,n,x,v,t,r,A,u,w){e=z.quadrilateralToQuadrilateral(e,d,c,f,l,g,k,q,n,x,v,t,r,A,u,w);return F.sampleGrid3(a,b,e)}};k.VERSION_DECODE_INFO=[31892,34236,39577,42195,48118,51042,55367,58893,63784,68472,70749,76311,79154, +84390,87683,92361,96236,102084,102881,110507,110734,117786,119615,126325,127568,133589,136944,141498,145311,150283,152622,158308,161089,167017];k.VERSIONS=[new k(1,[],new f(7,new a(1,19)),new f(10,new a(1,16)),new f(13,new a(1,13)),new f(17,new a(1,9))),new k(2,[6,18],new f(10,new a(1,34)),new f(16,new a(1,28)),new f(22,new a(1,22)),new f(28,new a(1,16))),new k(3,[6,22],new f(15,new a(1,55)),new f(26,new a(1,44)),new f(18,new a(2,17)),new f(22,new a(2,13))),new k(4,[6,26],new f(20,new a(1,80)),new f(18, +new a(2,32)),new f(26,new a(2,24)),new f(16,new a(4,9))),new k(5,[6,30],new f(26,new a(1,108)),new f(24,new a(2,43)),new f(18,new a(2,15),new a(2,16)),new f(22,new a(2,11),new a(2,12))),new k(6,[6,34],new f(18,new a(2,68)),new f(16,new a(4,27)),new f(24,new a(4,19)),new f(28,new a(4,15))),new k(7,[6,22,38],new f(20,new a(2,78)),new f(18,new a(4,31)),new f(18,new a(2,14),new a(4,15)),new f(26,new a(4,13),new a(1,14))),new k(8,[6,24,42],new f(24,new a(2,97)),new f(22,new a(2,38),new a(2,39)),new f(22, +new a(4,18),new a(2,19)),new f(26,new a(4,14),new a(2,15))),new k(9,[6,26,46],new f(30,new a(2,116)),new f(22,new a(3,36),new a(2,37)),new f(20,new a(4,16),new a(4,17)),new f(24,new a(4,12),new a(4,13))),new k(10,[6,28,50],new f(18,new a(2,68),new a(2,69)),new f(26,new a(4,43),new a(1,44)),new f(24,new a(6,19),new a(2,20)),new f(28,new a(6,15),new a(2,16))),new k(11,[6,30,54],new f(20,new a(4,81)),new f(30,new a(1,50),new a(4,51)),new f(28,new a(4,22),new a(4,23)),new f(24,new a(3,12),new a(8,13))), +new k(12,[6,32,58],new f(24,new a(2,92),new a(2,93)),new f(22,new a(6,36),new a(2,37)),new f(26,new a(4,20),new a(6,21)),new f(28,new a(7,14),new a(4,15))),new k(13,[6,34,62],new f(26,new a(4,107)),new f(22,new a(8,37),new a(1,38)),new f(24,new a(8,20),new a(4,21)),new f(22,new a(12,11),new a(4,12))),new k(14,[6,26,46,66],new f(30,new a(3,115),new a(1,116)),new f(24,new a(4,40),new a(5,41)),new f(20,new a(11,16),new a(5,17)),new f(24,new a(11,12),new a(5,13))),new k(15,[6,26,48,70],new f(22,new a(5, +87),new a(1,88)),new f(24,new a(5,41),new a(5,42)),new f(30,new a(5,24),new a(7,25)),new f(24,new a(11,12),new a(7,13))),new k(16,[6,26,50,74],new f(24,new a(5,98),new a(1,99)),new f(28,new a(7,45),new a(3,46)),new f(24,new a(15,19),new a(2,20)),new f(30,new a(3,15),new a(13,16))),new k(17,[6,30,54,78],new f(28,new a(1,107),new a(5,108)),new f(28,new a(10,46),new a(1,47)),new f(28,new a(1,22),new a(15,23)),new f(28,new a(2,14),new a(17,15))),new k(18,[6,30,56,82],new f(30,new a(5,120),new a(1,121)), +new f(26,new a(9,43),new a(4,44)),new f(28,new a(17,22),new a(1,23)),new f(28,new a(2,14),new a(19,15))),new k(19,[6,30,58,86],new f(28,new a(3,113),new a(4,114)),new f(26,new a(3,44),new a(11,45)),new f(26,new a(17,21),new a(4,22)),new f(26,new a(9,13),new a(16,14))),new k(20,[6,34,62,90],new f(28,new a(3,107),new a(5,108)),new f(26,new a(3,41),new a(13,42)),new f(30,new a(15,24),new a(5,25)),new f(28,new a(15,15),new a(10,16))),new k(21,[6,28,50,72,94],new f(28,new a(4,116),new a(4,117)),new f(26, +new a(17,42)),new f(28,new a(17,22),new a(6,23)),new f(30,new a(19,16),new a(6,17))),new k(22,[6,26,50,74,98],new f(28,new a(2,111),new a(7,112)),new f(28,new a(17,46)),new f(30,new a(7,24),new a(16,25)),new f(24,new a(34,13))),new k(23,[6,30,54,74,102],new f(30,new a(4,121),new a(5,122)),new f(28,new a(4,47),new a(14,48)),new f(30,new a(11,24),new a(14,25)),new f(30,new a(16,15),new a(14,16))),new k(24,[6,28,54,80,106],new f(30,new a(6,117),new a(4,118)),new f(28,new a(6,45),new a(14,46)),new f(30, +new a(11,24),new a(16,25)),new f(30,new a(30,16),new a(2,17))),new k(25,[6,32,58,84,110],new f(26,new a(8,106),new a(4,107)),new f(28,new a(8,47),new a(13,48)),new f(30,new a(7,24),new a(22,25)),new f(30,new a(22,15),new a(13,16))),new k(26,[6,30,58,86,114],new f(28,new a(10,114),new a(2,115)),new f(28,new a(19,46),new a(4,47)),new f(28,new a(28,22),new a(6,23)),new f(30,new a(33,16),new a(4,17))),new k(27,[6,34,62,90,118],new f(30,new a(8,122),new a(4,123)),new f(28,new a(22,45),new a(3,46)),new f(30, +new a(8,23),new a(26,24)),new f(30,new a(12,15),new a(28,16))),new k(28,[6,26,50,74,98,122],new f(30,new a(3,117),new a(10,118)),new f(28,new a(3,45),new a(23,46)),new f(30,new a(4,24),new a(31,25)),new f(30,new a(11,15),new a(31,16))),new k(29,[6,30,54,78,102,126],new f(30,new a(7,116),new a(7,117)),new f(28,new a(21,45),new a(7,46)),new f(30,new a(1,23),new a(37,24)),new f(30,new a(19,15),new a(26,16))),new k(30,[6,26,52,78,104,130],new f(30,new a(5,115),new a(10,116)),new f(28,new a(19,47),new a(10, +48)),new f(30,new a(15,24),new a(25,25)),new f(30,new a(23,15),new a(25,16))),new k(31,[6,30,56,82,108,134],new f(30,new a(13,115),new a(3,116)),new f(28,new a(2,46),new a(29,47)),new f(30,new a(42,24),new a(1,25)),new f(30,new a(23,15),new a(28,16))),new k(32,[6,34,60,86,112,138],new f(30,new a(17,115)),new f(28,new a(10,46),new a(23,47)),new f(30,new a(10,24),new a(35,25)),new f(30,new a(19,15),new a(35,16))),new k(33,[6,30,58,86,114,142],new f(30,new a(17,115),new a(1,116)),new f(28,new a(14,46), +new a(21,47)),new f(30,new a(29,24),new a(19,25)),new f(30,new a(11,15),new a(46,16))),new k(34,[6,34,62,90,118,146],new f(30,new a(13,115),new a(6,116)),new f(28,new a(14,46),new a(23,47)),new f(30,new a(44,24),new a(7,25)),new f(30,new a(59,16),new a(1,17))),new k(35,[6,30,54,78,102,126,150],new f(30,new a(12,121),new a(7,122)),new f(28,new a(12,47),new a(26,48)),new f(30,new a(39,24),new a(14,25)),new f(30,new a(22,15),new a(41,16))),new k(36,[6,24,50,76,102,128,154],new f(30,new a(6,121),new a(14, +122)),new f(28,new a(6,47),new a(34,48)),new f(30,new a(46,24),new a(10,25)),new f(30,new a(2,15),new a(64,16))),new k(37,[6,28,54,80,106,132,158],new f(30,new a(17,122),new a(4,123)),new f(28,new a(29,46),new a(14,47)),new f(30,new a(49,24),new a(10,25)),new f(30,new a(24,15),new a(46,16))),new k(38,[6,32,58,84,110,136,162],new f(30,new a(4,122),new a(18,123)),new f(28,new a(13,46),new a(32,47)),new f(30,new a(48,24),new a(14,25)),new f(30,new a(42,15),new a(32,16))),new k(39,[6,26,54,82,110,138, +166],new f(30,new a(20,117),new a(4,118)),new f(28,new a(40,47),new a(7,48)),new f(30,new a(43,24),new a(22,25)),new f(30,new a(10,15),new a(67,16))),new k(40,[6,30,58,86,114,142,170],new f(30,new a(19,118),new a(6,119)),new f(28,new a(18,47),new a(31,48)),new f(30,new a(34,24),new a(34,25)),new f(30,new a(20,15),new a(61,16)))];k.getVersionForNumber=function(a){if(1>a||40>2)}catch(b){throw"Error getVersionForNumber";}};k.decodeVersionInformation=function(a){for(var b=4294967295,e=0,d=0;d=b?this.getVersionForNumber(e):null};z.quadrilateralToQuadrilateral=function(a,b,e,d,c,f,g,m,k,q,n,x,v,t,r,u){a=this.quadrilateralToSquare(a,b,e,d,c,f,g,m);return this.squareToQuadrilateral(k, +q,n,x,v,t,r,u).times(a)};z.squareToQuadrilateral=function(a,b,e,d,c,f,g,m){var h=m-f,l=b-d+f-m;if(0==h&&0==l)return new z(e-a,c-e,a,d-b,f-d,b,0,0,1);var p=e-c,k=g-c;c=a-e+c-g;f=d-f;var n=p*h-k*f,h=(c*h-k*l)/n,l=(p*l-c*f)/n;return new z(e-a+h*e,g-a+l*g,a,d-b+h*d,m-b+l*m,b,h,l,1)};z.quadrilateralToSquare=function(a,b,e,d,c,f,g,m){return this.squareToQuadrilateral(a,b,e,d,c,f,g,m).buildAdjoint()};var N=[[21522,0],[20773,1],[24188,2],[23371,3],[17913,4],[16590,5],[20375,6],[19104,7],[30660,8],[29427, +9],[32170,10],[30877,11],[26159,12],[25368,13],[27713,14],[26998,15],[5769,16],[5054,17],[7399,18],[6608,19],[1890,20],[597,21],[3340,22],[2107,23],[13663,24],[12392,25],[16177,26],[14854,27],[9396,28],[8579,29],[11994,30],[11245,31]],B=[0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4];r.numBitsDiffering=function(a,b){a^=b;return B[a&15]+B[u(a,4)&15]+B[u(a,8)&15]+B[u(a,12)&15]+B[u(a,16)&15]+B[u(a,20)&15]+B[u(a,24)&15]+B[u(a,28)&15]};r.decodeFormatInformation=function(a){var b=r.doDecodeFormatInformation(a);return null!= +b?b:r.doDecodeFormatInformation(a^21522)};r.doDecodeFormatInformation=function(a){for(var b=4294967295,e=0,d=0;d=b?new r(e):null};C.forBits=function(a){if(0>a||a>=O.length)throw"ArgumentException";return O[a]};var Y=new C(0,1,"L"),Z=new C(1,0,"M"),aa=new C(2,3,"Q"),ba=new C(3,2,"H"),O=[Z,Y,ba,aa];G.getDataBlocks=function(a,b,e){if(a.length!=b.TotalCodewords)throw"ArgumentException"; +var d=b.getECBlocksForLevel(e);e=0;var c=d.getECBlocks();for(b=0;ba||7h)throw"ReedSolomonException Bad error location";a[h]=n.addOrSubtract(a[h],c[f])}};this.runEuclideanAlgorithm=function(a,e,d){if(a.Degree=Math.floor(d/2);){var k=a,q=b,n=h;a=e;b=f;h=g;if(a.Zero)throw"r_{i-1} was zero";e=k;g=this.field.Zero;f=a.getCoefficient(a.Degree); +for(f=this.field.inverse(f);e.Degree>=a.Degree&&!e.Zero;){var k=e.Degree-a.Degree,r=this.field.multiply(e.getCoefficient(e.Degree),f),g=g.addOrSubtract(this.field.buildMonomial(k,r));e=e.addOrSubtract(a.multiplyByMonomial(k,r))}f=g.multiply1(b).addOrSubtract(q);g=g.multiply1(h).addOrSubtract(n)}d=g.getCoefficient(0);if(0==d)throw"ReedSolomonException sigmaTilde(0) was zero";d=this.field.inverse(d);a=g.multiply2(d);d=e.multiply2(d);return[a,d]};this.findErrorLocations=function(a){var b=a.Degree;if(1== +b)return Array(a.getCoefficient(1));for(var d=Array(b),c=0,f=1;256>f&&cg.maxImgSize&&(f=d.width/d.height,e=Math.sqrt(g.maxImgSize/f),f*=e);a.width=f;a.height=e;b.drawImage(d,0,0,a.width,a.height);g.width=a.width;g.height=a.height;try{g.imagedata= +b.getImageData(0,0,a.width,a.height)}catch(y){g.result=Error("Cross domain image reading not supported in your browser! Save it to your computer then drag and drop the file!");null!=g.callback&&g.callback(g.result);return}try{g.result=g.process(b)}catch(y){console.log(y),g.result=Error("error decoding QR Code")}null!=g.callback&&g.callback(g.result)};d.onerror=function(){null!=g.callback&&g.callback(Error("Failed to load the image"))};d.src=a},isUrl:function(a){return/(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/.test(a)}, +decode_url:function(a){var b="";try{b=escape(a)}catch(e){console.log(e),b=a}a="";try{a=decodeURIComponent(b)}catch(e){console.log(e),a=b}return a},decode_utf8:function(a){return g.isUrl(a)?g.decode_url(a):a},process:function(a){var b=(new Date).getTime(),e=g.grayScaleToBitmap(g.grayscale());if(g.debug){for(var d=0;dc;c++){d[c]=Array(4);for(var f=0;4>f;f++)d[c][f]=[0,0]}for(c=0;4>c;c++)for(f= +0;4>f;f++){d[f][c][0]=255;for(var h=0;hd[f][c][1]&&(d[f][c][1]=k)}}a=Array(4);for(b=0;4>b;b++)a[b]=Array(4);for(c=0;4>c;c++)for(f=0;4>f;f++)a[f][c]=Math.floor((d[f][c][0]+d[f][c][1])/2);return a},grayScaleToBitmap:function(a){for(var b=g.getMiddleBrightnessPerArea(a),e=b.length,d=Math.floor(g.width/e),c=Math.floor(g.height/e),f=new ArrayBuffer(g.width*g.height),f=new Uint8Array(f),h=0;h=e&&d>=c?(d=a[0],e=a[1],c=a[2]):c>=d&&c>=e?(d=a[1], +e=a[0],c=a[2]):(d=a[2],e=a[0],c=a[1]);if(0>function(a,b,c){var d=b.x;b=b.y;return(c.x-d)*(a.y-b)-(c.y-b)*(a.x-d)}(e,d,c))var f=e,e=c,c=f;a[0]=e;a[1]=d;a[2]=c};return g}(); diff --git a/apps/authentiwatch/screenshot.png b/apps/authentiwatch/screenshot.png new file mode 100644 index 000000000..2a7bcbd9a Binary files /dev/null and b/apps/authentiwatch/screenshot.png differ diff --git a/apps/awairmonitor/ChangeLog b/apps/awairmonitor/ChangeLog new file mode 100644 index 000000000..0cc9a42b0 --- /dev/null +++ b/apps/awairmonitor/ChangeLog @@ -0,0 +1 @@ +0.01: Beta version for Bangle 2 paired with Chrome (2021/12/11) diff --git a/apps/awairmonitor/README.md b/apps/awairmonitor/README.md new file mode 100644 index 000000000..69894fea2 --- /dev/null +++ b/apps/awairmonitor/README.md @@ -0,0 +1,22 @@ +# Awair Monitor + +Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awair device. + +* What you need: + * A BangleJS 2 + * An Awair device [with local API enabled](https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Local-API-Feature) + * The web app [awair_to_bangle.html](awair_to_bangle.html) that will retrive the data from your Awair device and sent it to your BangleJS 2 through Chrome's Bluetooth LE connection +* How to get started + * Open awair_to_bangle.html with a text/code editor and input the IP address of your Awair on top (const awair_ip_1 = "192.168.xx.xx") + * Launch the Awair Monitor app on your BangleJS + * Open awair_to_bangle.html on Chrome and click "Connect BangleJS" - it connects to your watch the same way as the Bangle app store + * Once connected to the watch with the app running, the watch app is updated once per second + +![](screenshot.png) + +![](awair-monitor-photo.jpg) + +## Creator +[@alainsaas](https://github.com/alainsaas) + +Contributions are welcome, send me your Pull Requests! diff --git a/apps/awairmonitor/app-icon.js b/apps/awairmonitor/app-icon.js new file mode 100644 index 000000000..9d4dcf4a3 --- /dev/null +++ b/apps/awairmonitor/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/AD38g4FD8EAAoeAgE/AoUD/EfAgP+AYMPDgQPBw4FB/F///DAoPwAQPjAQPBAQPxDgJVCAoP4gYaCCwIcBAoM/8P8h0HjEP8f4h0Gp0H4/44lj5+H4/54lzj/jx/5/lyDgIFDh/xAoQRBAoXsuY8Bx4jCAoeEkYFB447CAoRxBOAPxM4RmC8IFD4ZZD/8H/DHDh/+AoaSBUAIABCoYATVwS2Ct4FE84REXQQLCk4RJAo0XGxY=")) diff --git a/apps/awairmonitor/app.js b/apps/awairmonitor/app.js new file mode 100644 index 000000000..a5a1d1a72 --- /dev/null +++ b/apps/awairmonitor/app.js @@ -0,0 +1,98 @@ +Graphics.prototype.setFontMichroma36 = function() { +g.setFontCustom(atob("AAAAAAAAAAAAAAAAeAAAAAeAAAAAeAAAAAeAAAAAAAAAAAAAAAAAAAAAAAGAAAAA+AAAAD+AAAAP+AAAA/8AAAD/wAAAf/AAAB/4AAAH/gAAAf+AAAB/4AAAH/gAAAf+AAAAfwAAAAfAAAAAcAAAAAAAAAAAAAAAAAAAAAAAA///AAD///wAH///4AP///8APwAD+APAAAeAeAAAeAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAeAPAAAeAPwAD+AP///8AH///4AD///wAA///AAAAAAAAAAAAAAAAAAAAAEAAAAAOAAAAAfAAAAA+AAAAB8AAAAD8AAAAH4AAAAPwAAAAPgAAAAfAAAAAf///+Af///+Af///+Af///+AAAAAAAAAAAAAAAAAAAAAAAAAA/Af+AD/A/+AH/B/+AP/D/+APwD4eAPADweAfADweAeADweAeADweAeADweAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAPgeAeAPAeAeAPAeAeAPAeAeAPAeAfAPAeAPw/AeAP/+AeAH/+AeAD/8AeAB/wAOAAAAAAAAAAAAAAAAAAAAAAAAAB8APgAD8AP4AH8AP8AP8AP8APgAB+AfAAAeAeAAAeAeAAAPAeAAAPAeAAAPAeAAAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAeAfAeAeAPx/h+AP///+AH///8AD///4AB/h/gAAAAAAAAAAAAAAAAAAAAAAeAAAAA/AAAAA/AAAAB/AAAAD/AAAAH/AAAAPvAAAAPPAAAAfPAAAA+PAAAB8PAAAD4PAAADwPAAAHwPAAAPgPAAAfAPAAA+APAAA8APAAB8APAAD4APAAHwAPAAPgAPAAPAAPAAfAAPAAf///+Af///+Af///+Af///+AAAAPAAAAAPAAAAAPAAAAAPAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAf/8PgAf/8P4Af/8P8Af/8P8AeB4A+AeB4AeAeDwAeAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAfAeDwAeAeD4A+AeD+D+AeB//8AeB//4AeA//4AAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAA///AAD///wAH///4AH///8AP4fB+APAeAeAfA8AeAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAfA8APAPA+AeAPgeAeAP8fh+AH8f/8AD8P/8AA8H/4AAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAeAAAAAeAAAAAeAAAAAeAAAAAeAAACAeAAAGAeAAAOAeAAAeAeAAA+AeAAD+AeAAH8AeAAP4AeAAfwAeAA/gAeAB/AAeAD+AAeAP4AAeAfwAAeA/gAAeB/AAAeD+AAAeH8AAAefwAAAe/gAAAf/AAAAf+AAAAf8AAAAf4AAAAfgAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAMAAB+B/wAD/j/4AH/3/8AP///+AP//A+AfB+AeAeA+AeAeA+APAeA+APAeA+APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA+APAeA+APAeA+APAeA+AOAeA+AeAPh/A+AP///+AP/3/8AH/3/8AB/D/wAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAD/4HAAH/8HwAP/+H4AP5/H8AfAfA8AeAPAeAeAPAeAeAPAeAeAHgfAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHAPAeAPAOAeAPAeAPAPAeAPwfB+AP///8AH///4AD///wAA///AAAAAAAAAAAAAAAAAAAAAAAAAAAB8DwAAB8HwAAB8HwAAB8DwAAAAAAAAAAAAA"), 46, atob("CBIkESMjJCMjIyMjCA=="), 36+(1<<8)+(1<<16)); +}; + +var drawTimeout; + +function queueNextDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 1000 - (Date.now() % 1000)); +} + +var locale = require("locale"); + +var bt_current_co2 = 0; +var bt_current_voc = 0; +var bt_current_pm25 = 0; +var bt_current_humi = 0; +var bt_current_temp = 0; +var bt_last_update = 0; + +var last_update = 0; +var bt_co2_history = new Array(10).fill(0); +var bt_voc_history = new Array(10).fill(0); +var bt_pm25_history = new Array(10).fill(0); +var bt_humi_history = new Array(10).fill(0); +var bt_temp_history = new Array(10).fill(0); + +var internal_last_update = -1; + +function draw() { + g.reset().clearRect(0,24,g.getWidth(),g.getHeight()); + + var date = new Date(); + g.setFontAlign(0,0); + g.setFont("Michroma36").drawString(locale.time(date,1), g.getWidth()/2, 56); + + g.setFont("6x8"); + g.drawString(locale.date(new Date(),1), g.getWidth()/2, 80); + + g.setFont("6x8"); + g.drawString("CO2", 20, 100); + g.drawString("VOC", 55, 100); + g.drawString("PM25", 90, 100); + g.drawString("Humi", 125, 100); + g.drawString("Temp", 160, 100); + + g.setFont("HaxorNarrow7x17"); + g.drawString(""+bt_current_co2, 18, 110); + g.drawString(""+bt_current_voc, 53, 110); + g.drawString(""+bt_current_pm25, 88, 110); + g.drawString(""+bt_current_humi, 123, 110); + g.drawString(""+bt_current_temp, 158, 110); + + if (last_update != bt_last_update) { + last_update = bt_last_update; + internal_last_update = last_update; + if (last_update % 10 == 0) { + bt_co2_history.shift(); bt_co2_history.push(bt_current_co2); + bt_voc_history.shift(); bt_voc_history.push(bt_current_voc); + bt_pm25_history.shift(); bt_pm25_history.push(bt_current_pm25); + bt_humi_history.shift(); bt_humi_history.push(bt_current_humi); + bt_temp_history.shift(); bt_temp_history.push(bt_current_temp); + } + } + + if (internal_last_update == -1) { + g.drawString("Waiting for connection", 88, 164); + } else if (internal_last_update > last_update + 5) { + g.drawString("Trying to reconnect since " + (internal_last_update - last_update), 88, 164); + } + + + for (i = 0; i < 10; i++) { + // max height = 32 + g.drawLine(10+i*2, 150-(Math.min(Math.max(bt_co2_history[i],400), 1200)-400)/25, 10+i*2, 150); + g.drawLine(45+i*2, 150-(Math.min(Math.max(bt_voc_history[i],0), 1440)-0)/45, 45+i*2, 150); + g.drawLine(80+i*2, 150-(Math.min(Math.max(bt_pm25_history[i],0), 32)-0)/1, 80+i*2, 150); + g.drawLine(115+i*2, 150-(Math.min(Math.max(bt_humi_history[i],20), 60)-20)/1.25, 115+i*2, 150); + g.drawLine(150+i*2, 150-(Math.min(Math.max(bt_temp_history[i],19), 27)-19)*4, 150+i*2, 150); + + // target humidity level + g.setColor("#00F").drawLine(115, 150-(40-20)/1.25, 115+18, 150-(40-20)/1.25); + g.reset(); + } + + if (internal_last_update != -1) { internal_last_update++; } + queueNextDraw(); +} + +// init +require("FontHaxorNarrow7x17").add(Graphics); +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +draw(); diff --git a/apps/awairmonitor/app.png b/apps/awairmonitor/app.png new file mode 100644 index 000000000..26a5d0cff Binary files /dev/null and b/apps/awairmonitor/app.png differ diff --git a/apps/awairmonitor/awair-monitor-photo.jpg b/apps/awairmonitor/awair-monitor-photo.jpg new file mode 100644 index 000000000..8b62faa24 Binary files /dev/null and b/apps/awairmonitor/awair-monitor-photo.jpg differ diff --git a/apps/awairmonitor/awair_to_bangle.html b/apps/awairmonitor/awair_to_bangle.html new file mode 100644 index 000000000..2926cca9e --- /dev/null +++ b/apps/awairmonitor/awair_to_bangle.html @@ -0,0 +1,195 @@ + + + + + + + + + + +

+How to use +

+Step 1: Enable the Local API on your Awair: https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Local-API-Feature +

+Step 2: Modify this HTML file to input the IP address of your Awair on top (const awair_ip_1 = "192.168.xx.xx") +

+Step 3: Launch the Awair Monitor app on your BangleJS +

+Step 4: Click "Connect BangleJS" +

+Step 5: Optionally, open the web inspector's console (Right click > Inspector > Console) to read the bluetooth logs +

+ +
+ + +
+ +

+ +
+
+
+
+
+ + diff --git a/apps/awairmonitor/screenshot.png b/apps/awairmonitor/screenshot.png new file mode 100644 index 000000000..51ca0aa44 Binary files /dev/null and b/apps/awairmonitor/screenshot.png differ diff --git a/apps/ballmaze/ChangeLog b/apps/ballmaze/ChangeLog index 5560f00bc..de6240f46 100644 --- a/apps/ballmaze/ChangeLog +++ b/apps/ballmaze/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Set LCD timeout for Espruino 2v10 compatibility \ No newline at end of file diff --git a/apps/ballmaze/app.js b/apps/ballmaze/app.js index 3e26277b7..862e6fc6c 100644 --- a/apps/ballmaze/app.js +++ b/apps/ballmaze/app.js @@ -1,4 +1,5 @@ -(() => { +(() => { + Bangle.setLCDTimeout(0); let intervalID; let settings = require("Storage").readJSON("ballmaze.json",true) || {}; diff --git a/apps/balltastic/ChangeLog b/apps/balltastic/ChangeLog index 5a62086c2..de6afabe8 100644 --- a/apps/balltastic/ChangeLog +++ b/apps/balltastic/ChangeLog @@ -1 +1,2 @@ 0.01: Initial version of Balltastic released! Happy! +0.02: Set LCD timeout for Espruino 2v10 compatibility \ No newline at end of file diff --git a/apps/balltastic/app.js b/apps/balltastic/app.js index 6c1de940c..ed5207e5f 100644 --- a/apps/balltastic/app.js +++ b/apps/balltastic/app.js @@ -1,5 +1,6 @@ Bangle.setLCDBrightness(1); Bangle.setLCDMode("doublebuffered"); +Bangle.setLCDTimeout(0); let points = 0; let level = 1; diff --git a/apps/banglebridge/README.md b/apps/banglebridge/README.md new file mode 100644 index 000000000..9897971f8 --- /dev/null +++ b/apps/banglebridge/README.md @@ -0,0 +1,10 @@ +Widget that allows Bangle Js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App +Part of smartPPE project https://jorgepramos.github.io/Smart_PPE/index.html + +# BangleBridge + +Widget that allows Bangle Js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App. + +## Full Project + +Part of smartPPE project [SmartPEE](https://jorgepramos.github.io/Smart_PPE/index.html). \ No newline at end of file diff --git a/apps/banglebridge/banglebridge.png b/apps/banglebridge/banglebridge.png new file mode 100644 index 000000000..3c1e693fc Binary files /dev/null and b/apps/banglebridge/banglebridge.png differ diff --git a/apps/banglebridge/heart.img b/apps/banglebridge/heart.img new file mode 100644 index 000000000..b8e339b30 --- /dev/null +++ b/apps/banglebridge/heart.img @@ -0,0 +1 @@ +00 ?? ?? ?? ''' ' ' ''' ' '''' '' ''''' '??''''''' '''''''' '''''''' ''''''''' '''''''''' ''''''''''' '''''''''''' '''''''''''' ''''''''''''' ''''''''''''' '''''''''''''' '''''''''''''' ''''''''''''''' ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' \ No newline at end of file diff --git a/apps/banglebridge/watch.img b/apps/banglebridge/watch.img new file mode 100644 index 000000000..4a8434583 Binary files /dev/null and b/apps/banglebridge/watch.img differ diff --git a/apps/banglebridge/widget.js b/apps/banglebridge/widget.js new file mode 100644 index 000000000..48078de30 --- /dev/null +++ b/apps/banglebridge/widget.js @@ -0,0 +1,302 @@ +(() => { + /** + * Widget measurements + * Description: + * name: connection.wid.js + *icon: conectionIcon.icon + * + */ + + //Font + + g.setFont("Vector", 100); + //variabangle.Sensorss + let acclS, bttS, compssS, gpsS, hrmS, stepS; //Strings + let accelN, compssN, gpsN, hrmN, stepN; //Num + let prueba = 1; + let data = [0, 0, 0, 0, 0, 0]; + //Constants for redabangle.Sensors code + let storage = require('Storage'); + let deCom = require('heatshrink'); + + + + + //Sensors code + /** + * + * @author Jorge + */ + function accel() { + + Bangle.on('accel', function (acc) { + // acc = {x,y,z,diff,mag} + accelN = acc; + }); + + setInterval(function () { + + acclS = accelN.x + "##" + accelN.y + "##" + accelN.z + "\n" + accelN.diff + "##" + accelN.mag; + data[3] = accelN; + }, 2 * 1000); + + } + + function btt() { + + setInterval(function () { + + bttS = E.getBattery(); //return String + data[2] = E.getBattery(); + }, 15 * 1000); + + } + + + + function compss() { + + Bangle.setCompassPower(1); + Bangle.on('mag', function (mag) { + // mag = {x,y,z,dx,dy,dz,heading} + compssN = mag; + }); + + + setInterval(function () { + + compssS = "A: " + compssN.x + " ## " + compssN.y + " ## " + compssN.z + "\n" + + "B: " + compssN.dx + " ## " + compssN.dy + " ## " + compssN.dz + " ## " + "\n" + + "C: " + compssN.heading; //return String + data[4] = compssN; + }, 2 * 1000); + + } + + + + function gps() { + + Bangle.setGPSPower(1); + Bangle.on('GPS', function (gps) { + // gps = {lat,lon,alt,speed,etc} + gpsN = gps; + + }); + + setInterval(function () { + + gpsS = "A: " + gpsN.lat + " ## " + gpsN.lon + " ## " + gpsN.alt + "\n" + "B: " + gpsN.speed + " ## " + gpsN.course + " ## " + gpsN.time + "\n" + + "C: " + gpsN.satellites + " ## " + gpsN.fix; //return String + // work out how to display the current time + var d = new Date(); + var year = d.getFullYear(); + + var month = d.getMonth() + 1; + var finalMonth = 0; + if (month < 10) { + finalMonth = "0" + month; + } else { + finalMonth = month; + } + var day = d.getDate(); + var finalDay = 0; + if (day < 10) { + finalDay = "0" + day; + } else { + finalDay = day; + } + var h = d.getHours(), + m = d.getMinutes(); + var finalh = 0; + if (h < 10) { + finalh = "0" + h; + } else { + finalh = h; + } + var finalM = 0; + if (m < 10) { + finalM = "0" + m; + } else { + finalM = m; + } + + var s = d.getSeconds(); + var finalS = 0; + if (s < 10) { + finalS = "0" + s; + } else { + finalS = s; + } + var z = d.getMilliseconds(); + var zFinal = new String(z); + zFinal = zFinal.replace('.', ''); + var completeTime = year + "-" + finalMonth + "-" + finalDay + "T" + finalh + ":" + finalM + ":" + finalS + "." + z + "Z"; + var time = h + ":" + ("0" + m).substr(-2); + gpsN.time = completeTime; + data[5] = gpsN; + }, 2 * 1000); + } + + //2021-06-11T19:21:58.000Z + + function hrm() { + + let msr = [0, 0, 0, 0, 0]; + let lastInsert = -1; + + function roundInsert(nueva) { + let indexFinal = (lastInsert + 1) % (msr.length); + //console.log("Index ==> "+ index); + msr[indexFinal] = nueva; + + item = nueva; + lastInsert = indexFinal; + + } + + function normalize(nueva) { + + let normalize = 0; + roundInsert(nueva); + + + msr.forEach(function (number) { + normalize += number; + }); + normalize = normalize / msr.length; + + return normalize; + + } + + + + + setInterval(function () { + + if (!isNaN(hrmN)) { + + + hrmN = normalize(hrmN); + var roundedRate = parseFloat(hrmN).toFixed(2); + hrmS = String.valueOf(roundedRate); //return String + //console.log("array----->" + msr); + data[0] = roundedRate; + + } + + + + + + }, 2 * 1000); + + } + + + function steps() { + + Bangle.on('step', s => { + + stepN = s; + }); + + + setInterval(function () { + + stepS = String.valueOf(stepN); //return String + data[1] = stepN; + }, 2 * 1000); + + + } + + function initSensors() { + + //need power control + Bangle.setHRMPower(1); + + Bangle.on('HRM', function (hrm) { + hrmN = hrm.bpm; + + + }); + console.log("Sensors are being Init...."); + accel(); + btt(); + compss(); + gps(); + hrm(); + steps(); + + } + + var flip = 1; + Bangle.on('lcdPower', function (on) { + /* + prueba ++; + Bangle.drawWidgets(); + g.setFont("Vector", 45); + g.drawString(prueba,100,200);*/ + if (flip == 1) { //when off + + flip = 0; + //Bangle.buzz(1000); + g.clear(); + } else { //when on + + flip = 1; + g.setFont("Vector", 30); + g.drawString(data[0], 65, 180); + Bangle.drawWidgets(); + } + + }); + + + function draw() { + g.drawImage(storage.read("banglebridge.watch.img"),this.x + 1,this.y + 1); + g.drawImage(storage.read("banglebridge.heart.img"), 145, 167); + } + + + // Finally add widget + + + initSensors(); + // Bangle.drawWidgets(); + // Terminal.println("Running BangleBridge"); + data[0] = 80.5; + g.setFont("Vector", 30); + g.drawString(data[0], 65, 180); + // Bangle.drawWidgets(); + setInterval(function () { + //console.log("---------------------------------------------------------------"); + //console.log(data); + //Bluetooth.println(data[0]); + var measurement = { + hrm: data[0], + step: data[1], + batt: data[2], + acc: data[3], + com: data[4], + gps: data[5] + }; + /* g.clear(); + g.drawString(compssS,100,200); + */ + + + + Bluetooth.println(JSON.stringify(measurement) + "#"); + //draw(); + + }, 5 * 1000); + + WIDGETS["banglebridge"]={ + area: "tl", + width: 10, + draw: draw, + }; +})(); //End of Widget diff --git a/apps/banglebridge/widget.png b/apps/banglebridge/widget.png new file mode 100644 index 000000000..3c1e693fc Binary files /dev/null and b/apps/banglebridge/widget.png differ diff --git a/apps/banglerun/ChangeLog b/apps/banglerun/ChangeLog old mode 100755 new mode 100644 diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog index 616ee66e9..316660fc6 100644 --- a/apps/barclock/ChangeLog +++ b/apps/barclock/ChangeLog @@ -2,4 +2,8 @@ 0.02: Apply locale, 12-hour setting 0.03: Fix dates drawing over each other at midnight 0.04: Small bugfix -0.05: Clock does not start if app Languages is not installed \ No newline at end of file +0.05: Clock does not start if app Languages is not installed +0.06: Improve accuracy +0.07: Update to use Bangle.setUI instead of setWatch +0.08: Use theme colors, Layout library +0.09: Fix time/date disappearing after fullscreen notification diff --git a/apps/barclock/README.md b/apps/barclock/README.md new file mode 100644 index 000000000..4b92313c5 --- /dev/null +++ b/apps/barclock/README.md @@ -0,0 +1,6 @@ +# Bar Clock +A simple digital clock showing seconds as a horizontal bar. + +| 24hr style | 12hr style | +| --- | --- | +| ![24-hour bar clock](screenshot.png) | ![12-hour bar clock with meridian](screenshot_pm.png) | diff --git a/apps/barclock/clock-bar.js b/apps/barclock/clock-bar.js index 0f2609298..5d46a1cb4 100644 --- a/apps/barclock/clock-bar.js +++ b/apps/barclock/clock-bar.js @@ -2,170 +2,107 @@ /** * A simple digital clock showing seconds as a bar **/ -{ - // Check settings for what type our clock should be - const is12Hour = (require('Storage').readJSON('setting.json', 1) || {})['12hour'] - let locale = require('locale') - { // add some more info to locale - let date = new Date() - date.setFullYear(1111) - date.setMonth(1, 3) // februari: months are zero-indexed - const localized = locale.date(date, true) - locale.dayFirst = /3.*2/.test(localized) - - locale.hasMeridian = false - if(typeof locale.meridian === 'function') { // function does not exists if languages app is not installed - locale.hasMeridian = (locale.meridian(date) !== '') - } - +// Check settings for what type our clock should be +const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; +let locale = require("locale"); +{ // add some more info to locale + let date = new Date(); + date.setFullYear(1111); + date.setMonth(1, 3); // februari: months are zero-indexed + const localized = locale.date(date, true); + locale.dayFirst = /3.*2/.test(localized); + + locale.hasMeridian = false; + if (typeof locale.meridian==="function") { // function does not exist if languages app is not installed + locale.hasMeridian = (locale.meridian(date)!==""); } - const screen = { - width: g.getWidth(), - height: g.getWidth(), - middle: g.getWidth() / 2, - center: g.getHeight() / 2, - } - - // hardcoded "settings" - const settings = { - time: { - color: -1, - font: '6x8', - size: (is12Hour && locale.hasMeridian) ? 6 : 8, - middle: screen.middle, - center: screen.center, - ampm: { - color: -1, - font: '6x8', - size: 2, - }, - }, - date: { - color: -1, - font: 'Vector', - size: 20, - middle: screen.height - 20, // at bottom of screen - center: screen.center, - }, - bar: { - color: -1, - top: 155, // just below time - thickness: 6, // matches 24h time "pixel" size - }, - } - - const SECONDS_PER_MINUTE = 60 - - const timeText = function (date) { - if (!is12Hour) { - return locale.time(date, true) - } - const date12 = new Date(date.getTime()) - const hours = date12.getHours() - if (hours === 0) { - date12.setHours(12) - } else if (hours > 12) { - date12.setHours(hours - 12) - } - return locale.time(date12, true) - } - const ampmText = function (date) { - return is12Hour ? locale.meridian(date) : '' - } - - const dateText = function (date) { - const dayName = locale.dow(date, true), - month = locale.month(date, true), - day = date.getDate() - const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}` - return `${dayName} ${dayMonth}` - } - - const drawDateTime = function (date) { - const t = settings.time - g.setColor(t.color) - g.setFont(t.font, t.size) - g.setFontAlign(0, 0) // centered - g.drawString(timeText(date), t.center, t.middle, true) - if (is12Hour && locale.hasMeridian) { - const a = settings.time.ampm - g.setColor(a.color) - g.setFont(a.font, a.size) - g.setFontAlign(1, -1) // right top - // at right edge of screen, aligned with time bottom - const left = screen.width - a.size * 2, - top = t.middle + t.size - a.size - g.drawString(ampmText(date), left, top, true) - } - - const d = settings.date - g.setColor(d.color) - g.setFont(d.font, d.size) - g.setFontAlign(0, 0) // centered - g.drawString(dateText(date), d.center, d.middle, true) - } - - const drawBar = function (date) { - const b = settings.bar - const seconds = date.getSeconds() - if (seconds === 0) { - // zero-size rect stills draws one line of pixels, we don't want that - return - } - const fraction = seconds / SECONDS_PER_MINUTE, - width = fraction * screen.width - g.setColor(b.color) - g.fillRect(0, b.top, width, b.top + b.thickness) - } - - const clearScreen = function () { - g.setColor(0) - const timeTop = settings.time.middle - (settings.time.size * 4) - g.fillRect(0, timeTop, screen.width, screen.height) - } - - let lastSeconds - const tick = function () { - g.reset() - const date = new Date() - const seconds = date.getSeconds() - if (lastSeconds > seconds) { - // new minute - clearScreen() - drawDateTime(date) - } - // the bar only gets larger, so drawing on top of the previous one is fine - drawBar(date) - - lastSeconds = seconds - } - - let iTick - const start = function () { - lastSeconds = 99 // force redraw - tick() - iTick = setInterval(tick, 1000) - } - const stop = function () { - if (iTick) { - clearInterval(iTick) - iTick = undefined - } - } - - // clean app screen - g.clear() - Bangle.loadWidgets() - Bangle.drawWidgets() - // Show launcher when middle button pressed - setWatch(Bangle.showLauncher, BTN2, {repeat: false, edge: 'falling'}) - - Bangle.on('lcdPower', function (on) { - if (on) { - start() - } else { - stop() - } - }) - start() } +Bangle.loadWidgets(); +function renderBar(l) { + if (!this.fraction) { + // zero-size fillRect stills draws one line of pixels, we don't want that + return; + } + const width = this.fraction*l.w; + g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1); +} + +const Layout = require("Layout"); +const layout = new Layout({ + type: "v", c: [ + { + type: "h", c: [ + {id: "time", label: "88:88", type: "txt", font: "6x8:5", bgCol: g.theme.bg}, // size updated below + {id: "ampm", label: " ", type: "txt", font: "6x8:2", bgCol: g.theme.bg}, + ], + }, + {id: "bar", type: "custom", fraction: 0, fillx: 1, height: 6, col: g.theme.fg2, render: renderBar}, + {height: 40}, + {id: "date", type: "txt", font: "10%", valign: 1}, + ], +}, {lazy: true}); +// adjustments based on screen size and whether we display am/pm +let thickness; // bar thickness, same as time font "pixel block" size +if (is12Hour) { + // Maximum font size = ( - ) / (5chars * 6px) + thickness = Math.floor((g.getWidth()-24)/(5*6)); +} else { + layout.ampm.label = ""; + thickness = Math.floor(g.getWidth()/(5*6)); +} +layout.bar.height = thickness+1; +layout.time.font = "6x8:"+thickness; +layout.update(); + +function timeText(date) { + if (!is12Hour) { + return locale.time(date, true); + } + const date12 = new Date(date.getTime()); + const hours = date12.getHours(); + if (hours===0) { + date12.setHours(12); + } else if (hours>12) { + date12.setHours(hours-12); + } + return locale.time(date12, true); +} +function ampmText(date) { + return (is12Hour && locale.hasMeridian)? locale.meridian(date) : ""; +} +function dateText(date) { + const dayName = locale.dow(date, true), + month = locale.month(date, true), + day = date.getDate(); + const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`; + return `${dayName} ${dayMonth}`; +} + +draw = function draw(force) { + if (!Bangle.isLCDOn()) {return;} // no drawing, also no new update scheduled + const date = new Date(); + layout.time.label = timeText(date); + layout.ampm.label = ampmText(date); + layout.date.label = dateText(date); + const SECONDS_PER_MINUTE = 60; + layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE; + if (force) { + Bangle.drawWidgets(); + layout.forgetLazyState(); + } + layout.render(); + // schedule update at start of next second + const millis = date.getMilliseconds(); + setTimeout(draw, 1000-millis); +}; + +// Show launcher when button pressed +Bangle.setUI("clock"); +Bangle.on("lcdPower", function(on) { + if (on) { + draw(true); + } +}); +g.reset().clear(); +Bangle.drawWidgets(); +draw(); diff --git a/apps/barclock/screenshot.png b/apps/barclock/screenshot.png new file mode 100644 index 000000000..9c2b7a50f Binary files /dev/null and b/apps/barclock/screenshot.png differ diff --git a/apps/barclock/screenshot_pm.png b/apps/barclock/screenshot_pm.png new file mode 100644 index 000000000..983f17aaa Binary files /dev/null and b/apps/barclock/screenshot_pm.png differ diff --git a/apps/batclock/ChangeLog b/apps/batclock/ChangeLog index 5d221b4c4..e6e21b146 100644 --- a/apps/batclock/ChangeLog +++ b/apps/batclock/ChangeLog @@ -1 +1,2 @@ 0.01: App Created! +0.02: Update to use Bangle.setUI instead of setWatch diff --git a/apps/batclock/bat-clock.app.js b/apps/batclock/bat-clock.app.js index abb5fbd3a..31b8f5b9b 100644 --- a/apps/batclock/bat-clock.app.js +++ b/apps/batclock/bat-clock.app.js @@ -256,8 +256,5 @@ Bangle.drawWidgets(); timeInterval = setInterval(showTime, 1000); showTime(); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, { - repeat: false, - edge: "falling" -}); +// Show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/battleship/bangle1-battle-ship-screenshot.png b/apps/battleship/bangle1-battle-ship-screenshot.png new file mode 100644 index 000000000..56225b32d Binary files /dev/null and b/apps/battleship/bangle1-battle-ship-screenshot.png differ diff --git a/apps/bclock/ChangeLog b/apps/bclock/ChangeLog index 7819dbe2a..5b2cf598c 100644 --- a/apps/bclock/ChangeLog +++ b/apps/bclock/ChangeLog @@ -1 +1,2 @@ 0.02: Modified for use with new bootloader and firmware +0.03: Update to use Bangle.setUI instead of setWatch diff --git a/apps/bclock/bangle1-binary-clock-screenshot.png b/apps/bclock/bangle1-binary-clock-screenshot.png new file mode 100644 index 000000000..bc7ce611b Binary files /dev/null and b/apps/bclock/bangle1-binary-clock-screenshot.png differ diff --git a/apps/bclock/clock-binary-icon.js b/apps/bclock/clock-binary-icon.js index 1c167ff57..2e5cb31c1 100644 --- a/apps/bclock/clock-binary-icon.js +++ b/apps/bclock/clock-binary-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAMGAAAAAAYDAAAAAAwBgAAAABgAwAAAABAAQAAAABAAQAAAABAAQAAAABAAQAAAABAAQAAAABgAwAAAAAwBgAAAAAYDAAAAAAMGAAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAP+AAAAAAf/AAAAAA//gAAAAB//wAAAAB//wAAAAB//wAAAAB//wAAAAB//wAAAAB//wAAAAB//wAAAAA//gAAAAAf/AAAAAAP+AAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) \ No newline at end of file +require("heatshrink").decompress(atob("mEwgIurg/wAocMjAFDjEMIAkGAodggYFDoBLEAq4jFF4o7FI4pTFOLsP/AFDj/8Aoc//wFDv//As4vFHYpHFOLoAPA==")) diff --git a/apps/bclock/clock-binary.js b/apps/bclock/clock-binary.js index 833aa00f6..fdf945ee6 100644 --- a/apps/bclock/clock-binary.js +++ b/apps/bclock/clock-binary.js @@ -105,5 +105,5 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); setInterval(() => { drawClock(); }, 1000); drawClock(); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); +// Show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/beebclock/ChangeLog b/apps/beebclock/ChangeLog index ac1c58c29..84ec7c1d7 100644 --- a/apps/beebclock/ChangeLog +++ b/apps/beebclock/ChangeLog @@ -1,3 +1,6 @@ 0.01: Initial commit. Not very efficient, and widgets not working for some reason. 0.02: Fixes; widget support 0.03: Remove hardcoded hour buzz (you can install widchime if you miss it) +0.04: Update to use Bangle.setUI instead of setWatch +0.05: Avoid 'loadWidgets' at LCD on, which will cause memory leak + Avoid clearTimeout() usage, as it may break other widgets diff --git a/apps/beebclock/bangle1-beeb-clock-screenshot.png b/apps/beebclock/bangle1-beeb-clock-screenshot.png new file mode 100644 index 000000000..00cb92e5c Binary files /dev/null and b/apps/beebclock/bangle1-beeb-clock-screenshot.png differ diff --git a/apps/beebclock/beebclock.js b/apps/beebclock/beebclock.js index bbf65697f..c85f68c55 100644 --- a/apps/beebclock/beebclock.js +++ b/apps/beebclock/beebclock.js @@ -5,6 +5,7 @@ const storage = require("Storage"); const filename = 'beebjson'; +var timeout; require('FontTeletext10x18Ascii').add(Graphics); @@ -51,341 +52,338 @@ Graphics.prototype.drawRotLine = function (sina, cosa, cx, cy, r1, r2) { ); }; +// Display modes +// +// 0: full-screen +// 1: with widgets +// 2: centred on Bangle (v.1), no widgets or time/date +// 3: centred with time above +// 4: centred with date above +// 5: centred with time and date above +let mode; -(function(g) { - // Display modes - // - // 0: full-screen - // 1: with widgets - // 2: centred on Bangle (v.1), no widgets or time/date - // 3: centred with time above - // 4: centred with date above - // 5: centred with time and date above - let mode; +// R1, R2: Outer and inner radii of hour marks +// RC1, RC2: Outer and inner radii of hub +// CX, CY: Centre location, relative to buffer (not screen, necessarily) +// HW2, MW2: Half-width of hour and minute hand +// HR, MR: Length of hour and minute hand, relative to CX,CY +// M: Half-width of gap in hour marks +// HSCALE: Half-width of hour mark as function(0 { + const fw = R1 * 2; + const fh = R1 * 2; + const fw2 = R1; + const fh2 = R1; + let hs = []; + + // Wipe the image and start with white + G.clear(); + G.setColor(1,1,1); + + // Draw the hour marks. for (let h=1; h<=12; h++) { - const a = Math.PI * h / 6; - ss[h] = Math.sin(a); - cs[h] = Math.cos(a); + hs[h] = HSCALE(h); + G.fillRotRect(ss[h], cs[h], CX, CY, -hs[h], hs[h], R2, R1); + } - // Draw the face with hour and minute hand. Ideally, we'd separate - // the face from the hands and double-buffer, but memory is limited, - // so we buffer once and minute, and draw the second hand dynamically - // (with a bit of flicker) - const drawFace = (G) => { - const fw = R1 * 2; - const fh = R1 * 2; - const fw2 = R1; - const fh2 = R1; - let hs = []; + // Draw the hub + G.fillCircle(CX, CY, RC1); - // Wipe the image and start with white - G.clear(); - G.setColor(1,1,1); + // Black + G.setColor(0,0,0); - // Draw the hour marks. - for (let h=1; h<=12; h++) { - hs[h] = HSCALE(h); - G.fillRotRect(ss[h], cs[h], CX, CY, -hs[h], hs[h], R2, R1); + // Clear the centre of the hub + G.fillCircle(CX, CY, RC2); + // Draw the gap in the hour marks + for (let h=1; h<=12; h++) { + G.fillRotRect(ss[h], cs[h], CX, CY, -M, M, R2-1, R1+1); + } + + // Back to white for future draw operations + G.setColor(1,1,1); + + // While the buffer remains full-screen, we may trim out the + // bottom of the image so we can shift the whole thing down for + // widgets. + const img = {width:GW,height:GH-TM,buffer:G.buffer}; + return img; +}; + +let hours, minutes, seconds, date; + +// Schedule event for calling at the start of the next second +const inOneSecond = (cb) => { + let now = new Date(); + if (timeout) clearTimeout(timeout); + timeout = setTimeout(function() { + timeout = undefined; + cb(); + }, 1000 - now.getMilliseconds()); +}; + +// Schedule event for calling at the start of the next minute +const inOneMinute = (cb) => { + let now = new Date(); + if (timeout) clearTimeout(timeout); + timeout = setTimeout(function() { + timeout = undefined; + cb(); + }, 60000 - (now.getSeconds() * 1000 + now.getMilliseconds())); +}; + +// Draw a fat hour/minute hand +const drawHand = (G, a, w2, r1, r2) => + G.fillRotRect(Math.sin(a), Math.cos(a), CX, CY, -w2, w2, r1, r2); + +// Redraw function +const drawAll = (force) => { + let now = new Date(); + + if (!faceImg) force = true; + + let face_changed = force; + let date_changed = false; + + tmp = hours; + hours = now.getHours(); + if (tmp !== hours) + face_changed = true; + + tmp = minutes; + minutes = now.getMinutes(); + if (tmp !== minutes) + face_changed = true; + + // If the face has been updated and/or needs a redraw, + // face_changed is true. + + let time_changed = face_changed; + + // If the screen needs an update, regardless of whether the face + // needs a redraw, time_changed is true. + + if (with_seconds) { + // If we're going by second, we always need an update. + seconds = now.getSeconds(); + time_changed = true; + } + + if (with_digital_date) { + // See if the date has changed. If it has, then we need a + // full-blown redraw of the screen and the face, plus text. + tmp = date; + date = now.getDate(); + if (tmp !== date) { + date_changed = true; + face_changed = true; // Should have changed anyway with hour/minute rollover + } + } + + if (face_changed) { + // Redraw the face and hands onto the buffer G1. + faceImg = drawFace(G1); + drawHand(G1, Math.PI*hours/6, HW2, RC1, HR); + drawHand(G1, Math.PI*minutes/30, MW2, RC1, MR); + } + + // Has the time updated? If so, we'll need to draw something. + if (time_changed) { + + // Are we adding text? + if (with_digital_date || with_digital_time) { + + // Construct the date/time text to add above the face + let d = now.toString(); + let da = d.toString().split(" "); + let txt; + + if (with_digital_time) { + txt = da[4].substr(0, 5); + if (with_digital_date) + G1.drawStringDH(txt+',', 24, 0, 'L', GW); + else + G1.drawStringDH(txt, 0, 0, 'C', GW); + } + + if (with_digital_date) { + let txt = [da[0], da[1], da[2]].join(" "); + if (with_digital_time) + G1.drawStringDH(txt, -24, 0, 'R', GW); + else + G1.drawStringDH(txt, 0, 0, 'C', GW); + } } - // Draw the hub - G.fillCircle(CX, CY, RC1); - - // Black - G.setColor(0,0,0); - - // Clear the centre of the hub - G.fillCircle(CX, CY, RC2); - - // Draw the gap in the hour marks - for (let h=1; h<=12; h++) { - G.fillRotRect(ss[h], cs[h], CX, CY, -M, M, R2-1, R1+1); - } - - // Back to white for future draw operations - G.setColor(1,1,1); - - // While the buffer remains full-screen, we may trim out the - // bottom of the image so we can shift the whole thing down for - // widgets. - const img = {width:GW,height:GH-TM,buffer:G.buffer}; - return img; - }; - - let hours, minutes, seconds, date; - - // Schedule event for calling at the start of the next second - const inOneSecond = (cb) => { - let now = new Date(); - clearTimeout(); - setTimeout(cb, 1000 - now.getMilliseconds()); - }; - - // Schedule event for calling at the start of the next minute - const inOneMinute = (cb) => { - let now = new Date(); - clearTimeout(); - setTimeout(cb, 60000 - (now.getSeconds() * 1000 + now.getMilliseconds())); - }; - - // Draw a fat hour/minute hand - const drawHand = (G, a, w2, r1, r2) => - G.fillRotRect(Math.sin(a), Math.cos(a), CX, CY, -w2, w2, r1, r2); - - // Redraw function - const drawAll = (force) => { - let now = new Date(); - - if (!faceImg) force = true; - - let face_changed = force; - let date_changed = false; - - tmp = hours; - hours = now.getHours(); - if (tmp !== hours) - face_changed = true; - - tmp = minutes; - minutes = now.getMinutes(); - if (tmp !== minutes) - face_changed = true; - - // If the face has been updated and/or needs a redraw, - // face_changed is true. - - let time_changed = face_changed; - - // If the screen needs an update, regardless of whether the face - // needs a redraw, time_changed is true. + // If the time has updated, we need to _at least_ draw the + // image to the screen. + g.setColor(1,1,1); + g.drawImage({width:GW, + height:GH-TM, + buffer:G1.buffer}, 0, TM); + // and possibly add the second hand if (with_seconds) { - // If we're going by second, we always need an update. - seconds = now.getSeconds(); - time_changed = true; + let a = 2.0 * Math.PI * seconds / 60.0; + g.drawRotLine(Math.sin(a), Math.cos(a), CX, CY+TM, RC1, R1); } - if (with_digital_date) { - // See if the date has changed. If it has, then we need a - // full-blown redraw of the screen and the face, plus text. - tmp = date; - date = now.getDate(); - if (tmp !== date) { - date_changed = true; - face_changed = true; // Should have changed anyway with hour/minute rollover - } - } + // And draw widgets if we're in that mode + if (with_widgets) + Bangle.drawWidgets(); + } - if (face_changed) { - // Redraw the face and hands onto the buffer G1. - faceImg = drawFace(G1); - drawHand(G1, Math.PI*hours/6, HW2, RC1, HR); - drawHand(G1, Math.PI*minutes/30, MW2, RC1, MR); - } + // Schedule to repeat this. A `setTimeout(1000)` isn't good + // enough, as all the above might've taken some milliseconds and + // we don't want to drift. + if (with_seconds) + inOneSecond(drawAll); + else + inOneMinute(drawAll); +}; - // Has the time updated? If so, we'll need to draw something. - if (time_changed) { +const setButtons = () => { + // Show launcher when button pressed + Bangle.setUI("clockupdown", btn=> { + if (btn==0) changeSeconds(); + if (btn==1) { ++mode; setMode(); drawAll(true); } + }); +}; - // Are we adding text? - if (with_digital_date || with_digital_time) { +// Load display parameters based on `mode` +const setMode = () => { + // Normalize mode to 0 <= mode <= 5 + mode = (6+mode) % 6; - // Construct the date/time text to add above the face - let d = now.toString(); - let da = d.toString().split(" "); - let txt; + // [R1, R2, RC1, RC2, HW2, MW3, HR, MR, M, HSCALE] = + const scales = [ + [120, 84, 17, 12.4, 4.6, 2.2, 8, 2, 1, h => (3.0 + Math.ceil(h/1.5)) ], + [102, 70, 14.6, 10.7, 3.88, 1.8, 8, 2, 1, h => (2.4 + Math.ceil(h/1.6)) ], + ]; - if (with_digital_time) { - txt = da[4].substr(0, 5); - if (with_digital_date) - G1.drawStringDH(txt+',', 24, 0, 'L', GW); - else - G1.drawStringDH(txt, 0, 0, 'C', GW); - } + if (mode < 3) { + // Face without time/date text. Might have widgets though. + with_digital_time = with_digital_date = false; + with_widgets = (mode == 1); + } + else { + // Face with time/date text, but no widgets + with_digital_time = (mode-2)&1; + with_digital_date = (mode-2)&2; + with_widgets = false; + } - if (with_digital_date) { - let txt = [da[0], da[1], da[2]].join(" "); - if (with_digital_time) - G1.drawStringDH(txt, -24, 0, 'R', GW); - else - G1.drawStringDH(txt, 0, 0, 'C', GW); - } - } + // Destructure the array to the global display parameters + let arr = scales[mode > 0 ? 1 : 0]; + R1 = arr[0]; + R2 = arr[1]; + RC1 = arr[2]; + RC2 = arr[3]; + HW2 = arr[4]; + MW2 = arr[5]; + HR = R2 - arr[6]; + MR = R1 - arr[7]; + M = arr[8]; + HSCALE = arr[9]; + TM = with_widgets ? 36 : 0; - // If the time has updated, we need to _at least_ draw the - // image to the screen. - g.setColor(1,1,1); - g.drawImage({width:GW, - height:GH-TM, - buffer:G1.buffer}, 0, TM); + CX = GW/2; + CY = R1; - // and possibly add the second hand - if (with_seconds) { - let a = 2.0 * Math.PI * seconds / 60.0; - g.drawRotLine(Math.sin(a), Math.cos(a), CX, CY+TM, RC1, R1); - } + // If we're in the small-face + text regime, we're going to buffer + // the full screen but draw the clock face further down to give + // space for the text. + // + // Compare with modes 0 (full-screen) and 1 (with_widgets==true) + // where the face is drawn at the top of the buffer, but drawn + // lower down the screen (so CY doesn't move) + if (mode > 1) { + CY += 36; + } - // And draw widgets if we're in that mode - if (with_widgets) - Bangle.drawWidgets(); - } + // We only don't bother redrawing the face from modes 2 to 5, as + // they're the same. + if (!faceImg || mode<3) { + faceImg = undefined; + } - // Schedule to repeat this. A `setTimeout(1000)` isn't good - // enough, as all the above might've taken some milliseconds and - // we don't want to drift. - if (with_seconds) - inOneSecond(drawAll); - else - inOneMinute(drawAll); - }; - - const setButtons = () => { - const opts = { repeat: true, edge:'rising', debounce:30}; - - // BTN1: enable/disable second hand - setWatch(changeSeconds, BTN1, opts); - - // BTN2: return to launcher - setWatch(Bangle.showLauncher, BTN2, { repeat:false, edge:'falling' }); - - // BTN3: change display mode - setWatch(function () { ++mode; setMode(); drawAll(true); }, BTN3, opts); - }; - - // Load display parameters based on `mode` - const setMode = () => { - // Normalize mode to 0 <= mode <= 5 - mode = (6+mode) % 6; - - // [R1, R2, RC1, RC2, HW2, MW3, HR, MR, M, HSCALE] = - const scales = [ - [120, 84, 17, 12.4, 4.6, 2.2, 8, 2, 1, h => (3.0 + Math.ceil(h/1.5)) ], - [102, 70, 14.6, 10.7, 3.88, 1.8, 8, 2, 1, h => (2.4 + Math.ceil(h/1.6)) ], - ]; - - if (mode < 3) { - // Face without time/date text. Might have widgets though. - with_digital_time = with_digital_date = false; - with_widgets = (mode == 1); - } - else { - // Face with time/date text, but no widgets - with_digital_time = (mode-2)&1; - with_digital_date = (mode-2)&2; - with_widgets = false; - } - - // Destructure the array to the global display parameters - let arr = scales[mode > 0 ? 1 : 0]; - R1 = arr[0]; - R2 = arr[1]; - RC1 = arr[2]; - RC2 = arr[3]; - HW2 = arr[4]; - MW2 = arr[5]; - HR = R2 - arr[6]; - MR = R1 - arr[7]; - M = arr[8]; - HSCALE = arr[9]; - TM = with_widgets ? 36 : 0; - - CX = GW/2; - CY = R1; - - // If we're in the small-face + text regime, we're going to buffer - // the full screen but draw the clock face further down to give - // space for the text. - // - // Compare with modes 0 (full-screen) and 1 (with_widgets==true) - // where the face is drawn at the top of the buffer, but drawn - // lower down the screen (so CY doesn't move) - if (mode > 1) { - CY += 36; - } - - // We only don't bother redrawing the face from modes 2 to 5, as - // they're the same. - if (!faceImg || mode<3) { - faceImg = undefined; - } - - // Store the settings for next time - try { - storage.writeJSON(filename, [mode,with_seconds]); - } catch (e) { - console.log(e); - } - - // Clear the screen: we need to make sure all parts are cleaned off. - g.clear(); - }; - - const changeSeconds = () => { - with_seconds = !with_seconds; - drawAll(true); - }; - - Bangle.loadWidgets(); - - // Restore mode + // Store the settings for next time try { - conf = storage.readJSON(filename); - mode = conf[0]; - with_seconds = conf[1]; + storage.writeJSON(filename, [mode,with_seconds]); } catch (e) { console.log(e); - mode = 1; } - setButtons(); - setMode(); - drawAll(); + // Clear the screen: we need to make sure all parts are cleaned off. + g.clear(); +}; - Bangle.on('lcdPower', (on) => { - if (on) { - Bangle.loadWidgets(); - Bangle.drawWidgets(); - drawAll(); - } else { - clearTimeout(); - } - }); +const changeSeconds = () => { + with_seconds = !with_seconds; + drawAll(true); +}; -})(g); +Bangle.loadWidgets(); +// widgets are drawn in drawAll() + +// Restore mode +try { + conf = storage.readJSON(filename); + mode = conf[0]; + with_seconds = conf[1]; +} catch (e) { + console.log(e); + mode = 1; +} + +setButtons(); +setMode(); +drawAll(); + +Bangle.on('lcdPower', (on) => { + if (on) { + drawAll(); + } else { + if (timeout) clearTimeout(timeout); + timeout = undefined; + } +}); diff --git a/apps/berlinc/ChangeLog b/apps/berlinc/ChangeLog index a33332bc4..9e9c1a6aa 100644 --- a/apps/berlinc/ChangeLog +++ b/apps/berlinc/ChangeLog @@ -1,2 +1,6 @@ 0.02: Modified for use with new bootloader and firmware 0.03: Shrinked size to avoid cut-off edges on the physical device. BTN3: show date. BTN1: show time in decimal. +0.04: Update to use Bangle.setUI instead of setWatch +0.05: Update *on* the minute rather than every 15 secs + Now show widgets + Make compatible with themes, and Bangle.js 2 diff --git a/apps/berlinc/berlin-clock-screenshot.png b/apps/berlinc/berlin-clock-screenshot.png new file mode 100644 index 000000000..92a4c7928 Binary files /dev/null and b/apps/berlinc/berlin-clock-screenshot.png differ diff --git a/apps/berlinc/berlin-clock.js b/apps/berlinc/berlin-clock.js index 3950147b8..0dd8ff8ee 100644 --- a/apps/berlinc/berlin-clock.js +++ b/apps/berlinc/berlin-clock.js @@ -1,7 +1,7 @@ // Berlin Clock see https://en.wikipedia.org/wiki/Mengenlehreuhr // https://github.com/eska-muc/BangleApps const fields = [4, 4, 11, 4]; -const offset = 20; +const offset = 24; const width = g.getWidth() - 2 * offset; const height = g.getHeight() - 2 * offset; const rowHeight = height / 4; @@ -10,13 +10,25 @@ var show_date = false; var show_time = false; var yy = 0; -rowlights = []; -time_digit = []; +var rowlights = []; +var time_digit = []; -function drawBerlinClock() { - g.clear(); +// 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() { + g.reset().clearRect(0,24,g.getWidth(),g.getHeight()); var now = new Date(); - + // show date below the clock if (show_date) { var yr = now.getFullYear(); @@ -24,11 +36,10 @@ function drawBerlinClock() { var day = now.getDate(); var dateString = `${yr}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`; var strWidth = g.stringWidth(dateString); - g.setColor(1, 1, 1); - g.setFontAlign(-1,-1); + g.setColor(g.theme.fg).setFontAlign(-1,-1); g.drawString(dateString, ( g.getWidth() - strWidth ) / 2, height + offset + 4); } - + rowlights[0] = Math.floor(now.getHours() / 5); rowlights[1] = now.getHours() % 5; rowlights[2] = Math.floor(now.getMinutes() / 5); @@ -50,8 +61,7 @@ function drawBerlinClock() { x2 = (col + 1) * boxWidth + offset; y2 = (row + 1) * rowHeight + offset; - g.setColor(1, 1, 1); - g.drawRect(x1, y1, x2, y2); + g.setColor(g.theme.fg).drawRect(x1, y1, x2, y2); if (col < rowlights[row]) { if (row === 2) { if (((col + 1) % 3) === 0) { @@ -62,47 +72,45 @@ function drawBerlinClock() { } else { g.setColor(1, 0, 0); } - g.fillRect(x1 + 2, y1 + 2, x2 - 2, y2 - 2); + g.fillRect(x1 + 2, y1 + 2, x2 - 2, y2 - 2); } if (row == 3 && show_time) { - g.setColor(1,1,1); - g.setFontAlign(0,0); + g.setColor(g.theme.fg).setFontAlign(0,0); g.drawString(time_digit[col],(x1+x2)/2,(y1+y2)/2); } } } + + queueDraw(); } function toggleDate() { show_date = ! show_date; - drawBerlinClock(); + draw(); } function toggleTime() { show_time = ! show_time; - drawBerlinClock(); + draw(); } -// special function to handle display switch on -Bangle.on('lcdPower', (on) => { - g.clear(); +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ if (on) { - Bangle.drawWidgets(); - // call your app function here - drawBerlinClock(); + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; } }); -// refesh every 15 sec -setInterval(drawBerlinClock, 15E3); +// Show launcher when button pressed, handle up/down +Bangle.setUI("clockupdown", dir=> { + if (dir<0) toggleTime(); + if (dir>0) toggleDate(); +}); g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); -drawBerlinClock(); -// Toggle date display, when BTN3 is pressed -setWatch(toggleTime,BTN1, { repeat : true, edge: "falling"}); -// Toggle date display, when BTN3 is pressed -setWatch(toggleDate,BTN3, { repeat : true, edge: "falling"}); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); +draw(); diff --git a/apps/binclock/ChangeLog b/apps/binclock/ChangeLog index 2378e52f8..dc4ed8308 100644 --- a/apps/binclock/ChangeLog +++ b/apps/binclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Fixed bug where screen didn't clear so incorrect time displayed. +0.03: Update to use Bangle.setUI instead of setWatch diff --git a/apps/binclock/app.js b/apps/binclock/app.js index 7808dfe45..f8cbe8dd5 100644 --- a/apps/binclock/app.js +++ b/apps/binclock/app.js @@ -23,17 +23,17 @@ function drawTime(d) { } function updateHourArray(hours){ - + var j; for(j=0;j 15){ hourLED[0] = 1; hours = hours - 16; @@ -53,9 +53,9 @@ function updateHourArray(hours){ if(hours > 0){ hourLED[4] = 1; } - + return hourLED; - + } function updateMinuteArray(minutes){ @@ -63,12 +63,12 @@ function updateMinuteArray(minutes){ for(j=0;j 31){ minuteLED[0] = 1; minutes = minutes - 32; @@ -92,20 +92,20 @@ function updateMinuteArray(minutes){ if(minutes > 0){ minuteLED[5] = 1; } - + return minuteLED; - + } function draw(){ - + // work out how to display the current time var d = new Date(); var h = d.getHours(), m = d.getMinutes(); - + updateHourArray(h); updateMinuteArray(m); - + var i; //Draw hour circles for(i=0; i{ // Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); -setWatch(function() { +// Show launcher when button pressed +Bangle.setUI("clockupdown", btn=>{ + if (btn!=1) return; if(displayTime == 0){ displayTime = 1; } else{ - displayTime = 0; + displayTime = 0; } -}, BTN, {edge:"rising", debounce:50, repeat:true}); +}); diff --git a/apps/binwatch/Background176_center.img b/apps/binwatch/Background176_center.img new file mode 100644 index 000000000..4d4b587de Binary files /dev/null and b/apps/binwatch/Background176_center.img differ diff --git a/apps/binwatch/Background176_center.png b/apps/binwatch/Background176_center.png new file mode 100644 index 000000000..c8c1c0148 Binary files /dev/null and b/apps/binwatch/Background176_center.png differ diff --git a/apps/binwatch/Background240_center.img b/apps/binwatch/Background240_center.img new file mode 100644 index 000000000..abf95107d Binary files /dev/null and b/apps/binwatch/Background240_center.img differ diff --git a/apps/binwatch/Background240_center.png b/apps/binwatch/Background240_center.png new file mode 100644 index 000000000..c2b108f4d Binary files /dev/null and b/apps/binwatch/Background240_center.png differ diff --git a/apps/binwatch/ChangeLog b/apps/binwatch/ChangeLog new file mode 100644 index 000000000..1e54f489c --- /dev/null +++ b/apps/binwatch/ChangeLog @@ -0,0 +1,4 @@ +0.01: start of development +0.02: first running version for BangleJs2 +0.03: corrected icon, added screen shot, extended description +0.04: corrected format of background image (raw binary) diff --git a/apps/binwatch/README.md b/apps/binwatch/README.md new file mode 100644 index 000000000..52e868e21 --- /dev/null +++ b/apps/binwatch/README.md @@ -0,0 +1,47 @@ +# TheBinWatch + +Binary watch to train Your brain +Inspired by the LCD wrist watch from TecRAL from 1989 + +![](screenshot.png) +![](screenshot2.png) + +## Usage + +- swipe to left or right to change displayed text (date, time, ...) +- currently only available for BangeJs2 +- Widgets will not be shown +- If bluetooth connection is not established an icon will show up + +## How it works +Binary means that every digit can represent 2 states: 0 or 1, displayed by a black bar. + +The principle is the same like in out well known and daily used decimal system with values from 0 to 9: + +We start from the most right position with the least significant bit (binary digit) which can have the value 0 or 1 +The 2nd bit from the right can have the value 0 or 2 (sum of all bits to the right set to 1 plus 1). +This principle is valid for all the remaining bits. + +Mathematically spoken: the value of a digit is the base number of the system (10 for decimal or 2 for binary) +to the power of the position (from the right, starting with 0). +That means in numbers: 2^5 = 32, 2^4 = 16, 2^3 = 8, 2^2 = 4, 2^1 = 2, 2^0 = 1 + +The upper row represents the hours with 4 bit (2^4 = 16 possible values in total, 12 are used: 1 to 12), + the 2nd row represents the minutes with 6 bit (2^6 = 64 possible values in total, 60 are used: 0 to 59). +Same holds for the thrid row: 0-59 seconds + +To read the values of a row we summ up the vaules of set bits (black bars). +E.g. the picture above, 3rd row (seconds): +101001 +is 1 * 32 + 0 * 16 + 1 * 8 + 0 * 4 + 0 * 2 + 1 * 1 +is (only the '1' bit): 32 + 8 + 1 = 41 + +for the minutes we do the same: 32 + 1 = 33 +and the hours: 8 + 2 = 10 + +So the time is 10:33:41 (that's all) + +## TRAIN YOUR BRAIN + +Remark: more infos about the original watch including manual can be found here: +https://timeartpiece.com/watches/tech-ral-binary diff --git a/apps/binwatch/app-icon.js b/apps/binwatch/app-icon.js new file mode 100644 index 000000000..10d7e84e8 --- /dev/null +++ b/apps/binwatch/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcCgEBkmSpICKCwQRRhMn/4AK+VACIU4A4PAz+27dt20ECI1IgEDCIOT+wRB2EkCIX+BwMCpE/8f+gmSvwRB2Mkz///v/5IRBpwRHwIRC5PzCIMSCIXwMQNP7dshMkyf/p+G/MgiV+CIPxCJFM8gRByf+CIIvBRIP7sCMCv/h8//C4P+g6ABCIdiCIVP/M///kFIPAj6iLCIYAOCPH4ibUC2zABdgW/8ARFUgILB2/8fwf/kB3BPobUD3/kz4pCTwMDCIrCBCIWTCINv/IREfAVJDoYpCv/JkmAv4RCYQYRM+ARCn4vCHYX+bQOQh4RBfAYRJyUBCI3/F4IFB/4RGdP4RHwDmC7/gmzaC//tbQWBR4UbfAWQgzIDfwVsR4QRCfAIRM/0DCIWSgDaDz4RBsDXDCIIdByVAfAb+CCIf/4AREjYRFgZ9D/D4DpEDfAT+Cj4REhoRJ7ARE/8PfAVJgbmDp/YWZHgv6zIkkSBYWB44sB/4CB/AREkESp4EBx4RBx0/CIPACAf5kECCIQAHPQIAB5MAgVJEYs4AwIjECIMACI0ACIv+pARCn5rDvwFDGoQRDhILDABHyoARBgKeCARQQBCKIA==")) \ No newline at end of file diff --git a/apps/binwatch/app.js b/apps/binwatch/app.js new file mode 100644 index 000000000..28d7a06a5 --- /dev/null +++ b/apps/binwatch/app.js @@ -0,0 +1,379 @@ +/*************************************************** +* BINARY WATCH +* for Bangle 1 / 2 +* inspired by RAL tec binary wrist watch +* +* TODO: +* - vibrate on full hour +* - +****************************************************/ + +/* reuqirements */ +require("Font7x11Numeric7Seg").add(Graphics); +require("Font5x7Numeric7Seg").add(Graphics); + +/* constants and definitions */ + +/* Bangle 2: 176 x 176 */ + +/* month images */ + +var month = [ + /* JAN */ {width : 53, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("AAMf/0D8AFBkM/9EvwMAgcM/3B30YgE4uEOh354EB4eAuFz90Ah0cgeDx9wgFw8Ecjk7wEDw8A8AIBgEcnEHg4IBgFh4EYnEDHYMF/8AwBID/BODgN/4EgAoI0BgODwExGgkDzg0FAII0D88A8PAnAIBAIMOgPBBAPAiBpCgPAQIOAmFwg0P/B5BwcAiE/JYYAHA"))}, + /* FEB */ {width : 51, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("v/8n/+g/+if/hP/wM/8c/4Of8Ez/FwgE4gEHgFOAYUA8Ed4ADBgEcge4AYMAgeAu4DCgFwhwQBEIMOgPcAYMAgPAjN/4G/8EX/kf/EP/kB/+F/8C/+Ar/xGQkBGTE7wADBMIMHMotMgEGv+A7/hEYOf/EH/hvBh6FBIIKYFA"))}, + /* MAR */ {width : 52, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("v/4j/+gf/hP/gV/6FP/HP8kz/cGv8OgHDwFwuEE8McnEHg8A905BgcO8ecBiM4BgMwuEGoeEi/8gX/wE4gH/4Ef/AMFx0QDIcA8BADnEOgIzCufABgk+Bglx+AMEh+OBgdwvnghk4gcGgfsgFDgEQoEeSgvg"))}, + /* APR */ {width : 52, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("v/4j/4gf/hP/oV/4FP/HP9kz/EGv8OgPDwEguEE8EcnEHg8A9wMCuFwhwMTgAMBmFwg1f+EX/kC/+D/8A//AJIIMFxwZCgFwgAmCgEHnBNDgFz4AMEnwMEuPwBgkPxwMDuF88EMBgMGgfsgFDRgNAjyUF8A="))}, + /* MAY */ {width : 52, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("v/4j/+AoMJ/8Cv/QAwPP8kz/cEgEugHDwFwsEG8McnEHg8A905BgVwh3jzgMRnAMBmAMBoeEi/8BgNgnEA//Ah/4BgcB/+OiAZCBgPgIARTB90BGYUAhwMahk4gYMBpkAocAiEP+CSDIAOAAwYMB"))}, + /* JUN */ {width : 53, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("AAcD8ADBkMAhEvwIJBhkA4O+jEAnFwh0O/PAgPDwFwufugEOjkDwePuEAuHgjkcneAgeHgHgBAMAjk4g8HBAMAsPAjE4gY7BggCBwBPLkACBGgMBweAmI0EgecGgoBBGgfngHh4E4BAIBBh0B4IIB4EQmEEBAPA/0An5qBg0P/ED/xNBiAKBh6PCAAw="))}, + /* JUL */ {width : 53, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("AA8hgEImAFBgcMgHB4AGBnFwh0OAoMB4eAuFwAwMOjkDweAAwNw8EcjggCw8A8HgAwMcnEHg40CsPAjE4AwUEAQIgCABMgGgcBGgMBGgo/BGggKBGgYBB8PAnA0BBQMOgJpC4EQmEENIX+gE/wFn/EP/ED/0Cv/gBQMP8EP/5QGA"))}, + /* AUG */ {width : 52, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("n/8AocE/+gAgMP/1n+0QgGA//HgHhwEYl/wuEOjkDw8Ag4MB4E4uEABilhBgcv/EcgOCgEB/+AwBBB/AMBAgMCj/ngFgAwNw/wmCgImBBgIzDhwzFBikGhkBgUAs0AkEf4EH+A3Bgf+gBLBAwIMD"))}, + /* SEP */ {width : 51, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("h/4j//g/+gf/wn/4M/8ABB5/wmf4mEAjkAg8Ap0AgeAgHgjvAgFwBwMD3EAhwOCu8AgIOCh3ggE4BwMB7gjCBwMYv/Ar/wi/8j/8IYMB/+BIYIODDwIyCLIMHGQYGB8JBDB4IyCAoMDw5BDB4JBDgEEMoZ6Cn/A8A6B8FP/kYgEf/EH/4eCA"))}, + /* OCT */ {width : 50, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("h/gg/+j//w/8gf/h//+H+gF/wP//OAkHADAXgjlwAoU4g8cAgMYh0B44pCgeAuIYBgfADAnwnEDDAUcghCDgRMIsACBkAYFGKZKDngYFgJjBwAYCPgX4DAMHPgQYBgB8C8EGgAA="))}, + /* NOV */ {width : 51, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("vkAgf4AoMX4GA/+ABIN8mEP8EggP350MgMGgF+vvDwFw8Ef4+4uEOjgiBu8OgIOBv8A8PAnFwEQMcnEHBwP8gOHgFh4EdHYNAgEQgJLFggFEhPAjFwg0cg4jDGQPnGQk8GQkPI4IyB8PDKwYOB+BWBMoMHnkOgHAn+A98BwEIh/4jnAHgX+gaGBAAcggAA=="))}, + /* DEC */ {width : 49, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("v/gj//gP/5/4iYFC2f4hn/CAOcgMHgEBwEOgPDwEB4AJB8PAgHggeAuHggFwBoM4uEAnANBjgDBjgNBgwDBh0AiEAgowBAAQ6BwEAggFBv/BwAwBsIwWhwwDnEHAYIiBjhhDgEN/0Dn/Aj/hO4M/+Ef/JABv/8g/+A=="))}, + /* MAI */ {width : 44, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("v/4j/+gEJ/8Cv/QgnP8kz/cA50A4eAuEc8McnEHgPOnIKD8ecBR04BQMwhlDwkX/kAoE4gH/4EABQlOiAVD8A2EgIrDBS0MnEDgHMGQMAiEEPwo="))}, + /* OKT */ {width : 51, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("g/wAQMP//B/8DgPh//8j/AuF8n//jECh0fDAUA8PH4AGB8EcnIhBsEcgeHvkAj0DwFw98AgYjBh0dDAN4h0A4eAEgQDBl/4gFAE4MD/5OE3/ggIyBhk4gcAuAyCBIIyDIIIyDAgOAGQMBGQNwh8B4E4BwMB8BlCBIM8gF/AgMYg+Aj/wmA3B+EB/hBChiYGA"))}, + /* DEZ */ {width : 51, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("n/wh//w//xP/gV/8F//Of4Fn/EH/04gUODAUHgHh4AFBnHgjk4BYUcgeHAoMB8eAuHgAwN4uEOjgFBh4jB4eAgED4ADBl/4gFwB4MD/4DBgQCB3/gC4PghgyBgPAGQl4gYyDjwgBGQQrBh0BGQVwDQM4F4MMLIJlEg3/gOfPAPgn/gk/+j/+h/8IoPh//gA="))} +]; + +var imgSquid = {width : 88, height : 26, bpp : 1, buffer : require("heatshrink").decompress(atob("gE/AYUYgEH////0B//gBQM8BQgDB/AKHh/A/gKBvwKBAgMOj8AnwKHBAIMBgH/BQgmCAoPnBQl4AoOAgPnwAKDuEAgYKB4YKIgfD4AKDMAMB4EDwIKIg+B8AKIgAKIh8A+AKHh0AuAKHj0AvBMG4EcgE4K458Bnh4HnEAjiOHBwMeBQpKBEgMOXQ/wBwIKDaAZQBg4KDcwT0BAAOHfgoKHgE/wDaBAAL8DA="))}; + +var imgNoBT = {width : 20, height : 20, bpp : 3, transparent : 0, buffer : require("heatshrink").decompress(atob("///8mSpM/AoP/yUT/8yuYGB5AMB/1MyYUBkmT/P85MP+USBwOT8mQ/8JBwXyoVnyGSv8//Mhk14pMn//8BYNMwmSp/+pFJkgyBDoMkkgODpOSuQOE5M/KgIOCsmfz/JknPhMyof5n+Ss/wzMhn4OBk1+smQLoWTn/mHAM/+VJz4KBwhZBEYJ/CkM8yZVBAAQxBCgP/A="))}; + +const V2_X_STEP = 26; +const V2_Y_STEP = 34; + +const V2_TIME_Y_OFFSET = 8; +const V2_HX = 36; +const V2_HY = 0 + V2_TIME_Y_OFFSET; +const V2_MX = 10; +const V2_MY = 51 + V2_TIME_Y_OFFSET; +const V2_SX = 10; +const V2_SY = 95 + V2_TIME_Y_OFFSET; +const V2_BT_X = 137; /* 145, 35 */ +const V2_BT_Y = 20; +const V2_DX = 100; +const V2_DY = 141; + +const V2_BAT_POS_X = 21; +const V2_BAT_POS_Y = 40; +const V2_BAT_SIZE_X = 13; +const V2_BAT_SIZE_Y = 2; + +const V2_SCREEN_SIZE_X = 176; +const V2_SCREEN_SIZE_Y = 176; +const V2_BACKGROUND_IMAGE = "binwatch.bg176.img"; +const V2_BG_COLOR = 0; +const V2_FG_COLOR = 1; + +/* Bangle 1: 240 x 240 */ + +const V1_X_STEP = 35; +const V1_Y_STEP = 46; + +const V1_TIME_Y_OFFSET = 41; +const V1_HX = 48; +const V1_HY = 0 + V1_TIME_Y_OFFSET; +const V1_MX = 14; +const V1_MY = 55 + V1_TIME_Y_OFFSET; +const V1_SX = 14; +const V1_SY = 110 + V1_TIME_Y_OFFSET; +const V1_BT_X = 41; +const V1_BT_Y = 14; +//var BT_X = 20, BT_Y = 14; +const V1_DX = 160; +const V1_DY = 205; + +const V1_BAT_POS_X = 175; +const V1_BAT_POS_Y = 21; +const V1_BAT_SIZE_X = 3; +const V1_BAT_SIZE_Y = 5; +const V1_SCREEN_SIZE_X = 240; +const V1_SCREEN_SIZE_Y = 240; +const V1_BACKGROUND_IMAGE = "binwatch.bg240.img"; +const V1_BG_COLOR = 1; +const V1_FG_COLOR = 0; + +/* runtime settings */ + +var x_step = 0; +var y_step = 0; + +var time_y_offset = 0; +var hx = 0, hy = 0; +var mx = 0, my = 0; +var sx = 0, sy = 0; +var bt_x = 0, bt_y = 0; +var dx = 0, dy = 0; + +var bat_pos_x, bat_pos_y, bat_size_x, bat_size_y; +var backgroundImage = ""; +var screen_size_x = 0; +var screen_size_y = 0; +var bg_color = 0; +var fg_color = 1; + +/* global variables */ + +var showDateTime = 2; /* show noting, time or date */ +var cg; +var cgimg; + +/* local functions */ + +/** + * function drawSquare(...) + * + * go through all bits and draw a square if a bit + * is set. So we get the binary representation + * of the value + * used to draw block for hours, mintutes, seconds, date + * + * @param gfx: graphic object to use + * @param x: x-coordinate of 1st the square + * @param y: y-coordinate of 1st the square + * @param data: data conatining the bit information + * @param numOfBits: number of bits to draw +*/ +function drawSquare(gfx, x, y, data, numOfBits) { + + for(i = numOfBits; i > 0 ; i--) { + if( (data & 1) != 0) { + gfx.fillRect(x + (i - 1) * x_step, y, + x + i * x_step , y + y_step); + } + data >>= 1; /* shift one bit right */ + } +} + +/** + * function drawBinary(...) + * draw the time in binary format + * default display for geeks and real men + + * @param h: hours + * @param m: minutes + * @param s: seconds +*/ +function drawBinary(gfx, hour, minute, second) { + gfx.clear(0); + + if(hour > 12) { + hour -= 12; /* we use for bit for hours so we only display 12 hours*/ + } + drawSquare(gfx, hx, hy, hour, 4); /* set hour */ + drawSquare(gfx, mx, my, minute, 6); /* set minute */ + drawSquare(gfx, sx, sy, second, 6); /* set second */ +} + +/** + * function drawTime(...) + * show time under the graphic + * for wimps and commies + * + * @param h: hours + * @param m: minutes + * @param s: seconds +*/ + +function drawTime(gfx, h, m, s) { + var time = (" "+h).substr(-2) + ":" + ("0"+m).substr(-2)+ ":" + ("0"+s).substr(-2); + + gfx.setFontAlign(0,-1); // align right bottom + gfx.setFont("7x11Numeric7Seg", 2); + gfx.drawString(time, gfx.getWidth() / 2, dy + 1, false /*clear background*/); +} + +/** + * function drawDate(...) + * show date under the graphic + * (optionally) + * + * @param gfx: graphic object to use + * @param d: date object +*/ +var vMonth = 0; +function drawDate(gfx, d) { + var dateString = "" + + ("0" + d.getDate()).substr(-2) +// + " " +// + ("0" + d.getMonth()).substr(-2) +// + " " +// + ("0" + d.getFullYear()).substr(-2) + ; + + gfx.setFontAlign(-1,-1); // align right bottom + gfx.setFont("7x11Numeric7Seg",2); /* draw the current time font */ + gfx.drawString(dateString, dx, dy + 1, false /* don't clear background*/); + gfx.drawImage(month[d.getMonth()], 40, dy); +} + +function toggleDateTime() { + showDateTime++; + if(showDateTime > 2){ + showDateTime = 0; + } +} + +function updateVTime() { + vMonth++; + if(vMonth >= 12 + 3) { + vMonth = 0; + } + second++; + if(second > 59) { + second = 0; + minute++; + if(minute > 59) { + minute = 0; + hour++; + if(hour > 12) { + hour = 0; + } + } + } +} + +/** + * function drawBattery(...) + * fill the battery symbol with blocks + * according to the battery level + * + * @param gfx: graphic object + * @param level: current battery level +*/ +function drawBattery(gfx, level) { + var pos_y = bat_pos_y - 1; + var stepLevel = Math.round((level + 10) / 20); + + for(i = 0; i < stepLevel; i++) { + pos_y -= bat_size_y + 2; + gfx.fillRect(bat_pos_x, pos_y, + bat_pos_x + bat_size_x, pos_y + bat_size_y); + } +} + +/** + * function drawBattery(...) + * fill the battery symbol with blocks + * according to the battery level + * + * @param gfx: graphic object + * @param level: current battery level +*/ +function drawBT(gfx, status) { + if(!status) { + gfx.drawImage(imgNoBT, bt_x, bt_y); + } +} +function setRuntimeValues(resolution) { + if(240 == resolution) { + x_step = V1_X_STEP; + y_step = V1_Y_STEP; + + time_y_offset = V1_TIME_Y_OFFSET; + hx = V1_HX; + hy = V1_HY; + mx = V1_MX; + my = V1_MY; + sx = V1_SX; + sy = V1_SY; + bt_x = V1_BT_X; + bt_y = V1_BT_Y; + dx = V1_DX; + dy = V1_DY; + + screen_size_x = V1_SCREEN_SIZE_X; + screen_size_y = V1_SCREEN_SIZE_Y; + backgroundImage = V1_BACKGROUND_IMAGE; + + bat_pos_x = V1_BAT_POS_X; + bat_pos_y = V1_BAT_POS_Y; + bat_size_x = V1_BAT_SIZE_X; + bat_size_y = V1_BAT_SIZE_Y; + + setWatch(toggleDateTime, BTN1, { repeat : true, edge: "falling"}); + + } else { + x_step = V2_X_STEP; + y_step = V2_Y_STEP; + + time_y_offset = V2_TIME_Y_OFFSET; + + hx = V2_HX; + hy = V2_HY; + mx = V2_MX; + my = V2_MY; + sx = V2_SX; + sy = V2_SY; + + bt_x = V2_BT_X; + bt_y = V2_BT_Y; + + dx = V2_DX; + dy = V2_DY; + + screen_size_x = V2_SCREEN_SIZE_X; + screen_size_y = V2_SCREEN_SIZE_Y; + backgroundImage = V2_BACKGROUND_IMAGE; + + bat_pos_x = V2_BAT_POS_X; + bat_pos_y = V2_BAT_POS_Y; + bat_size_x = V2_BAT_SIZE_X; + bat_size_y = V2_BAT_SIZE_Y; + + Bangle.on('swipe', function(direction) { toggleDateTime(direction);}); +} + cg = Graphics.createArrayBuffer( + screen_size_x,screen_size_y, 1, {msb:true}); + + cgimg = {width:screen_size_x, height:screen_size_y, bpp:1, + transparent:0, buffer:cg.buffer}; + +} +var hour = 0, minute = 1, second = 50; +var batVLevel = 20; + + +function draw() { + var d = new Date(); + var h = d.getHours(), m = d.getMinutes(), s = d.getSeconds(); + g.reset(); + + drawBinary(cg, h, m, s); + + switch(showDateTime) { + case 1: + drawTime(cg, h, m, s); + break; + case 2: + drawDate(cg, d); + break; + default: + cg.drawImage(imgSquid, cg.getWidth() / 2 - 44, dy); + } + drawBattery(cg, /*batVLevel*/ E.getBattery()); + + batVLevel += 2; + if(batVLevel > 100) { + batVLevel = 0; + } + updateVTime(); + g.clear(); + g.drawImages([{image:cgimg}, + {image:require("Storage").read(backgroundImage)} + ]); + drawBT(g, NRF.getSecurityStatus().connected); +// Bangle.drawWidgets(); + const millis = d.getMilliseconds(); + setTimeout(draw, 1000-millis); + // Bangle.loadWidgets(); +} + +// Show launcher when button pressed +Bangle.setUI("clock"); +setRuntimeValues(g.getWidth()); +g.reset().clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +draw(); diff --git a/apps/binwatch/app.png b/apps/binwatch/app.png new file mode 100644 index 000000000..e1a0c88ff Binary files /dev/null and b/apps/binwatch/app.png differ diff --git a/apps/binwatch/bt-icon.png b/apps/binwatch/bt-icon.png new file mode 100644 index 000000000..cba9b5973 Binary files /dev/null and b/apps/binwatch/bt-icon.png differ diff --git a/apps/binwatch/screenshot.png b/apps/binwatch/screenshot.png new file mode 100644 index 000000000..ebab4670e Binary files /dev/null and b/apps/binwatch/screenshot.png differ diff --git a/apps/binwatch/screenshot2.png b/apps/binwatch/screenshot2.png new file mode 100644 index 000000000..fa171d253 Binary files /dev/null and b/apps/binwatch/screenshot2.png differ diff --git a/apps/blackjack/ChangeLog b/apps/blackjack/ChangeLog index c941d90e5..25b5f9195 100644 --- a/apps/blackjack/ChangeLog +++ b/apps/blackjack/ChangeLog @@ -1 +1,2 @@ -0.01: New game! BTN4- Hit card, BTN5- Stand \ No newline at end of file +0.01: New game! BTN4- Hit card, BTN5- Stand +0.02: ignore buttons on pauses \ No newline at end of file diff --git a/apps/blackjack/bangle1-black-jack-game-screenshot.png b/apps/blackjack/bangle1-black-jack-game-screenshot.png new file mode 100644 index 000000000..532b784f4 Binary files /dev/null and b/apps/blackjack/bangle1-black-jack-game-screenshot.png differ diff --git a/apps/blackjack/blackjack.app.js b/apps/blackjack/blackjack.app.js index bbee8137b..b88432fd9 100644 --- a/apps/blackjack/blackjack.app.js +++ b/apps/blackjack/blackjack.app.js @@ -18,6 +18,7 @@ const Diamonds = { width : 48, height : 48, bpp : 4, var deck = []; var player = {Hand:[]}; var computer = {Hand:[]}; +var ctx = {ready:true}; function createDeck() { var suits = ["Spades", "Hearts", "Diamonds", "Clubs"]; @@ -44,6 +45,7 @@ function shuffle(a) { } function EndGameMessdage(msg){ + ctx.ready = false; g.drawString(msg, 155, 200); setTimeout(function(){ startGame(); @@ -52,6 +54,7 @@ function EndGameMessdage(msg){ } function hitMe() { + if (!ctx.ready) return; player.Hand.push(deck.pop()); renderOnScreen(1); var playerWeight = calcWeight(player.Hand, 0); @@ -97,6 +100,8 @@ function calcWeight(hand, hideCard) { } function stand(){ + if (!ctx.ready) return; + ctx.ready = false; function sleepFor( sleepDuration ){ console.log("Sleeping..."); var now = new Date().getTime(); @@ -156,6 +161,7 @@ function renderOnScreen(HideCard) { function dealHands() { player.Hand= []; computer.Hand= []; + ctx.ready = false; setTimeout(function(){ player.Hand.push(deck.pop()); @@ -175,6 +181,7 @@ function dealHands() { setTimeout(function(){ computer.Hand.push(deck.pop()); renderOnScreen(1); + ctx.ready = true; }, 2000); } diff --git a/apps/blobclk/ChangeLog b/apps/blobclk/ChangeLog index 10983d7e1..9c4ef5b7b 100644 --- a/apps/blobclk/ChangeLog +++ b/apps/blobclk/ChangeLog @@ -4,3 +4,4 @@ 0.03: Modified for use with new bootloader and firmware 0.04: Modified to account for changes in the behavior of Graphics.fillPoly 0.05: Slight increase to draw speed after LCD on +0.06: Update to use Bangle.setUI instead of setWatch, allow themes and different size screens diff --git a/apps/blobclk/bangle1-large-digit-blob-clock-screenshot.png b/apps/blobclk/bangle1-large-digit-blob-clock-screenshot.png new file mode 100644 index 000000000..fcad01e50 Binary files /dev/null and b/apps/blobclk/bangle1-large-digit-blob-clock-screenshot.png differ diff --git a/apps/blobclk/bangle2-large-digit-blob-clock-screenshot.png b/apps/blobclk/bangle2-large-digit-blob-clock-screenshot.png new file mode 100644 index 000000000..5cf48bda7 Binary files /dev/null and b/apps/blobclk/bangle2-large-digit-blob-clock-screenshot.png differ diff --git a/apps/blobclk/clock-blob.js b/apps/blobclk/clock-blob.js index 9b68bd4bd..c84b8a1e6 100644 --- a/apps/blobclk/clock-blob.js +++ b/apps/blobclk/clock-blob.js @@ -1,4 +1,6 @@ -const buf = Graphics.createArrayBuffer(144,200,1,{msb:true}); +let big = g.getHeight() > 200; +const buf = Graphics.createArrayBuffer(big ? 144 : 120, big ? 180 : 150,1,{msb:true}); +// TODO: convert these to Polys -> much faster and cleaner! 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 @@ -14,8 +16,10 @@ const NUMBERS = [ 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); + g.reset(); + g.drawImage({width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer}, + (g.getWidth() - buf.getWidth())/2, + 26 + (g.getHeight() - (buf.getHeight()+24))/2); } function drawPixel(ox,oy,x,y,r,p) { let x1 = ox+x*(r*2); @@ -53,26 +57,31 @@ function redraw() { let newDigits = [Math.floor(hours/10),hours%10,Math.floor(mins/10),mins%10,Math.floor(secs/10),secs%10]; + let s = big?6:5; // size of main digits + let y2 = big?72:55; + let y3 = big?144:110; + + 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]); + drawPixel(0,0,px,py,s,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]); + drawPixel(13*s,0,px,py,s,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]); + drawPixel(0,y2,px,py,s,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]); + drawPixel(13*s,y2,px,py,s,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]); + drawPixel(17*s - 3*12,y3,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]); + drawPixel(17*s,y3,px,py,3,NUMBERS[newDigits[5]][p]); } } digits = newDigits; @@ -99,5 +108,5 @@ Bangle.on('lcdPower',function(on) { clearTimers(); } }); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); +// Show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/boldclk/ChangeLog b/apps/boldclk/ChangeLog index 0d02bf644..c7a4ba7b4 100644 --- a/apps/boldclk/ChangeLog +++ b/apps/boldclk/ChangeLog @@ -1,2 +1,4 @@ 0.02: Modified for use with new bootloader and firmware 0.03: Tweak for more efficient rendering, and firmware 2v06 +0.04: Work with themes, smaller screens +0.05: Adjust hand lengths to be within 'tick' points diff --git a/apps/boldclk/README.md b/apps/boldclk/README.md new file mode 100644 index 000000000..0e7865b99 --- /dev/null +++ b/apps/boldclk/README.md @@ -0,0 +1,4 @@ +# Bold Clock + +![](screenshot_bold.png) + diff --git a/apps/boldclk/bold_clock.js b/apps/boldclk/bold_clock.js index b7eaa8968..4358b2e29 100644 --- a/apps/boldclk/bold_clock.js +++ b/apps/boldclk/bold_clock.js @@ -12,9 +12,9 @@ var minute_hand = { //g.fillRect(0,24,239,239); // Apps area let intervalRef = null; const p180 = Math.PI/180; -const clock_center = {x:Math.floor((240-1)/2), y:24+Math.floor((239-24)/2)}; +const clock_center = {x:Math.floor((g.getWidth()-1)/2), y:24+Math.floor((g.getHeight()-25)/2)}; // ={ x: 119, y: 131 } -const radius = Math.floor((239-24+1)/2); // =108 +const radius = Math.floor((g.getWidth()-24+1)/2); // =108 let tick0 = Graphics.createArrayBuffer(30,8,1,{msb:true}); tick0.fillRect(0,0,tick0.getWidth()-1, tick0.getHeight()-1); @@ -23,6 +23,10 @@ tick5.fillRect(0,0,tick5.getWidth()-1, tick5.getHeight()-1); let tick1 = Graphics.createArrayBuffer(8,4,1,{msb:true}); tick1.fillRect(0,0,tick1.getWidth()-1, tick1.getHeight()-1); +// Adjust hand lengths to be within 'tick' points +minute_hand.width=radius-tick1.getWidth()-6; +hour_hand.width=radius-tick5.getWidth()-6; + function big_wheel_x(angle){ return clock_center.x + radius * Math.cos(angle*p180); } @@ -60,18 +64,15 @@ function hour_angle(date){ function draw_clock(){ //console.log("draw_clock"); let date = new Date(); - //g.clear(); - g.setBgColor(0,0,0); - g.setColor(0,0,0); - g.fillRect(0,24,239,239); // clear app area - g.setColor(1,1,1); + g.reset(); + g.clearRect(0,24,239,239); // clear app area // draw cross lines for testing // g.setColor(1,0,0); // g.drawLine(clock_center.x - radius, clock_center.y, clock_center.x + radius, clock_center.y); // g.drawLine(clock_center.x, clock_center.y - radius, clock_center.x, clock_center.y + radius); - g.setColor(1,1,1); + g.setColor(g.theme.fg); let ticks = [0, 90, 180, 270]; ticks.forEach((item)=>{ let agl = item+180; @@ -87,13 +88,13 @@ function draw_clock(){ let minute_agl = minute_angle(date); g.drawImage(hour_hand, hour_pos_x(hour_agl), hour_pos_y(hour_agl), {rotate:hour_agl*p180}); // g.drawImage(minute_hand, minute_pos_x(minute_agl), minute_pos_y(minute_agl), {rotate:minute_agl*p180}); // - g.setColor(1,1,1); + g.setColor(g.theme.fg); g.fillCircle(clock_center.x, clock_center.y, 6); - g.setColor(0,0,0); + g.setColor(g.theme.bg); g.fillCircle(clock_center.x, clock_center.y, 3); // draw minute ticks. Takes long time to draw! - g.setColor(1,1,1); + g.setColor(g.theme.fg); for (var i=0; i<60; i++){ let agl = i*6+180; g.drawImage(tick1.asImage(), rotate_around_x(big_wheel_x(i*6), agl, tick1), rotate_around_y(big_wheel_y(i*6), agl, tick1), {rotate:agl*p180}); @@ -141,5 +142,5 @@ g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); startTimers(); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); +// Show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/boldclk/screenshot_bold.png b/apps/boldclk/screenshot_bold.png new file mode 100644 index 000000000..4024fca40 Binary files /dev/null and b/apps/boldclk/screenshot_bold.png differ diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 7e9fd4a81..5c929421b 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -21,3 +21,25 @@ 0.20: Allow Gadgetbridge to work even with programmable:off 0.21: Handle echo off char from Gadgetbridge app when programmable:off (fix #558) 0.22: Stop LCD timeout being disabled on first run (when there is no settings.json) +0.23: Move to a precalculated .boot0 file which should speed up load time +0.24: Add Bangle.setUI polyfill +0.25: Fix error in 'no clock app' message +0.26: Remove buzz in setUI polyfill (#750) +0.27: Update polyfill for most recent changes +0.28: Fix double clock load after settings are changed +0.29: Update boot0 to avoid code block (faster execution) + Fix issues where 'Uncaught Error: Function not found' could happen with multiple .boot.js +0.30: Remove 'Get GPS time' at boot. Latest firmwares keep time through reboots, so this is not needed now +0.31: Add polyfills for g.wrapString, g.imageMetrics, g.stringMetrics +0.32: Fix single quote error in g.wrapString polyfill + improve g.stringMetrics polyfill + Fix issue where re-running bootupdate could disable existing polyfills +0.33: Add E.showScroller polyfill +0.34: Use Storage.hash if available + Rearrange NRF.setServices to allow .boot.js files to add services (eg ANCS) +0.35: Add Bangle.appRect polyfill + Don't set beep vibration up on Bangle.js 2 (built in) +0.36: Add comments to .boot0 to make debugging a bit easier +0.37: Remove Quiet Mode settings: now handled by Quiet Mode Schedule app +0.38: Option to log to file if settings.log==2 +0.39: Fix passkey support (fix https://github.com/espruino/Espruino/issues/2035) diff --git a/apps/boot/boot0.js b/apps/boot/boot0.js index 550513b11..3e567d9b8 100644 --- a/apps/boot/boot0.js +++ b/apps/boot/boot0.js @@ -1,68 +1,2 @@ -// This ALWAYS runs at boot -E.setFlags({pretokenise:1}); -// Load settings... -var s = require('Storage').readJSON('setting.json',1)||{}; -if (s.ble!==false) { - if (s.HID) { // Human interface device - if (s.HID=="joy") Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA=")); - else if (s.HID=="kb") Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA==")); - else /*kbmedia*/Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA==")); - NRF.setServices({}, {uart:true, hid:Bangle.HID}); - } -} -if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth - if (s.log) Terminal.setConsole(true); // if showing debug, force REPL onto terminal - else E.setConsole(null,{force:true}); // on new (2v05+) firmware we have E.setConsole which allows a 'null' console - /* If not programmable add our own handler for Bluetooth data - to allow Gadgetbridge commands to be received*/ - Bluetooth.line=""; - Bluetooth.on('data',function(d) { - var l = (Bluetooth.line + d).split("\n"); - Bluetooth.line = l.pop(); - l.forEach(n=>Bluetooth.emit("line",n)); - }); - Bluetooth.on('line',function(l) { - if (l.startsWith('\x10')) l=l.slice(1); - if (l.startsWith('GB({') && l.endsWith('})') && global.GB) - try { global.GB(JSON.parse(l.slice(3,-1))); } catch(e) {} - }); -} else { - if (s.log && !NRF.getSecurityStatus().connected) Terminal.setConsole(); // if showing debug, put REPL on terminal (until connection) - else Bluetooth.setConsole(true); // else if no debug, force REPL to Bluetooth -} -// we just reset, so BLE should be on. -// Don't disconnect if something is already connected to us -if (s.ble===false && !NRF.getSecurityStatus().connected) NRF.sleep(); -// Set time, vibrate, beep, etc -if (!Bangle.F_BEEPSET) { - if (!s.vibrate) Bangle.buzz=Promise.resolve; - if (s.beep===false) Bangle.beep=Promise.resolve; - else if (s.beep=="vib") Bangle.beep = function (time, freq) { - return new Promise(function(resolve) { - if ((0|freq)<=0) freq=4000; - if ((0|time)<=0) time=200; - if (time>5000) time=5000; - analogWrite(D13,0.1,{freq:freq}); - setTimeout(function() { - digitalWrite(D13,0); - resolve(); - }, time); - }); - }; -} -if (s.timeout!==undefined) Bangle.setLCDTimeout(s.timeout); -if (!s.timeout) Bangle.setLCDPower(1); -E.setTimeZone(s.timezone); -delete s; -// Draw out of memory errors onto the screen -E.on('errorFlag', function(errorFlags) { - g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip(); - print("Interpreter error:", errorFlags); - E.getErrorFlags(); // clear flags so we get called next time -}); -// stop users doing bad things! -global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); } -// Load *.boot.js files -require('Storage').list(/\.boot\.js/).forEach(bootFile=>{ - eval(require('Storage').read(bootFile)); -}); +// Initially this runs and rewrites itself +eval(require('Storage').read('bootupdate.js')); diff --git a/apps/boot/bootloader.js b/apps/boot/bootloader.js index df3718dcc..3cf885ac9 100644 --- a/apps/boot/bootloader.js +++ b/apps/boot/bootloader.js @@ -14,30 +14,6 @@ if (!clockApp) { if (clockApp) clockApp = require("Storage").read(clockApp.src); } -if (!clockApp) clockApp=`E.showMessage("No Clock Found"); -setWatch(() => { - Bangle.showLauncher(); -}, BTN2, {repeat:false,edge:"falling"});) -`; -// check to see if our clock is wrong - if it is use GPS time -if ((new Date()).getFullYear()<2000) { - E.showMessage("Searching for\nGPS time"); - Bangle.on("GPS",function cb(g) { - Bangle.setGPSPower(0); - Bangle.removeListener("GPS",cb); - if (!g.time || (g.time.getFullYear()<2000) || - (g.time.getFullYear()>2200)) { - // GPS receiver's time not set - just boot clock anyway - eval(clockApp); - delete clockApp; - return; - } - // We have a GPS time. Set time and reboot (to load alarms properly) - setTime(g.time.getTime()/1000); - load(); - }); - Bangle.setGPSPower(1); -} else { - eval(clockApp); - delete clockApp; -} +if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, BTN2, {repeat:false,edge:"falling"});`; +eval(clockApp); +delete clockApp; diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js new file mode 100644 index 000000000..e338d9020 --- /dev/null +++ b/apps/boot/bootupdate.js @@ -0,0 +1,211 @@ +/* This rewrites boot0.js based on current settings. If settings changed then it +recalculates, but this avoids us doing a whole bunch of reconfiguration most +of the time. */ +E.showMessage("Updating boot0..."); +var s = require('Storage').readJSON('setting.json',1)||{}; +var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 +var boot = ""; +if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed + var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/); + boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)!=${CRC})`; +} else { + var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/)); + boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))!=${CRC})`; +} +boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`; +boot += `E.setFlags({pretokenise:1});\n`; +boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`; +if (s.ble!==false) { + if (s.HID) { // Human interface device + if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`; + else if (s.HID=="kb") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));` + else /*kbmedia*/boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));`; + boot += `bleServiceOptions.hid=Bangle.HID;\n`; + } +} +if (s.log==2) { // logging to file + boot += `_DBGLOG=require("Storage").open("log.txt","a"); +`; +} if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth + if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a"); +LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);}); +LoopbackA.setConsole(true);\n`; + else if (s.log) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal + else boot += `E.setConsole(null,{force:true});\n`; // on new (2v05+) firmware we have E.setConsole which allows a 'null' console + /* If not programmable add our own handler for Bluetooth data + to allow Gadgetbridge commands to be received*/ + boot += ` +Bluetooth.line=""; +Bluetooth.on('data',function(d) { + var l = (Bluetooth.line + d).split("\n"); + Bluetooth.line = l.pop(); + l.forEach(n=>Bluetooth.emit("line",n)); +}); +Bluetooth.on('line',function(l) { + if (l.startsWith('\x10')) l=l.slice(1); + if (l.startsWith('GB({') && l.endsWith('})') && global.GB) + try { global.GB(JSON.parse(l.slice(3,-1))); } catch(e) {} +});\n`; +} else { + if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a"); +LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);}); +if (!NRF.getSecurityStatus().connected) LoopbackA.setConsole();\n`; + else if (s.log) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection) + else boot += `Bluetooth.setConsole(true);\n`; // else if no debug, force REPL to Bluetooth +} +// we just reset, so BLE should be on. +// Don't disconnect if something is already connected to us +if (s.ble===false) boot += `if (!NRF.getSecurityStatus().connected) NRF.sleep();\n`; +// Set time +if (s.timeout!==undefined) boot += `Bangle.setLCDTimeout(${s.timeout});\n`; +if (!s.timeout) boot += `Bangle.setLCDPower(1);\n`; +boot += `E.setTimeZone(${s.timezone});`; +// Set vibrate, beep, etc IF on older firmwares +if (!Bangle.F_BEEPSET) { + if (!s.vibrate) boot += `Bangle.buzz=Promise.resolve;\n` + if (s.beep===false) boot += `Bangle.beep=Promise.resolve;\n` + else if (s.beep=="vib" && !BANGLEJS2) boot += `Bangle.beep = function (time, freq) { + return new Promise(function(resolve) { + if ((0|freq)<=0) freq=4000; + if ((0|time)<=0) time=200; + if (time>5000) time=5000; + analogWrite(D13,0.1,{freq:freq}); + setTimeout(function() { + digitalWrite(D13,0); + resolve(); + }, time); + }); + };\n`; +} +// Draw out of memory errors onto the screen +boot += `E.on('errorFlag', function(errorFlags) { + g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip(); + print("Interpreter error:", errorFlags); + E.getErrorFlags(); // clear flags so we get called next time +});\n`; +// stop users doing bad things! +if (global.save) boot += `global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }\n`; +// Apply any settings-specific stuff +if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`; +if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; +if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`; +if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; +// Pre-2v10 firmwares without a theme/setUI +delete g.theme; // deleting stops us getting confused by our own decl. builtins can't be deleted +if (!g.theme) { + boot += `g.theme={fg:-1,bg:0,fg2:-1,bg2:7,fgH:-1,bgH:0x02F7,dark:true};\n`; +} +delete Bangle.setUI; // deleting stops us getting confused by our own decl. builtins can't be deleted +if (!Bangle.setUI) { // assume this is just for F18 - Q3 should already have it + boot += `Bangle.setUI=function(mode, cb) { +if (Bangle.btnWatches) { + Bangle.btnWatches.forEach(clearWatch); + delete Bangle.btnWatches; +} +if (Bangle.swipeHandler) { + Bangle.removeListener("swipe", Bangle.swipeHandler); + delete Bangle.swipeHandler; +} +if (Bangle.touchHandler) { + Bangle.removeListener("touch", Bangle.touchHandler); + delete Bangle.touchHandler; +} +if (!mode) return; +else if (mode=="updown") { + Bangle.btnWatches = [ + setWatch(function() { cb(-1); }, BTN1, {repeat:1}), + setWatch(function() { cb(1); }, BTN3, {repeat:1}), + setWatch(function() { cb(); }, BTN2, {repeat:1}) + ]; +} else if (mode=="leftright") { + Bangle.btnWatches = [ + setWatch(function() { cb(-1); }, BTN1, {repeat:1}), + setWatch(function() { cb(1); }, BTN3, {repeat:1}), + setWatch(function() { cb(); }, BTN2, {repeat:1}) + ]; + Bangle.swipeHandler = d => {cb(d);}; + Bangle.on("swipe", Bangle.swipeHandler); + Bangle.touchHandler = d => {cb();}; + Bangle.on("touch", Bangle.touchHandler); +} else if (mode=="clock") { + Bangle.CLOCK=1; + Bangle.btnWatches = [ + setWatch(Bangle.showLauncher, BTN2, {repeat:1,edge:"falling"}) + ]; +} else if (mode=="clockupdown") { + Bangle.CLOCK=1; + Bangle.btnWatches = [ + setWatch(function() { cb(-1); }, BTN1, {repeat:1}), + setWatch(function() { cb(1); }, BTN3, {repeat:1}), + setWatch(Bangle.showLauncher, BTN2, {repeat:1,edge:"falling"}) + ]; +} else + throw new Error("Unknown UI mode"); +};\n`; +} +delete E.showScroller; // deleting stops us getting confused by our own decl. builtins can't be deleted +if (!E.showScroller) { // added in 2v11 - this is a limited functionality polyfill + boot += `E.showScroller = (function(a){function n(){g.reset();b>=l+c&&(c=1+b-l);bm||m>=a.c)break;var f=24+d*a.h;a.draw(m,{x:0,y:f,w:h,h:a.h});d+c==b&&g.setColor(g.theme.fg).drawRect(0,f,h-1,f+a.h-1).drawRect(1,f+1,h-2,f+a.h-2)}g.setColor(c?g.theme.fg:g.theme.bg);g.fillPoly([e,6,e-14,20,e+14,20]);g.setColor(a.c>l+c?g.theme.fg:g.theme.bg);g.fillPoly([e,k-7,e-14,k-21,e+14,k-21])}if(!a)return Bangle.setUI();var b=0,c=0,h=g.getWidth(), +k=g.getHeight(),e=h/2,l=Math.floor((k-48)/a.h);g.reset().clearRect(0,24,h-1,k-1);n();Bangle.setUI("updown",d=>{d?(b+=d,0>b&&(b=a.c-1),b>=a.c&&(b=0),n()):a.select(b)})});\n`; +} +delete g.imageMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted +if (!g.imageMetrics) { // added in 2v11 - this is a limited functionality polyfill + boot += `Graphics.prototype.imageMetrics=function(src) { + if (src[0]) return {width:src[0],height:src[1]}; + else if ('object'==typeof src) return { + width:("width" in src) ? src.width : src.getWidth(), + height:("height" in src) ? src.height : src.getHeight()}; + var im = E.toString(src); + return {width:im.charCodeAt(0), height:im.charCodeAt(1)}; +};\n`; +} +delete g.stringMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted +if (!g.stringMetrics) { // added in 2v11 - this is a limited functionality polyfill + boot += `Graphics.prototype.stringMetrics=function(txt) { + txt = txt.toString().split("\\n"); + return {width:Math.max.apply(null,txt.map(x=>g.stringWidth(x))), height:this.getFontHeight()*txt.length}; +};\n`; +} +delete g.wrapString; // deleting stops us getting confused by our own decl. builtins can't be deleted +if (!g.wrapString) { // added in 2v11 - this is a limited functionality polyfill + boot += `Graphics.prototype.wrapString=function(str, maxWidth) { + var lines = []; + for (var unwrappedLine of str.split("\\n")) { + var words = unwrappedLine.split(" "); + var line = words.shift(); + for (var word of words) { + if (g.stringWidth(line + " " + word) > maxWidth) { + lines.push(line); + line = word; + } else { + line += " " + word; + } + } + lines.push(line); + } + return lines; +};\n`; +} +delete Bangle.appRect; // deleting stops us getting confused by our own decl. builtins can't be deleted +if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares + boot += `Bangle.appRect = ((y,w,h)=>({x:0,y:0,w:w,h:h,x2:w-1,y2:h-1}))(g.getWidth(),g.getHeight()); + (lw=>{ Bangle.loadWidgets = () => { lw(); Bangle.appRect = ((y,w,h)=>({x:0,y:y,w:w,h:h-y,x2:w-1,y2:h-(1+h)}))(global.WIDGETS?24:0,g.getWidth(),g.getHeight()); }; })(Bangle.loadWidgets);\n`; +} + +// Append *.boot.js files +// These could change bleServices/bleServiceOptions if needed +require('Storage').list(/\.boot\.js/).forEach(bootFile=>{ + // we add a semicolon so if the file is wrapped in (function(){ ... }() + // with no semicolon we don't end up with (function(){ ... }()(function(){ ... }() + // which would cause an error! + boot += "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n"; +}); +// update ble +boot += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`; +// write file +require('Storage').write('.boot0',boot); +delete boot; +E.showMessage("Reloading..."); +eval(require('Storage').read('.boot0')); +// .bootcde should be run automatically after if required, since +// we normally get called automatically from '.boot0' diff --git a/apps/bthrm/ChangeLog b/apps/bthrm/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/bthrm/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/bthrm/README.md b/apps/bthrm/README.md new file mode 100644 index 000000000..f0c7775c2 --- /dev/null +++ b/apps/bthrm/README.md @@ -0,0 +1,45 @@ +# Bluetooth Heart Rate Monitor + +When this app is installed it overrides Bangle.js's build in heart rate monitor with an external Bluetooth one. + +HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM'` event as if it came from the on board monitor. + +This means it's compatible with many Bangle.js apps including: + +* [Heart Rate Widget](https://banglejs.com/apps/#widhrt) +* [Heart Rate Recorder](https://banglejs.com/apps/#heart) + +It it NOT COMPATIBLE with [Heart Rate Monitor](https://banglejs.com/apps/#hrm) +as that requires live sensor data (rather than just BPM readings). + +## Usage + +Just install the app, then install an app that uses the heart rate monitor. + +Once installed it'll automatically try and connect to the first bluetooth +heart rate monitor it finds. + +**To disable this and return to normal HRM, uninstall the app** + +## Compatible Heart Rate Monitors + +This works with any heart rate monitor providing the standard Bluetooth +Heart Rate Service (`180D`) and characteristic (`2A37`). + +So far it has been tested on: + +* CooSpo Bluetooth Heart Rate Monitor + +## Internals + +This replaces `Bangle.setHRMPower` with its own implementation. + +## TODO + +* Maybe a `bthrm.settings.js` and app (that calls it) to enable it to be turned on and off +* A widget to show connection state? +* Specify a specific device by address? + +## Creator + +Gordon Williams diff --git a/apps/bthrm/app-icon.js b/apps/bthrm/app-icon.js new file mode 100644 index 000000000..04a5ee610 --- /dev/null +++ b/apps/bthrm/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///g3yy06AoIZNitUAg8AgtVqtQAgoRCAwITBAggABAoIABAgsAgIGDoIEDoApDAAwwBFIV1BYo1E+oLTAgQLGJon9BZNXBatdBYRVFBYN/r9fHoxTBBYYlEL4QLFq/a1WUgE///fr4xBv/+1Wq1EAh/3/tX6/fv/6BYOqwCzBBYf9tWq9QLF79X+oLBDIOgKgILEEIIxBGAMVNAP/BYf/BYUFBYJSB6wLC9QLBeAQLBqwLCGAL9BBYmr9X+GAILBbIIlBBYP6/wwBBYMFBYZGB/4XDGAILD34vEcwYLB15HBBYYkBBYWrFwILDKoRTCVIQLCEgQXIEgVaF44YCoRHHAAMUgQuBNgILFgECO4W/BZCPFBYinGBY6/CAArXFBY7vDAAsq1QuB0ALIOwOABY0KEgJGGGAguHDAYDBA==")) diff --git a/apps/bthrm/app.png b/apps/bthrm/app.png new file mode 100644 index 000000000..40c2ab024 Binary files /dev/null and b/apps/bthrm/app.png differ diff --git a/apps/bthrm/boot.js b/apps/bthrm/boot.js new file mode 100644 index 000000000..88e574480 --- /dev/null +++ b/apps/bthrm/boot.js @@ -0,0 +1,79 @@ +(function() { + var log = function() {};//print + var gatt; + var status; + + Bangle.isHRMOn = function() { + return (status=="searching" || status=="connecting") || (gatt!==undefined); + } + Bangle.setHRMPower = function(isOn, app) { + // Do app power handling + if (!app) app="?"; + log("setHRMPower ->", isOn, app); + if (Bangle._PWR===undefined) Bangle._PWR={}; + if (Bangle._PWR.HRM===undefined) Bangle._PWR.HRM=[]; + if (isOn && !Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM.push(app); + if (!isOn && Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM = Bangle._PWR.HRM.filter(a=>a!=app); + isOn = Bangle._PWR.HRM.length; + // so now we know if we're really on + if (isOn) { + log("setHRMPower on", app); + if (!Bangle.isHRMOn()) { + log("HRM not already on"); + status = "searching"; + NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) { + log("Found device "+device.id); + status = "connecting"; + device.on('gattserverdisconnected', function(reason) { + gatt = undefined; + }); + return device.gatt.connect(); + }).then(function(g) { + log("Connected"); + gatt = g; + return gatt.getPrimaryService(0x180D); + }).then(function(service) { + return service.getCharacteristic(0x2A37); + }).then(function(characteristic) { + log("Got characteristic"); + characteristic.on('characteristicvaluechanged', function(event) { + var dv = event.target.value; + var flags = dv.getUint8(0); + // 0 = 8 or 16 bit + // 1,2 = sensor contact + // 3 = energy expended shown + // 4 = RR interval + var bpm = (flags&1) ? (dv.getUint16(1)/100/* ? */) : dv.getUint8(1); // 8 or 16 bit + /* var idx = 2 + (flags&1); // index of next field + if (flags&8) idx += 2; // energy expended + if (flags&16) { + var interval = dv.getUint16(idx,1); // in milliseconds + }*/ + Bangle.emit('HRM',{ + bpm:bpm, + confidence:100 + }); + }); + return characteristic.startNotifications(); + }).then(function() { + log("Ready"); + status = "ok"; + }).catch(function(err) { + log("Error",err); + gatt = undefined; + status = "error"; + }); + } + } else { // not on + log("setHRMPower off", app); + if (gatt) { + log("HRM connected - disconnecting"); + status = undefined; + try {gatt.disconnect();}catch(e) { + log("HRM disconnect error", e); + } + gatt = undefined; + } + } + }; +})(); diff --git a/apps/buffgym/buffgym-scrn1.png b/apps/buffgym/buffgym-scrn1.png old mode 100755 new mode 100644 diff --git a/apps/buffgym/buffgym-scrn2.png b/apps/buffgym/buffgym-scrn2.png old mode 100755 new mode 100644 diff --git a/apps/buffgym/buffgym-scrn3.png b/apps/buffgym/buffgym-scrn3.png old mode 100755 new mode 100644 diff --git a/apps/buffgym/buffgym-scrn4.png b/apps/buffgym/buffgym-scrn4.png old mode 100755 new mode 100644 diff --git a/apps/buffgym/buffgym-scrn5.png b/apps/buffgym/buffgym-scrn5.png old mode 100755 new mode 100644 diff --git a/apps/buffgym/buffgym-scrn6.png b/apps/buffgym/buffgym-scrn6.png old mode 100755 new mode 100644 diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js old mode 100755 new mode 100644 diff --git a/apps/buffgym/buffgym.png b/apps/buffgym/buffgym.png old mode 100755 new mode 100644 diff --git a/apps/calculator/ChangeLog b/apps/calculator/ChangeLog index 3b9b23270..6a3308c50 100644 --- a/apps/calculator/ChangeLog +++ b/apps/calculator/ChangeLog @@ -1,2 +1,4 @@ 0.01: New App! 0.02: fix precision rounding issue + no reset when equals pressed +0.03: Support for different screen sizes and touchscreen +0.04: Display current operation on LHS diff --git a/apps/calculator/app.js b/apps/calculator/app.js index a736b715d..9f801cf22 100644 --- a/apps/calculator/app.js +++ b/apps/calculator/app.js @@ -6,13 +6,11 @@ */ g.clear(); -Graphics.prototype.setFont7x11Numeric7Seg = function() { - this.setFontCustom(atob("ACAB70AYAwBgC94AAAAAAAAAAB7wAAPQhhDCGELwAAAAhDCGEMIXvAAeACAEAIAQPeAA8CEMIYQwhA8AB70IYQwhhCB4AAAIAQAgBAB7wAHvQhhDCGEL3gAPAhDCGEMIXvAAe9CCEEIIQPeAA94EIIQQghA8AB70AYAwBgCAAAAHgQghBCCF7wAHvQhhDCGEIAAAPehBCCEEIAAAAA=="), 46, atob("AgAHBwcHBwcHBwcHAAAAAAAAAAcHBwcHBw=="), 11); -}; +require("Font7x11Numeric7Seg").add(Graphics); var DEFAULT_SELECTION = '5'; -var BOTTOM_MARGIN = 10; var RIGHT_MARGIN = 20; +var RESULT_HEIGHT = 40; var COLORS = { // [normal, selected] DEFAULT: ['#7F8183', '#A6A6A7'], @@ -123,7 +121,7 @@ function drawKey(name, k, selected) { var bMargin = 0; var color = k.color || COLORS.DEFAULT; g.setColor(color[selected ? 1 : 0]); - g.setFont('Vector', 20); + g.setFont('Vector', 20).setFontAlign(0,0); g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); g.setColor(-1); // correct margins to center the texts @@ -141,7 +139,7 @@ function drawKey(name, k, selected) { } else if (name === '%') { rMargin = -3; } - g.drawString(k.val || name, k.xy[0] + RIGHT_MARGIN + rMargin, k.xy[1] + BOTTOM_MARGIN + bMargin); + g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2); } function getIntWithPrecision(x) { @@ -201,8 +199,7 @@ function doMath(x, y, operator) { function displayOutput(num) { var len; var minusMarge = 0; - g.setColor(0); - g.fillRect(0, 0, 240, 39); + g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1); g.setColor(-1); if (num === Infinity || num === -Infinity || isNaN(num)) { // handle division by 0 @@ -240,17 +237,16 @@ function displayOutput(num) { num = num.substr(1); } } - - len = (num + '').length; - if (numNumeric < 0 || (numNumeric === 0 && 1/numNumeric === -Infinity)) { - // minus is not available in font 7x11Numeric7Seg, we use Vector - g.setFont('Vector', 20); - g.drawString('-', 220 - (len * 15), 10); - minusMarge = 15; - } + num = num.toString(); + num = num.replace("-","- "); // fix padding for '-' g.setFont('7x11Numeric7Seg', 2); } - g.drawString(num, 220 - (len * 15) + minusMarge, 10); + g.setFontAlign(1,0); + g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2); + if (operator) { + g.setFont('Vector', 22).setFontAlign(1,0); + g.drawString(operator, g.getWidth()-1, RESULT_HEIGHT/2); + } } var wasPressedEquals = false; var hasPressedNumber = false; @@ -370,14 +366,6 @@ function buttonPress(val) { } } -for (var k in keys) { - if (keys.hasOwnProperty(k)) { - drawKey(k, keys[k], k == '5'); - } -} -g.setFont('7x11Numeric7Seg', 2.8); -g.drawString('0', 205, 10); - function moveDirection(d) { drawKey(selected, keys[selected]); prevSelected = selected; @@ -385,8 +373,37 @@ function moveDirection(d) { drawKey(selected, keys[selected], true); } -setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100}); -setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100}); -setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100}); -setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100}); -setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100}); +if (global.BTN4) { + setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100}); + setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100}); + setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100}); + setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100}); + setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100}); +} else { // touchscreen? + selected = "NONE"; + Bangle.on('touch',(n,e)=>{ + for (var key in keys) { + var r = keys[key].xy; + if (e.x>=r[0] && e.y>=r[1] && + e.x n*g.getWidth()/240); + } +} +// draw keys +for (var k in keys) { + if (keys.hasOwnProperty(k)) { + drawKey(k, keys[k], k == selected); + } +} +displayOutput(0); diff --git a/apps/calculator/screenshot_calculator.png b/apps/calculator/screenshot_calculator.png new file mode 100644 index 000000000..7a259fe2c Binary files /dev/null and b/apps/calculator/screenshot_calculator.png differ diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index 3cf79ffe8..e164d6be9 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -1 +1,4 @@ 0.01: Basic calendar +0.02: Make Bangle 2 compatible +0.03: Add setting to start week on Sunday +0.04: Add setting to switch color schemes. On Bangle 2 non-dithering colors will be used by default. Use localized names for months and days of the week (Language app needed). diff --git a/apps/calendar/README.md b/apps/calendar/README.md index 19a60afc0..4fc6962cf 100644 --- a/apps/calendar/README.md +++ b/apps/calendar/README.md @@ -6,3 +6,9 @@ Basic calendar - Use `BTN4` (left screen tap) to go to the previous month - Use `BTN5` (right screen tap) to go to the next month + +## Settings + +- Starts Sunday: whether the calendar should start on Sunday (default is Monday). +- B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme. + diff --git a/apps/calendar/calendar-icon.js b/apps/calendar/calendar-icon.js index ed1bf3667..ed6690033 100644 --- a/apps/calendar/calendar-icon.js +++ b/apps/calendar/calendar-icon.js @@ -1,5 +1 @@ -require("heatshrink").decompress( - atob( - "mEwxH+AH4A/ADuIUCARRDhgePCKIv13YAEDoYJFAA4RJFyQvcGBYRGy4dDy4uLCJgv/DoOBDgOBF5oRLF6IeBDgIvNCJYvQDwQuNCJovRADov/F9OsAEgv/F/4vhwIACAqYv/F/4vnd94vvX/4v/F/7vvF96//F/4v/d94v/F/4wsFxQwjFxgA/AH4A/AH4AZA==" - ) -) +require("heatshrink").decompress(atob("mEwwcCpMkyQC3wAIFgIRJn8JAoeQ/gRYwB0Bn57F/gCBHAgfCn8EDgdI/kSAoIR8oBkFgAFCCIysKCPM//4AKZAgR3/0Aj+Ag/ggP4gF/CPpr/Nf5r/NfYRhw4RL8IRDyEAABUJCIYC/AVI=")) \ No newline at end of file diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index 720986162..01977570a 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -1,5 +1,6 @@ -const maxX = 240; -const maxY = 240; +const maxX = g.getWidth(); +const maxY = g.getHeight(); +const fontSize = g.getWidth() > 200 ? 2 : 1; const rowN = 7; const colN = 7; const headerH = maxY / 7; @@ -9,22 +10,109 @@ const color1 = "#035AA6"; const color2 = "#4192D9"; const color3 = "#026873"; const color4 = "#038C8C"; -const color5 = "#03A696"; +const gray1 = "#bbbbbb"; const black = "#000000"; const white = "#ffffff"; -const gray1 = "#444444"; -const gray2 = "#888888"; -const gray3 = "#bbbbbb"; const red = "#d41706"; +const blue = "#0000ff"; +const yellow = "#ffff00"; + +let settings = require('Storage').readJSON("calendar.json", true) || {}; +if (settings.startOnSun === undefined) + settings.startOnSun = false; +if (settings.ndColors === undefined) + if (process.env.HWVERSION == 2) { + settings.ndColors = true; + } else { + settings.ndColors = false; + } + +if (settings.ndColors === true) { + let bgColor = white; + let bgColorMonth = blue; + let bgColorDow = black; + let bgColorWeekend = yellow; + let fgOtherMonth = blue; + let fgSameMonth = black; +} else { + let bgColor = color4; + let bgColorMonth = color1; + let bgColorDow = color2; + let bgColorWeekend = color3; + let fgOtherMonth = gray1; + let fgSameMonth = white; +} + +function getDowLbls(locale) { + let dowLbls; + //TODO: Find some clever way to generate this programmatically from locale lib + switch (locale) { + case "de_AT": + case "de_CH": + case "de_DE": + if (settings.startOnSun) { + dowLbls = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]; + } else { + dowLbls = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; + } + break; + case "nl_NL": + if (settings.startOnSun) { + dowLbls = ["zo", "ma", "di", "wo", "do", "vr", "za"]; + } else { + dowLbls = ["ma", "di", "wo", "do", "vr", "za", "zo"]; + } + break; + case "fr_BE": + case "fr_CH": + case "fr_FR": + if (settings.startOnSun) { + dowLbls = ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"]; + } else { + dowLbls = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"]; + } + break; + case "sv_SE": + if (settings.startOnSun) { + dowLbls = ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"]; + } else { + dowLbls = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"]; + } + break; + case "it_CH": + case "it_IT": + if (settings.startOnSun) { + dowLbls = ["Do", "Lu", "Ma", "Me", "Gi", "Ve", "Sa"]; + } else { + dowLbls = ["Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"]; + } + break; + case "oc_FR": + if (settings.startOnSun) { + dowLbls = ["dg", "dl", "dm", "dc", "dj", "dv", "ds"]; + } else { + dowLbls = ["dl", "dm", "dc", "dj", "dv", "ds", "dg"]; + } + break; + default: + if (settings.startOnSun) { + dowLbls = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; + } else { + dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]; + } + break; + } + return dowLbls; +} function drawCalendar(date) { - g.setBgColor(color4); + g.setBgColor(bgColor); g.clearRect(0, 0, maxX, maxY); - g.setBgColor(color1); + g.setBgColor(bgColorMonth); g.clearRect(0, 0, maxX, headerH); - g.setBgColor(color2); + g.setBgColor(bgColorDow); g.clearRect(0, headerH, maxX, headerH + rowH); - g.setBgColor(color3); + g.setBgColor(bgColorWeekend); g.clearRect(colW * 5, headerH + rowH, maxX, maxY); for (let y = headerH; y < maxY; y += rowH) { g.drawLine(0, y, maxX, y); @@ -35,38 +123,25 @@ function drawCalendar(date) { const month = date.getMonth(); const year = date.getFullYear(); - const monthMap = { - 0: "January", - 1: "February", - 2: "March", - 3: "April", - 4: "May", - 5: "June", - 6: "July", - 7: "August", - 8: "September", - 9: "October", - 10: "November", - 11: "December" - }; + const localeMonth = require('locale').month(date); g.setFontAlign(0, 0); - g.setFont("6x8", 2); + g.setFont("6x8", fontSize); g.setColor(white); - g.drawString(`${monthMap[month]} ${year}`, maxX / 2, headerH / 2); + g.drawString(`${localeMonth} ${year}`, maxX / 2, headerH / 2); g.drawPoly([10, headerH / 2, 20, 10, 20, headerH - 10], true); g.drawPoly( [maxX - 10, headerH / 2, maxX - 20, 10, maxX - 20, headerH - 10], true ); - g.setFont("6x8", 2); - const dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]; + g.setFont("6x8", fontSize); + let dowLbls = getDowLbls(require('locale').name); dowLbls.forEach((lbl, i) => { g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2); }); date.setDate(1); - const dow = date.getDay(); + const dow = date.getDay() + (settings.startOnSun ? 1 : 0); const dowNorm = dow === 0 ? 7 : dow; const monthMaxDayMap = { @@ -110,14 +185,19 @@ function drawCalendar(date) { today.year === year && today.month === month && today.day === day - 50; if (isToday) { g.setColor(red); + let x1 = x * colW; + let y1 = y * rowH + headerH + rowH; + let x2 = x * colW + colW; + let y2 = y * rowH + headerH + rowH + rowH; + g.drawRect(x1, y1, x2, y2); g.drawRect( - x * colW, - y * rowH + headerH + rowH, - x * colW + colW - 1, - y * rowH + headerH + rowH + rowH + x1 + 1, + y1 + 1, + x2 - 1, + y2 - 1 ); } - g.setColor(day < 50 ? gray3 : white); + g.setColor(day < 50 ? fgOtherMonth : fgSameMonth); g.drawString( (day > 50 ? day - 50 : day).toString(), x * colW + colW / 2, @@ -135,26 +215,21 @@ const today = { }; drawCalendar(date); clearWatch(); -setWatch( - () => { - const month = date.getMonth(); - const prevMonth = month > 0 ? month - 1 : 11; +Bangle.on("touch", area => { + const month = date.getMonth(); + let prevMonth; + if (area == 1) { + let prevMonth = month > 0 ? month - 1 : 11; if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1); date.setMonth(prevMonth); - drawCalendar(date); - }, - BTN4, - { repeat: true } -); -setWatch( - () => { - const month = date.getMonth(); - const prevMonth = month < 11 ? month + 1 : 0; + } else { + let prevMonth = month < 11 ? month + 1 : 0; if (prevMonth === 0) date.setFullYear(date.getFullYear() + 1); date.setMonth(month + 1); - drawCalendar(date); - }, - BTN5, - { repeat: true } -); -setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); + } + drawCalendar(date); +}); + +// Show launcher when button pressed +Bangle.setUI("clock"); // TODO: ideally don't set 'clock' mode +// No space for widgets! diff --git a/apps/calendar/calendar.png b/apps/calendar/calendar.png index 056cab3b7..ccbcce5ff 100644 Binary files a/apps/calendar/calendar.png and b/apps/calendar/calendar.png differ diff --git a/apps/calendar/screenshot_calendar.png b/apps/calendar/screenshot_calendar.png new file mode 100644 index 000000000..8285932c4 Binary files /dev/null and b/apps/calendar/screenshot_calendar.png differ diff --git a/apps/calendar/settings.js b/apps/calendar/settings.js new file mode 100644 index 000000000..3c8f7d8e8 --- /dev/null +++ b/apps/calendar/settings.js @@ -0,0 +1,38 @@ +(function (back) { + var FILE = "calendar.json"; + var settings = require('Storage').readJSON(FILE, true) || {}; + if (settings.startOnSun === undefined) + settings.startOnSun = false; + if (settings.ndColors === undefined) + if (process.env.HWVERSION == 2) { + settings.ndColors = true; + } else { + settings.ndColors = false; + } + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + "": { "title": "Calendar" }, + "< Back": () => back(), + 'Start Sunday': { + value: settings.startOnSun, + format: v => v ? "Yes" : "No", + onchange: v => { + settings.startOnSun = v; + writeSettings(); + } + }, + 'B2 Colors': { + value: settings.ndColors, + format: v => v ? "Yes" : "No", + onchange: v => { + settings.ndColors = v; + writeSettings(); + } + }, + }); +}) + diff --git a/apps/carcrazy/ChangeLog b/apps/carcrazy/ChangeLog new file mode 100644 index 000000000..f697617b4 --- /dev/null +++ b/apps/carcrazy/ChangeLog @@ -0,0 +1,3 @@ +0.01: Car Crazy is now avialable for testing in beta! +0.02: 10 Levels are now added making the game harder as it goes along. Some of the levels include multiple cars and faster cars. More levels coming soon. +0.03: Settings are now added so that you can reset your high score. diff --git a/apps/carcrazy/README.md b/apps/carcrazy/README.md new file mode 100644 index 000000000..e1eadea5e --- /dev/null +++ b/apps/carcrazy/README.md @@ -0,0 +1,60 @@ +# Car Crazy +Car crazy is a fun game where you tilt your wrist left and right to avoid incoming cars. If you get hit by a car you lose a heart. In the game you have three hearts, if you get hit 3 times you are sent to the game over screen. Recently levels have been added making the game get harder as you play. Your goal is to try to last as long as you can. Because this game is still in production please report any bugs here: https://forms.office.com/r/HnwYzG9Sk7. + +### Images: +(Coming Soon) + +### Instructions: + +BNT2: Hold down this button to start the game if you are on the starting page and game over page. + +Tilting Left-Right: Tilt your wrist left and right to steer your car and try not to get hit by the enemy car. + +### Feautures Coming Soon: +0.02: Levels are creating making the game get harder as it goes along. (Completed) + +0.03: Setting for reseting high score. (Completed) + +0.04: Optional soundtrack in settings. More levels. + +0.05: With higher scores you can now unlock different colors of cars. More settings. + +0.06: Car selector at game over screen and start screen. + +0.07 More levels. + +0.08 More types of Cars. + +### Levels +Here is a list of all the levels: + +##### Level 1: +Basic single car moving at 10 speed. + +##### Level 2: +A single car can come from any position at the top of the screen. + +##### Level 3: +A single car are coming at 12 speed and can come from any position at the top of the screen. + +##### Level 4: +2 cars come at 8 speed. + +##### Level 5: +2 cars come at 9 speed. + +##### Level 6: +2 cars come at 9.5 and 8 speed. + +##### Level 7: +2 cars come at 10 and 8 speed. + +##### Level 8: +2 cars come at 11.5 speed. + +##### Level 9 and Above: +2 cars come at 13 and 14 speed. + +### Other + +-Settings are now avialable for resetting your high score. diff --git a/apps/carcrazy/app-icon.js b/apps/carcrazy/app-icon.js new file mode 100644 index 000000000..b3f122ea8 --- /dev/null +++ b/apps/carcrazy/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4Ay90A8AWT8HuhwXSCQPuAAIXShwWCC6YuDAAQwWGKQWWFwoJDxAAFF5gHChAXODAhHCxGDmYAE3YAC2AwKxAWFC4gABC4YBCIwYXGoNb2tUpYxDRYsIIw0zqoXBqtVDAIXVDAJiEUYYXNqhhDUYgWGC41VSYgXSjYXHAA4XGJAULDQQXQJALFDGBTXFAAuwgQXV3cP+coC6m////nAXYiIAEC5m4FIYXN2gKBpYXFwIXMkIKBiQXSFwQwCC4hIFC42xiMjmURlYXFDAgXImcziMbC4ynKC4uxC5MAAAsBI4cQAoMRC50AKYYdCDYIQHGBASCC6QwCDgwA/AH4AVA==")) diff --git a/apps/carcrazy/app.js b/apps/carcrazy/app.js new file mode 100644 index 000000000..0fb765871 --- /dev/null +++ b/apps/carcrazy/app.js @@ -0,0 +1,386 @@ +Bangle.setLCDPower(1); +Bangle.setLCDTimeout(0); +var numberofHearts = 3; + +Bangle.setLCDMode("doublebuffered"); + + +//var popUp = require("heatshrink").decompress(atob("isFwMBCJoA==")); + +var backgroundImage = require("heatshrink").decompress(atob("isZxH+woAB6YBBBodXAgYLCCIQUJAgoUOFP4p/FP4p/FP4p/FP4pYA==")); + +var heartImage = require("heatshrink").decompress(atob("hUKxH+ABckAAYIDpFstlIudzpARCtmQyGXz2luYVBkgIBBQXCy4JCCYQKCtgpCE4IJCFQIyFBAwKDBAw/CAogA==")); + +var gameOverImage = require("heatshrink").decompress(atob("kcV4MB+Nj/4AJwATBgfwDAV+AYUP4ADBgP4BAU/AYUHERMP+AiBgP+EQU/4AiBg/4EQX/EQUf8EH8ED/kAAIN/wEfEQ9/FwX+A==")); + +var RedCar = +require("heatshrink").decompress(atob("ol7xH+ABWBqwAqwI5LABKAtIf4AClZASq0rp9zudIDYNIABgSBABd5AAtPEoNPAoUrqxDPlcAkmXy9OIYVsABYSBABWdzwAFIYVzAoMkRSJDCNAJDBkhDiuckFQOevMqklPZBo7DZAUkHagAKIoyKCkl5IwIwBwJDLYYRDrWoMAAgJDCqxDZyxDXzoABIcpAXRRRD/If5D/If5D/If5D9uYASId9OBoIAPpxDvBoRD2AAlOADlPAAckIbJ0QABjeNIf4ACkhDUIgNIUQQEBAAtyIaOXvIAFp4lBp4FBIapFCA4wACyxCRzo2BAAlzEoNzA4ZDVQ4ZDGQyRDHQ4RDgHyQAHIf5DhklIHoRDrudzA4RDNIoRDtFYRD/Iac5rsykgABaANyILQACzudIgTGClcrrowBIZ9ktdkRQhCdRQ8rGAxD/If5D/If4ACzpD/Io5D/IbOI2eIIdowBwJDP1nX1hDtGAJD/Ialkx7eCZdgwDIZ1rIdwwEIf5D/If5DZmU5mRDtGAhDNAAhDrAAhD/IaM5rrLmzpCCIYYwEIZtktdkIcpBCIYgwEIf5DZAARD/If8ylcrpFIIcd5uYpBmRDUAAUyA4JECIb9zEoNdFoZDVnJDkp5DZx+z2YHCIbudAAJDFFYOPIanX65DgIAIACIYWBFYJD/IYSHBx5DQ1idCx5DFAAjITIIjLFFYIvBwJDQLINrtc5IZOWIbq1DIaJXBslkmUHg9IAA1yRCV5AAtPg8rnIrBQ6VkbwcyDQMrAoIAEpxCRpwaGmSwBmUAg4wEIZ1rIYYdBg5DZpBDLGAt6II+BwJ8CIY85mQAFp9OACFPDQ1dIZMrHYJCFLghDHAA4lCAB85DpgwEAARD9rpDHq0rp9zAAcrmTQCIZVkroAQGoNr2YABIY4wCHAlPldWBgMky4ADkhTDIZQAV2fXIZA4JlYABQ4pDCg8HIcePAAJDCFYJDCYAoABZoOBdw0HoFAVoQAdIASKFslkIgSMEIAIACIZBBgIonXZwpDHgBDE/1Wq0rIe8rHYJDFIoRD3IJBD6vRD1AASH/AA5DWoFkAARD9IgIADoA+hrooEGQhDQAAk5IcM5FpJDNkgAGnNAaQjTTC4gACmUklQAEkhDPpFsAAoYCR68yDQ1PzwAFvJD/IbNyuVOAA1WnIAQp4aGuedzpDauWXABFIOgwAKpwdJIghDgpxD/AAVzACRDvADpD/If5D/If5D/If5D/If5D/If5D/If5DGthD/AApD/If8kkhFGuVyIMJCDp4ABIZ5FCRIyKgQgmekgzDIf5DRp1zpzOIZ4I+dZAUqldzvLLQueXuaKiIQ2elYrBAgN5IaSHBRAIADIcOdp8klSSBOgRDNHIQiBpzjDRjDIEvMkEYdzBAIPBIbUAIboiEIaWBq1WYYVyy9yZwTQGAAVOp9OABlzAAdPAAlzzuduYeBGoJCJAAcrYYZFBAAReCADUkRgeezomBaYMrIJpD/Z496YoRFEUoIAaYoQgBEIMkp+BwJCQAARkCRQKJDAD2WEoKEBgBBTZw1zIcNyIYbIRAA1WRQQABEQIAbEAQlBY6hDIAARCcpAiEIbWBlYADpAAcEQhCZAAyrCADI+hAAhCbIc+BqwAawIwS")); + +var OrangeCar = require("heatshrink").decompress(atob("ol7xH+ABXGAFfHHJYAJQFpD/Ia1OgEkpwDBAAUqABYSBABd4AAskEgQFBAgOcQiQXBlQdCGhdyGowAFznGAAnHNQVO4/GRSQSCGIJDPIRhDH4xDD0SHCkhAMCAQ7BIAQbBHaYAKIoyEDKII1C45DJLIRDw0XHIYXGIa5BYZwWiIZKHDIeZFCIf5D/If5D/If5D/If4ADBQgAMIeEkBoQAOlRDvISJDnAAkklQAWDAQbGIcB0JABreNIf5DYIgJFEW4xCRJZYeBznGIagABIQYHDAAQ7SGwQAD44mCp3HBARD64xDDA4ZDbYzRDdPghDs45DQgEkHoRDrE4MkIaCKDIdYyEIf5DRzjeDaAZBaAAOcAAI9B0RmBlQqB0QwDIZvP5/ORQhCcRRAqCGAhD1Q4RD/If5D/ABOiIf6KHIf5DZ63X55DtGAhDN6/X6xDtGAPQIf5DT5/WZdwwB5xDQ54SCIdgwDIf5D/If5DZvHGAgRDrGAhDNAApDqAAhD/IaOc494Icui0RCBIYeiGAZDN5/P5xDkQgRDFGAhD/IbIABuRD/AAZD658qkkAlUqIcdyuUkkkqIaoABvAHBIgRDfFoXGFoZD+45DW6HX6+cIb+czmiIYnOFYJDU6wXB4xDfIAIACIYvQIf5DVCgPX63WZYoAEZCZBEIYnHFYLNCIaSKBvBDoFYKJCIafOIYUklQAFZKecAAsqEoOi5xDCQ6BYBCQVO5zjCAApESkgaGpwqBIwQEBFYRDPCQRD54/HDARDHzlOAAdyuTTHABd4CwIACp3HIZMAHYJCEPQpDHAAjYCTQQAPzgWCAAxDIAAJDXIgRDi5pDHpy0BXokAdoZDK56lBAB4UC64ABIY4wCHAkkkl4BgV4AAZTEIZYAUIZY4LI4IACuQUFITxDD6HQA4TpFIY7NB47vHQj4AE6yKG5w1HIAIACBpBDk6DOHIZn+lTdFIekqpxDFAALeFIeXGII5D/IeHP63X6xD/ABBD/IbIAE45Dh44tJIfHGIb9445FCaCzrGvBDXlVOAAodIISI7HknGAAtyIeQzCIZhTCIad4vEqkgAFp2cACAZGDQZDaIQIAIeZIAIlQdJ0RDkkhD/AAbbEABgcKIcwAcIf5D/If5D/If5D/If5D/If5D/If5D/If5DKI1BDVIQxD8AAMkIc+cAAJBCp0qGYRDPRVBBDAAIyEIf5DRlSbDZwtOuQ+eAAIrCNQIECIZofBRgaJfIQyED43HvBDPPYJDKRC6GD0WcAAUkFQILBIaB9EToRIJRqDIEzgiFBAR0DIZsqPgZDp46FBIZ3OGAMkkkqCogACBQIAFKwIATEQl4aIIJBD4RCJAAZ/EN4olEADCMDZAQlDIJpD/Z47pCZwQACYSgAIzg/CvEqeAPH5xCQRRoAdEoKETIdlyIbQABcgZIDADYiE45BXAAPGIcMqEQnPIbPOEAkqADgiEITIAGEogAYH0BD/ABHH4wAa5wwSA")); + +var PurpleCar = require("heatshrink").decompress(atob("ol74UBitg///BIP/7lVqtUDJUVBwIABq2qABOVCCkolQJC0AwDgWolAQD1EqwBCH0EqCCdKxQuEAAkKwGhCH4Q/CGD7ECDINEAAoQIwBACgQQYwQ+EAAcC1EACAeAlQfDAAheBCG2oCBUCCB8qCCiQBdp3+fxW/CH4Q/CH4QxgQQKwAQD1QQK1QQUmQQKxiH/CBGqCB3/5WACA+j/4Qf14Q/KhWqRI0CCAv+x4QBEYcC1Wo/SpFCBtUlQQBBgISBAAUAgYQB1fVr2qCAP//8qB4WoAwIQB1WVCCEoFwIQCAA36GwMo1AQOlQQM+QQCqWq1YPIAAPqKgYQOqwQOiqpBCBi6DCH4QDYoIOH/gKBCAs/CD2AZoISEBwUAgAQYAYIABgWqfIIQGAAISBAAQHCCDIAICH4Q/CH4Q/CAWoB5UKCB8qCCmK0AxJwQQDgQQKwAQE1Q0ICAuAlQQHlQrBCCdy1EK1QABCAmq0EKgoQBqofBIoIAFWYMqB4QQRuQ5BCAxlBxoQDqyHKyoQUZpLJDCBmq1oQFvRTGAAOlCApFBB4xBFCCdUIY3VBgY=")); + +var LightGreenCar = require("heatshrink").decompress(atob("ol74UBocF///BIP1z9VqtUDJUVBwIABq2qABOVCCkolQJC0AwDgWolAQD1EqwBCH0EqCCdKxQuEAAkKwGhCH4Q/CGD7ECDINEAAoQIwBACgQQYwQ+EAAcC1EACAeAlQfDAAheBCG2oCBUCCB8qCCiQBdp3+fxW/CH4Q/CH4QxgQQKwAQD1QQK1QQUmQQKxiH/CBGqCB3/5WACA+j/4Qf14QxKjGqRI0CCAv+x4QBEYcC1Wo/SpFCBtUlQQBBgISBAAQGBCAOo6te1QQB///lQPC1AGBCAOqyoQQlAuBCAXPAQIAD/Q2BlGoCAgAGCAUqCBnyCAVS1WrB5AAB9RUDCB1WCB0VVIIQMXQYQ/CAbFBBw/8BQIQFn4QewDNBCQgOCgEACDADBAAMC1T5BCAwABCQIACA4QQZABAQ/CH4Q/CH4QC1APKhQQPlQQUxWgGJOCCAcCCBWACAmqGhAQFwEqCA8qFYIQTuWohWqAAIQE1WghUFCANVD4JFBAAqzBlQPCCCNyHIIQGMoONCAdWQ5WVCCjNJZIYQM1WtCAt6KYwAB0oQFIoIPGIIoQTqhDG6oMDA")); + +function consoleDebug(message) { + //console.log(message); +} + +function getRandomInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min) + min); //The maximum is exclusive and the minimum is inclusive +} + +function moveEnemyPosition(){ + score += 1; + checkForNextLevel(); + if(level == 1){ + randomRoadPositionIndicator = getRandomInt(1, 4); + if ((randomRoadPositionIndicator == 1)) { + enemyPositonCenterX = 85; + }else if((randomRoadPositionIndicator == 2)){ + enemyPositonCenterX = 120; + }else { + enemyPositonCenterX = 155; + } + }else if(level == 2||level==3){ + enemyPositonCenterX = getRandomInt(85, 155); + }else if(level == 4 || level == 5 || level == 6 || level == 8 || level == 9 || level == 10 || level > 10){ + do{ + randomRoadPositionIndicator = getRandomInt(1, 4); + randomRoadPositionIndicator2 = getRandomInt(1, 4); + }while(randomRoadPositionIndicator==randomRoadPositionIndicator2); + + if ((randomRoadPositionIndicator == 1)) { + enemyPositonCenterX = 85; + }else if((randomRoadPositionIndicator == 2)){ + enemyPositonCenterX = 120; + }else if((randomRoadPositionIndicator == 3)){ + enemyPositonCenterX = 155; + } + + if ((randomRoadPositionIndicator2 == 1)) { + enemyPositonCenterX2 = 85; + }else if((randomRoadPositionIndicator2 == 2)){ + enemyPositonCenterX2 = 120; + }else if((randomRoadPositionIndicator2 == 3)){ + enemyPositonCenterX2 = 155; + }else if(level == 7||level == 8){ + + } + } +} + +function collision(){ + if(gameStatus == GAMEPLAYING){ + consoleDebug("Px:"+playerCarLeftX+", "+playerCarRightX); + consoleDebug("1x:"+enemyCarLeftX+", "+enemyCarRightX); + consoleDebug("2x:"+enemyCarLeftX2+", "+enemyCarRightX2); + consoleDebug("Py:"+playerCarFrontY); + consoleDebug("1y:"+enemyCarFrontY); + consoleDebug("2y:"+enemyCarFrontY2); + if + ( + (enemyCarFrontY < 300 && enemyCarFrontY > playerCarFrontY) + && + ( + (enemyCarLeftX > playerCarLeftX && enemyCarLeftX < playerCarRightX) + || + (enemyCarRightX > playerCarLeftX && enemyCarRightX < playerCarRightX) + ) + ){ + // hit car 1 + consoleDebug("1 HIT"); + enemyPositonCenterY = 300; + numberofHearts -= 1; + Bangle.buzz(50,50); + }else if + ( + (enemyCarFrontY2 < 300 && enemyCarFrontY2 > playerCarFrontY) + && + ( + (enemyCarLeftX2 > playerCarLeftX && enemyCarLeftX2 < playerCarRightX) + || + (enemyCarRightX2 > playerCarLeftX && enemyCarRightX2 < playerCarRightX) + ) + ){ + // hit car 2 + consoleDebug("2 HIT"); + enemyPositonCenterY2 = 300; + numberofHearts -= 1; + Bangle.buzz(50,50); + } + setTimeout(collision, 50); // try again in 50 milliseconds. + } +} + +function checkForNextLevel(){ + if(score < 10){ + level = 1; + }else if(score >= 10 && score < 20){ + level = 2; + }else if(score >= 20 && score < 30){ + level = 3; + }else if(score >= 30 && score < 40){ + level = 4; + }else if(score >= 40 && score < 50){ + level = 5; + }else if(score >= 50 && score < 60){ + level = 6; + }else if(score >= 60 && score < 70){ + level = 7; + }else if(score >= 70 && score < 80){ + level = 8; + }else if(score >= 80 && score < 90){ + level = 9; + }else if(score >= 90){ + level = 10; + } +} + +var accel = Bangle.getAccel(); + +var file = require("Storage").open("CarCrazy.csv","r"); +var currentHighScore = file.readLine(); +if (currentHighScore == undefined) currentHighScore = 0; + +var BackgroundStartingPosition = 75; +var BackgroundYPosition = BackgroundStartingPosition; + +var randomRoadPositionIndicator; +var randomRoadPositionIndicator2; +var enemyPositonCenterX; +var enemyPositonCenterX2; + +var carScale = 0.5; +var carWidth = 30; +var carHeight = 60; + +var playerCarCenterY = 130; +var playerCarCenterX; + +var enemyPositonCenterY = 0 - carHeight/2; +var enemyPositonCenterY2 = 0 - carHeight/2; + +var playerCarLeftX; +var playerCarRightX; +var playerCarFrontY; + +var playerCarFrontY; +var playerCarBackY; +var playerCarLeftX; +var playerCarRightX; + +var enemyCarFrontY; +var enemyCarBackY; +var enemyCarLeftX; +var enemyCarRightX; + +var enemyCarFrontY2; +var enemyCarBackY2; +var enemyCarLeftX2; +var enemyCarRightX2; + +var GAMEPLAYING = 1; +var GAMEOVER = 2; +var GAMESTART = 3; +var gameStatus = GAMESTART; +var score = 0; +var level = 1; + + +moveEnemyPosition(); +collision(); + + +g.setFontAlign(-1,-1); + +function clearHighScore() { + currentHighScore = 0; + file = require("Storage").open("CarCrazy.csv","w"); + file.erase(); +} + +function draw(){ + if(gameStatus == GAMEPLAYING){ + BackgroundYPosition += 10; + accel = Bangle.getAccel(); + playerCarCenterX = Math.round(120-accel.x*120); + if (playerCarCenterX > 170) { playerCarCenterX = 170; } + if (playerCarCenterX < 70) { playerCarCenterX = 70; } + g.flip(); + g.drawImage(backgroundImage,125,BackgroundYPosition, {scale:13,rotate:0}); + g.drawImage(RedCar,playerCarCenterX,playerCarCenterY, {scale:carScale,rotate:3.142}); + g.drawImage(OrangeCar,enemyPositonCenterX,enemyPositonCenterY, {scale:carScale,rotate:0}); + if(level>=4){ + g.drawImage(OrangeCar,enemyPositonCenterX2,enemyPositonCenterY2, {scale:carScale,rotate:0}); + } + + if(numberofHearts==3){ + g.drawImage(heartImage,10,10, {scale:2,rotate:0}); + g.drawImage(heartImage,10,50, {scale:2,rotate:0}); + g.drawImage(heartImage,10,30, {scale:2,rotate:0}); + }else if(numberofHearts==2){ + g.drawImage(heartImage,10,50, {scale:2,rotate:0}); + g.drawImage(heartImage,10,30, {scale:2,rotate:0}); + }else if(numberofHearts==1){ + g.drawImage(heartImage,10,50, {scale:2,rotate:0}); + }else{ + gameStatus = GAMEOVER; + //clearHighScore(); + if(score >= currentHighScore){ + currentHighScore = score; + file = require("Storage").open("CarCrazy.csv","w"); + file.erase(); + file = require("Storage").open("CarCrazy.csv","w"); + file.write(currentHighScore+"\n"); + } + } + + playerCarFrontY = playerCarCenterY-carHeight/2; + playerCarBackY = playerCarCenterY+carHeight/2; + playerCarLeftX = playerCarCenterX-carWidth/2; + playerCarRightX = playerCarCenterX+carWidth/2; + + enemyCarFrontY = enemyPositonCenterY+carHeight/2; + enemyCarBackY = enemyPositonCenterY-carHeight/2; + enemyCarLeftX = enemyPositonCenterX-carWidth/2; + enemyCarRightX = enemyPositonCenterX+carWidth/2; + + enemyCarFrontY2 = enemyPositonCenterY2+carHeight/2; + enemyCarBackY2 = enemyPositonCenterY2-carHeight/2; + enemyCarLeftX2 = enemyPositonCenterX2-carWidth/2; + enemyCarRightX2 = enemyPositonCenterX2+carWidth/2; + + g.setColor(255,0,0); + //g.drawRect(playerCarLeftX, playerCarFrontY, playerCarRightX, playerCarBackY); + //g.drawRect(enemyCarLeftX, enemyCarFrontY, enemyCarRightX, enemyCarBackY); + //g.drawRect(enemyCarLeftX2, enemyCarFrontY2, enemyCarRightX2, enemyCarBackY2); + + g.setColor(0,0,0); + g.drawString("Score: "+score,180,5); + g.drawString("HighScore:",178,15); + g.drawString(currentHighScore,205,25); + g.drawString("Level: "+level,180,150); + + //g.drawString("P:"+playerCarLeftX+", "+playerCarRightX,180,90); + //g.drawString("1:"+enemyCarLeftX+", "+enemyCarRightX,180,100); + //g.drawString("2:"+enemyCarLeftX2+", "+enemyCarRightX2,180,110); + //g.drawString("P:"+playerCarFrontY,180,120); + //g.drawString("1:"+enemyCarFrontY,180,130); + //g.drawString("2:"+enemyCarFrontY2,180,140); + + if(BackgroundYPosition > 170){ + BackgroundYPosition = BackgroundStartingPosition; + } + + + }else if(gameStatus == GAMEOVER){ + + BackgroundYPosition += 10; + g.flip(); + g.drawImage(backgroundImage,125,BackgroundYPosition, {scale:13,rotate:0}); + g.drawImage(gameOverImage,125,80, {scale:8,rotate:0}); + if(BackgroundYPosition > 170){ + BackgroundYPosition = BackgroundStartingPosition; + } + g.setColor(255,0,0); + g.setFont("6x8",4); + g.drawString("Game Over",13,17); + g.setFont("6x8",1.5); + g.drawString("Score: "+score,10,75); + g.drawString("High",10,100); + g.drawString("Score: " + currentHighScore,10,110); + g.drawString("Hold Button",10,130); + g.drawString("2 To Play",10,140); + g.drawImage(LightGreenCar,180,115, {scale:0.5,rotate:3}); + g.drawImage(PurpleCar,215,115, {scale:0.5,rotate:3}); + }else if(gameStatus == GAMESTART){ + g.flip(); + g.drawImage(backgroundImage,125,BackgroundYPosition, {scale:13,rotate:0}); + g.setColor(255,0,0); + BackgroundYPosition += 10; + g.setFont("6x8",3); + g.drawImage(gameOverImage,125,80, {scale:8,rotate:0}); + g.drawString("Welcome to",13,11); + g.drawString("Car Crazy",13,31); + g.setFont("6x8",1.8); + g.drawString("High",10,80); + g.drawString("Score: "+currentHighScore,10,90); + g.drawString("Hold Button",10,120); + g.drawString("2 To Start",10,130); + g.drawImage(LightGreenCar,180,115, {scale:0.5,rotate:3}); + g.drawImage(PurpleCar,215,115, {scale:0.5,rotate:3}); + } +} +setInterval(draw, 50); + + +function moveEnemyCar(){ + if(gameStatus == GAMEPLAYING){ + if(level==1||level==2){ + enemyPositonCenterY = enemyPositonCenterY + 10; + enemyPositonCenterY2 = 0; + }else if(level==3){ + enemyPositonCenterY = enemyPositonCenterY + 12; + enemyPositonCenterY2 = 0; + }else if(level==4){ + enemyPositonCenterY = enemyPositonCenterY + 8; + enemyPositonCenterY2 = enemyPositonCenterY2 + 8; + }else if(level==5){ + enemyPositonCenterY = enemyPositonCenterY + 9; + enemyPositonCenterY2 = enemyPositonCenterY2 + 9; + }else if(level==6){ + enemyPositonCenterY = enemyPositonCenterY + 9.5; + enemyPositonCenterY2 = enemyPositonCenterY2 + 8; + }else if(level==7){ + enemyPositonCenterY = enemyPositonCenterY + 10; + enemyPositonCenterY2 = enemyPositonCenterY2 + 8; + }else if(level==8){ + enemyPositonCenterY = enemyPositonCenterY + 11.5; + enemyPositonCenterY2 = enemyPositonCenterY2 + 11.5; + }else if(level>=9){ + enemyPositonCenterY = enemyPositonCenterY + 13; + enemyPositonCenterY2 = enemyPositonCenterY2 + 14; + } + if(enemyPositonCenterY > 200){ + enemyPositonCenterY = 300; + } + if(enemyPositonCenterY2 > 200){ + enemyPositonCenterY2 = 300; + } + if(enemyPositonCenterY > 200 && (enemyPositonCenterY2 > 200 || level < 4)){ + enemyPositonCenterY = 0 - carHeight/2; + if (level >= 4) { enemyPositonCenterY2 = 0 - carHeight/2; } + moveEnemyPosition(); + } + } +} +setInterval(moveEnemyCar,50); + +setWatch(() => { + if(gameStatus == GAMESTART){ + gameStatus = GAMEPLAYING; + collision(); + numberofHearts = 3; + enemyPositonCenterX = 120; + enemyPositonCenterY = 0 - carHeight/2; + enemyPositonCenterX2 = 120; + enemyPositonCenterY2 = 0 - carHeight/2; + score = 0; + level = 1; + checkForNextLevel(); + }else if(gameStatus == GAMEOVER){ + gameStatus = GAMEPLAYING; + collision(); + enemyPositonCenterX = 120; + enemyPositonCenterY = 0 - carHeight/2; + enemyPositonCenterX2 = 120; + enemyPositonCenterY2 = 0 - carHeight/2; + numberofHearts = 3; + score = 0; + level = 1; + checkForNextLevel(); + } +}, BTN2, {repeat:true}); diff --git a/apps/carcrazy/carcrash.png b/apps/carcrazy/carcrash.png new file mode 100644 index 000000000..1678c54c8 Binary files /dev/null and b/apps/carcrazy/carcrash.png differ diff --git a/apps/carcrazy/settings.js b/apps/carcrazy/settings.js new file mode 100644 index 000000000..ee3bbd417 --- /dev/null +++ b/apps/carcrazy/settings.js @@ -0,0 +1,20 @@ + +(function (back) { + const menu = { + '': { 'title': 'Car Crazy' }, + '< Back': back, + 'Reset Highscore': () => { + E.showPrompt('Reset Highscore?').then((v) => { + let delay = 50; + if (v) { + delay = 500; + E.showMessage('Resetting'); + var f = require('Storage').open('CarCrazy.csv', 'w'); + f.write('0\n'); + } + setTimeout(() => E.showMenu(menu), delay); + }); + } + }; + E.showMenu(menu); +}); diff --git a/apps/chargeanim/ChangeLog b/apps/chargeanim/ChangeLog index 5560f00bc..a7262b0c9 100644 --- a/apps/chargeanim/ChangeLog +++ b/apps/chargeanim/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Bangle.js 2 compatibility diff --git a/apps/chargeanim/app.js b/apps/chargeanim/app.js index c2702337a..68d0cdff5 100644 --- a/apps/chargeanim/app.js +++ b/apps/chargeanim/app.js @@ -1,8 +1,12 @@ +g.setBgColor(0, 0, 0); g.clear().flip(); -var imgbat = require("heatshrink").decompress(atob("nlWhH+AH4A/AH4AHwoAQHXQ8pHf47rF6YAXHXQ8OHVo8NHf47/Hf47/Hf47/Hf47/Hf47/Hf47r1I766Y756Z351I766ayTHco6BHfCxBHfI6CdyY7jHQQ73WIayUHcQ6DHew6EHeqxEdyo7gOwo70HQqyVHbyxFHeo6GHeY6Hdyo7cWI47zHQ6yWHbY6IHeKxIABa9MHbI6TQJo7YHUI7YWMKzbQKQYOHdYYPHcK9IWJw7sDKA7hHTA7pWKA7qDKQ7gdwwaTHcyxSHcR2ZHcwZUHcqxUHcLuEHSo7kHSw7gWLI7kHS47iHTA7fdwKxYHcQ6ZHb46bO8A76ADg7/Hf47/Hf47/Hf47/Hf47/Hf47/HbY8uHRg8tHRwA/AH4AsA==")); -var imgbubble = require("heatshrink").decompress(atob("ikQhH+AAc0AAgKEAAwRFCpgMDnVerwULCIuCCYoUGCQQQBnQ9MA4Q3GChI5DEpATIJYISKCY46LCYwANCa4UObJ7INeCoSOCpAOI")); +var imgbat = require("heatshrink").decompress(atob("nFYhBC/AH4A/AGUeACA22HEo3/G8YrTAC422HBQ2tHBI3/G/43/G/43/G/43/G/43/G/43/G+fTG+vSN+w326Q31GwI3/G9g2WG742CG/43rGwY3yGwg33RKo3bNzQ3bGwo3/G9A2GG942dG/43QGw43uGxA34IKw3VGyY3iG0I3pb8pBRG+wYPG8wYQG/42uG8oZSG/43bDKY3iDKg3cNzI3iRKo3gGyo3/G7A2WG7g2aG/43WGzA3dGzI3/G6fTGzRvcG/43/G/43/G/43/G/43/G/43/G/437HFw2IHFo2KAH4A/AH4Aa")); +var imgbubble = require("heatshrink").decompress(atob("i0UhAebgoAFCaYXNBocjAAIWNCYoVHCw4UFIZwqELJQWFKZQVOChYVzABwVaCx7wKCqIWNCg4WMChIXJCZgAnA==")); - var W=240,H=240; +var W=g.getWidth(),H=g.getHeight(); +var b2v = (W != 240)?-1:1; +var b2rot = (W != 240)?Math.PI:0; +var b2scale = W/240.0; var bubbles = []; for (var i=0;i<10;i++) { bubbles.push({y:Math.random()*H,ly:0,x:(0.5+(i<5?i:i+8))*W/18,v:0.6+Math.random(),s:0.5+Math.random()}); @@ -12,12 +16,16 @@ function anim() { /* we don't use any kind of buffering here. Just draw one image at a time (image contains a background) too, and there is minimal flicker. */ - var mx = 120, my = 120; + var mx = W/2.0, my = H/2.0; bubbles.forEach(f=>{ - f.y-=f.v;if (f.y<-24) f.y=H+8; - g.drawImage(imgbubble,f.y,f.x,{scale:f.s}); + f.y-=f.v * b2v; + if (f.y<-24) + f.y=H+8; + else if (f.y > (H+8)) + f.y=0; + g.drawImage(imgbubble,f.y,f.x,{scale:f.s * b2scale, rotate:b2rot}); }); - g.drawImage(imgbat, mx,my,{rotate:Math.sin(getTime()*2)*0.5-Math.PI/2}); + g.drawImage(imgbat, mx,my,{scale:b2scale, rotate:Math.sin(getTime()*2)*0.5-Math.PI/2 + b2rot}); g.flip(); } diff --git a/apps/chargeanim/bangle-charge-animation-screenshot.png b/apps/chargeanim/bangle-charge-animation-screenshot.png new file mode 100644 index 000000000..83ef1dbda Binary files /dev/null and b/apps/chargeanim/bangle-charge-animation-screenshot.png differ diff --git a/apps/chargeanim/bangle2-charge-animation-screenshot.png b/apps/chargeanim/bangle2-charge-animation-screenshot.png new file mode 100644 index 000000000..c3fb7c8c8 Binary files /dev/null and b/apps/chargeanim/bangle2-charge-animation-screenshot.png differ diff --git a/apps/choozi/ChangeLog b/apps/choozi/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/choozi/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/choozi/README.md b/apps/choozi/README.md new file mode 100644 index 000000000..f1e4255bc --- /dev/null +++ b/apps/choozi/README.md @@ -0,0 +1,27 @@ +# Choozi + +Choose people or things at random using Bangle.js. + + + +## Usage + +You can use Choozi to pick a person to play first in a board game. With all +the players seated in a circle, set the number of segments equal to the number +of players, ensure that each person knows which colour represents them, and then +choose a segment. After a short animation, the chosen segment will fill the screen. + +You can use Choozi to randomly select an element from any set with 2 to 13 members, +as long as you can define a bijection between members of the set and coloured +segments on the Bangle.js display. + +## Controls + +BTN1: increase the number of segments +BTN2: choose a segment at random +BTN3: decrease the number of segments + +## Creator + +James Stanley +September 2021 diff --git a/apps/choozi/app-icon.js b/apps/choozi/app-icon.js new file mode 100644 index 000000000..51b3bead3 --- /dev/null +++ b/apps/choozi/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwggLIrnM4uqAAIhPgvMAAPFzIABzWgCxkMCweqC4QABDBYtC5QVFDBoWCCo5KLOQIWKDARFICxhJIFwOpC5owFFyAwGUYIuOGAwuRC4guSJAgXBCyIwDIyQXF5IXSzJeVMAReUAAOQhheTMAVcC6yOUC4aOUC7GZUyoXXzWqhQXVxGqC9mYC7OqC9eoxEKC6uBC6uIwAXBPCSmBwEAC6Z2BiAXBJCR2BgEAjQXSlGBC4JgSLwYABJCJGBLwJIDGB+IIwRIDGByNBIwZIDGBhdBRoQwSLoIuFGAYYKCwIuGGAgYI1QWBRgYYJMYmaFoSMEAAyrBAAgVCCxgYGjAWQAAMBC4UILZQA==")) diff --git a/apps/choozi/app.js b/apps/choozi/app.js new file mode 100644 index 000000000..1a5b2f17e --- /dev/null +++ b/apps/choozi/app.js @@ -0,0 +1,208 @@ + +/* Choozi - Choose people or things at random using Bangle.js. + * Inspired by the "Chwazi" Android app + * + * James Stanley 2021 + */ + +var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff']; + +var stepAngle = 0.18; // radians - resolution of polygon +var gapAngle = 0.035; // radians - gap between segments +var perimMin = 110; // px - min. radius of perimeter +var perimMax = 120; // px - max. radius of perimeter + +var segmentMax = 106; // px - max radius of filled-in segment +var segmentStep = 5; // px - step size of segment fill animation +var circleStep = 4; // px - step size of circle fill animation + +// rolling ball animation: +var maxSpeed = 0.08; // rad/sec +var minSpeed = 0.001; // rad/sec +var animStartSteps = 300; // how many steps before it can start slowing? +var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate +var ballSize = 3; // px - ball radius +var ballTrack = 100; // px - radius of ball path + +var centreX = 120; // px - centre of screen +var centreY = 120; // px - centre of screen + +var fontSize = 50; // px + +var radians = 2*Math.PI; // radians per circle + +var defaultN = 3; // default value for N +var minN = 2; +var maxN = colours.length; +var N; +var arclen; + +// https://www.frankmitchell.org/2015/01/fisher-yates/ +function shuffle (array) { + var i = 0 + , j = 0 + , temp = null; + + for (i = array.length - 1; i > 0; i -= 1) { + j = Math.floor(Math.random() * (i + 1)); + temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } +} + +// draw an arc between radii minR and maxR, and between +// angles minAngle and maxAngle +function arc(minR, maxR, minAngle, maxAngle) { + var step = stepAngle; + var angle = minAngle; + var inside = []; + var outside = []; + var c, s; + while (angle < maxAngle) { + c = Math.cos(angle); + s = Math.sin(angle); + inside.push(centreX+c*minR); // x + inside.push(centreY+s*minR); // y + // outside coordinates are built up in reverse order + outside.unshift(centreY+s*maxR); // y + outside.unshift(centreX+c*maxR); // x + angle += step; + } + c = Math.cos(maxAngle); + s = Math.sin(maxAngle); + inside.push(centreX+c*minR); + inside.push(centreY+s*minR); + outside.unshift(centreY+s*maxR); + outside.unshift(centreX+c*maxR); + + var vertices = inside.concat(outside); + g.fillPoly(vertices, true); +} + +// draw the arc segments around the perimeter +function drawPerimeter() { + g.clear(); + for (var i = 0; i < N; i++) { + g.setColor(colours[i%colours.length]); + var minAngle = (i/N)*radians; + arc(perimMin,perimMax,minAngle,minAngle+arclen); + } +} + +// animate a ball rolling around and settling at "target" radians +function animateChoice(target) { + var angle = 0; + var speed = 0; + var oldx = -10; + var oldy = -10; + var decelFromAngle = -1; + var allowDecel = false; + for (var i = 0; true; i++) { + angle = angle + speed; + if (angle > radians) angle -= radians; + if (i < animStartSteps || (speed < maxSpeed && !allowDecel)) { + speed = speed + accel; + if (speed > maxSpeed) { + speed = maxSpeed; + /* when we reach max speed, we know how long it takes + * to accelerate, and therefore how long to decelerate, so + * we can work out what angle to start decelerating from */ + if (decelFromAngle < 0) { + decelFromAngle = target-angle; + while (decelFromAngle < 0) decelFromAngle += radians; + while (decelFromAngle > radians) decelFromAngle -= radians; + } + } + } else { + if (!allowDecel && (angle < decelFromAngle) && (angle+speed >= decelFromAngle)) allowDecel = true; + if (allowDecel) speed = speed - accel; + if (speed < minSpeed) speed = minSpeed; + if (speed == minSpeed && angle < target && angle+speed >= target) return; + } + + var r = i/2; + if (r > ballTrack) r = ballTrack; + var x = centreX+Math.cos(angle)*r; + var y = centreY+Math.sin(angle)*r; + g.setColor('#000000'); + g.fillCircle(oldx,oldy,ballSize+1); + g.setColor('#ffffff'); + g.fillCircle(x, y, ballSize); + oldx=x; + oldy=y; + } +} + +// choose a winning segment and animate its selection +function choose() { + var chosen = Math.floor(Math.random()*N); + var minAngle = (chosen/N)*radians; + var maxAngle = minAngle + arclen; + animateChoice((minAngle+maxAngle)/2); + g.setColor(colours[chosen%colours.length]); + for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep) + arc(i, perimMax, minAngle, maxAngle); + arc(0, perimMax, minAngle, maxAngle); + for (var r = 1; r < segmentMax; r += circleStep) + g.fillCircle(centreX,centreY,r); + g.fillCircle(centreX,centreY,segmentMax); +} + +// draw the current value of N in the middle of the screen, with +// up/down arrows +function drawN() { + g.setColor('#ffffff'); + g.setFont("Vector",fontSize); + g.drawString(N,centreX-g.stringWidth(N)/2+4,centreY-fontSize/2); + if (N < maxN) + g.fillPoly([centreX-6,centreY-fontSize/2-7, centreX+6,centreY-fontSize/2-7, centreX, centreY-fontSize/2-14]); + if (N > minN) + g.fillPoly([centreX-6,centreY+fontSize/2+5, centreX+6,centreY+fontSize/2+5, centreX, centreY+fontSize/2+12]); +} + +// update number of segments, with min/max limit, "arclen" update, +// and screen reset +function setN(n) { + N = n; + if (N < minN) N = minN; + if (N > maxN) N = maxN; + arclen = radians/N - gapAngle; + drawPerimeter(); +} + +// save N to choozi.txt +function writeN() { + var file = require("Storage").open("choozi.txt","w"); + file.write(N); +} + +// load N from choozi.txt +function readN() { + var file = require("Storage").open("choozi.txt","r"); + var n = file.readLine(); + if (n !== undefined) setN(parseInt(n)); + else setN(defaultN); +} + +shuffle(colours); // is this really best? +Bangle.setLCDMode("direct"); +Bangle.setLCDTimeout(0); // keep screen on +readN(); +drawN(); + +setWatch(() => { + setN(N+1); + drawN(); +}, BTN1, {repeat:true}); + +setWatch(() => { + writeN(); + drawPerimeter(); + choose(); +}, BTN2, {repeat:true}); + +setWatch(() => { + setN(N-1); + drawN(); +}, BTN3, {repeat:true}); diff --git a/apps/choozi/app.png b/apps/choozi/app.png new file mode 100644 index 000000000..99c9fa07a Binary files /dev/null and b/apps/choozi/app.png differ diff --git a/apps/choozi/bangle1-choozi-screenshot1.png b/apps/choozi/bangle1-choozi-screenshot1.png new file mode 100644 index 000000000..104024958 Binary files /dev/null and b/apps/choozi/bangle1-choozi-screenshot1.png differ diff --git a/apps/choozi/bangle1-choozi-screenshot2.png b/apps/choozi/bangle1-choozi-screenshot2.png new file mode 100644 index 000000000..f3b6868bf Binary files /dev/null and b/apps/choozi/bangle1-choozi-screenshot2.png differ diff --git a/apps/chronowid/ChangeLog b/apps/chronowid/ChangeLog index e173467a1..ded543397 100644 --- a/apps/chronowid/ChangeLog +++ b/apps/chronowid/ChangeLog @@ -1,3 +1,5 @@ 0.01: New widget and app! 0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme) -0.03: Display only minutes:seconds when less than 1 hour left \ No newline at end of file +0.03: Display only minutes:seconds when less than 1 hour left +0.04: Change to 7 segment font, move to top widget bar + Better auto-update behaviour, less RAM used diff --git a/apps/chronowid/README.md b/apps/chronowid/README.md index f422dd956..6e0aba681 100644 --- a/apps/chronowid/README.md +++ b/apps/chronowid/README.md @@ -5,11 +5,14 @@ The advantage is, that you can still see your normal watchface and other widgets The widget is always active, but only shown when the timer is on. Hours, minutes, seconds and timer status can be set with an app. -Depending on when you start the timer, it may alert up to 0,999 seconds early. This is because it checks only for full seconds. When there is less than one seconds left, it buzzes. This cannot be avoided without checking more than every second, which I would like to avoid. +When there is less than one second left on the timer it buzzes. + +The widget has been tested on Bangle 1 and Bangle 2 ## Screenshots -TBD +![](screenshot.png) + ## Features @@ -24,15 +27,15 @@ There are no settings section in the settings app, timer can be set using an app * Hours: Set the hours for the timer * Minutes: Set the minutes for the timer * Seconds: Set the seconds for the timer -* Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app to load the widget which starts the timer. The widget is always there, but only visible when timer is on. +* Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app to load the widget which starts the timer. The widget is always there, but only visible when timer is on. ## Releases -* Offifical app loader: https://github.com/espruino/BangleApps/tree/master/apps/chronowid (https://banglejs.com/apps/) +* Official app loader: https://github.com/espruino/BangleApps/tree/master/apps/chronowid (https://banglejs.com/apps/) * Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/chronowid (https://purple-tentacle.github.io/BangleApps/index.html#) * Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/chronowid ## Requests -If you have any feature requests, please write here: http://forum.espruino.com/conversations/345972/ \ No newline at end of file +If you have any feature requests, please write here: http://forum.espruino.com/conversations/345972/ diff --git a/apps/chronowid/app.js b/apps/chronowid/app.js index 0cacdee23..f38105e34 100644 --- a/apps/chronowid/app.js +++ b/apps/chronowid/app.js @@ -3,7 +3,6 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); const storage = require('Storage'); -const boolFormat = v => v ? "On" : "Off"; let settingsChronowid; function updateSettings() { @@ -12,6 +11,7 @@ function updateSettings() { now.getHours() + settingsChronowid.hours, now.getMinutes() + settingsChronowid.minutes, now.getSeconds() + settingsChronowid.seconds); settingsChronowid.goal = goal.getTime(); storage.writeJSON('chronowid.json', settingsChronowid); + if (WIDGETS["chronowid"]) WIDGETS["chronowid"].reload(); } function resetSettings() { @@ -44,6 +44,7 @@ function showMenu() { timerMenu.started.value = settingsChronowid.started; } }, + '< Back' : ()=>{load();}, 'Reset values': function() { settingsChronowid.hours = 0; settingsChronowid.minutes = 0; @@ -84,15 +85,15 @@ function showMenu() { }, 'Timer on': { value: settingsChronowid.started, - format: boolFormat, + format: v => v ? "On" : "Off", onchange: v => { settingsChronowid.started = v; updateSettings(); } }, }; - timerMenu['-Exit-'] = ()=>{load();}; + return E.showMenu(timerMenu); } -showMenu(); \ No newline at end of file +showMenu(); diff --git a/apps/chronowid/screenshot.png b/apps/chronowid/screenshot.png new file mode 100644 index 000000000..f94eece94 Binary files /dev/null and b/apps/chronowid/screenshot.png differ diff --git a/apps/chronowid/widget.js b/apps/chronowid/widget.js index f0e785efd..2d1c78941 100644 --- a/apps/chronowid/widget.js +++ b/apps/chronowid/widget.js @@ -1,93 +1,79 @@ (() => { - const storage = require('Storage'); - settingsChronowid = storage.readJSON("chronowid.json",1)||{}; //read settingsChronowid from file - var height = 23; - var width = 58; + var settingsChronowid; var interval = 0; //used for the 1 second interval timer - var now = new Date(); + var diff; - var time = 0; - var diff = settingsChronowid.goal - now; - //Convert ms to time function getTime(t) { var milliseconds = parseInt((t % 1000) / 100), seconds = Math.floor((t / 1000) % 60), minutes = Math.floor((t / (1000 * 60)) % 60), hours = Math.floor((t / (1000 * 60 * 60)) % 24); - - hours = (hours < 10) ? "0" + hours : hours; - minutes = (minutes < 10) ? "0" + minutes : minutes; - seconds = (seconds < 10) ? "0" + seconds : seconds; - - return hours + ":" + minutes + ":" + seconds; + return hours.toString().padStart(2,0) + ":" + minutes.toString().padStart(2,0) + ":" + seconds.toString().padStart(2,0); } - function printDebug() { - print ("Nowtime: " + getTime(now)); - print ("Now: " + now); + /*function printDebug() { print ("Goaltime: " + getTime(settingsChronowid.goal)); print ("Goal: " + settingsChronowid.goal); print("Difftime: " + getTime(diff)); print("Diff: " + diff); print ("Started: " + settingsChronowid.started); print ("----"); - } + }*/ //counts down, calculates and displays function countDown() { - now = new Date(); + var now = new Date(); diff = settingsChronowid.goal - now; //calculate difference - WIDGETS["chronowid"].draw(); - //time is up + // time is up if (settingsChronowid.started && diff < 1000) { Bangle.buzz(1500); //write timer off to file settingsChronowid.started = false; - storage.writeJSON('chronowid.json', settingsChronowid); + require('Storage').writeJSON('chronowid.json', settingsChronowid); clearInterval(interval); //stop interval + interval = undefined; } - //printDebug(); + // calculates width and redraws accordingly + WIDGETS["chronowid"].redraw(); } - // draw your widget - function draw() { - if (!settingsChronowid.started) { - width = 0; - return; //do not draw anything if timer is not started - } - g.reset(); - if (diff >= 0) { - if (diff < 3600000) { //less than 1 hour left - width = 58; - g.clearRect(this.x,this.y,this.x+width,this.y+height); - g.setFont("6x8", 2); - g.drawString(getTime(diff).substring(3), this.x+1, this.y+5); //remove hour part 00:00:00 -> 00:00 - } - if (diff >= 3600000) { //one hour or more left - width = 48; - g.clearRect(this.x,this.y,this.x+width,this.y+height); - g.setFont("6x8", 1); - g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00 - } - } - // not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed. - // else { - // width = 58; - // g.clearRect(this.x,this.y,this.x+width,this.y+height); - // g.setFont("6x8", 2); - // g.drawString("END", this.x+15, this.y+5); - // } - } - - if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second - // add the widget - WIDGETS["chronowid"]={area:"bl",width:width,draw:draw,reload:function() { - reload(); - Bangle.drawWidgets(); // relayout all widgets + WIDGETS["chronowid"]={area:"tl",width:0,draw:function() { + if (!this.width) return; + g.reset().setFontAlign(0,0).clearRect(this.x,this.y,this.x+this.width,this.y+23); + //g.drawRect(this.x,this.y,this.x+this.width-1, this.y+23); + var scale; + var timeStr; + if (diff < 3600000) { //less than 1 hour left + width = 58; + scale = 2; + timeStr = getTime(diff).substring(3); // remove hour part 00:00:00 -> 00:00 + } else { //one hour or more left + width = 48; + scale = 1; + timeStr = getTime(diff); //display hour 00:00:00 but small + } + // Font5x9Numeric7Seg - just build this in as it's tiny + g.setFontCustom(atob("AAAAAAAAAAIAAAQCAQAAAd0BgMBdwAAAAAAAdwAB0RiMRcAAAERiMRdwAcAQCAQdwAcERiMRBwAd0RiMRBwAAEAgEAdwAd0RiMRdwAcERiMRdwAFAAd0QiEQdwAdwRCIRBwAd0BgMBAAABwRCIRdwAd0RiMRAAAd0QiEQAAAAAAAAAA="), 32, atob("BgAAAAAAAAAAAAAAAAYCAAYGBgYGBgYGBgYCAAAAAAAABgYGBgYG"), 9 + (scale<<8)); + g.drawString(timeStr, this.x+this.width/2, this.y+12); + }, redraw:function() { + var last = this.width; + if (!settingsChronowid.started) this.width = 0; + else this.width = (diff < 3600000) ? 58 : 48; + if (last != this.width) Bangle.drawWidgets(); + else this.draw(); + }, reload:function() { + settingsChronowid = require('Storage').readJSON("chronowid.json",1)||{}; + if (interval) clearInterval(interval); + interval = undefined; + // start countdown each second + if (settingsChronowid.started) interval = setInterval(countDown, 1000); + // reset everything + countDown(); }}; //printDebug(); - countDown(); -})(); \ No newline at end of file + // set width correctly, start countdown each second + WIDGETS["chronowid"].reload(); +})(); diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog new file mode 100644 index 000000000..fa2139fff --- /dev/null +++ b/apps/circlesclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: New clock +0.02: Fix icon & add battery warn functionality diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md new file mode 100644 index 000000000..27c0566d3 --- /dev/null +++ b/apps/circlesclock/README.md @@ -0,0 +1,21 @@ +# Circles clock + +A clock with circles for different data at the bottom in a probably familiar style + +It shows besides time, date and day of week the following information: + * Steps (requires [pedometer widget](https://banglejs.com/apps/#pedometer)) + * Heart rate (when screen is on and unlocked) + * Battery (including charging and battery low) + +## Screenshot + +![Screenshot](screenshot.png) + +## TODO +* Show weather information + +## Creator +Marco ([myxor](https://github.com/myxor)) + +## Icons +Icons taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 diff --git a/apps/circlesclock/app-icon.js b/apps/circlesclock/app-icon.js new file mode 100644 index 000000000..a5a7fdfed --- /dev/null +++ b/apps/circlesclock/app-icon.js @@ -0,0 +1 @@ + require("heatshrink").decompress(atob("mEwwcCIf4ALv///gFCv0Agf+CJP/wAODAwPAEpAjCCIX8h4RMj/+g/8gP4CA4LBDoP/GpkH8EP4/8LIIRMAQIOCCJU/CgQOBEwMPI5ARCR4YRJgP/gB3CI5Z0CCIiABfHRfEj+BAoN+n4FBLIkP/8chwRBx5cC//8v4REhytDgYRCv//8fxEYwRFgfxA4I1FRgI1D+JHE/7FINZzCBAAc4CRU4/kB44FCjgRKLQRlBPQ4RHgYCB/jpBABB6BPoKzBCJYAGuD/vAB1JkgLJm3bAgUCpMnwDdCPwIFChu27dgAoMSCIP+FAQRB+AFBtoRBtgFByQCBRIIoBAocDtonBAQWQdgXAgVIAocDEAUNwEEyEHBYUSoE//gRCsI7BxvACIILDCIcBCIYFCCJ3/wIRCIIYRBI4h6CAoJrDLJYRDDwJ9LAoKhBoMDUIcEgFwUIQREgUBaAcIkhPCAAQzBAAUBdIhhDAAMGCIkAkAFEdAQAFA==")) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js new file mode 100644 index 000000000..7607fa71f --- /dev/null +++ b/apps/circlesclock/app.js @@ -0,0 +1,235 @@ +const locale = require("locale"); +const heatshrink = require("heatshrink"); + +const shoesIcon = heatshrink.decompress(atob("h0OwYJGgmAAgUBkgECgVJB4cSoAUDyEBkARDpADBhMAyQRBgVAkgmDhIUDAAuQAgY1DAAYA=")); +const heartIcon = heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM")); +const powerIcon = heatshrink.decompress(atob("h0OwYQNsAED7AEDmwEDtu2AgUbtuABwXbBIUN23AAoYOCgEDFIgODABI")); +const powerIconGreen = heatshrink.decompress(atob("h0OwYQNkAEDpAEDiQEDkmSAgUJkmABwVJBIUEyVAAoYOCgEBFIgODABI")); +const powerIconRed = heatshrink.decompress(atob("h0OwYQNoAEDyAEDkgEDpIFDiVJBweSAgUJkmAAoYZDgQpEBwYAJA")); + +const SETTINGS_FILE = "circlesclock.json"; +let settings; + +function loadSettings() { + settings = require("Storage").readJSON(SETTINGS_FILE, 1) || { + 'maxHR': 200, + 'stepGoal': 10000, + 'batteryWarn': 30 + }; +} + +const colorFg = '#fff'; +const colorBg = '#000'; +const colorGrey = '#808080'; +const colorRed = '#ff0000'; +const colorGreen = '#00ff00'; + +let hrtValue; + +const h = g.getHeight(); +const w = g.getWidth(); +const hOffset = 30; +const h1 = Math.round(1 * h / 5 - hOffset); +const h2 = Math.round(3 * h / 5 - hOffset); +const h3 = Math.round(8 * h / 8 - hOffset); +const w1 = Math.round(w / 6); +const w2 = Math.round(3 * w / 6); +const w3 = Math.round(5 * w / 6); +const radiusOuter = 22; +const radiusInner = 16; + +function draw() { + g.reset(); + g.setColor(colorBg); + g.fillRect(0, 0, w, h); + + // time + g.setFont("Vector:50"); + g.setFontAlign(-1, -1); + g.setColor(colorFg); + g.drawString(locale.time(new Date(), 1), w / 10, h1 + 8); + + // date & dow + g.setFont("Vector:20"); + g.setFontAlign(-1, 0); + g.drawString(locale.date(new Date()), w / 10, h2); + g.drawString(locale.dow(new Date()), w / 10, h2 + 22); + + // Steps circle + drawSteps(); + + // Heart circle + drawHeartRate(); + + // Battery circle + drawBattery(); +} + + + +function drawSteps() { + const steps = getSteps(); + const blue = '#0000ff'; + g.setColor(colorGrey); + g.fillCircle(w1, h3, radiusOuter); + + const stepGoal = settings.stepGoal; + if (stepGoal > 0) { + let percent = steps / stepGoal; + if (stepGoal < steps) percent = 1; + drawGauge(w1, h3, percent, blue); + } + + g.setColor(colorBg); + g.fillCircle(w1, h3, radiusInner); + + g.fillPoly([w1, h3, w1 - 15, h3 + radiusOuter + 5, w1 + 15, h3 + radiusOuter + 5]); + + g.setFont("Vector:12"); + g.setFontAlign(0, 0); + g.setColor(colorFg); + g.drawString(shortValue(steps), w1 + 2, h3); + + g.drawImage(shoesIcon, w1 - 6, h3 + radiusOuter - 6); +} + +function drawHeartRate() { + g.setColor(colorGrey); + g.fillCircle(w2, h3, radiusOuter); + + if (hrtValue != undefined) { + const percent = hrtValue / settings.maxHR; + drawGauge(w2, h3, percent, colorRed); + } + + g.setColor(colorBg); + g.fillCircle(w2, h3, radiusInner); + + g.fillPoly([w2, h3, w2 - 15, h3 + radiusOuter + 5, w2 + 15, h3 + radiusOuter + 5]); + + g.setFont("Vector:12"); + g.setFontAlign(0, 0); + g.setColor(colorFg); + g.drawString(hrtValue != undefined ? hrtValue : "-", w2, h3); + + g.drawImage(heartIcon, w2 - 6, h3 + radiusOuter - 6); +} + +function drawBattery() { + const battery = E.getBattery(); + const yellow = '#ffff00'; + g.setColor(colorGrey); + g.fillCircle(w3, h3, radiusOuter); + + if (battery > 0) { + const percent = battery / 100; + drawGauge(w3, h3, percent, yellow); + } + + g.setColor(colorBg); + g.fillCircle(w3, h3, radiusInner); + + g.fillPoly([w3, h3, w3 - 15, h3 + radiusOuter + 5, w3 + 15, h3 + radiusOuter + 5]); + + g.setFont("Vector:12"); + g.setFontAlign(0, 0); + + let icon = powerIcon; + let color = colorFg; + if (Bangle.isCharging()) { + color = colorGreen; + icon = powerIconGreen; + } + else { + if (settings.batteryWarn != undefined && battery <= settings.batteryWarn) { + color = colorRed; + icon = powerIconRed; + } + } + g.setColor(color); + g.drawString(battery + '%', w3, h3); + + g.drawImage(icon, w3 - 6, h3 + radiusOuter - 6); +} + +function radians(a) { + return a * Math.PI / 180; +} + + +function drawGauge(cx, cy, percent, color) { + let offset = 30; + let end = 300; + var i = 0; + var r = radiusInner + 3; + + if (percent > 1) percent = 1; + + var startrot = -offset; + var endrot = startrot - ((end - offset) * percent); + + g.setColor(color); + + // draw gauge + for (i = startrot; i > endrot; i -= 4) { + x = cx + r * Math.sin(radians(i)); + y = cy + r * Math.cos(radians(i)); + g.fillCircle(x, y, 4); + } +} + +function shortValue(v) { + if (isNaN(v)) return '-'; + if (v <= 999) return v; + if (v >= 1000 && v < 10000) { + v = Math.floor(v / 100) * 100; + return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; + } + if (v >= 10000) { + v = Math.floor(v / 1000) * 1000; + return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; + } +} + +function getSteps() { + if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom.getSteps(); + } + return 0; +} + +Bangle.on('lock', function(isLocked) { + if (!isLocked) { + Bangle.setHRMPower(1, "watch"); + } else { + Bangle.setHRMPower(0, "watch"); + } + drawHeartRate(); + drawSteps(); +}); + +Bangle.on('HRM', function(hrm) { + //if(hrm.confidence > 90){ + hrtValue = hrm.bpm; + if (Bangle.isLCDOn()) + drawHeartRate(); + //} else { + // hrtValue = undefined; + //} +}); + +g.clear(); +Bangle.loadWidgets(); +/* + * we are not drawing the widgets as we are taking over the whole screen + * so we will blank out the draw() functions of each widget and change the + * area to the top bar doesn't get cleared. + */ +for (let wd of WIDGETS) { + wd.draw = () => {}; + wd.area = ""; +} +loadSettings(); +setInterval(draw, 60000); +draw(); +Bangle.setUI("clock"); diff --git a/apps/circlesclock/app.png b/apps/circlesclock/app.png new file mode 100644 index 000000000..493bfa567 Binary files /dev/null and b/apps/circlesclock/app.png differ diff --git a/apps/circlesclock/screenshot.png b/apps/circlesclock/screenshot.png new file mode 100644 index 000000000..94ff885fa Binary files /dev/null and b/apps/circlesclock/screenshot.png differ diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js new file mode 100644 index 000000000..ffda51538 --- /dev/null +++ b/apps/circlesclock/settings.js @@ -0,0 +1,43 @@ +(function(back) { + const SETTINGS_FILE = "circlesclock.json"; + const storage = require('Storage'); + let settings = storage.readJSON(SETTINGS_FILE, 1) || {}; + function save(key, value) { + settings[key] = value; + storage.write(SETTINGS_FILE, settings); + } + E.showMenu({ + '': { 'title': 'circlesclock' }, + 'max heartrate': { + value: "maxHR" in settings ? settings.maxHR : 200, + min: 20, + max : 250, + step: 10, + format: x => { + return x; + }, + onchange: x => save('maxHR', x), + }, + 'step goal': { + value: "stepGoal" in settings ? settings.stepGoal : 10000, + min: 2000, + max : 50000, + step: 2000, + format: x => { + return x; + }, + onchange: x => save('stepGoal', x), + }, + 'battery warn lvl': { + value: "batteryWarn" in settings ? settings.batteryWarn : 30, + min: 10, + max : 100, + step: 10, + format: x => { + return x + '%'; + }, + onchange: x => save('batteryWarn', x), + }, + '< Back': back, + }); +}); diff --git a/apps/cliclockJS2Enhanced/ChangeLog b/apps/cliclockJS2Enhanced/ChangeLog new file mode 100644 index 000000000..f4d146d5f --- /dev/null +++ b/apps/cliclockJS2Enhanced/ChangeLog @@ -0,0 +1,3 @@ +0.01: Submitted to App Loader +0.02: Removed unneded code, added HID controlls thanks to t0m1o1 for his code :p +0.03: Load widgets after Bangle.setUI to ensure widgets know if they're on a clock or not (fix #970) diff --git a/apps/cliclockJS2Enhanced/app.icon.js b/apps/cliclockJS2Enhanced/app.icon.js new file mode 100644 index 000000000..b2974fe7a --- /dev/null +++ b/apps/cliclockJS2Enhanced/app.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIA/AH4A/AH4A8gAAKC8gKUC7Rf/C/PM5gDBjnBC6EcC4PBDIIbCC5/BAIIXVA4YXXAoRHUC6R3EC6KnEMAbv/C6oAKC8YA/AH4A/AH4Ax")) \ No newline at end of file diff --git a/apps/cliclockJS2Enhanced/app.js b/apps/cliclockJS2Enhanced/app.js new file mode 100644 index 000000000..b6172b497 --- /dev/null +++ b/apps/cliclockJS2Enhanced/app.js @@ -0,0 +1,158 @@ +var fontsize = g.getWidth()>200 ? 3 : 2; +var fontsizeTime = g.getWidth()>200 ? 4 : 4; + +var fontheight = 10*fontsize; +var fontheightTime = 10*fontsizeTime; +var locale = require("locale"); +var marginTop = 25; +var flag = false; + +var storage = require('Storage'); + +const settings = storage.readJSON('setting.json',1) || { HID: false }; + +var sendHid, next, prev, toggle, up, down, profile; +var lasty = 0; +var lastx = 0; + +if (settings.HID=="kbmedia") { + profile = 'Music'; + sendHid = function (code, cb) { + try { + NRF.sendHIDReport([1,code], () => { + NRF.sendHIDReport([1,0], () => { + if (cb) cb(); + }); + }); + } catch(e) { + print(e); + } + }; + next = function (cb) { sendHid(0x01, cb); }; + prev = function (cb) { sendHid(0x02, cb); }; + toggle = function (cb) { sendHid(0x10, cb); }; + up = function (cb) {sendHid(0x40, cb); }; + down = function (cb) { sendHid(0x80, cb); }; +} else { + E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) { + if (enable) { + settings.HID = "kbmedia"; + require("Storage").write('setting.json', settings); + setTimeout(load, 1000, "hidmsicswipe.app.js"); + } else setTimeout(load, 1000); + }); +} + +if (next) { + setWatch(function(e) { + var len = e.time - e.lastTime; + E.showMessage('lock'); + setTimeout(drawApp, 1000); + Bangle.setLocked(true); + }, BTN1, { edge:"falling",repeat:true,debounce:50}); + Bangle.on('drag', function(e) { + if(!e.b){ + console.log(lasty); + console.log(lastx); + if(lasty > 40){ + writeLine('Down', 3); + // setTimeout(drawApp, 1000); + // Bluetooth.println(JSON.stringify({t:"music", n:"volumedown"})); + down(() => {}); + } + else if(lasty < -40){ + writeLine('Up', 3); + // setTimeout(drawApp, 1000); + //Bluetooth.println(JSON.stringify({t:"music", n:"volumeup"})); + + up(() => {}); + } else if(lastx < -40){ + writeLine('Prev', 3); + // setTimeout(drawApp, 1000); + // Bluetooth.println(JSON.stringify({t:"music", n:"previous"})); + prev(() => {}); + } else if(lastx > 40){ + writeLine('Next', 3); + // setTimeout(drawApp, 1000); + // Bluetooth.println(JSON.stringify({t:"music", n:"next"})); + next(() => {}); + } else if(lastx==0 && lasty==0){ + writeLine('play/pause', 3); + //setTimeout(drawApp, 1000); + // Bluetooth.println(JSON.stringify({t:"music", n:"play"})); + + toggle(() => {}); + } + lastx = 0; + lasty = 0; + } + else{ + lastx = lastx + e.dx; + lasty = lasty + e.dy; + } + }); + +} + + +let textCol = g.theme.dark ? "#0f0" : "#080"; + +function drawAll(){ + updateTime(); + updateRest(new Date()); +} + +function updateRest(now){ + writeLine(locale.dow(now),1); + writeLine(locale.date(now,1),2); +} +function updateTime(){ + if (!Bangle.isLCDOn()) return; + let now = new Date(); + writeLine(locale.time(now,1),0); + writeLine(flag?" ":"_ ",3); + flag = !flag; + if(now.getMinutes() == 0) + updateRest(now); +} +function writeLineStart(line){ + if (line==0){ + g.drawString(">",0,marginTop+(line)*fontheight); + } else { + g.drawString(">",4,marginTop+(line-1)*fontheight + fontheightTime); + + } +} + +function writeLine(str,line){ + if (line == 0){ + var y = marginTop+line*fontheightTime; + g.setFont("6x8",fontsizeTime); + g.setColor(textCol).setFontAlign(-1,-1); + g.clearRect(0,y,((str.length+1)*40),y+fontheightTime-1); + writeLineStart(line); + g.drawString(str,25,y); + } else { + var y = marginTop+(line-1)*fontheight+fontheightTime; + g.setFont("6x8",fontsize); + g.setColor(textCol).setFontAlign(-1,-1); + g.clearRect(0,y,((str.length+10)*40),y+fontheightTime-1); + writeLineStart(line); + g.drawString(str,25,y); + } + +} + +g.clear(); + +Bangle.on('lcdPower',function(on) { + if (on) drawAll(); +}); +var click = setInterval(updateTime, 1000); +// Show launcher when button pressed +Bangle.setUI("clockupdown", btn=>{ + drawAll(); // why do we redraw here?? +}); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +drawAll(); diff --git a/apps/cliclockJS2Enhanced/app.js.png b/apps/cliclockJS2Enhanced/app.js.png new file mode 100644 index 000000000..6d74fcf71 Binary files /dev/null and b/apps/cliclockJS2Enhanced/app.js.png differ diff --git a/apps/cliclockJS2Enhanced/app.png b/apps/cliclockJS2Enhanced/app.png new file mode 100644 index 000000000..e70692034 Binary files /dev/null and b/apps/cliclockJS2Enhanced/app.png differ diff --git a/apps/cliclockJS2Enhanced/screengrab.png b/apps/cliclockJS2Enhanced/screengrab.png new file mode 100644 index 000000000..fd4556fb3 Binary files /dev/null and b/apps/cliclockJS2Enhanced/screengrab.png differ diff --git a/apps/clicompleteclk/ChangeLog b/apps/clicompleteclk/ChangeLog new file mode 100644 index 000000000..50c84593e --- /dev/null +++ b/apps/clicompleteclk/ChangeLog @@ -0,0 +1,3 @@ +0.01: New clock! +0.02: Load steps from Health Tracking app (if installed) +0.03: ... diff --git a/apps/clicompleteclk/README.md b/apps/clicompleteclk/README.md new file mode 100644 index 000000000..8b8094633 --- /dev/null +++ b/apps/clicompleteclk/README.md @@ -0,0 +1,24 @@ +# Command line complete clock + +Command line styled clock with lots of information: + +It can show the following (depending on availability) information: +* Time data: + * Time + * Day of week + * Date +* Additional information (can be toggled via settings): + * Weather conditions and temperature (requires app [Weather](https://banglejs.com/apps/#weather)) + * Steps (requires app [Health Tracking](https://banglejs.com/apps/#health%20tracking) or a step widget) + * Heart rate (when screen is on and unlocked) + +## TODO +* Make time font bigger +* Show progress of steps (if any goal is set) +* Show trend of HRM out of history data + +## Creator +Marco ([myxor](https://github.com/myxor)) + +## Icon +Icon taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 diff --git a/apps/clicompleteclk/app-icon.js b/apps/clicompleteclk/app-icon.js new file mode 100644 index 000000000..b874bb6fa --- /dev/null +++ b/apps/clicompleteclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgI/8/4ACAqYv/F/PwAqgA6A==")) diff --git a/apps/clicompleteclk/app.js b/apps/clicompleteclk/app.js new file mode 100644 index 000000000..a39b37e58 --- /dev/null +++ b/apps/clicompleteclk/app.js @@ -0,0 +1,250 @@ +const storage = require('Storage'); +const locale = require("locale"); + +const font12 = g.getFonts().includes("12x20"); +const font = font12 ? "12x20" : "6x8"; +const fontsize = font12 ? 1: 2; +const fontheight = 19; + +const marginTop = 5; +const marginLeftTopic = 3; // margin of topics +const marginLeftData = font12 ? 64 : 75; // margin of data values + +const topicColor = g.theme.dark ? "#fff" : "#000"; +const textColor = g.theme.dark ? "#0f0" : "#080"; +const textColorRed = g.theme.dark ? "#FF0000" : "#FF0000"; + +let hrtValue; +let hrtValueIsOld = false; + +let localTempValue; +let weatherTempString; +let lastHeartRateRowIndex; +let lastStepsRowIndex; +let i = 2; + +let settings; + +function loadSettings() { + settings = storage.readJSON('clicompleteclk.json', 1) || {}; +} + +function setting(key) { + if (!settings) { loadSettings(); } + const DEFAULTS = { + 'battery': true, + 'batteryLvl': 30, + 'weather': true, + 'steps': true, + 'heartrate': true + }; + return (key in settings) ? settings[key] : DEFAULTS[key]; +} + + +let showBattery = setting('battery'); +let batteryWarnLevel = setting('batteryLvl'); +let showWeather = setting('weather'); +let showSteps = setting('steps'); +let showHeartRate = setting('heartrate'); + + +var drawTimeout; +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + drawAll(true); + }, 60000 - (Date.now() % 60000)); +} + +function drawAll(drawInfoToo){ + let now = new Date(); + updateTime(now); + if (drawInfoToo) { + drawInfo(now); + } + queueDraw(); +} + +function updateTime(now){ + if (!Bangle.isLCDOn()) return; + writeLineTopic("TIME", 1); + writeLine(locale.time(now,1),1); +} + +function drawInfo(now) { + if (now == undefined) + now = new Date(); + + i = 2; + + writeLineTopic("DOWK", i); + writeLine(locale.dow(now),i); + i++; + + writeLineTopic("DATE", i); + writeLine(locale.date(now,1),i); + i++; + + if (showBattery) { + writeLineTopic("BATT", i); + const b = E.getBattery(); + writeLine(b + "%", i, b < batteryWarnLevel ? textColorRed : textColor); + i++; + } + + if (showWeather) { + drawWeather(); + } + + if (showSteps) { + drawSteps(i); + i++; + } + + if (showHeartRate) { + drawHeartRate(i); + } +} + +function drawWeather() { + const weatherJson = getWeather(); + if(weatherJson && weatherJson.weather){ + const currentWeather = weatherJson.weather; + + const weatherTempValue = locale.temp(currentWeather.temp-273.15); + weatherTempString = weatherTempValue; + writeLineTopic("WTHR", i); + writeLine(currentWeather.txt,i); + i++; + + writeLineTopic("TEMP", i); + writeLine(weatherTempValue,i); + i++; + } +} + +function drawSteps(i) { + if (!showSteps) return; + if (i == undefined) + i = lastStepsRowIndex; + const steps = getSteps(); + if (steps != undefined) { + writeLineTopic("STEP", i); + writeLine(steps, i); + } + lastStepsRowIndex = i; +} + +function drawHeartRate(i) { + if (!showHeartRate) return; + if (i == undefined) + i = lastHeartRateRowIndex; + writeLineTopic("HRTM", i); + if (hrtValue != undefined) { + if (!hrtValueIsOld) + writeLine(hrtValue,i); + else + writeLine(hrtValue,i, topicColor); + } + lastHeartRateRowIndex = i; +} + + +function writeLineTopic(str, line) { + var y = marginTop+line*fontheight; + g.setFont(font,fontsize); + g.setColor(topicColor).setFontAlign(-1,-1); + + g.clearRect(0,y,g.getWidth(),y+fontheight-1); + g.drawString("[" + str + "]",marginLeftTopic,y); +} + +function writeLine(str,line,pColor){ + if (pColor == undefined) + pColor = textColor; + var y = marginTop+line*fontheight; + g.setFont(font,fontsize); + g.setColor(pColor).setFontAlign(-1,-1); + g.drawString(str,marginLeftData,y); +} + + +function getSteps() { + var steps = 0; + let health; + try { + health = require("health"); + } catch (e) { + // Module health not found + } + if (health != undefined) { + health.readDay(new Date(), h=>steps+=h.steps); + } else if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom.getSteps(); + } else if (WIDGETS.activepedom !== undefined) { + return WIDGETS.activepedom.getSteps(); + } + return steps; +} + +function getWeather() { + let jsonWeather = storage.readJSON('weather.json'); + return jsonWeather; +} + +// EVENTS: + +// turn on HRM when the LCD is unlocked +Bangle.on('lock', function(isLocked) { + if (!isLocked) { + if (showHeartRate) { + Bangle.setHRMPower(1,"clicompleteclk"); + if (hrtValue == undefined) + hrtValue = "..."; + else + hrtValueIsOld = true; + } + } else { + if (showHeartRate) { + hrtValueIsOld = true; + Bangle.setHRMPower(0,"clicompleteclk"); + } + } + // Update steps and heart rate + drawSteps(); + drawHeartRate(); +}); + +Bangle.on('lcdPower',function(on) { + if (on) { + drawAll(true); + } else { + if (showHeartRate) { + hrtValueIsOld = true; + } + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +if (showHeartRate) { + Bangle.on('HRM', function(hrm) { + //if(hrm.confidence > 90){ + hrtValueIsOld = false; + hrtValue = hrm.bpm; + if (Bangle.isLCDOn()) + drawHeartRate(); + //} else { + // hrtValue = undefined; + //} + }); +} + +g.clear(); +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +loadSettings(); +drawAll(true); diff --git a/apps/clicompleteclk/app.png b/apps/clicompleteclk/app.png new file mode 100644 index 000000000..104e6124a Binary files /dev/null and b/apps/clicompleteclk/app.png differ diff --git a/apps/clicompleteclk/settings.js b/apps/clicompleteclk/settings.js new file mode 100644 index 000000000..2df20ed3e --- /dev/null +++ b/apps/clicompleteclk/settings.js @@ -0,0 +1,54 @@ +(function(back) { + const storage = require('Storage'); + let settings = storage.readJSON('clicompleteclk.json', 1) || {}; + function save(key, value) { + settings[key] = value; + storage.write('clicompleteclk.json', settings); + } + E.showMenu({ + '': { 'title': 'CLI complete clk' }, + 'Show battery': { + value: "battery" in settings ? settings.battery : false, + format: () => (settings.battery ? 'Yes' : 'No'), + onchange: () => { + settings.battery = !settings.battery; + save('battery', settings.battery); + }, + }, + 'Battery warn': { + value: "batteryLvl" in settings ? settings.batteryLvl : 30, + min: 0, + max : 100, + step: 10, + format: x => { + return x + "%"; + }, + onchange: x => save('batteryLvl', x), + }, + 'Show weather': { + value: "weather" in settings ? settings.weather : false, + format: () => (settings.weather ? 'Yes' : 'No'), + onchange: () => { + settings.weather = !settings.weather; + save('weather', settings.weather); + }, + }, + 'Show steps': { + value: "steps" in settings ? settings.steps : false, + format: () => (settings.steps ? 'Yes' : 'No'), + onchange: () => { + settings.steps = !settings.steps; + save('steps', settings.steps); + }, + }, + 'Show heartrate': { + value: "heartrate" in settings ? settings.heartrate : false, + format: () => (settings.heartrate ? 'Yes' : 'No'), + onchange: () => { + settings.heartrate = !settings.heartrate; + save('heartrate', settings.heartrate); + }, + }, + '< Back': back, + }); +}); diff --git a/apps/cliock/ChangeLog b/apps/cliock/ChangeLog index 07b38e189..68249b622 100644 --- a/apps/cliock/ChangeLog +++ b/apps/cliock/ChangeLog @@ -1,5 +1,10 @@ 0.07: Submitted to App Loader -0.08: Fixes issue where face would redraw on wake leading to all memory being used and watch crashing. +0.08: Fixes issue where face would redraw on wake leading to all memory being used and watch crashing. 0.09: Add BTN1 status line with ID,Fw ver, mem %, battery % 0.10: Icon fixed for transparency 0.11: added Heart Rate Monitor status and ability to turn on/off +0.12: added support for different locales +0.13: Use setUI, work with smaller screens and themes +0.14: Fix BTN1 (fix #853) + Add light/dark theme support +0.15: Load widgets after Bangle.setUI to ensure widgets know if they're on a clock or not (fix #970) diff --git a/apps/cliock/app.js b/apps/cliock/app.js index ca48bb26f..d9271bf15 100644 --- a/apps/cliock/app.js +++ b/apps/cliock/app.js @@ -1,8 +1,8 @@ -var fontsize = 3; +var fontsize = g.getWidth()>200 ? 3 : 2; +var fontheight = 10*fontsize; var locale = require("locale"); var marginTop = 40; var flag = false; -var WeekDays = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]; var hrtOn = false; var hrtStr = "Hrt: ??? bpm"; @@ -20,47 +20,43 @@ const HRT_FN_MODE = "fn_hrt"; let infoMode = NONE_MODE; let functionMode = NONE_FN_MODE; +let textCol = g.theme.dark ? "#0f0" : "#080"; + function drawAll(){ updateTime(); updateRest(new Date()); } function updateRest(now){ - let date = locale.date(now,false); - writeLine(WeekDays[now.getDay()],1); - writeLine(date,2); + writeLine(locale.dow(now),1); + writeLine(locale.date(now,1),2); drawInfo(5); } function updateTime(){ if (!Bangle.isLCDOn()) return; let now = new Date(); - let h = now.getHours(); - let m = now.getMinutes(); - h = h>=10?h:"0"+h; - m = m>=10?m:"0"+m; - writeLine(h+":"+m,0); + writeLine(locale.time(now,1),0); writeLine(flag?" ":"_",3); flag = !flag; if(now.getMinutes() == 0) updateRest(now); } function writeLineStart(line){ - g.drawString(">",4,marginTop+line*30); + g.drawString(">",4,marginTop+line*fontheight); } function writeLine(str,line){ + var y = marginTop+line*fontheight; g.setFont("6x8",fontsize); - //g.setColor(0,1,0); - g.setColor(0,0x07E0,0); - g.setFontAlign(-1,-1); - g.clearRect(0,marginTop+line*30,((str.length+1)*20),marginTop+25+line*30); + g.setColor(textCol).setFontAlign(-1,-1); + g.clearRect(0,y,((str.length+1)*20),y+fontheight-1); writeLineStart(line); - g.drawString(str,25,marginTop+line*30); -} + g.drawString(str,25,y); +} function drawInfo(line) { let val; let str = ""; - let col = 0x07E0; // green + let col = textCol; // green //console.log("drawInfo(), infoMode=" + infoMode + " funcMode=" + functionMode); @@ -68,15 +64,15 @@ function drawInfo(line) { case NONE_FN_MODE: break; case HRT_FN_MODE: - col = 0x07FF; // cyan + col = g.theme.dark ? "#0ff": "#088"; // cyan str = "HRM: " + (hrtOn ? "ON" : "OFF"); drawModeLine(line,str,col); return; } - + switch(infoMode) { case NONE_MODE: - col = 0x0000; + col = g.theme.bg; str = ""; break; case HRT_MODE: @@ -106,10 +102,10 @@ function drawInfo(line) { function drawModeLine(line, str, col) { g.setColor(col); - g.fillRect(0, marginTop-3+line*30, 239, marginTop+25+line*30); - g.setColor(0,0,0); - g.setFontAlign(0, -1); - g.drawString(str, g.getWidth()/2, marginTop+line*30); + var y = marginTop+line*fontheight; + g.fillRect(0, y, 239, y+fontheight-1); + g.setColor(g.theme.bg).setFontAlign(0, 0); + g.drawString(str, g.getWidth()/2, y+fontheight/2); } function changeInfoMode() { @@ -166,7 +162,7 @@ function changeFunctionMode() { functionMode = NONE_FN_MODE; } //console.log(functionMode); - + } function stepsWidget() { @@ -187,14 +183,16 @@ Bangle.on('HRM', function(hrm) { }); g.clear(); +Bangle.on('lcdPower',function(on) { + if (on) drawAll(); +}); +var click = setInterval(updateTime, 1000); +// Show launcher when button pressed +Bangle.setUI("clockupdown", btn=>{ + if (btn<0) changeInfoMode(); + if (btn>0) changeFunctionMode(); + drawAll(); +}); Bangle.loadWidgets(); Bangle.drawWidgets(); drawAll(); -Bangle.on('lcdPower',function(on) { - if (on) - drawAll(); -}); -var click = setInterval(updateTime, 1000); -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); -setWatch(() => { changeInfoMode(); drawAll(); }, BTN1, {repeat: true}); -setWatch(() => { changeFunctionMode(); drawAll(); }, BTN3, {repeat: true}); diff --git a/apps/cliock/screenshot_cli.png b/apps/cliock/screenshot_cli.png new file mode 100644 index 000000000..fe1c6299b Binary files /dev/null and b/apps/cliock/screenshot_cli.png differ diff --git a/apps/clock2x3/ChangeLog b/apps/clock2x3/ChangeLog index 88876affa..ef8057d6e 100644 --- a/apps/clock2x3/ChangeLog +++ b/apps/clock2x3/ChangeLog @@ -1,3 +1,4 @@ 0.02: Modified for use with new bootloader and firmware 0.03: Added 'reset' so we don't get the font color from widgets 0.04: Changed name from clck3x2 to clock2x3 +0.05: Use setUI, work with smaller screens and themes diff --git a/apps/clock2x3/README.md b/apps/clock2x3/README.md new file mode 100644 index 000000000..0b5d25f9d --- /dev/null +++ b/apps/clock2x3/README.md @@ -0,0 +1,4 @@ +# 2x3 Pixel Clock + +![](screenshot_pixel.png) + diff --git a/apps/clock2x3/clock2x3-app.js b/apps/clock2x3/clock2x3-app.js index 4caec28cb..bfe6a9ea8 100644 --- a/apps/clock2x3/clock2x3-app.js +++ b/apps/clock2x3/clock2x3-app.js @@ -1,8 +1,10 @@ + +const big = g.getWidth()>200; const ox=10; // x offset -const oy=80; -const pw=20; // pixel width -const ps=5; // pixel spacing -const ds=10; // digit spacing +const oy=big ? 80 : 70; +const pw=big ? 20 : 14; // pixel width +const ps=big ? 5 : 3; // pixel spacing +const ds=big ? 10 : 8; // digit spacing const ms=20; // middle space const x00=ox; // digit 0, pixel 0, x position @@ -90,7 +92,7 @@ Bangle.on('lcdPower', function(on){ } }); +// Show launcher when button pressed +Bangle.setUI("clock"); Bangle.loadWidgets(); drawTime(); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); diff --git a/apps/clock2x3/screenshot_pixel.png b/apps/clock2x3/screenshot_pixel.png new file mode 100644 index 000000000..4b09f06a1 Binary files /dev/null and b/apps/clock2x3/screenshot_pixel.png differ diff --git a/apps/clotris/bangle1-clock-tris-screenshot.png b/apps/clotris/bangle1-clock-tris-screenshot.png new file mode 100644 index 000000000..4b7a7257f Binary files /dev/null and b/apps/clotris/bangle1-clock-tris-screenshot.png differ diff --git a/apps/compass/ChangeLog b/apps/compass/ChangeLog index e70a5688b..4bb7838ac 100644 --- a/apps/compass/ChangeLog +++ b/apps/compass/ChangeLog @@ -1,3 +1,5 @@ 0.01: New App! 0.02: Show text if uncalibrated -0.03: Eliminate flickering \ No newline at end of file +0.03: Eliminate flickering +0.04: Fix for Bangle.js 2 and themes +0.05: Fix bearing not clearing correctly (visible in single or double digit bearings) diff --git a/apps/compass/compass.js b/apps/compass/compass.js index 9b7ed56b7..65ad83c4f 100644 --- a/apps/compass/compass.js +++ b/apps/compass/compass.js @@ -1,60 +1,72 @@ -var tg = Graphics.createArrayBuffer(120,20,1,{msb:true}); -var timg = { - width:tg.getWidth(), - height:tg.getHeight(), - bpp:1, - buffer:tg.buffer -}; - -var ag = Graphics.createArrayBuffer(160,160,2,{msb:true}); +var W = g.getWidth(); +var M = W/2; // middle of screen +// Angle buffer +var AGS = W > 200 ? 160 : 120; // buffer size +var AGM = AGS/2; // midpoint/radius +var AGH = AGM-10; // hand size +var ag = Graphics.createArrayBuffer(AGS,AGS,2,{msb:true}); var aimg = { width:ag.getWidth(), height:ag.getHeight(), bpp:2, buffer:ag.buffer, - palette:new Uint16Array([0,0x03FF,0xF800,0x001F]) + palette:new Uint16Array([ + g.theme.bg, + g.toColor("#07f"), + g.toColor("#f00"), + g.toColor("#00f")]) }; -ag.setColor(1); -ag.fillCircle(80,80,79,79); -ag.setColor(0); -ag.fillCircle(80,80,69,69); +ag.setColor(1).fillCircle(AGM,AGM,AGM-1,AGM-1); +ag.setColor(0).fillCircle(AGM,AGM,AGM-11,AGM-11); function arrow(r,c) { r=r*Math.PI/180; var p = Math.PI/2; - ag.setColor(c); - ag.fillPoly([ - 80+60*Math.sin(r), 80-60*Math.cos(r), - 80+10*Math.sin(r+p), 80-10*Math.cos(r+p), - 80+10*Math.sin(r-p), 80-10*Math.cos(r-p), + ag.setColor(c).fillPoly([ + AGM+AGH*Math.sin(r), AGM-AGH*Math.cos(r), + AGM+10*Math.sin(r+p), AGM-10*Math.cos(r+p), + AGM+10*Math.sin(r-p), AGM-10*Math.cos(r-p), ]); } +var wasUncalibrated = false; var oldHeading = 0; Bangle.on('mag', function(m) { if (!Bangle.isLCDOn()) return; - tg.clear(); - tg.setFont("6x8",1); - tg.setColor(1); + g.reset(); if (isNaN(m.heading)) { - tg.setFontAlign(0,-1); - tg.setFont("6x8",1); - tg.drawString("Uncalibrated",60,4); - tg.drawString("turn 360° around",60,12); + if (!wasUncalibrated) { + g.clearRect(0,24,W,48); + g.setFontAlign(0,-1).setFont("6x8"); + g.drawString("Uncalibrated\nturn 360° around",M,24+4); + wasUncalibrated = true; + } + } else { + if (wasUncalibrated) { + g.clearRect(0,24,W,48); + wasUncalibrated = false; + } + g.setFontAlign(0,0).setFont("6x8",3); + var y = 36; + g.clearRect(M-40,24,M+40,48); + g.drawString(Math.round(m.heading),M,y,true); } - else { - tg.setFontAlign(0,0); - tg.setFont("6x8",2); - tg.drawString(Math.round(m.heading),60,12); - } - g.drawImage(timg,0,0,{scale:2}); + ag.setColor(0); arrow(oldHeading,0); arrow(oldHeading+180,0); arrow(m.heading,2); arrow(m.heading+180,3); - g.drawImage(aimg,40,50); + g.drawImage(aimg, + (W-ag.getWidth())/2, + g.getHeight()-(ag.getHeight()+4)); oldHeading = m.heading; }); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); Bangle.setCompassPower(1); +Bangle.setLCDPower(1); +Bangle.setLCDTimeout(0); diff --git a/apps/compass/screenshot_compass.png b/apps/compass/screenshot_compass.png new file mode 100644 index 000000000..63579bab7 Binary files /dev/null and b/apps/compass/screenshot_compass.png differ diff --git a/apps/coretemp/ChangeLog b/apps/coretemp/ChangeLog new file mode 100644 index 000000000..115067b80 --- /dev/null +++ b/apps/coretemp/ChangeLog @@ -0,0 +1 @@ +0.01: New app diff --git a/apps/coretemp/README.md b/apps/coretemp/README.md new file mode 100644 index 000000000..fac25df21 --- /dev/null +++ b/apps/coretemp/README.md @@ -0,0 +1,20 @@ +# CoreTemp display + +Basic bare-bones example of connecting to a bluetooth [CoreTemp](https://corebodytemp.com/) device and displaying the current body core temperature readings. + +## Usage + +On startup connects to a CoreTemp device (1809/2A1C) and emits a "Core, temp" value for each reading. +The app simply displays these readings on screen. + +## TODO + +* Integrate with other tracking/sports apps to log data. +* Add device selection +* Provide enable/disable option +* Check status, add Retry/reconnect +* Also provide skin temp reading + +## Creator + +Ivor Hewitt diff --git a/apps/coretemp/boot.js b/apps/coretemp/boot.js new file mode 100644 index 000000000..59e227dad --- /dev/null +++ b/apps/coretemp/boot.js @@ -0,0 +1,23 @@ +(function() { + var gatt; + + //Would it be better to scan by uuid rather than name? + NRF.requestDevice({ timeout: 20000, filters: [{ name: 'CORE [a]' }] }).then(function(device) { + return device.gatt.connect(); + }).then(function(g) { + gatt = g; + return gatt.getPrimaryService("1809"); + }).then(function(service) { + return service.getCharacteristic("2A1C"); + }).then(function(characteristic) { + characteristic.on('characteristicvaluechanged', function(event) { + var dv = event.target.value; + var core = (dv.buffer[2]*256+dv.buffer[1])/100; + Bangle.emit('Core',{ + temp:core + }); + }); + return characteristic.startNotifications(); + }).then(function() { + }); +})(); diff --git a/apps/coretemp/coretemp-icon.js b/apps/coretemp/coretemp-icon.js new file mode 100644 index 000000000..5f36b9090 --- /dev/null +++ b/apps/coretemp/coretemp-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///k0DxUFgsDCY8KwAfJlQLHhWglWq1WgBIcCA4QCB1WoComq0+iBYWqCwl//4OBAAQxChWlv/2BYIlCBYUqv9VvQLBwA9BBYWlqtV/QLBGoRIBgQLBr9aBYQ2BBYMKroLBtQLCgALClIKC1AXG1NVuoFBF4sC09V+woCBAJHCgWXq9oPQZrDgWdq9gBZG9rqgCTwSbCgVVqysDBYkK6tWYoa/DkEJ6vaaIgWBaAILCbQhUCBYXoc4wNBBZWqBfBtB1ALKKZILCR4J3FToQLBU4KPEWoQLNZYILIa4NVcYReEcYOnqtaDAbvDgALBcg4EBlNVqtqDoOgd4YoBBYNWytWCwQdCgQLBAAVaBYkA0oLDuwLFkv1BgZGDAAMJuoKCroWEGAOnDAVftShGr////1tDdG14LB+wiEAAdqHAjTHBYgA==")) diff --git a/apps/coretemp/coretemp.js b/apps/coretemp/coretemp.js new file mode 100644 index 000000000..226508c83 --- /dev/null +++ b/apps/coretemp/coretemp.js @@ -0,0 +1,19 @@ +Bangle.setLCDPower(1); +Bangle.setLCDTimeout(0); +var btm = g.getHeight()-1; + +function onCore(c) { + var px = g.getWidth()/2; + g.setFontAlign(0,0); + g.clearRect(0,24,g.getWidth(),80); + var str = c.temp + "C"; + g.setFontVector(40).drawString(str,px,45); +} +Bangle.on('Core', onCore); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +g.reset().setFont("6x8",2).setFontAlign(0,0); +g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16); diff --git a/apps/coretemp/coretemp.png b/apps/coretemp/coretemp.png new file mode 100644 index 000000000..a573828f8 Binary files /dev/null and b/apps/coretemp/coretemp.png differ diff --git a/apps/counter/bangle1-counter-screenshot.png b/apps/counter/bangle1-counter-screenshot.png new file mode 100644 index 000000000..1d6c471bf Binary files /dev/null and b/apps/counter/bangle1-counter-screenshot.png differ diff --git a/apps/cprassist/bangle1-CPR-assist-screenshot.png b/apps/cprassist/bangle1-CPR-assist-screenshot.png new file mode 100644 index 000000000..9d217efce Binary files /dev/null and b/apps/cprassist/bangle1-CPR-assist-screenshot.png differ diff --git a/apps/cscsensor/ChangeLog b/apps/cscsensor/ChangeLog index 7be2ed3e2..8f23fa9f3 100644 --- a/apps/cscsensor/ChangeLog +++ b/apps/cscsensor/ChangeLog @@ -2,4 +2,6 @@ 0.02: Add wheel circumference settings dialog 0.03: Save total distance traveled 0.04: Add sensor battery level indicator - +0.05: Add cadence sensor support +0.06: Now read wheel rev as well as cadence sensor + Improve connection code diff --git a/apps/cscsensor/README.md b/apps/cscsensor/README.md index a31a4dc28..9740fd9cf 100644 --- a/apps/cscsensor/README.md +++ b/apps/cscsensor/README.md @@ -9,10 +9,16 @@ Currently the app displays the following data: - maximum speed - trip distance traveled - total distance traveled -- an icon with the battery status of the remote sensor +- an icon with the battery status of the remote sensor Button 1 resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app. If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 will attempt to reconnect to the sensor. +Button 2 switches between the display for cycling speed and cadence. -I do not have access to a cadence sensor at the moment, so only the speed part is currently implemented. Values displayed are imperial or metric (depending on locale), -the wheel circumference can be adjusted in the global settings app. +Values displayed are imperial or metric (depending on locale), cadence is in RPM, the wheel circumference can be adjusted in the global settings app. + +# TODO + +* Use Layout Library to provide proper Bangle.js 2 support +* Turn CSC sensor support into a library +* Support for `Recorder` app, to allow CSC readings to be logged alongside GPS diff --git a/apps/cscsensor/cscsensor.app.js b/apps/cscsensor/cscsensor.app.js index c402c06da..e2af0db16 100644 --- a/apps/cscsensor/cscsensor.app.js +++ b/apps/cscsensor/cscsensor.app.js @@ -5,6 +5,8 @@ var characteristic; const SETTINGS_FILE = 'cscsensor.json'; const storage = require('Storage'); +const W = g.getWidth(); +const H = g.getHeight(); class CSCSensor { constructor() { @@ -28,6 +30,10 @@ class CSCSensor { this.distFactor = this.qMetric ? 1.609344 : 1; this.screenInit = true; this.batteryLevel = -1; + this.lastCrankTime = 0; + this.lastCrankRevs = 0; + this.showCadence = false; + this.cadence = 0; } reset() { @@ -40,6 +46,11 @@ class CSCSensor { this.screenInit = true; } + toggleDisplayCadence() { + this.showCadence = !this.showCadence; + this.screenInit = true; + } + setBatteryLevel(level) { if (level!=this.batteryLevel) { this.batteryLevel = level; @@ -62,11 +73,11 @@ class CSCSensor { else g.setFontVector(14).setFontAlign(0, 0, 0).setColor(0xffff).drawString("?", 16, 66); } - updateScreen() { + updateScreenRevs() { var dist = this.distFactor*(this.lastRevs-this.lastRevsStart)*this.wheelCirc/63360.0; var ddist = Math.round(100*dist)/100; var tdist = Math.round(this.distFactor*this.totaldist*10)/10; - var dspeed = Math.round(10*this.distFactor*this.speed)/10; + var dspeed = Math.round(10*this.distFactor*this.speed)/10; var dmins = Math.floor(this.movingTime/60).toString(); if (dmins.length<2) dmins = "0"+dmins; var dsecs = (Math.floor(this.movingTime) % 60).toString(); @@ -108,10 +119,52 @@ class CSCSensor { g.setColor(0).fillRect(88, 209, 238, 238); g.setColor(0xffff).drawString(tdist + " " + this.distUnit, 92, 226); } - + + updateScreenCadence() { + if (this.screenInit) { + for (var i=0; i<2; ++i) { + if ((i&1)==0) g.setColor(0, 0, 0); + else g.setColor(0x30cd); + g.fillRect(0, 48+i*32, 86, 48+(i+1)*32); + if ((i&1)==1) g.setColor(0); + else g.setColor(0x30cd); + g.fillRect(87, 48+i*32, 239, 48+(i+1)*32); + g.setColor(0.5, 0.5, 0.5).drawRect(87, 48+i*32, 239, 48+(i+1)*32).drawLine(0, 239, 239, 239);//.drawRect(0, 48, 87, 239); + g.moveTo(0, 80).lineTo(30, 80).lineTo(30, 48).lineTo(87, 48).lineTo(87, 239).lineTo(0, 239).lineTo(0, 80); + } + g.setFontAlign(1, 0, 0).setFontVector(19).setColor(1, 1, 0); + g.drawString("Cadence:", 87, 98); + this.drawBatteryIcon(); + this.screenInit = false; + } + g.setFontAlign(-1, 0, 0).setFontVector(26); + g.setColor(0).fillRect(88, 81, 238, 111); + g.setColor(0xffff).drawString(Math.round(this.cadence), 92, 98); + } + + updateScreen() { + if (!this.showCadence) { + this.updateScreenRevs(); + } else { + this.updateScreenCadence(); + } + } + updateSensor(event) { var qChanged = false; if (event.target.uuid == "0x2a5b") { + if (event.target.value.getUint8(0, true) & 0x2) { + // crank revolution - if enabled + const crankRevs = event.target.value.getUint16(1, true); + const crankTime = event.target.value.getUint16(3, true); + if (crankTime > this.lastCrankTime) { + this.cadence = (crankRevs-this.lastCrankRevs)/(crankTime-this.lastCrankTime)*(60*1024); + qChanged = true; + } + this.lastCrankRevs = crankRevs; + this.lastCrankTime = crankTime; + } + // wheel revolution var wheelRevs = event.target.value.getUint32(1, true); var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0); if (dRevs>0) { @@ -140,7 +193,7 @@ class CSCSensor { else { this.speedFailed++; qChanged = false; - if (this.speedFailed>3) { + if (this.speedFailed>3) { this.speed = 0; qChanged = (this.lastSpeed>0); } @@ -163,43 +216,47 @@ function getSensorBatteryLevel(gatt) { }); } -function parseDevice(d) { - device = d; - g.clearRect(0, 60, 239, 239).setFontAlign(0, 0, 0).setColor(0, 1, 0).drawString("Found device", 120, 120).flip(); - device.gatt.connect().then(function(ga) { - gatt = ga; - g.clearRect(0, 60, 239, 239).setFontAlign(0, 0, 0).setColor(0, 1, 0).drawString("Connected", 120, 120).flip(); - return gatt.getPrimaryService("1816"); -}).then(function(s) { - service = s; - return service.getCharacteristic("2a5b"); -}).then(function(c) { - characteristic = c; - characteristic.on('characteristicvaluechanged', (event)=>mySensor.updateSensor(event)); - return characteristic.startNotifications(); -}).then(function() { - console.log("Done!"); - g.clearRect(0, 60, 239, 239).setColor(1, 1, 1).flip(); - getSensorBatteryLevel(gatt); - mySensor.updateScreen(); -}).catch(function(e) { - g.clearRect(0, 60, 239, 239).setColor(1, 0, 0).setFontAlign(0, 0, 0).drawString("ERROR"+e, 120, 120).flip(); - console.log(e); -})} - function connection_setup() { - NRF.setScan(); mySensor.screenInit = true; - NRF.setScan(parseDevice, { filters: [{services:["1816"]}], timeout: 2000}); - g.clearRect(0, 48, 239, 239).setFontVector(18).setFontAlign(0, 0, 0).setColor(0, 1, 0); - g.drawString("Scanning for CSC sensor...", 120, 120); + E.showMessage("Scanning for CSC sensor..."); + NRF.requestDevice({ filters: [{services:["1816"]}]}).then(function(d) { + device = d; + E.showMessage("Found device"); + return device.gatt.connect(); + }).then(function(ga) { + gatt = ga; + E.showMessage("Connected"); + return gatt.getPrimaryService("1816"); + }).then(function(s) { + service = s; + return service.getCharacteristic("2a5b"); + }).then(function(c) { + characteristic = c; + characteristic.on('characteristicvaluechanged', (event)=>mySensor.updateSensor(event)); + return characteristic.startNotifications(); + }).then(function() { + console.log("Done!"); + g.reset().clearRect(Bangle.appRect).flip(); + getSensorBatteryLevel(gatt); + mySensor.updateScreen(); + }).catch(function(e) { + E.showMessage(e.toString(), "ERROR"); + console.log(e); + }); } connection_setup(); -setWatch(function() { mySensor.reset(); g.clearRect(0, 48, 239, 239); mySensor.updateScreen(); }, BTN1, {repeat:true, debounce:20}); -E.on('kill',()=>{ if (gatt!=undefined) gatt.disconnect(); mySensor.settings.totaldist = mySensor.totaldist; storage.writeJSON(SETTINGS_FILE, mySensor.settings); }); -setWatch(function() { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); }, BTN3, {repeat:true, debounce:20}); -NRF.on('disconnect', connection_setup); +E.on('kill',()=>{ + if (gatt!=undefined) gatt.disconnect(); + mySensor.settings.totaldist = mySensor.totaldist; + storage.writeJSON(SETTINGS_FILE, mySensor.settings); +}); +NRF.on('disconnect', connection_setup); // restart if disconnected +Bangle.setUI("updown", d=>{ + if (d<0) { mySensor.reset(); g.clearRect(0, 48, W, H); mySensor.updateScreen(); } + if (d==0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); } + if (d>0) { mySensor.toggleDisplayCadence(); g.clearRect(0, 48, W, H); mySensor.updateScreen(); } +}); Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/ctrclk/ChangeLog b/apps/ctrclk/ChangeLog index 7819dbe2a..e4659c1ae 100644 --- a/apps/ctrclk/ChangeLog +++ b/apps/ctrclk/ChangeLog @@ -1 +1,2 @@ 0.02: Modified for use with new bootloader and firmware +0.03: Changed setWatch to Bangle.setUI diff --git a/apps/ctrclk/app.js b/apps/ctrclk/app.js index 060aac2f9..7f6ab0570 100644 --- a/apps/ctrclk/app.js +++ b/apps/ctrclk/app.js @@ -46,26 +46,25 @@ function drawSegment (align, base, str) { point = base + (maxSegmentWidth / 2) - (g.stringWidth(str) / 2); } - g.setColor(1, 1, 1); + g.setColor(g.theme.fg); g.drawString(str, point, middleY - 4, false); } function drawDots (center) { - g.setColor(0xFD20); + g.setColor("#FA0"); g.fillCircle(center, middleY + 10, 2); g.fillCircle(center, middleY + 40, 2); } function drawLines () { - g.setColor(0.5, 0.5, 0.5); + g.setColor("#777"); g.drawLine(middleX - lineLength, lineY1, middleX + lineLength, lineY1); g.drawLine(middleX - lineLength, lineY2, middleX + lineLength, lineY2); } function drawDate (str) { let maxSegmentWidth = 236; - g.setColor(0.5, 0.5, 0.5); - g.setColor(0.5, 0.5, 0.5); + g.setColor("#777"); g.drawString(str, (maxSegmentWidth) - (g.stringWidth(str)), middleY - 22, false); } @@ -149,6 +148,9 @@ function start () { } start(); +// Show launcher when middle button pressed +Bangle.setUI("clock"); + Bangle.loadWidgets(); Bangle.drawWidgets(); Bangle.on('lcdPower', function (on) { @@ -158,6 +160,3 @@ Bangle.on('lcdPower', function (on) { stop(); } }); - -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); diff --git a/apps/ctrclk/bangle1-center-clock-screenshot.png b/apps/ctrclk/bangle1-center-clock-screenshot.png new file mode 100644 index 000000000..613fa4fb5 Binary files /dev/null and b/apps/ctrclk/bangle1-center-clock-screenshot.png differ diff --git a/apps/cubescramble/ChangeLog b/apps/cubescramble/ChangeLog new file mode 100644 index 000000000..46852864a --- /dev/null +++ b/apps/cubescramble/ChangeLog @@ -0,0 +1,4 @@ +0.01: Initial Release +0.02: Replace icon with one found on https://icons8.com +0.03: Re-render icon fixing display in settings +0.04: Improved UX and display solve time diff --git a/apps/cubescramble/README.md b/apps/cubescramble/README.md new file mode 100644 index 000000000..1c1603372 --- /dev/null +++ b/apps/cubescramble/README.md @@ -0,0 +1,17 @@ +# Cube Scramble + +A random scramble generator for the 3x3 Rubik's cube with a basic timer. + +## Future features + +I'm keen to complete this project with + +* Add the ability for times to be stored and exported + +## Requests + +Please reach out if you have feature requests or notice bugs. + +## Creator + +Made by [Nathan Lisgo](https://github.com/nlisgo) diff --git a/apps/cubescramble/bangle1-cube-scramble-screenshot.png b/apps/cubescramble/bangle1-cube-scramble-screenshot.png new file mode 100644 index 000000000..5a35238e3 Binary files /dev/null and b/apps/cubescramble/bangle1-cube-scramble-screenshot.png differ diff --git a/apps/cubescramble/bangle2-cube-scramble-screenshot.png b/apps/cubescramble/bangle2-cube-scramble-screenshot.png new file mode 100644 index 000000000..ae37b4aff Binary files /dev/null and b/apps/cubescramble/bangle2-cube-scramble-screenshot.png differ diff --git a/apps/cubescramble/cube-scramble-icon.js b/apps/cubescramble/cube-scramble-icon.js new file mode 100644 index 000000000..d22d6d33b --- /dev/null +++ b/apps/cubescramble/cube-scramble-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AHcRACAWEgIXRiAXEjGIAAWBif/AAUxj3uAAXRC5cfC4fxC4nhC/4Xv+aPFC50zAAIXSiYWCC7Mda5T/JAA4Xeu4ACiMU7oACoMTVocxC4sXC4dxjoXD6LFFC//M4IXS7nMAAIXSjgWCC4vzR5gXHeoYXRiYWCC47XCu1ma4IXDa4IXLACAXdj3uAAXRjGIAAWBKYQABC5fhC4qDFC/4X/C9sda5t2szXGf5IAHC4kAC6IWEAH4A1")) diff --git a/apps/cubescramble/cube-scramble.js b/apps/cubescramble/cube-scramble.js new file mode 100644 index 000000000..73c4e95ef --- /dev/null +++ b/apps/cubescramble/cube-scramble.js @@ -0,0 +1,93 @@ +// Scramble code from: https://raw.githubusercontent.com/bjcarlson42/blog-post-sample-code/master/Rubik's%20Cube%20JavaScript%20Scrambler/part_two.js +const makeScramble = () => { + const options = ["F", "F2", "F'", "R", "R2", "R'", "U", "U2", "U'", "B", "B2", "B'", "L", "L2", "L'", "D", "D2", "D'"]; + const numOptions = [0, 1, 2, 3, 4, 5]; // 0 = F, 1 = R, 2 = U, 3 = B, 4 = L, 5 = D + const scrambleMoves = []; + let bad = true; + + while (bad) { + let scramble = []; + for (let i = 0; i < 20; i++) { + scramble.push(numOptions[getRandomInt(6)]); + } + // check if moves directly next to each other involve the same letter + for (let i = 0; i < 20 - 1; i++) { + if (scramble[i] == scramble[i + 1]) { + bad = true; + break; + } else { + bad = false; + } + } + } + // switch numbers to letters + let move; + for (let i = 0; i < 20; i++) { + switch (scramble[i]) { + case 0: + move = options[getRandomInt(3)]; // 0,1,2 + scrambleMoves.push(move); + break; + case 1: + move = options[getRandomIntBetween(3, 6)]; // 3,4,5 + scrambleMoves.push(move); + break; + case 2: + move = options[getRandomIntBetween(6, 9)]; // 6,7,8 + scrambleMoves.push(move); + break; + case 3: + move = options[getRandomIntBetween(9, 12)]; // 9,10,11 + scrambleMoves.push(move); + break; + case 4: + move = options[getRandomIntBetween(12, 15)]; // 12,13,14 + scrambleMoves.push(move); + break; + case 5: + move = options[getRandomIntBetween(15, 18)]; // 15,16,17 + scrambleMoves.push(move); + break; + } + } + return scrambleMoves; +}; + +const getRandomInt = max => Math.floor(Math.random() * Math.floor(max)); // returns up to max - 1 + +const getRandomIntBetween = (min, max) => Math.floor(Math.random() * (max - min) + min); + +const presentScramble = () => { + showPrompt(makeScramble().join(" "), { + buttons: {"solve": true, "reset": false} + }).then((v) => { + if (v) { + const start = new Date(); + showPrompt(" ", { + buttons: {"stop": true} + }).then(() => { + const time = parseFloat(((new Date()).getTime() - start.getTime()) / 1000); + showPrompt(String(time.toFixed(3)), { + buttons: {"next": true} + }).then(() => { + presentScramble(); + }); + }); + } else { + presentScramble(); + } + }); +}; + +const showPrompt = (text, options = {}) => { + options.title = options.title || "cube scramble"; + return E.showPrompt(text, options); +}; + +const init = () => { + Bangle.setLCDTimeout(0); + Bangle.setLCDPower(1); + presentScramble(); +}; + +init(); diff --git a/apps/cubescramble/cube-scramble.png b/apps/cubescramble/cube-scramble.png new file mode 100644 index 000000000..cdf4a31c2 Binary files /dev/null and b/apps/cubescramble/cube-scramble.png differ diff --git a/apps/dclock/ChangeLog b/apps/dclock/ChangeLog index edf7da4c2..aa8ae23fe 100644 --- a/apps/dclock/ChangeLog +++ b/apps/dclock/ChangeLog @@ -7,3 +7,4 @@ 0.07: add days in current month (md) and days since new moon (l) 0.08: update icon 0.09: Use localised month and day of the week from locale +0.10: Changed setWatch to Bangle.setUI and allow small screen diff --git a/apps/dclock/bangle1-dev-clock-screenshot.png b/apps/dclock/bangle1-dev-clock-screenshot.png new file mode 100644 index 000000000..ac136e48e Binary files /dev/null and b/apps/dclock/bangle1-dev-clock-screenshot.png differ diff --git a/apps/dclock/bangle2-dev-clock-screenshot.png b/apps/dclock/bangle2-dev-clock-screenshot.png new file mode 100644 index 000000000..0deb6dc2e Binary files /dev/null and b/apps/dclock/bangle2-dev-clock-screenshot.png differ diff --git a/apps/dclock/clock-dev.js b/apps/dclock/clock-dev.js index d2c08726a..d2c3893d5 100644 --- a/apps/dclock/clock-dev.js +++ b/apps/dclock/clock-dev.js @@ -1,17 +1,18 @@ var locale = require("locale"); /* jshint esversion: 6 */ -const timeFontSize = 4; -const dateFontSize = 3; -const smallFontSize = 2; +const big = g.getWidth()>200; +const timeFontSize = big?4:3; +const dateFontSize = big?3:2; +const smallFontSize = big?2:1; const font = "6x8"; const xyCenter = g.getWidth() / 2; const yposTime = 50; -const yposDate = 85; -const yposTst = 115; -const yposDml = 170; -const yposDayMonth = 195; -const yposGMT = 220; +const yposDate = big?85:75; +const yposTst = big?115:95; +const yposDml = big?170:130; +const yposDayMonth = big?195:140; +const yposGMT = big?220:150; // Check settings for what type our clock should be var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; @@ -99,6 +100,8 @@ Bangle.on('lcdPower', function(on) { // clean app screen g.clear(); +// Show launcher when button pressed +Bangle.setUI("clock"); Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -107,6 +110,3 @@ setInterval(drawSimpleClock, 100); // draw now drawSimpleClock(); - -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); \ No newline at end of file diff --git a/apps/de-stress/ChangeLog b/apps/de-stress/ChangeLog new file mode 100644 index 000000000..fcd7580b1 --- /dev/null +++ b/apps/de-stress/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Adjust for different screen types and themes diff --git a/apps/de-stress/app.js b/apps/de-stress/app.js index 445e853c5..b292fe601 100644 --- a/apps/de-stress/app.js +++ b/apps/de-stress/app.js @@ -1,29 +1,21 @@ +g.setBgColor(0).clear(); -//g.clear(); - -var img = { - width : 100, height : 100, bpp : 8, - transparent : 254, - buffer : require("heatshrink").decompress(atob("/wA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AglYAGE/44gHz4nnHKtPumez3C4WezFzp49ZLAgoCE4WeugnaHStPG4QAHzw9DE/Y6VHJQ9YMJwnEMk46CF4lzfglPug9WCAWYDQl0E4tzN4gLCMVC5Cp+YGoOezxBCubKHExxUEuiFCEoInECAhkjMQuYLArCFGwLLEHpjsGMIRpEfAsrMkwhBFAQ6BHJA9EKApDBHpAJBQYhPBE5iZBzAsDMb4gBEwg6MJhBkIYorgBPQiMMUAhkeHgkrug7ObI5qBHwhiGZYomOEojGeJQVzTx5kOMQ6JREAR3CDIJkcHobwEMiw+CAAYJEMSYWCAgRjcDgI4CYyiiDXopiFBooAWRwJkaHwjGVKwdzH4rADuhiXZAaICMbbtGHy2YBQ+YRDCiEMbidCULBkDLIwIIdqbmCp5jZPwIfDAYQAXXwNPzwACIQLQIACt0ZDIZBTwRFBHjWeHoSJCETlPc4ZjdAYYA7L4Jj/Mf5jCEYQDDAHhEEMbzH+McBfCMf4ADzxjep+YL/1PMb10MYQDCAHd0MYV0MbdzEYoA7UYdPMTBkCc4hj9leeAYRjbL4aIDAHN0UwRjeL4WYZHlPzBnCMbZkBQojtCAG6gEp5ibZARfCdwjG3ugDBzzGcMYTIEFAQA1ujGFMbjIFeAgA0HobGeZA9PAgYAyG4jGfZAyPBzBizf4LGjMggtCFAJpDAFw0FMURjCeAd0AgYAup90AgZjjMgWYFYZkwGImYMUhkDeYdPuZituZiDzximMYUrFwj6DAFF0TAg6CMcpkCSYpkqMQtPMVBkDuZktMQtzMVRkDLwZkoMQoFBMVZkJp5ijX4JizMhFPMkQjBMWpkHIAKjEADTrGMWZkDuY8DuZkdMQKKEMWpkDUIxFEACocGdoJi1MhCqBAwgATLYLkEMXJkDIY4GEAB+ep9PC4aDBMXRkJukruhiRCgxi+MglzJAtPMR7cGNIJi+MglPJYhSBzBhLzB0FzwWBMX5lGMghVGAAtzld0bwph/MhNzWYzKGNwR2Euhi/MhhTHA4ZrHA4Rh/Mp10YIlzA4JoCBQjE/ZTK8BA45i/MqtzX4jE/Mj0rYQjECBYZP/MrFPMoWep5h/ZT90ujE/MsZh/MsZB/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/ACg")) -}; +var img = require("heatshrink").decompress(atob("plB4X/ln7A4OGmMs5dVAANa1WlAoQADBI9W1QAByoJNtWv4f61ISEtWrBI2q4EAgeqCQgJHq2sLoU6IYdaBIg5CrWAn2q2EDF4dq4EO1W8gQcCtUD1fP9cA9QSC1cA9f/9XADgWohxBBh2whQvBq2gAwMAh+wlQSB1k7IIXogYvBrXAlYJC9k6DYOw9gIChXA9JBC0AJCncOytWwAtBAAMDF4RBDAAMOgWVrXDwDjD9kKy2gIIcAgXD0taDYgvCRIJBDF4OA0trhwIDJgK2B4BKDAAXptcLA4kC4HrD4IJE8HptE4BAhfBLAJBEgEslISGL4JOBBAoSB1ksBIs6/70DBIgSHh+q+AIFnASIABASU0EgCR0IhWgEp4SBHCBxJLzusXowAIaBISLhYSO8EptcOCR2w9NagXACJkDwGlrXDwASMgXDCQOA2ASMh0C0tW2HgCRkLh2Vq2glASMlEKytV1iFN9k6qtVtEOORcD2EpCQNqOwJwL4GpCQNa4GgCRUKgelCQJyBlgSKnBwBCQWgnZdLOAQAB1BfKLoMqCIVVtYHBXZPA9ISDL4PsCRE7LoZfDnRKJLoYAC1kOTI8CDoIREJgMA1gSGFwJKEJgaGH9hKFTIaGGPQKVEAAeogbTFhXASogADtXAlYSE9cDeYRMG2A5EG4MOJQxMC1g5EG4M6JQ4AB1Q5E9ED1QRIHIatBG5Y5EVoM6G5Y5DnXDCwJvIHIsD32AG5Y5C1aUBgHqG5atDLwI3MHIReCG5hgD4aUKHI2+G5xgC1RcNAAdpBJA")) function hr(){ - -Bangle.buzz(100,0.1).then(()=>{ - g.clear(); - return new Promise(resolve=>setTimeout(resolve,250)); // wait 250ms -}).then(()=>{ - return Bangle.buzz(150); -}).then(()=>{ - g.drawImage(img, 25, 40, {scale:2}); -}); - + Bangle.buzz(100,0.1).then(()=>{ + g.clear(); + return new Promise(resolve=>setTimeout(resolve,250)); // wait 250ms + }).then(()=>{ + return Bangle.buzz(150); + }).then(()=>{ + g.drawImage(img, g.getWidth()/2 - 76, g.getHeight()/2 - 65, {scale:2}); + }); } setInterval(hr, 2000); g.flip(); -setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); - - +// TODO - not clock but we still want a press to show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/demoapp/ChangeLog b/apps/demoapp/ChangeLog index 5560f00bc..53e9cf268 100644 --- a/apps/demoapp/ChangeLog +++ b/apps/demoapp/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Minor adjustment to fix out of memory errors diff --git a/apps/demoapp/app.js b/apps/demoapp/app.js index 13c043587..f1cf5af07 100644 --- a/apps/demoapp/app.js +++ b/apps/demoapp/app.js @@ -60,8 +60,8 @@ var scenes = [ }; }, function() { + Bangle.setLCDMode("120x120"); var img = require("heatshrink").decompress(atob("oNBxH+5wA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHGpAAoQKv4ADCBQAeqsrAAejBw9/B4oABqt/IGepHw5CEQspALH5hBC5pAvv4/MAALFkIBWpPI6IHqpAu0Z3GfYOpRYdPQEhALYIp2FBYNVI4JAvvL4LH0yBYAFJAQQQ5Ay1JAFftBAQBYxCDv+qIGiCHIQiGnIBfOv5BJIQRAyIJkrvKEkIBrFBB4qEGIGRCNYsZAQIQV/IZDEiICRCDQVJAUIQVPC4lVIF6yJQYpAZ5t/FYvNIBepqtVIJGjIDoqBDY2pdYo3DfAhBIQLmpvIcDvIrC5oJEIAhTCGQmj5qgEC4t5e7YrBqt5BI6UFBg15v4XHbQwAQb4oAKv7NKABdVRoYATUAwnICqjZFIMdVE4+jXI4XGYCxBFFZN/M5OpCxUrvJ/ZFYmjvNVAAY+KCwpDBC6YAV5vNC9oA/AH4A/AHYA==")); - g.clear(); y = 0; var step = 4; @@ -70,8 +70,7 @@ var scenes = [ g.clear(); g.drawImage(img,60,60,{rotate:Math.sin(y*0.03)*0.5}); g.flip(); - }, 20); - Bangle.setLCDMode("120x120"); + }, 20); return function() { if (i) clearInterval(i); }; diff --git a/apps/demoapp/bangle1-demo-loop-screenshot1.png b/apps/demoapp/bangle1-demo-loop-screenshot1.png new file mode 100644 index 000000000..9618f7044 Binary files /dev/null and b/apps/demoapp/bangle1-demo-loop-screenshot1.png differ diff --git a/apps/demoapp/bangle1-demo-loop-screenshot2.png b/apps/demoapp/bangle1-demo-loop-screenshot2.png new file mode 100644 index 000000000..0d39685ba Binary files /dev/null and b/apps/demoapp/bangle1-demo-loop-screenshot2.png differ diff --git a/apps/demoapp/bangle1-demo-loop-screenshot3.png b/apps/demoapp/bangle1-demo-loop-screenshot3.png new file mode 100644 index 000000000..2a98f79a1 Binary files /dev/null and b/apps/demoapp/bangle1-demo-loop-screenshot3.png differ diff --git a/apps/demoapp/bangle1-demo-loop-screenshot4.png b/apps/demoapp/bangle1-demo-loop-screenshot4.png new file mode 100644 index 000000000..8f43cac50 Binary files /dev/null and b/apps/demoapp/bangle1-demo-loop-screenshot4.png differ diff --git a/apps/devstopwatch/ChangeLog b/apps/devstopwatch/ChangeLog index c6d24e9bc..e2b392fe9 100644 --- a/apps/devstopwatch/ChangeLog +++ b/apps/devstopwatch/ChangeLog @@ -1,2 +1,3 @@ 0.01: App created 0.02: Persist state to storage to enable stopwatch to continue in the background +0.03: Modified to use setUI, theme and different screens diff --git a/apps/devstopwatch/app.js b/apps/devstopwatch/app.js index 665ba084e..83bb693a9 100644 --- a/apps/devstopwatch/app.js +++ b/apps/devstopwatch/app.js @@ -2,13 +2,16 @@ const EMPTY_LAP = '--:--:---'; const EMPTY_H = '00:00:000'; const MAX_LAPS = 6; const XY_CENTER = g.getWidth() / 2; +const big = g.getWidth()>200; const Y_CHRONO = 40; -const Y_HEADER = 80; -const Y_LAPS = 125; -const Y_BTN3 = 225; +const Y_HEADER = big?80:60; +const Y_LAPS = big?125:90; +const H_LAPS = big?15:8; +const Y_BTN3 = big?225:165; const FONT = '6x8'; const CHRONO = '/* C H R O N O */'; + var reset = false; var currentLap = ''; var chronoInterval; @@ -22,11 +25,11 @@ var state = require("Storage").readJSON("devstopwatch.state.json",1) || { laps: [EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP], }; -// Set laps. -setWatch(() => { - +// Show launcher when button pressed +Bangle.setUI("clockupdown", btn=>{ + if (btn==0) { reset = false; - + if (state.started) { changeLap(); } else { @@ -34,13 +37,9 @@ setWatch(() => { chronoInterval = setInterval(chronometer, 10); } } -}, BTN1, { repeat: true, edge: 'rising' }); - -// Reset chronometre. -setWatch(() => { resetChrono(); }, BTN3, { repeat: true, edge: 'rising' }); - -// Show launcher when middle button pressed. -setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: 'falling' }); +} + if (btn==1) resetChrono(); +}); function resetChrono() { state.laps = [EMPTY_H, EMPTY_H, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP, EMPTY_LAP]; @@ -106,33 +105,33 @@ function printChrono() { var print = ''; - g.setFont(FONT, 2); + g.setFont(FONT, big?2:1); print = CHRONO; g.drawString(print, XY_CENTER, Y_CHRONO, true); - g.setColor(0, 220, 0); - g.setFont(FONT, 3); + g.setColor("#0e0"); + g.setFont(FONT, big?3:2); print = ` T ${state.laps[0]}\n`; print += ` C ${state.laps[1]}\n`; g.drawString(print, XY_CENTER, Y_HEADER, true); - g.setColor(255, 255, 255); - g.setFont(FONT, 2); + g.setColor(g.theme.fg); + g.setFont(FONT, big?2:1); for (var i = 2; i < MAX_LAPS + 1; i++) { - g.setColor(255, 255, 255); + g.setColor(g.theme.fg); let suffix = ' '; if (state.currentLapIndex === i) { let suffix = '*'; - g.setColor(255, 200, 0); + g.setColor("#f70"); } const lapLine = `L${i - 1} ${state.laps[i]} ${suffix}\n`; - g.drawString(lapLine, XY_CENTER, Y_LAPS + (15 * (i - 1)), true); + g.drawString(lapLine, XY_CENTER, Y_LAPS + (H_LAPS * (i - 1)), true); } - g.setColor(255, 255, 255); + g.setColor(g.theme.fg); g.setFont(FONT, 1); print = 'Press 3 to reset'; g.drawString(print, XY_CENTER, Y_BTN3, true); @@ -166,7 +165,7 @@ E.on('kill', function(){ }); if(state.started){ - chronoInterval = setInterval(chronometer, 10); + chronoInterval = setInterval(chronometer, 10); } else { - resetChrono(); + resetChrono(); } diff --git a/apps/devstopwatch/bangle1-dev-stopwatch-screenshot.png b/apps/devstopwatch/bangle1-dev-stopwatch-screenshot.png new file mode 100644 index 000000000..b668794b1 Binary files /dev/null and b/apps/devstopwatch/bangle1-dev-stopwatch-screenshot.png differ diff --git a/apps/digiclock/ChangeLog b/apps/digiclock/ChangeLog index 0bb55854e..e41fae573 100644 --- a/apps/digiclock/ChangeLog +++ b/apps/digiclock/ChangeLog @@ -1 +1,2 @@ 0.01: App Made! +0.02: Changed setWatch to Bangle.setUI, code tidy diff --git a/apps/digiclock/digiclock-icon.js b/apps/digiclock/digiclock-icon.js index 737561863..da6305724 100644 --- a/apps/digiclock/digiclock-icon.js +++ b/apps/digiclock/digiclock-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("/wA/AH4A/AH4A/ACmsAEQuMlcAAD0rGBQKBFr4ADGBOsqwvjqwvJRsCRFF/8Gg4ADEZYQEgwvWg8+AAgwKCJgvQDgoABF5IRMF5xEBJpBhGCJwvNDQM4AYMNAAQaBnCAFCJ4vNIwQeBAAkxQAwGCmIRFFwIRDF64dDgwGBgwRNF/4v/F/4v/F/4v/F/4dJmIdECIkxF7MHFwUHhoACg4eCAYIACCJ4vNDQIgCAAgICKwoROF5yAEAAgtFCKAvQJpAAICJgvQgEGg4ADFxIwCAAcGBYovRADov6qwvjqwvJ1gvjEoIvHGASRgRoIuJGAYAhFxQA/AH4A/AH4A/ABQ")) +require("heatshrink").decompress(atob("mEw4UA///A4N551ulxL/ACkK1QAG0ALBlNVAA1oBYOlBY9aBYO1BY9eBYOVBY9WBbf/+oIBr//BYlX//9BYN///VC599qtX6oBBqt9BYYRBr/1AIIdBBf4L/BY6bLZcb7MBau1BY9eBYOlBY9aBYMpBY9oBYMK1QAG0ALBAH4ASA")) diff --git a/apps/digiclock/digiclock.js b/apps/digiclock/digiclock.js index 7f74f2242..f404d30b2 100644 --- a/apps/digiclock/digiclock.js +++ b/apps/digiclock/digiclock.js @@ -1,141 +1,50 @@ //load fonts require("Font7x11Numeric7Seg").add(Graphics); -require("FontHaxorNarrow7x17").add(Graphics); +require("FontHaxorNarrow7x17").add(Graphics); //screen position -const X = 170; -const Y = 140; +const X = 170; +const Y = 140; function draw() { // Date Variables - var date = new Date(); - var h = date.getHours(); - var m = date.getMinutes(); - var day = date.getDay(); - var month = date.getMonth(); - var dateNum = date.getDate(); - var year = date.getFullYear(); - var half = "AM"; - var time = (" " + h).substr(-2) + ":" + ("0" + m).substr(-2); - - //convert day into string - switch (day) { - case 0: - day = "Sunday"; - break; - - case 1: - day = "Monday"; - break; - - case 2: - day = "Tuesday"; - break; - - case 3: - day = "Wednesday"; - break; - - case 4: - day = "Thursday"; - break; - - case 5: - day = "Friday"; - break; - - case 6: - day = "Saturday"; - break; - - default: - day = "ERROR"; - break; - } - - //convert month into String - switch(month) { - case 0: - month = "Jan"; - break; - - case 1: - month = "Feb"; - break; - - case 2: - month = "Mar"; - break; - - case 3: - month = "Apr"; - break; - - case 4: - month = "May"; - break; - - case 5: - month = "Jun"; - break; - - case 6: - month = "Jul"; - break; - - case 7: - month = "Aug"; - break; - - case 8: - month = "Sep"; - break; - - case 9: - month = "Oct"; - break; - - case 10: - month = "Nov"; - break; - - case 11: - month = "Dec"; - break; - - default: - month = "ERROR"; - break; - - } - + var date = new Date(); + var h = date.getHours(); + var m = date.getMinutes(); + var day = require("locale").dow(date); + var month = require("locale").month(date,1); + var dateNum = date.getDate(); + var year = date.getFullYear(); + var half = "AM"; + var time = (" " + h).substr(-2) + ":" + ("0" + m).substr(-2); + if (h > 12) { - half = "PM"; - h = h - 12; + half = "PM"; + h = h - 12; } //reset graphics - g.reset(); + g.reset(); //draw the time g.setFont("7x11Numeric7Seg", 5); g.setFontAlign(1,1); g.drawString(time, X, Y, true /*clear background*/); - g.setFont("7x11Numeric7Seg", 3); - g.drawString(("0"+date.getSeconds()).substr(-2), X+50, Y, true /*clear background*/); + g.setFont("7x11Numeric7Seg", 3); + g.drawString(("0"+date.getSeconds()).substr(-2), X+50, Y, true /*clear background*/); g.setFontAlign(0,1); g.setFont("HaxorNarrow7x17", 2); g.drawString(half, X+30, Y-35, true); g.setFont("HaxorNarrow7x17", 3); g.drawString(day, X-60, Y+53, true); - g.drawString(month, X-100, Y+95, true); - g.drawString(dateNum, X-40, Y+95, true); - g.drawString(year, X-90, Y-55, true); - - + g.drawString(month, X-100, Y+95, true); + g.drawString(dateNum, X-40, Y+95, true); + g.drawString(year, X-90, Y-55, true); + + } //clear screen at startup -g.clear(); +g.clear(); //draw immediatly -draw(); +draw(); var secondInterval = setInterval(draw, 1000); // Stop updates when LCD is off, restart when on @@ -148,7 +57,7 @@ Bangle.on('lcdPower',on=>{ } }); +// Show launcher when button pressed +Bangle.setUI("clock"); Bangle.loadWidgets(); Bangle.drawWidgets(); - -setWatch(Bangle.showLauncher, BTN2, {repeat : false, edge: "falling"}); diff --git a/apps/dotclock/ChangeLog b/apps/dotclock/ChangeLog index c9658afb8..563db87e7 100644 --- a/apps/dotclock/ChangeLog +++ b/apps/dotclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: Based on the Analog Clock app, minimal dot 0.02: Remove hardcoded hour buzz (you can install widchime if you miss it) +0.03: Use setUI, adjust for themes and different size screens diff --git a/apps/dotclock/bangle1-dot-clock-screenshot.png b/apps/dotclock/bangle1-dot-clock-screenshot.png new file mode 100644 index 000000000..767cd2d55 Binary files /dev/null and b/apps/dotclock/bangle1-dot-clock-screenshot.png differ diff --git a/apps/dotclock/bangle2-dot-clcok-screenshot.png b/apps/dotclock/bangle2-dot-clcok-screenshot.png new file mode 100644 index 000000000..3aadddb8f Binary files /dev/null and b/apps/dotclock/bangle2-dot-clcok-screenshot.png differ diff --git a/apps/dotclock/clock-dot.js b/apps/dotclock/clock-dot.js index c4a8be921..66255d1b4 100644 --- a/apps/dotclock/clock-dot.js +++ b/apps/dotclock/clock-dot.js @@ -1,20 +1,22 @@ -let g; -let Bangle; - +const big = g.getWidth()>200; const locale = require('locale'); const p = Math.PI / 2; const pRad = Math.PI / 180; -const faceWidth = 100; // watch face radius let timer = null; let currentDate = new Date(); -let hourRadius = 60; -let minRadius = 80; -const centerPx = g.getWidth() / 2; +const faceWidth = big?100:65; // watch face radius +let hourRadius = big?60:40; +let minRadius = big?80:55; +const centerX = g.getWidth() / 2; +const centerY = 24 + (g.getHeight()-24) / 2; +let colSecA = g.theme.dark ? "#00A" : "#58F"; // before the second +let colSecB = g.theme.dark ? "#58F" : "#00A"; // after the second +let colSec1 = g.theme.dark ? "#F83" : "#000"; // ON the second const seconds = (angle) => { const a = angle * pRad; - const x = centerPx + Math.sin(a) * faceWidth; - const y = centerPx - Math.cos(a) * faceWidth; + const x = centerX + Math.sin(a) * faceWidth; + const y = centerY - Math.cos(a) * faceWidth; // if 15 degrees, make hour marker larger const radius = (angle % 15) ? 2 : 4; @@ -23,15 +25,15 @@ const seconds = (angle) => { const hourDot = (angle,radius) => { const a = angle * pRad; - const x = centerPx + Math.sin(a) * hourRadius; - const y = centerPx - Math.cos(a) * hourRadius; + const x = centerX + Math.sin(a) * hourRadius; + const y = centerY - Math.cos(a) * hourRadius; g.fillCircle(x, y, radius); }; const minDot = (angle,radius) => { const a = angle * pRad; - const x = centerPx + Math.sin(a) * minRadius; - const y = centerPx - Math.cos(a) * minRadius; + const x = centerX + Math.sin(a) * minRadius; + const y = centerY - Math.cos(a) * minRadius; g.fillCircle(x, y, radius); }; @@ -45,54 +47,49 @@ const drawAll = () => { // draw all secs for (let i = 0; i < 60; i++) { - if (i > currentSec) { - g.setColor(0, 0, 0.6); - } else { - g.setColor(0.3, 0.3, 1); - } + g.setColor((i > currentSec) ? colSecA : colSecB); seconds((360 * i) / 60); } onSecond(); }; const resetSeconds = () => { - g.setColor(0, 0, 0.6); + g.setColor(colSecA); for (let i = 0; i < 60; i++) { seconds((360 * i) / 60); } }; const drawMin = () => { - g.setColor(0.5, 0.5, 0.5); + g.setColor("#777"); for (let i = 0; i < 60; i++) { minDot((360 * i) / 60,1); } }; const drawHour = () => { - g.setColor(0.5, 0.5, 0.5); + g.setColor("#777"); for (let i = 0; i < 12; i++) { hourDot((360 * 5 * i) / 60,1); } }; const onSecond = () => { - g.setColor(0.3, 0.3, 1); + g.setColor(colSecB); seconds((360 * currentDate.getSeconds()) / 60); if (currentDate.getSeconds() === 59) { resetSeconds(); onMinute(); } - g.setColor(1, 0.7, 0.2); + g.setColor(colSec1); currentDate = new Date(); seconds((360 * currentDate.getSeconds()) / 60); - g.setColor(1, 1, 1); + g.setColor(g.theme.fg); }; const drawDate = () => { g.reset(); - g.setColor(1, 1, 1); - g.setFont('6x8', 2); + g.setFont('6x8', big?2:1); const dayString = locale.dow(currentDate, true); // pad left date @@ -101,7 +98,7 @@ const drawDate = () => { // console.log(`${dayString}|${dateString}`); // center date const l = (g.getWidth() - g.stringWidth(dateDisplay)) / 2; - const t = centerPx - 6 ; + const t = centerY - 6 ; g.drawString(dateDisplay, l, t); // console.log(l, t); }; @@ -111,7 +108,7 @@ const onMinute = () => { resetSeconds(); } // clear existing hands - g.setColor(0, 0, 0); + g.setColor(g.theme.bg); hourDot((360 * currentDate.getHours()) / 12,4); minDot((360 * currentDate.getMinutes()) / 60,3); @@ -125,7 +122,7 @@ const onMinute = () => { g.setColor(1, 0, 0); // Hour hourDot((360 * currentDate.getHours()) / 12,4); - g.setColor(1, 0.9, 0.9); + g.setColor(g.theme.fg2); // Minute minDot((360 * currentDate.getMinutes()) / 60,3); drawDate(); @@ -152,8 +149,8 @@ g.clear(); resetSeconds(); startTimers(); drawAll(); +// Show launcher when button pressed +Bangle.setUI("clock"); + Bangle.loadWidgets(); Bangle.drawWidgets(); - -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); diff --git a/apps/dotmatrixclock/app.js b/apps/dotmatrixclock/app.js old mode 100755 new mode 100644 diff --git a/apps/dotmatrixclock/dotmatrix-clock-screen-shot.png b/apps/dotmatrixclock/dotmatrix-clock-screen-shot.png old mode 100755 new mode 100644 diff --git a/apps/dotmatrixclock/dotmatrixclock.png b/apps/dotmatrixclock/dotmatrixclock.png old mode 100755 new mode 100644 diff --git a/apps/doztime/ChangeLog b/apps/doztime/ChangeLog index de00f009c..6c4a25b26 100644 --- a/apps/doztime/ChangeLog +++ b/apps/doztime/ChangeLog @@ -1,4 +1,4 @@ 0.01: New App! -0.02 added emulator capability and display of widgets -0.03 bug of advancing time fixed; doztime now correct within ca. 1 second -0.04 changed time colour from slightly off white to pure white +0.02: added emulator capability and display of widgets +0.03: bug of advancing time fixed; doztime now correct within ca. 1 second +0.04: changed time colour from slightly off white to pure white diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog index 3df4ab63b..62a0cab9f 100644 --- a/apps/dtlaunch/ChangeLog +++ b/apps/dtlaunch/ChangeLog @@ -1,4 +1,7 @@ 0.01: Initial version 0.02: Multiple pages 0.03: cycle thru pages - +0.04: reset to clock after 2 mins of inactivity +0.05: add Bangle 2 version +0.06: Adds settings page (hide clocks or launchers) +0.07: Adds setting for directly launching app on touch for Bangle 2 diff --git a/apps/dtlaunch/README.md b/apps/dtlaunch/README.md index 70f7ff931..ba2301d91 100644 --- a/apps/dtlaunch/README.md +++ b/apps/dtlaunch/README.md @@ -3,7 +3,7 @@ ![](screenshot.jpg) In the picture above, the Settings app is selected. -## Controls +## Controls- Bangle **BTN1** - move backward through app icons on a page @@ -13,4 +13,12 @@ In the picture above, the Settings app is selected. **Swipe Left** - move to next page of app icons +**Swipe Right** - move to previous page of app icons + +## Controls- Bangle 2 + +**Touch** - icon to select, scond touch launches app + +**Swipe Left** - move to next page of app icons + **Swipe Right** - move to previous page of app icons \ No newline at end of file diff --git a/apps/dtlaunch/app.js b/apps/dtlaunch/app-b1.js similarity index 71% rename from apps/dtlaunch/app.js rename to apps/dtlaunch/app-b1.js index 329a96958..ec0569127 100644 --- a/apps/dtlaunch/app.js +++ b/apps/dtlaunch/app-b1.js @@ -2,8 +2,33 @@ * */ +var settings = Object.assign({ + showClocks: true, + showLaunchers: true, +}, require('Storage').readJSON("dtlaunch.json", true) || {}); + +function wdog(handle,timeout){ + if(handle !== undefined){ + wdog.handle = handle; + wdog.timeout = timeout; + } + if(wdog.timer){ + clearTimeout(wdog.timer) + } + wdog.timer = setTimeout(wdog.handle,wdog.timeout) +} + +// reset after two minutes of inactivity +wdog(load,120000) + var s = require("Storage"); -var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); +var apps = s.list(/\.info$/).map(app=>{ + var a=s.readJSON(app,1); + return a && { + name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src + };}).filter( + app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type)); + apps.sort((a,b)=>{ var n=(0|a.sortorder)-(0|b.sortorder); if (n) return n; // do sortorder first @@ -42,6 +67,7 @@ function drawPage(p){ } Bangle.on("swipe",(dir)=>{ + wdog() selected = 0; oldselected=-1; if (dir<0){ @@ -54,6 +80,7 @@ Bangle.on("swipe",(dir)=>{ }); function nextapp(d){ + wdog(); oldselected = selected; selected+=d; selected = selected<0?5:selected>5?0:selected; diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js new file mode 100644 index 000000000..800ec456c --- /dev/null +++ b/apps/dtlaunch/app-b2.js @@ -0,0 +1,117 @@ +/* Desktop launcher +* +*/ + +var settings = Object.assign({ + showClocks: true, + showLaunchers: true, + direct: false, +}, require('Storage').readJSON("dtlaunch.json", true) || {}); + +var s = require("Storage"); +var apps = s.list(/\.info$/).map(app=>{ + var a=s.readJSON(app,1); + return a && { + name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src + };}).filter( + app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type)); + +apps.sort((a,b)=>{ + var n=(0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; +}); +apps.forEach(app=>{ + if (app.icon) + app.icon = s.read(app.icon); // should just be a link to a memory area + }); + +var Napps = apps.length; +var Npages = Math.ceil(Napps/4); +var maxPage = Npages-1; +var selected = -1; +var oldselected = -1; +var page = 0; +const XOFF = 24; +const YOFF = 30; + +function draw_icon(p,n,selected) { + var x = (n%2)*72+XOFF; + var y = n>1?72+YOFF:YOFF; + (selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52); + g.clearRect(x+12,y+4,x+59,y+51); + g.setColor(g.theme.fg); + try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){} + g.setFontAlign(0,-1,0).setFont("6x8",1); + var txt = apps[p*4+n].name.split(" "); + for (var i = 0; i < txt.length; i++) { + txt[i] = txt[i].trim(); + g.drawString(txt[i],x+36,y+54+i*8); + } +} + +function drawPage(p){ + g.reset(); + g.clearRect(0,24,175,175); + var O = 88+YOFF/2-12*(Npages/2); + for (var j=0;j{ + selected = 0; + oldselected=-1; + if (dir<0){ + ++page; if (page>maxPage) page=0; + drawPage(page); + } else { + --page; if (page<0) page=maxPage; + drawPage(page); + } +}); + +function isTouched(p,n){ + if (n<0 || n>3) return false; + var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF; + var x2 = x1+71; var y2 = y1+81; + return (p.x>x1 && p.y>y1 && p.x{ + var i; + for (i=0;i<4;i++){ + if((page*4+i)=0 || settings.direct) { + if (selected!=i && !settings.direct){ + draw_icon(page,selected,false); + } else { + load(apps[page*4+i].src); + } + } + selected=i; + break; + } + } + } + if ((i==4 || (page*4+i)>Napps) && selected>=0) { + draw_icon(page,selected,false); + selected=-1; + } +}); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +drawPage(0); diff --git a/apps/dtlaunch/settings-b1.js b/apps/dtlaunch/settings-b1.js new file mode 100644 index 000000000..f3101da16 --- /dev/null +++ b/apps/dtlaunch/settings-b1.js @@ -0,0 +1,33 @@ +(function(back) { + var FILE = "dtlaunch.json"; + + var settings = Object.assign({ + showClocks: true, + showLaunchers: true + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + "" : { "title" : "Desktop launcher" }, + "< Back" : () => back(), + 'Show clocks': { + value: settings.showClocks, + format: v => v?"On":"Off", + onchange: v => { + settings.showClocks = v; + writeSettings(); + } + }, + 'Show launchers': { + value: settings.showLaunchers, + format: v => v?"On":"Off", + onchange: v => { + settings.showLaunchers = v; + writeSettings(); + } + } + }); +}) diff --git a/apps/dtlaunch/settings-b2.js b/apps/dtlaunch/settings-b2.js new file mode 100644 index 000000000..7f667d213 --- /dev/null +++ b/apps/dtlaunch/settings-b2.js @@ -0,0 +1,42 @@ +(function(back) { + var FILE = "dtlaunch.json"; + + var settings = Object.assign({ + showClocks: true, + showLaunchers: true, + direct: false + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + "" : { "title" : "Desktop launcher" }, + "< Back" : () => back(), + 'Show clocks': { + value: settings.showClocks, + format: v => v?"On":"Off", + onchange: v => { + settings.showClocks = v; + writeSettings(); + } + }, + 'Show launchers': { + value: settings.showLaunchers, + format: v => v?"On":"Off", + onchange: v => { + settings.showLaunchers = v; + writeSettings(); + } + }, + 'Direct launch': { + value: settings.direct, + format: v => v?"On":"Off", + onchange: v => { + settings.direct = v; + writeSettings(); + } + } + }); +}) diff --git a/apps/dtlaunch/shot1.png b/apps/dtlaunch/shot1.png new file mode 100644 index 000000000..e6a9bcd3a Binary files /dev/null and b/apps/dtlaunch/shot1.png differ diff --git a/apps/dtlaunch/shot2.png b/apps/dtlaunch/shot2.png new file mode 100644 index 000000000..4c0c33c91 Binary files /dev/null and b/apps/dtlaunch/shot2.png differ diff --git a/apps/dtlaunch/shot3.png b/apps/dtlaunch/shot3.png new file mode 100644 index 000000000..1ffdf8090 Binary files /dev/null and b/apps/dtlaunch/shot3.png differ diff --git a/apps/emojuino/ChangeLog b/apps/emojuino/ChangeLog new file mode 100644 index 000000000..04367183f --- /dev/null +++ b/apps/emojuino/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Upgraded text to images, added welcome screen and subtitles. +0.03: Advertise app name as Espruino manufacturer data when idle. diff --git a/apps/emojuino/README.md b/apps/emojuino/README.md new file mode 100644 index 000000000..568d06dfb --- /dev/null +++ b/apps/emojuino/README.md @@ -0,0 +1,28 @@ +# Emojuino + +Emojis & Espruino! + + +## Usage + +Select an emoji and then tap to transmit! The emoji will be recognised by [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware and any other program which observes the [InteroperaBLE Identifier](https://reelyactive.github.io/interoperable-identifier/) open standard. + + +## Features + +Currently implements a tiny subset of possible [Unicode emojis](https://unicode.org/emoji/charts/full-emoji-list.html) which are advertised as an [InteroperaBLE Identifier](https://reelyactive.github.io/interoperable-identifier/) encapsulated as Eddystone UID. + + +## Controls + +Swipe left/right to select the emoji to broadcast. Tap the screen to initiate the broadcast. Emoji will flash while broadcasting, which lasts for 5 seconds. + + +## Requests + +[Contact reelyActive](https://www.reelyactive.com/contact/) for support/updates. + + +## Creator + +Developed by [jeffyactive](https://github.com/jeffyactive) of [reelyActive](https://www.reelyactive.com) diff --git a/apps/emojuino/emojuino-icon.js b/apps/emojuino/emojuino-icon.js new file mode 100644 index 000000000..d56749250 --- /dev/null +++ b/apps/emojuino/emojuino-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIAHkUoxGIwUiBxAAGiQVCAAeCkIWNCooADDBYWKDBYWEkc////+cyDBhxDCoQAD+YLDCw0YBQQVFAAYYCwIXFHQRDElGCJYgOCFw8vBwPyOgoJFGAg4BIoQWGDAhJCIwoLBHgYAGJQIjCIwguCnCRFRoeDGAZICAgOPFwaRGDAQfB/AwDBAYuCX44wDAgTrDBoIDBGYP/manBmYFBFYQPDwJeBD4iRGRoQ/FC4QqBEYIbERooTBCAeBNAIjBBQIDDAAggBG4IDDwQXBEQIDDUAgcCHASaBAYQTFMQpcFDYp+EEII9DAARRDFIIfDHIwXBVISlDC4YzD9wA0osFpwIF8lQqgWK8kAgEEBItABIIhGAAfgBoMABIoIChwX0jwED8oNBgoXFqAJBrwHD8IXEBwQNEEIYgFC4wAQ8MRC6sRC+BgULwIwHSINVpwuLC43kaAQABqgaHC4bZHAAkFqhGHGAovFAAYyDCwgwFL4IwGFxAwNFxIwG8lVCoSTEFw7bPCxAYNCxT0LIpIxMCpoyHFhI")) \ No newline at end of file diff --git a/apps/emojuino/emojuino.js b/apps/emojuino/emojuino.js new file mode 100644 index 000000000..d241063e6 --- /dev/null +++ b/apps/emojuino/emojuino.js @@ -0,0 +1,185 @@ +/** + * Copyright reelyActive 2021 + * We believe in an open Internet of Things + */ + + +// Emoji images are 96px x 96px, 4bpp (https://www.espruino.com/Image+Converter) +// and adapted from Font Awesome 5 +const GRIN = "sFgwkBiIATDwoaUFi4ynQZ4uuGDzlTF1wwaFyowYFy4wWiAvZgIutGCgubSKRecMCQudMCBeeMCAufMBxegMBwuhMBheiMBgujMBRekMBQvvF0qQIL0xgIF94unSA4vuR1CQGF94upSAovuR1SQEF94urSAY/PCBivQF5z/DEBQ+DEB5ePCJYOEMBgNNF8MBHpogNHwqBNF/4vsEAovOX7TviBhYgFD5Q/EEJoANEAY/OLxgAQPx5edAH4A/AH4A/AH4A/AEUQF1sBF/4v/F/4vviILJBRQANEZYLJHQIMKFpYABQhIiKC4QaMIhBHLF6AAVEhRQIF8ZuCF5B6GACYjMF9ZrOF8jAiKRgvvSEJROBo5gYEBw+IMCwfPB5BgWDxBPHCCBeVJxBgdJqIvJMCQcTCRAwRFxJ8KChQwODKwVJGBouKbZgXLDBQVLPBoZLDYxDMLxocQACLXOMBwARFxxgfLx5gfFyBgdLyIwcFyaRbFygwZFywwXFzAwVFzQwTFzgwRFzwxOFsIyKDSg"; +const MEH = "sFgwkBiIATDwoaUFi4ynQZ4uuGDzlTF1wwaFyowYFy4wWiAvZgIutGCgubSKRecMCQudMCBeeMCAufMBxegMBwuhMBheiMBgujMBRekMBQvvF0qQIL0xgIF94unSA4vuR1CQGF94upSAovuR1SQEF94urSAY/PCBivQF5z/DEBQ+DEB5ePCJYOEMBgNNF8MBHpogNHwqBNF/4vsEAovOX7TviBhYgFD5Q/EEJoANEAY/OLxgAQPx5edAH4A/AH4A/AH4A/AEUQF1sBF/4v/F/4vviIvtiIv/F9qeBACDgNB5ouSECAOLFyaBMKAYvrByQvgSBS/fD4jAfXxwQMADxAQF8iQLADjeGF96QoFwxgnLw4vwSEwuIMEpeJMEouKMEZeLMEYuMMEJeNMEIuOMD5ePMD4uQMDpeRGDguTSLYuUGDIuWGC4uYGCouaGCYucGCIueGJwthGRQaUA"; +const FROWN = "sFgwkBiIATDwoaUFi4ynQZ4uuGDzlTF1wwaFyowYFy4wWiAvZgIutGCgubSKRecMCQudMCBeeMCAufMBxegMBwuhMBheiMBgujMBRekMBQvvF0qQIL0xgIF94unSA4vuR1CQGF94upSAovuR1SQEF94urSAY/PCBivQF5z/DEBQ+DEB5ePCJYOEMBgNNF8MBHpogNHwqBNF/4vsEAovOX7TviBhYgFD5Q/EEJoANEAY/OLxgAQPx5edAH4A/AH4A/AH4A/AEUQF1sBF/4v/F/4vUgMRAAQZWFqwxWCgIuZGCYvSFxIcUFzYdTOZyNKSKQdCCJwuNMB5NDLzZOPIKAviCJguPJxpNEF94RLRyBONIKAvHNRQvRCKAMUJpIvOZxx9WAEbSTADReHF+CQmFxBglLxJglFxRgjLxZgjFxhghLxpghFxxgfLx5gfFyBgdLyIwcFyaRbFygwZFywwXFzAwVFzQwTFzgwRFzwxOFsIyKDSg"; +const THUMBS_UP = "sFgwkBiIAaiAiBDzYAQKYZQcLyAwsF4qSpcoxgoF4xgnRwwvxSEwvvFw4vwYEwv/F/4AOiAv/R1Av/F/6+PgIv/RzwvjLxQvkFxTujLxYvjFxaOiLxgvvR1wviR3gviR3YviFxg6iF7AwVRxowhFzUAgIvuMCSObF6YucSCJedF6IudSARQIHQheeAAIgKGAYufF+CbMF/4v/WYQv/F/6yPF/6OeF9wgNL/4v/F/4vhEQIv/R/4v/F/7ueF/4v/Xx4v/F/4v/F/4v/F/4v/F7ogOF/6OSEAgHCiAvrAwQHHRz4v/F/4v/F58QF8cBE4wPDGLYvHB5aTaKwQvUMS4vYGCx8QF5AwULwgvWYiZJQIAowXDowvYGJyqRFx4bKDRQA=="; +const THUMBS_DOWN = "sFgwkBiIAbiAoGEroAHLZgttMcK9RXEZgmFyZgHDZA/JFyogFDZQwHFqovXLiyQHB5wtaF6gubF/4v/F/4vwgIv/F7wgPF/6QTF/4v/F/4v/F/4v/F/4AdF/4v/YCIv/F/4v9EQIv/R/4v/F/7ueL+gFBiMQF8oiBE4wHHF/6QQF/4v/YigvugInBiAvrM5QvvM4gvqMFgvDMD0BF55gegJPKgIvEMDoeLF4pgdJ5QuGF7gjHABaQbFyRgbFygvZFyqQOEixgYF8RgMgIv/SH5gPYH6QfF8aQvMBgvjMBaQjMBYvkMBQv/SEAv/F/7APF/6QfF/4v/F/0BF8sQF/4vnF0rAJF9yOmSBAunF4xeoSAouqMAYTQA=="; +const HEART = "sFgwkBiIA/AH4A/AH4AogAADC1EQC4gaQCo8BIqYwRCyxdJDJoVLMJYuMGBIVNGBQYNDI5FOO5IXODI4WWI6BgGCywYTDIYVVO6gvXSAoYTDIQVTMAgYTDIJFUMAgYUACyOXAC7XWF7YurSAYvuR1iQCF/4v/F54utAH4A/AH4A/AH4A/AGMQF1sBF/4v/F58RF9sRF/4vgYFi+BMFouCF+CQqRwYvwSFQuEMFJeFMFIuGME5eHME4uIMEpeJMEouKMEZeLMEYuMMEJeNMEIuOMD5ePMD4uQMDpeRMDouSMDZeTMDYuUMDJeVMDIuWMC5eXMC4uYMCpeZMCouaMCZebMCYucMCJedF+CQQFzxgPFz5gPF8JgMXr5gPF0RgLL0ZgLF0hgJL0pgJF0xgHL05gHF1BgFL1JgFF1QwDF1gA/AH4A/AH4AJA="; +const TX = "k8XwkBiIAYEYogLHBAUIiBNKGxooKEggvJCYYHDKxAMFAoRrOCRAsHCYqbNHQibLKAauOLBCJHQw6JMQBIJBRJDWJThK5JJJi5KbpaJKFBaKEE5ybGHRhcOACEQA"; + + +// Emojis are pairs with the form [ Image String, Unicode code point ] +// For code points see https://unicode.org/emoji/charts/emoji-list.html +const EMOJIS = [ + [ GRIN, 0x1f642 ], // Slightly smiling + [ MEH, 0x1f610 ], // Neutral + [ FROWN, 0x1f641 ], // Slightly frowning + [ THUMBS_UP, 0x1f44d ], // Thumbs up + [ THUMBS_DOWN, 0x1f44e ], // Thumbs down + [ HEART, 0x02764 ], // Heart +]; +const EMOJI_TRANSMISSION_MILLISECONDS = 5000; +const BLINK_PERIOD_MILLISECONDS = 500; +const TRANSMIT_BUZZ_MILLISECONDS = 200; +const CYCLE_BUZZ_MILLISECONDS = 50; +const WELCOME_MESSAGE = 'Emojuino:\r\n\r\n< Swipe >\r\nto select\r\n\r\nTap\r\nto transmit'; + +// Non-user-configurable constants +const APP_ID = 'emojuino'; +const IMAGE_INDEX = 0; +const CODE_POINT_INDEX = 1; +const EMOJI_PX = 96; +const EMOJI_X = (g.getWidth() - EMOJI_PX) / 2; +const EMOJI_Y = (g.getHeight() - EMOJI_PX) / 2; +const TX_X = 68; +const TX_Y = 12; +const FONT_SIZE = 24; +const ESPRUINO_COMPANY_CODE = 0x0590; +const UNICODE_CODE_POINT_ELIDED_UUID = [ 0x49, 0x6f, 0x49, 0x44, 0x55, + 0x54, 0x46, 0x2d, 0x33, 0x32 ]; + + +// Global variables +let emojiIndex = 0; +let isToggleOn = false; +let isTransmitting = false; +let lastDragX = 0; +let lastDragY = 0; + + +// Cycle through emojis +function cycleEmoji(isForward) { + if(isTransmitting) { return; } + + if(isForward) { + emojiIndex = (emojiIndex + 1) % EMOJIS.length; + } + else if(--emojiIndex < 0) { + emojiIndex = EMOJIS.length - 1; + } + + drawImage(EMOJIS[emojiIndex][IMAGE_INDEX]); + Bangle.buzz(CYCLE_BUZZ_MILLISECONDS); +} + + +// Handle a touch: transmit displayed emoji +function handleTouch(zone, event) { + if(isTransmitting) { return; } + + let emoji = EMOJIS[emojiIndex]; + transmitEmoji(emoji[IMAGE_INDEX], emoji[CODE_POINT_INDEX], + EMOJI_TRANSMISSION_MILLISECONDS); + Bangle.buzz(TRANSMIT_BUZZ_MILLISECONDS); +} + + +// Transmit the given code point for the given duration in milliseconds, +// blinking the image once per second. +function transmitEmoji(image, codePoint, duration) { + let instance = [ 0x00, 0x00, (codePoint >> 24) & 0xff, + (codePoint >> 16) & 0xff, (codePoint >> 8) & 0xff, + codePoint & 0xff ]; + + require('ble_eddystone_uid').advertise(UNICODE_CODE_POINT_ELIDED_UUID, + instance); + isTransmitting = true; + drawImage(EMOJIS[emojiIndex][IMAGE_INDEX], true); + + let displayIntervalId = setInterval(toggleImage, BLINK_PERIOD_MILLISECONDS, + image); + + setTimeout(terminateEmoji, duration, displayIntervalId); +} + + +// Transmit the app name under the Espruino company code to facilitate discovery +function transmitAppName() { + let options = { + showName: false, + manufacturer: ESPRUINO_COMPANY_CODE, + manufacturerData: JSON.stringify({ name: APP_ID }), + interval: 2000 + } + + NRF.setAdvertising({}, options); +} + + +// Terminate the emoji transmission +function terminateEmoji(displayIntervalId) { + transmitAppName(); + isTransmitting = false; + clearInterval(displayIntervalId); + drawImage(EMOJIS[emojiIndex][IMAGE_INDEX], false); +} + + +// Toggle the display between image/off +function toggleImage(image) { + if(isToggleOn) { + drawImage(EMOJIS[emojiIndex][IMAGE_INDEX], true); + } + else { + g.clear(); + } + isToggleOn = !isToggleOn; +} + + +// Draw the given emoji +function drawImage(image, isTx) { + g.clear(); + g.drawImage(require("heatshrink").decompress(atob(image)), EMOJI_X, EMOJI_Y); + if(isTx) { + g.drawImage(require("heatshrink").decompress(atob(TX)), TX_X, TX_Y); + } + else { + g.drawString("< Swipe >", g.getWidth() / 2, g.getHeight() - FONT_SIZE); + } + g.flip(); +} + + +// Handle a drag event +function handleDrag(event) { + let isFingerReleased = (event.b === 0); + + if(isFingerReleased) { + let isHorizontalDrag = (Math.abs(lastDragX) >= Math.abs(lastDragY)) && + (lastDragX !== 0); + + if(isHorizontalDrag) { + cycleEmoji(lastDragX > 0); + } + } + else { + lastDragX = event.dx; + lastDragY = event.dy; + } +} + + +// Special function to handle display switch on +Bangle.on('lcdPower', (on) => { + if(on) { + drawImage(EMOJIS[emojiIndex][IMAGE_INDEX], false); + } +}); + + +// On start: display the first emoji and handle drag and touch events +g.clear(); +g.setFont('Vector', FONT_SIZE); +g.setFontAlign(0, 0); +g.drawString(WELCOME_MESSAGE, g.getWidth() / 2, g.getHeight() / 2); +Bangle.on('touch', handleTouch); +Bangle.on('drag', handleDrag); +transmitAppName(); diff --git a/apps/emojuino/emojuino.png b/apps/emojuino/emojuino.png new file mode 100644 index 000000000..614cc025e Binary files /dev/null and b/apps/emojuino/emojuino.png differ diff --git a/apps/emojuino/screenshot-swipe.png b/apps/emojuino/screenshot-swipe.png new file mode 100644 index 000000000..a870724b9 Binary files /dev/null and b/apps/emojuino/screenshot-swipe.png differ diff --git a/apps/emojuino/screenshot-tx.png b/apps/emojuino/screenshot-tx.png new file mode 100644 index 000000000..212d41f88 Binary files /dev/null and b/apps/emojuino/screenshot-tx.png differ diff --git a/apps/emojuino/screenshot-welcome.png b/apps/emojuino/screenshot-welcome.png new file mode 100644 index 000000000..4cf1fecdf Binary files /dev/null and b/apps/emojuino/screenshot-welcome.png differ diff --git a/apps/fclock/ChangeLog b/apps/fclock/ChangeLog index a8f708a0a..30e049f69 100644 --- a/apps/fclock/ChangeLog +++ b/apps/fclock/ChangeLog @@ -1 +1,2 @@ 0.01: First published version of app +0.02: Move to Bangle.setUI to launcher support diff --git a/apps/fclock/app-icon.js b/apps/fclock/app-icon.js index ba506d3ac..b9075b857 100644 --- a/apps/fclock/app-icon.js +++ b/apps/fclock/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("7OwBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AAAAAAAB//AAAAAAAAAAAAAAAAAAAAAAAAAAD//AAAAAAAB//8AAAAAAAAAAAAAAAAAAAAAAAAAD//wAAAAAAB///wAAAAAAAAAAAAAAAAAAAAAAAAB//8AAAAAAB////AAAAAAAAAAAAAAAAAAAAAAAAB///AAAAAAB////8AAAAAAAAAAAAAAAAAAAAAAAA///wAAAAAB/////wAAAAAAAAAAAAAAAAAAAAAAA///8AAAAAB//////AAAAAAAAAAAAAAAAAAAAAAAf///AAAAAA//////4AAAAAAAAAAAAAAAAAAAAAAf///wAAAAAP/////+AAAAAAAAAAAAAAAAAAAAAAP///8AAAAAH//////wAAAAAAAAAAAAAAAAAAAAAP////AAAAAB//////8AAAAAAAAAAAAAAAAAAAAAH////wAAAAA///////gAAAAAAAAAAAAAAAAAAAAH////8AAAAAP//////4AAAAAAAAAAAAAAAAAAAAB//9//AAAAAH//AAH//AAAAAAAAAAAAAAAAAAAAAf/+f/wAAAAB//gAA//wAAAAAAAAAAAAAAAAAAAAH/+H/8AAAAA//wAAH/+AAAAAAAAAAAAAAAAAAAAB//B//AAAAAP/4AAA//gAAAAAAAAAAAAAAAAAAAAf/Af/wAAAAH/8AAAH/8AAAAAAAAAAAAAAAAAAAAH/gH/8AAAAB/+AAAA//AAAAAAAAAAAAAAAAAAAAB/gB//AAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAfwAf/wAAAAH/4AAAAAAAAAAAAAAAAAAAAAAAAAAHwAH/8AAAAB/+AAAAAAAAAAAAAAAAAAAAAAAAAAB4AB//AAAAAf/gAAAAAAAAP4AAAAD8AAAAAAAAAAYAAf/wAAAAH/4AAAAAAAAH+AAAAB/AAAAAAAAAAEAAH/8AAAAB/+AAAAAAAAH/gAAAAfwAAAAAAAAAAAAB//AAAAAf/gf/wAAAAD/4AAAAP8AAAAAAAAAAAAAf/wAAAAH/4f//AAAAD/+AAAAH/AAAAAAAAAAAAAH/8AAAAB/+f//8AAAB//gAAAD/wAAAAAAAAAAAAB//AAAAAf/v///wAAA/34AAAA/8AAAAAAAAAAAAAf/wAAAAH/7////AAAP5+AAAAf/AAAAAAAAAAAAAH/8AAAAB/+////8AAD8fgAAAP/wAAAAAAAAAAAAB//AAAAAf/v////wAA8H4AAAD/8AAAAAAAAAAAAAf/wAAAAH/7////+AAOB+AAAB+/AAAAAAAAAAAAAH/8AAAAB/+/////gACAfgAAA/vwAAAAAAAAAAAAB//AAAAAf/v////8AAAH4AAAfz8AAPAB/AAAAAAAf/wAAAAH/7/////AAAB+AAAH4/AAPwA/8AAAAAAH/8AAAAB/+/////4AAAfgAAD8PwAH8AP/AAAAAAB//AAAAAf/v////+AAAH4AAB/D8AB3AHB4AAAAAAf/wAAAAH/7wAB//wAAB+AAAfg/AARwAAOAAAAAAH/8AAAAB/+4AAP/8AAAfgAAPwPwAAcAADgAAAAAB//AAAAAf/sAAB//gAAH4AAH4D8AAHAAB4AAAAAAf/wAAAAH/6AAAP/4AAB+AAD+A/AABwAH8AAAAAAH/8AAAAB/+AAAB//AAAfgAA/APwAAcAB/AAAAAAB//AAAAAf/gAAAP/wAAH4AAfgD8AAHAAfwAAAAAAf/wAAAAH/4AAAD/8AAB+AAH///4ABwAA8AAAAAAH/8AAAAB/+AAAA//AAAfgAB///+AAcAAHgAAAAAB//AAAAAf/gAAAP/wAAH4AAf///gAHAAA4AAAAAAf/wAAAAH/4AAAD/8AAB+AAH///4ABwAAOAAAAAAH/8AAAAB/+AAAA//AAAfgAB///+AAcAcHAAAAAAB//AAAAAf/gAAAP/wAAH4AAf///gB/+D/wAAAAAAf/wAAAAH/4AAAD/8AAB+AAAAA/AAf/g/4AAAAAAH/8AAAAB/+AAAA//AAAfgAAAAPwAH/4D8AAAAAAB//AAAAAf/gAAAP/wAAH4AAAAD8AAAAAAAAAAAAAf/wAAAAH/4AAAD/8AP///wAAA/AAAAAAAAAAAAAH/8AAAAB/+AAAA//AD///8AAAPwAAAAAAAAAAAAB//AAAAAf/gAAAP/wA////AAAD8AAAAAAAAAAAAAf/wAAAAH/4AAAD/8AP///wAAA/AAAAAAAAAAAAAH/8AAAAB//AAAB//AD///8AAAPwAAAAAAAAAAAAB//AAAAAP/4AAA//gA////AAAD8AAAAAAAAAAAAAf/wAAAAD//AAAf/4AP///wAAA/AAAAAAAAAAAAAH/8AAAAAf/4AAP/8AAAAAAAAAAAAAAAAAAAAAAAB//AAAAAH//AAH//AAAAAAAAAAAAAAAAAAAAAAAAf/wAAAAA//4AD//gAAAAAAAAAAAAAAAAAAAAH///////8AAP//////4AAAAAAAAAAAAAAAAAAAAB////////AAB//////8AAAAAAAAAAAAAAAAAAAAAf///////wAAf//////AAAAAAAAAAAAAAAAAAAAAH///////8AAD//////gAAAAAAAAAAAAAAAAAAAAB////////AAA//////4AAAAAAAAAAAAAAAAAAAAAf///////wAAH/////8AAAAAAAAAAAAAAAAAAAAAH///////8AAA/////+AAAAAAAAAAAAAAAAAAAAAB////////AAAD////+AAAAAAAAAAAAAAAAAAAAAAf///////wAAAP///+AAAAAAAAAAAAAAAAAAAAAAH///////8AAAA///+AAAAAAAAAAAAAAAAAAAAAAB////////AAAAD//+AAAAAAAAAAAAAAAAAAAAAAAf///////wAAAAP/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8P4DDwDAMPB4PAwAAAAAAAAAAAAAAAAAAAAAAAAfj+Ax+BwDH4/H48AAAAAAAAAAAAAAAAAAAAAAAAGMgAYYwcBhjMZjLAAAAAAAAAAAAAAAAAAAAAAAABDIAGEMPAYAyGAwwAAAAAAAAAAAAAAAAAAAAAAAAQx8DBDGwMAYhgYMAAAAAAAAAAAAAAAAAAAAAAAAEM/AwQzMDAOIYODAAAAAAAAAAAAAAAAAAAAAAAABDAYYEM/hgHCGHAwAAAAAAAAAAAAAAAAAAAAAAAAQwGGBDP4YDAhjAMAAAAAAAAAAAAAAAAAAAAAAAAEMhjAQwMMBgIZgDAAAAAAAAAAAAAAAAAAAAAAAAB+PwwH4DDAfz8fz+AAAAAAAAAAAAAAAAAAAAAAAAPB4YA8AxgH8eH8/gAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")) \ No newline at end of file +require("heatshrink").decompress(atob("mEwgRC/AH4AfgkBBA0BqADBAQQAEhfwAQP+BY1/BYM///ABQkC//wgm///gBYkT+4XB/sDAYIXEwfwgP4qFQlMAlBGCCYMB/47B/sA3gYCoYLC//R90R/kUikAC4f/8Mdifslm9BYf9g/wl0LnkYloLD/FC+kdh/+jttF4PxiP4qX0t1G3suvAXD/kC+v/6E8KYWn+2Q/1b+OqyMYO4REB8F/CoKDFKAUv/+AqtAXweq1WQgWof+IA/AE0GsNRiqnEBYe21Vq1WABY2p1WKXAILFgNUqFFqtQHBw")) diff --git a/apps/fclock/fclock.app.js b/apps/fclock/fclock.app.js index 044cde71f..afa0c5e2d 100644 --- a/apps/fclock/fclock.app.js +++ b/apps/fclock/fclock.app.js @@ -1,206 +1,203 @@ -{ - var minutes; - var seconds; - var hours; - var date; - var first = true; - var locale = require('locale'); - var _12hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"] || false; +var minutes; +var seconds; +var hours; +var date; +var first = true; +var locale = require('locale'); +var _12hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"] || false; - //HR variables - var id = 0; - var grow = true; - var size=10; +//HR variables +var id = 0; +var grow = true; +var size=10; - //Screen dimensions - const screen = { - width: g.getWidth(), - height: g.getWidth(), - middle: g.getWidth() / 2, - center: g.getHeight() / 2, - }; +//Screen dimensions +const screen = { + width: g.getWidth(), + height: g.getWidth(), + middle: g.getWidth() / 2, + center: g.getHeight() / 2, +}; - // Ssettings - const settings = { - time: { - color: '#dddddd', - font: 'Vector', - size: 100, - middle: screen.middle, - center: screen.center, - }, - date: { - color: '#dddddd', - font: 'Vector', - size: 15, - middle: screen.height-17, // at bottom of screen - center: screen.center, - }, - circle: { - colormin: '#ffffff', - colorsec: '#ffffff', - width: 10, - middle: screen.middle, - center: screen.center, - height: screen.height - }, - hr: { - color: '#333333', - size: 20, - x: screen.center, - y: screen.middle + 65 - } - }; +// Ssettings +const settings = { + time: { + color: '#dddddd', + font: 'Vector', + size: 100, + middle: screen.middle, + center: screen.center, + }, + date: { + color: '#dddddd', + font: 'Vector', + size: 15, + middle: screen.height-17, // at bottom of screen + center: screen.center, + }, + circle: { + colormin: '#ffffff', + colorsec: '#ffffff', + width: 10, + middle: screen.middle, + center: screen.center, + height: screen.height + }, + hr: { + color: '#333333', + size: 20, + x: screen.center, + y: screen.middle + 65 + } +}; - const dateStr = function (date) { - return locale.date(new Date(), 1); - }; +const dateStr = function (date) { + return locale.date(new Date(), 1); +}; - const getFormated = function(val) { - if (val<10) { - val='0'+val; - } +const getFormated = function(val) { + if (val<10) { + val='0'+val; + } - return val; - }; + return val; +}; - const drawMin = function (sections, color) { - - g.setFontAlign(0, 0, 0); - g.setColor('#000000'); - g.setFont(settings.time.font, settings.time.size/2); - g.drawString(getFormated(sections-1), settings.time.center+50, settings.time.middle); - g.setColor(settings.time.color); - g.setFont(settings.time.font, settings.time.size/2); - g.drawString(getFormated(sections), settings.time.center+50, settings.time.middle); - }; +const drawMin = function (sections, color) { - const drawSec = function (sections, color) { - g.setFontAlign(0, 0, 0); - g.setColor('#000000'); - g.setFont(settings.time.font, settings.time.size/4); - g.drawString(getFormated(sections-1), settings.time.center+100, settings.time.middle); - g.setColor(settings.time.color); - g.setFont(settings.time.font, settings.time.size/4); - g.drawString(getFormated(sections), settings.time.center+100, settings.time.middle); - }; + g.setFontAlign(0, 0, 0); + g.setColor('#000000'); + g.setFont(settings.time.font, settings.time.size/2); + g.drawString(getFormated(sections-1), settings.time.center+50, settings.time.middle); + g.setColor(settings.time.color); + g.setFont(settings.time.font, settings.time.size/2); + g.drawString(getFormated(sections), settings.time.center+50, settings.time.middle); +}; - const drawClock = function () { +const drawSec = function (sections, color) { + g.setFontAlign(0, 0, 0); + g.setColor('#000000'); + g.setFont(settings.time.font, settings.time.size/4); + g.drawString(getFormated(sections-1), settings.time.center+100, settings.time.middle); + g.setColor(settings.time.color); + g.setFont(settings.time.font, settings.time.size/4); + g.drawString(getFormated(sections), settings.time.center+100, settings.time.middle); +}; - currentTime = new Date(); +const drawClock = function () { - //Get date as a string - date = dateStr(currentTime); - - if(seconds==59) { - g.clear(); - } + currentTime = new Date(); - // Update minutes when needed - if (minutes != currentTime.getMinutes()) { - minutes = currentTime.getMinutes(); - drawMin(minutes, settings.circle.colormin); - } + //Get date as a string + date = dateStr(currentTime); - //Update seconds when needed - if (seconds != currentTime.getSeconds()) { - seconds = currentTime.getSeconds(); - drawSec(seconds, settings.circle.colorsec); - } + if(seconds==59) { + g.clear(); + } - //Write the time as configured in the settings - hours = currentTime.getHours(); - if (_12hour && hours > 13) { - hours = hours - 12; - } + // Update minutes when needed + if (minutes != currentTime.getMinutes()) { + minutes = currentTime.getMinutes(); + drawMin(minutes, settings.circle.colormin); + } - var meridian; + //Update seconds when needed + if (seconds != currentTime.getSeconds()) { + seconds = currentTime.getSeconds(); + drawSec(seconds, settings.circle.colorsec); + } - if (typeof locale.meridian === "function") { - meridian = locale.meridian(new Date()); - } else { - meridian = ""; - } + //Write the time as configured in the settings + hours = currentTime.getHours(); + if (_12hour && hours > 13) { + hours = hours - 12; + } - var timestr; + var meridian; - if (meridian.length > 0 && _12hour) { - timestr = hours + " " + meridian; - } else { - timestr = hours; - } - g.setFontAlign(0, 0, 0); - g.setColor(settings.time.color); - g.setFont(settings.time.font, settings.time.size); - g.drawString(timestr, settings.time.center-40, settings.time.middle); + if (typeof locale.meridian === "function") { + meridian = locale.meridian(new Date()); + } else { + meridian = ""; + } - //Write the date as configured in the settings - g.setColor(settings.date.color); - g.setFont(settings.date.font, settings.date.size); - g.drawString(date, settings.date.center, settings.date.middle); - }; + var timestr; - //setInterval for HR visualisation - const newBeats = function (hr) { - if (id != 0) { - changeInterval(id, 6e3 / hr.bpm); - } else { - id = setInterval(drawHR, 6e3 / hr.bpm); - } - }; + if (meridian.length > 0 && _12hour) { + timestr = hours + " " + meridian; + } else { + timestr = hours; + } + g.setFontAlign(0, 0, 0); + g.setColor(settings.time.color); + g.setFont(settings.time.font, settings.time.size); + g.drawString(timestr, settings.time.center-40, settings.time.middle); - //visualize HR with circles pulsating - const drawHR = function () { - if (grow && size < settings.hr.size) { - size++; - } + //Write the date as configured in the settings + g.setColor(settings.date.color); + g.setFont(settings.date.font, settings.date.size); + g.drawString(date, settings.date.center, settings.date.middle); +}; - if (!grow && size > 3) { - size--; - } +//setInterval for HR visualisation +const newBeats = function (hr) { + if (id != 0) { + changeInterval(id, 6e3 / hr.bpm); + } else { + id = setInterval(drawHR, 6e3 / hr.bpm); + } +}; - if (size == settings.hr.size || size == 3) { - grow = !grow; - } +//visualize HR with circles pulsating +const drawHR = function () { + if (grow && size < settings.hr.size) { + size++; + } - if (grow) { - color = settings.hr.color; - g.setColor(color); - g.fillCircle(settings.hr.x, settings.hr.y, size); - } else { - color = "#000000"; - g.setColor(color); - g.drawCircle(settings.hr.x, settings.hr.y, size); - } - }; + if (!grow && size > 3) { + size--; + } - // clean app screen - g.clear(); - Bangle.loadWidgets(); - Bangle.drawWidgets(); + if (size == settings.hr.size || size == 3) { + grow = !grow; + } - //manage when things should be enabled and not - Bangle.on('lcdPower', function (on) { - if (on) { - Bangle.setHRMPower(1); - } else { - Bangle.setHRMPower(0); - } - }); + if (grow) { + color = settings.hr.color; + g.setColor(color); + g.fillCircle(settings.hr.x, settings.hr.y, size); + } else { + color = "#000000"; + g.setColor(color); + g.drawCircle(settings.hr.x, settings.hr.y, size); + } +}; - // refesh every second - setInterval(drawClock, 1E3); +// clean app screen +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); - //start HR monitor and update frequency of update - Bangle.setHRMPower(1); - Bangle.on('HRM', function (d) { - newBeats(d); - }); +//manage when things should be enabled and not +Bangle.on('lcdPower', function (on) { + if (on) { + Bangle.setHRMPower(1); + } else { + Bangle.setHRMPower(0); + } +}); - // draw now - drawClock(); +// refesh every second +setInterval(drawClock, 1E3); - // Show launcher when middle button pressed - setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); +//start HR monitor and update frequency of update +Bangle.setHRMPower(1); +Bangle.on('HRM', function (d) { + newBeats(d); +}); -} \ No newline at end of file +// draw now +drawClock(); + +// Show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/fd6fdetect/ChangeLog b/apps/fd6fdetect/ChangeLog new file mode 100644 index 000000000..b85df5ace --- /dev/null +++ b/apps/fd6fdetect/ChangeLog @@ -0,0 +1,2 @@ +0.1: Added source code +0.2: Added a README file diff --git a/apps/fd6fdetect/README.md b/apps/fd6fdetect/README.md new file mode 100644 index 000000000..1a7cce8bd --- /dev/null +++ b/apps/fd6fdetect/README.md @@ -0,0 +1,3 @@ +# FD6FDetect + +An app dedicated to letting you know how many Exposure Notification beacons are near you. diff --git a/apps/fd6fdetect/app-icon.js b/apps/fd6fdetect/app-icon.js new file mode 100644 index 000000000..e8868099f --- /dev/null +++ b/apps/fd6fdetect/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIjgg/gAp0IgfAiAFBjkP+E4AoM8n/8ngFBvn//8+AoP//Ef/4FBv/Agf+AoMPwEB+AFCjEYAoUenk8vAvCAoIvCnAFBjgFCC4IFCCgUeEQNwAoMO+EPuPD4eOAoPz8fH54FH+IRBx4FBDogpFGoxBFJopZFMopxFPoqJFSoqhFVoq5FgAFBa6gAW")) diff --git a/apps/fd6fdetect/app.js b/apps/fd6fdetect/app.js new file mode 100644 index 000000000..a7ef71994 --- /dev/null +++ b/apps/fd6fdetect/app.js @@ -0,0 +1,23 @@ +g.clear(); +let amount = 'global value'; +function FindFD6FBeacons() { +NRF.findDevices(function(devices) { + g.setFont('Vector', 75); + g.setFontAlign(0,0); + var amount = devices.length; + g.clear(); + g.drawString(amount, 125, 100); + if (amount == 1) { + g.setFont('Vector', 25); + g.drawString('FD6F', 125, 150); + g.drawString('beacon', 125, 175); + g.drawString('nearby', 125, 200); + } else{ + g.setFont('Vector', 25); + g.drawString('FD6F', 125, 150); + g.drawString('beacons', 125, 175); + g.drawString('nearby', 125, 200); + } +}, {timeout : 1000, filters : [{services: ['fd6f'] }] }); +} +setInterval(FindFD6FBeacons, 2000); diff --git a/apps/fd6fdetect/app.png b/apps/fd6fdetect/app.png new file mode 100644 index 000000000..cd0a92f7c Binary files /dev/null and b/apps/fd6fdetect/app.png differ diff --git a/apps/ffcniftya/ChangeLog b/apps/ffcniftya/ChangeLog new file mode 100644 index 000000000..18bc264a3 --- /dev/null +++ b/apps/ffcniftya/ChangeLog @@ -0,0 +1 @@ +0.01: New Clock Nifty A diff --git a/apps/ffcniftya/README.md b/apps/ffcniftya/README.md new file mode 100644 index 000000000..f1fee9b1f --- /dev/null +++ b/apps/ffcniftya/README.md @@ -0,0 +1,4 @@ +# Nifty-A Clock + +![](screenshot_nifty.png) + diff --git a/apps/ffcniftya/app-icon.js b/apps/ffcniftya/app-icon.js new file mode 100644 index 000000000..f0a2393b1 --- /dev/null +++ b/apps/ffcniftya/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEIf4A5gX/+AGEn//mIWLgP/C4gGCAAMgC5UvC4sDC4YICkIhBgMQiEBE4Uxn4XDj//iEAn/yA4ICBgUikEikYXBBAIXEn/xJYURAYMygERkQHBiYLBKYIXF+AVDC4czgUSmIXBCQgED+ZeBR4YXBLYICDC5CPGC4IAIC40zmaPDC4MSLQQXK+ayCR4QXCiRoEC44ECh4bCC4MTiTDBC6ZHOC5B3NLYcvC4kBgL5BAAUikT+BfIIrB/8ykf/eYQXBkUTI4cBW4YQCgQGDmAXDkJfEC46GBAoJKCR4geCAAMRAAZRDAoIODO4UBPRIAJR5QXWgKNCTApNDC5Mv/6/DAwR3GAAyHCC4anJIo3/+bvEa4Uia4oXHkEvC4cvIgUf+YXKHYIvEAgcPC5QSGC5UBSwYXJLYQXFkUhgABBC5Ef/4mBl4XEmETmIXKgaXBmYCBC4cTkMxiQXJS4IACL4p3MgESCwJHFR5oxCiB3FkERC5cSToQXFmUyiAZFR48Bn7zCAQMjkfykQkBN4n/XgKPBAAQgCUQIfBUwYXHFgIGCdI4XDmYADmIIEkAWJAH4A4A==")) \ No newline at end of file diff --git a/apps/ffcniftya/app.js b/apps/ffcniftya/app.js new file mode 100644 index 000000000..31742f64a --- /dev/null +++ b/apps/ffcniftya/app.js @@ -0,0 +1,95 @@ +const locale = require("locale"); +const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; + +/* Clock *********************************************/ +const scale = g.getWidth() / 176; + +const widget = 24; + +const viewport = { + width: g.getWidth(), + height: g.getHeight(), +} + +const center = { + x: viewport.width / 2, + y: Math.round(((viewport.height - widget) / 2) + widget), +} + +function d02(value) { + return ('0' + value).substr(-2); +} + +function draw() { + g.reset(); + g.clearRect(0, widget, viewport.width, viewport.height); + const now = new Date(); + + const hour = d02(now.getHours() - (is12Hour && now.getHours() > 12 ? 12 : 0)); + const minutes = d02(now.getMinutes()); + const day = d02(now.getDate()); + const month = d02(now.getMonth() + 1); + const year = now.getFullYear(); + + const month2 = locale.month(now, 3); + const day2 = locale.dow(now, 3); + + g.setFontAlign(1, 0).setFont("Vector", 90 * scale); + g.drawString(hour, center.x + 32 * scale, center.y - 31 * scale); + g.drawString(minutes, center.x + 32 * scale, center.y + 46 * scale); + + g.fillRect(center.x + 30 * scale, center.y - 72 * scale, center.x + 32 * scale, center.y + 74 * scale); + + g.setFontAlign(-1, 0).setFont("Vector", 16 * scale); + g.drawString(year, center.x + 40 * scale, center.y - 62 * scale); + g.drawString(month, center.x + 40 * scale, center.y - 44 * scale); + g.drawString(day, center.x + 40 * scale, center.y - 26 * scale); + g.drawString(month2, center.x + 40 * scale, center.y + 48 * scale); + g.drawString(day2, center.x + 40 * scale, center.y + 66 * scale); +} + + +/* Minute Ticker *************************************/ + +let tickTimer; + +function clearTickTimer() { + if (tickTimer) { + clearTimeout(tickTimer); + tickTimer = undefined; + } +} + +function queueNextTick() { + clearTickTimer(); + tickTimer = setTimeout(tick, 60000 - (Date.now() % 60000)); + // tickTimer = setTimeout(tick, 3000); +} + +function tick() { + draw(); + queueNextTick(); +} + +/* Init **********************************************/ + +// Clear the screen once, at startup +g.clear(); +// Start ticking +tick(); + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower', (on) => { + if (on) { + tick(); // Start ticking + } else { + clearTickTimer(); // stop ticking + } +}); + +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); \ No newline at end of file diff --git a/apps/ffcniftya/app.png b/apps/ffcniftya/app.png new file mode 100644 index 000000000..1cd8a49b7 Binary files /dev/null and b/apps/ffcniftya/app.png differ diff --git a/apps/ffcniftya/screenshot_nifty.png b/apps/ffcniftya/screenshot_nifty.png new file mode 100644 index 000000000..0df056223 Binary files /dev/null and b/apps/ffcniftya/screenshot_nifty.png differ diff --git a/apps/ffcniftyb/ChangeLog b/apps/ffcniftyb/ChangeLog new file mode 100644 index 000000000..dedd31452 --- /dev/null +++ b/apps/ffcniftyb/ChangeLog @@ -0,0 +1,2 @@ +0.01: New Clock Nifty B +0.02: Added configuration \ No newline at end of file diff --git a/apps/ffcniftyb/README.md b/apps/ffcniftyb/README.md new file mode 100644 index 000000000..e04243a0b --- /dev/null +++ b/apps/ffcniftyb/README.md @@ -0,0 +1,9 @@ +# Nifty Series B Clock + +- Display Time and Date +- Color Configuration + +## + +![](screenshot.png) + diff --git a/apps/ffcniftyb/app-icon.js b/apps/ffcniftyb/app-icon.js new file mode 100644 index 000000000..1aac04351 --- /dev/null +++ b/apps/ffcniftyb/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkB/4A/AH4ARgMRBA3xBBIJCAYIFDAAYHGCAYJBDYQABj4PD+AXFCwgXGCAg9ECwwJBJQooGCxAXCIYQpBAgg9IC5yPCCw4XKBYIsFPwUBXQQXHAYREIF5ZEC+MfWQYXODQYTGC5ZDEOw0QMAIXMPggvSC44vRL5b8EAYIACC5i0FCwaOBC5C0DA4ZLCC5hfC/4DBIwwXKCInwgAWEKIwXJAA4XXCxYXCEwR2EgJeLR5LbCGRYXIAgzvKh7zGZg4XGIYisBA4JJCC6B5DAoYXWF6xfRC4fwAgMBC6cBU5I6CC5AECCo0QJwQXJaZJHMEYR1JC5QKBXo8QC4oCBAZAwHgKXBTQwSDBIKmGgJ3DEYheEA4ZfJKgkPdJQXHDAQWBC44eIC4QAMDA4A==")) \ No newline at end of file diff --git a/apps/ffcniftyb/app.js b/apps/ffcniftyb/app.js new file mode 100644 index 000000000..75d217ab4 --- /dev/null +++ b/apps/ffcniftyb/app.js @@ -0,0 +1,118 @@ +const locale = require("locale"); +const storage = require('Storage'); + +const is12Hour = (storage.readJSON("setting.json", 1) || {})["12hour"]; +const color = (storage.readJSON("ffcniftyb.json", 1) || {})["color"] || 63488 /* red */; + + +/* Clock *********************************************/ +const scale = g.getWidth() / 176; + +const screen = { + width: g.getWidth(), + height: g.getHeight() - 24, +}; + +const center = { + x: screen.width / 2, + y: screen.height / 2, +}; + +function d02(value) { + return ('0' + value).substr(-2); +} + +function renderEllipse(g) { + g.fillEllipse(center.x - 5 * scale, center.y - 70 * scale, center.x + 160 * scale, center.y + 90 * scale); +} + +function renderText(g) { + const now = new Date(); + + const hour = d02(now.getHours() - (is12Hour && now.getHours() > 12 ? 12 : 0)); + const minutes = d02(now.getMinutes()); + const day = d02(now.getDate()); + const month = d02(now.getMonth() + 1); + const year = now.getFullYear(); + + const month2 = locale.month(now, 3); + const day2 = locale.dow(now, 3); + + g.setFontAlign(1, 0).setFont("Vector", 90 * scale); + g.drawString(hour, center.x + 32 * scale, center.y - 31 * scale); + g.drawString(minutes, center.x + 32 * scale, center.y + 46 * scale); + + g.setFontAlign(1, 0).setFont("Vector", 16 * scale); + g.drawString(year, center.x + 80 * scale, center.y - 42 * scale); + g.drawString(month, center.x + 80 * scale, center.y - 26 * scale); + g.drawString(day, center.x + 80 * scale, center.y - 10 * scale); + g.drawString(month2, center.x + 80 * scale, center.y + 44 * scale); + g.drawString(day2, center.x + 80 * scale, center.y + 60 * scale); +} + +const buf = Graphics.createArrayBuffer(screen.width, screen.height, 1, { + msb: true +}); + +function draw() { + + const img = { + width: screen.width, + height: screen.height, + transparent: 0, + bpp: 1, + buffer: buf.buffer + }; + + // cleat screen area + g.clearRect(0, 24, g.getWidth(), g.getHeight()); + + // render outside text with ellipse + buf.clear(); + renderText(buf.setColor(1)); + renderEllipse(buf.setColor(0)); + g.setColor(color).drawImage(img, 0, 24); + + // render ellipse with inside text + buf.clear(); + renderEllipse(buf.setColor(1)); + renderText(buf.setColor(0)); + g.setColor(color).drawImage(img, 0, 24); +} + + +/* Minute Ticker *************************************/ + +let ticker; + +function stopTick() { + if (ticker) { + clearTimeout(ticker); + ticker = undefined; + } +} + +function startTick(run) { + stopTick(); + run(); + ticker = setTimeout(() => startTick(run), 60000 - (Date.now() % 60000)); + // ticker = setTimeout(() => startTick(run), 3000); +} + +/* Init **********************************************/ + +g.clear(); +startTick(draw); + +Bangle.on('lcdPower', (on) => { + if (on) { + startTick(draw); + } else { + stopTick(); + } +}); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +Bangle.setUI("clock"); diff --git a/apps/ffcniftyb/app.png b/apps/ffcniftyb/app.png new file mode 100644 index 000000000..a6acf0121 Binary files /dev/null and b/apps/ffcniftyb/app.png differ diff --git a/apps/ffcniftyb/screenshot.png b/apps/ffcniftyb/screenshot.png new file mode 100644 index 000000000..f7d3fd678 Binary files /dev/null and b/apps/ffcniftyb/screenshot.png differ diff --git a/apps/ffcniftyb/settings.js b/apps/ffcniftyb/settings.js new file mode 100644 index 000000000..00abf80b5 --- /dev/null +++ b/apps/ffcniftyb/settings.js @@ -0,0 +1,49 @@ +(function (back) { + const storage = require('Storage'); + const SETTINGS_FILE = "ffcniftyb.json"; + + const colors = { + 65535: 'White', + 63488: 'Red', + 65504: 'Yellow', + 2047: 'Cyan', + 2016: 'Green', + 31: 'Blue', + 0: 'Black', + } + + function load(settings) { + return Object.assign(settings, storage.readJSON(SETTINGS_FILE, 1) || {}); + } + + function save(settings) { + storage.write(SETTINGS_FILE, settings) + } + + const settings = load({ + color: 63488 /* red */, + }); + + const saveColor = (color) => () => { + settings.color = color; + save(settings); + back(); + }; + + function showMenu(items, opt) { + items[''] = opt || {}; + items['< Back'] = back; + E.showMenu(items); + } + + showMenu( + Object.keys(colors).reduce((menu, color) => { + menu[colors[color]] = saveColor(color); + return menu; + }, {}), + { + title: 'Color', + selected: Object.keys(colors).indexOf(settings.color) + } + ); +}); diff --git a/apps/files/ChangeLog b/apps/files/ChangeLog index b4037a733..1908f7e5c 100644 --- a/apps/files/ChangeLog +++ b/apps/files/ChangeLog @@ -2,4 +2,5 @@ 0.03: Add support for data files 0.04: Add functionality to sort apps manually or alphabetically ascending/descending. 0.05: Tweaks to help with memory usage -0.06: Reduce memory usage \ No newline at end of file +0.06: Reduce memory usage +0.07: Allow negative numbers when manual-sorting \ No newline at end of file diff --git a/apps/files/files-icon.js b/apps/files/files-icon.js index 7e55db9e0..7f7ea4d0c 100644 --- a/apps/files/files-icon.js +++ b/apps/files/files-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwghC/AEkIxAABwUiAAwKBC6+AC6ERiIXDGBAXPGA8JzIAByQXKGA4XUA4eDmYAGJwQXVxEizAXPIgIXDwWZC6uIxIwCC6eIGAQX/C9i/FC5mCCw0yC5wAMC/4Xnx//ABf4C/Xzdw8zn4XkL/5f/L+oUDI6YX3AB4XeAH4AdA==")) +require("heatshrink").decompress(atob("mEw4cA///7c0AYMXlm3gf42s1yvb5xT/ABdJkmStu27YCCtMkCKOACJdm7YRCyARQyQRLBwIRDoARTgVLtu3K4tJl4RQkvpCJdbtwRBkm5CKGZCKGTCKGSsgR/R4gRHpIMBCInaCJIIBARAR/CJtPB5FLCI1KEhMSCLN//4AE/QRbI/5H/CI4PCGpwRXp4RIpZFDCIQiJAQIRWAH4AGA")) diff --git a/apps/files/files.js b/apps/files/files.js index 9e6c97702..e7b42c101 100644 --- a/apps/files/files.js +++ b/apps/files/files.js @@ -7,13 +7,9 @@ function showMainMenu() { '': { 'title': 'App Manager', }, - 'Free': { - value: undefined, - format: (v) => { - return store.getFree(); - }, - onchange: () => {} - }, + '< Back': ()=> {load();}, + 'Sort Apps': () => showSortAppsMenu(), + 'Manage Apps': ()=> showApps(), 'Compact': () => { E.showMessage('Compacting...'); try { @@ -22,9 +18,13 @@ function showMainMenu() { } showMainMenu(); }, - 'Apps': ()=> showApps(), - 'Sort Apps': () => showSortAppsMenu(), - '< Back': ()=> {load();} + 'Free': { + value: undefined, + format: (v) => { + return store.getFree(); + }, + onchange: () => {} + }, }; E.showMenu(mainmenu); } @@ -180,7 +180,7 @@ function showSortAppsManually() { appList.reduce((menu, app) => { menu[app.name] = { value: app.sortorder || 0, - min: 0, + min: -appList.length, max: appList.length, step: 1, onchange: val => setSortorder(app, val) diff --git a/apps/findphone/ChangeLog b/apps/findphone/ChangeLog index 86558abf5..29100f3c1 100644 --- a/apps/findphone/ChangeLog +++ b/apps/findphone/ChangeLog @@ -1,2 +1,3 @@ 0.01: First Version 0.02: Remove HID requirement, update screen +0.03: Fix for Bangle 2, toggle find with top half of screen, exit touch bottom half of screen diff --git a/apps/findphone/README.md b/apps/findphone/README.md index c655457a2..64e719d6a 100644 --- a/apps/findphone/README.md +++ b/apps/findphone/README.md @@ -6,3 +6,8 @@ Ring your phone via GadgetBridge if you lost it somewhere. 2. Lose phone 3. Open app 4. Click any button or screen + +## On a Bangle 2 + +- You can touch the top half of the screen to toggle Find / Stop +- You can touch the bottom half of the screen to exit the app. diff --git a/apps/findphone/app.js b/apps/findphone/app.js index 34f729bc7..e5e32739a 100644 --- a/apps/findphone/app.js +++ b/apps/findphone/app.js @@ -1,13 +1,15 @@ //notify your phone +const fontSize = g.getWidth() / 8; var finding = false; function draw() { // show message - g.clear(1); - require("Font8x12").add(Graphics); - g.setFont("8x12",3); + g.clear(g.theme.bg); + g.setColor(g.theme.fg); + g.setFont("Vector", fontSize); g.setFontAlign(0,0); + if (finding) { g.drawString("Finding...", g.getWidth()/2, (g.getHeight()/2)-20); g.drawString("Click to stop", g.getWidth()/2, (g.getHeight()/2)+20); @@ -17,17 +19,37 @@ function draw() { g.flip(); } +function findPhone(v) { + Bluetooth.println(JSON.stringify({t:"findPhone", n:v})); +} + function find(){ finding = !finding; draw(); - Bluetooth.println("\n"+JSON.stringify({t:"findPhone", n:finding})); + findPhone(finding); } draw(); //register all buttons and screen to find phone setWatch(find, BTN1, {repeat:true}); -setWatch(find, BTN2, {repeat:true}); -setWatch(find, BTN3, {repeat:true}); -setWatch(find, BTN4, {repeat:true}); -setWatch(find, BTN5, {repeat:true}); + +if (process.env.HWVERSION == 1) { + setWatch(find, BTN2, {repeat:true}); + setWatch(find, BTN3, {repeat:true}); + setWatch(find, BTN4, {repeat:true}); + setWatch(find, BTN5, {repeat:true}); +} + +if (process.env.HWVERSION == 2) { + Bangle.on('touch', function(button, xy) { + + // click top part of the screen to stop start + if (xy.y < g.getHeight() / 2) { + find(); + } else { + findPhone(false); + setTimeout(load, 100); // exit in 100ms + } + }); +} diff --git a/apps/flappy/ChangeLog b/apps/flappy/ChangeLog index 62f107d11..349cb9d07 100644 --- a/apps/flappy/ChangeLog +++ b/apps/flappy/ChangeLog @@ -1,3 +1,4 @@ 0.02: Tweaks to make flappy bird run with less RAM available 0.03: A few tweaks to improve rendering speed 0.04: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast +0.05: Don't use Bangle.setLCDMode, just use offscreen buffer (allows widgets) diff --git a/apps/flappy/README.md b/apps/flappy/README.md new file mode 100644 index 000000000..7a8f6c548 --- /dev/null +++ b/apps/flappy/README.md @@ -0,0 +1,5 @@ +# Flappy Bird + +![](screenshot1_flappy.png) +![](screenshot2_flappy.png) + diff --git a/apps/flappy/app.js b/apps/flappy/app.js index 402614a07..e9ca31fa5 100644 --- a/apps/flappy/app.js +++ b/apps/flappy/app.js @@ -1,3 +1,21 @@ +b = Graphics.createArrayBuffer(120,120,8); +var gimg = { + width:120, + height:104, + bpp:8, + buffer:b.buffer + }; + +if (process.env.HWVERSION==2) { + b.flip = function() { + g.drawImage(gimg,28,50); + }; +} else { + b.flip = function() { + g.drawImage(gimg,0,24,{scale:2}); + }; +} + var BIRDIMG = E.toArrayBuffer(atob("EQyI/v7+/v7+/gAAAAAAAP7+/v7+/v7+/gYG0tLS0gDXAP7+/v7+/v4A0tLS0tIA19fXAP7+/v4AAAAA0tLS0gDX1wDXAP7+ANfX19cA0tLSANfXANcA/v4A19fX19cA0tLSANfX1wD+/gDS19fX0gDS0tLSAAAAAAD+/gDS0tIA0tLS0gDAwMDAwAD+/gAAAM3Nzc0AwAAAAAAA/v7+/v4Azc3Nzc0AwMDAwAD+/v7+/v4AAM3Nzc0AAAAAAP7+/v7+/v7+AAAAAP7+/v7+/g==")) var FLOORIMG = require("heatshrink").decompress(atob("iEKxH+kklABuLAAlgAAwNFB34OLmAAO0YAO5wAOA")); @@ -33,26 +51,26 @@ function gameStop() { function draw() { "ram" - var H = g.getHeight()-24; - g.setColor("#71c6cf"); - g.fillRect(0,0,g.getWidth(),H-1); + var H = b.getHeight()-24; + b.setColor("#71c6cf"); + b.fillRect(0,0,b.getWidth(),H-1); floorpos++; - for (var x=-(floorpos&15);x H) gameStop(); // draw bird - g.drawImage(BIRDIMG, 6,birdy, {rotate:Math.atan2(birdvy,15)}); + b.drawImage(BIRDIMG, 6,birdy, {rotate:Math.atan2(birdvy,15)}); // draw barriers - barriers.forEach(function(b) { - b.x1--; - b.x2--; - var btop = b.y-b.gap; - var bbot = b.y+b.gap; - g.setColor("#73bf2f"); // middle - g.fillRect(b.x1+4, 0, b.x2-4, btop-1); - g.fillRect(b.x1+4, bbot, b.x2-4, H-1); - g.setColor("#c0f181"); // left - g.fillRect(b.x1+1, 0, b.x1+3, btop-1); - g.fillRect(b.x1+1, bbot, b.x1+3, H-1); - g.setColor("#538917"); // right - g.fillRect(b.x2-3, 0, b.x2-1, btop-1); - g.fillRect(b.x2-3, bbot, b.x2-1, H-1); - g.setColor("#808080"); // outlines - g.drawRect(b.x1, btop-5, b.x2, btop); // top - g.drawLine(b.x1+1, 0, b.x1+1, btop-6); - g.drawLine(b.x2-2, 0, b.x2-2, btop-6); - g.drawRect(b.x1, bbot, b.x2, bbot+5); // bottom - g.drawLine(b.x1+1, bbot+6, b.x1+1, H-1); - g.drawLine(b.x2-1, bbot+6, b.x2-1, H-1); - if (b.x1<6 && (birdy-3bbot)) + barriers.forEach(function(r) { + r.x1--; + r.x2--; + var btop = r.y-r.gap; + var bbot = r.y+r.gap; + b.setColor("#73bf2f"); // middle + b.fillRect(r.x1+4, 0, r.x2-4, btop-1); + b.fillRect(r.x1+4, bbot, r.x2-4, H-1); + b.setColor("#c0f181"); // left + b.fillRect(r.x1+1, 0, r.x1+3, btop-1); + b.fillRect(r.x1+1, bbot, r.x1+3, H-1); + b.setColor("#538917"); // right + b.fillRect(r.x2-3, 0, r.x2-1, btop-1); + b.fillRect(r.x2-3, bbot, r.x2-1, H-1); + b.setColor("#808080"); // outlines + b.drawRect(r.x1, btop-5, r.x2, btop); // top + b.drawLine(r.x1+1, 0, r.x1+1, btop-6); + b.drawLine(r.x2-2, 0, r.x2-2, btop-6); + b.drawRect(r.x1, bbot, r.x2, bbot+5); // bottom + b.drawLine(r.x1+1, bbot+6, r.x1+1, H-1); + b.drawLine(r.x2-1, bbot+6, r.x2-1, H-1); + if (r.x1<6 && (birdy-3bbot)) gameStop(); }); while (barriers.length && barriers[0].x2<=0) { @@ -94,7 +112,7 @@ function draw() { newBarrier(g.getWidth()); } - g.flip(); + b.flip(); } Bangle.on('touch', function(button) { @@ -105,11 +123,9 @@ Bangle.on('touch', function(button) { } }); -// Finally, start everything going -setTimeout(()=>{ - Bangle.setLCDMode("120x120"); - g.setBgColor("#e3db9d"); - g.clear(); - gameStart(); - setInterval(draw, 100); -},10); +Bangle.loadWidgets(); +g.clear(); +Bangle.drawWidgets(); +b.setBgColor("#e3db9d"); +gameStart(); +setInterval(draw, 100); diff --git a/apps/flappy/screenshot1_flappy.png b/apps/flappy/screenshot1_flappy.png new file mode 100644 index 000000000..63b283db1 Binary files /dev/null and b/apps/flappy/screenshot1_flappy.png differ diff --git a/apps/flappy/screenshot2_flappy.png b/apps/flappy/screenshot2_flappy.png new file mode 100644 index 000000000..0c589cdec Binary files /dev/null and b/apps/flappy/screenshot2_flappy.png differ diff --git a/apps/floralclk/ChangeLog b/apps/floralclk/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/floralclk/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/floralclk/README.md b/apps/floralclk/README.md new file mode 100644 index 000000000..6cffc6e55 --- /dev/null +++ b/apps/floralclk/README.md @@ -0,0 +1,4 @@ +# Floral Clock + +![](screenshot_floral.png) + diff --git a/apps/floralclk/app-icon.js b/apps/floralclk/app-icon.js new file mode 100644 index 000000000..4dfc57191 --- /dev/null +++ b/apps/floralclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhE+sVin0/tVjsdim84sdro1GAQNrAAlHAYVqABk/FINosc/AoNpF4cTGoMTnIhBo1qEgIvFABJACoAvEFwJaDGoNjnFpn5eCik/DYQwBAAQwOFIMUDYKBBLwQwBnwoBBAM3GIIEBhs5E4RLBPYqMKFwU4AAM+nwCBF4SJCAwMxXII2BnBeDJogAGNQIAFBIJMBRIYvCLQK+Bow7BhsNCINjm45BXwZgHF5ITBigoBF4NpQoIwBLwLJBn8Oh0NBoU4F4J6CF4RiKR47iCtIrBiaGBEgdknMOnBABiYKBtNkKoaUIdo5hCQoKvBYgKGBGAIJBMANjhqfBHgLQBNgKcBEpAxBBA9HHoiwBF4S3BcoM4Nwdim83sVEGAINBMQIfBEASYGLII4ECISFBnEyFgKHBGwRsDHYKfBaQOGrifCXw4qBNgIEBXoQHBCQZXBnArCAQNpnBWBFwUTBINiwGkwFcsbzDEwJcFG4pcCAAMUik/EIKJBn6JBMYNpnzABsY0BwGeAAN6wLnCEQQACF4ztCF4UUJ4QNDGAKTCtMTnASBHwOezAwCveIP4ReEeQzNDFwgvDDQU/oDlDJYVkF4e8z2Hx2Px2IAAKKEGo1qnAuBtLYBBwRJCAoIuCbgVqxAsCAQWB1mBwN6AANWmSwBJwRcCDIIuDnxAGAYU5HQmPF4W84QBBvWlGoOY4TIBmMxnJGCYYc5ik+coNjn8UhsUiqRDGQQUBQQJUBz3CzAxBYYYADvWGsTZBDoNHcQUTXgU+n1pB4LmBFQSUCAoNkw4uCF4QBBF4QFBAAIFBF4IjBoBMCn84nwtCMAIABm8TnLREXYd7KQSLBzoBDAQJlBBQN5w1osU/VAQuBnCLBGQNpGYM4R4LRCAATTBRgJcCy4kBz3I5HO5HHy4JBYwRfBcYIfBoE4m5YCho0BA4M4FwzzCxCMBEIO73guB5wAC5BgBSoWAF4KQBsdkKoKNChoACik4nIuHF4i0BdQOdF4XNAQK8Cz2lqzjCnCIBFwTnBS4IrBTQQuBdoLuCBAWOdoOYWgRfC5ovDy4vDriDCAAS8CFQYADdQgvCowvDSAK0CF4SPC4QwCvVcmUymMxFwSUBFwQoDFQToBAoIsBBoSQBMASRBy6QDXwIJCSIWAFwjzDWowAKGYOHwIhBYIOezooC3YuBF4d6GAK/BYAVkF6CUExBiBYQQCBFQIvCzAvCAYM3LoUTLwIeDF5pBDSQRgCLQYDBZQZrBz1cw9iiZeCWoQvWeYQuBfIIxCLwd6w1inEULyQvFtYvFEwOB0uBz4zCX4QuBnDsCDortNCQNHL4mYFwN7ZAOIfgN6AANcsc+m6NBDoRgQFwNGo5FBx2HKoZeBHYNqsg7BqtVsS8BRoReCL6AgBSYQ1CwJWBveHZYlkmMxLwM4h05sgADPwRRCF5ouDMIKKCxwPDsk4mM4XgMTXwLvBikOYYQvOBoQOBoE/JA4DBn8UFwNim8NF4QABhsNnIvQUgVAnMOVoQ4CAANqscUidiRoMNm4zBAAQHBF6CLDO4JIBGAVHXgYiBn1jn0NGYVoAAIvBIwYvOBgM/hyxBAAQXBHYU5RANjscTLwNjLgIuBny+FF5xeBhtcPYU+DYJeDRog0CCIYSBoAvRGAMUmOHJgcbF4QuBFIUNmIBBeYItCIIRNBd54ABisUVgNED4QJBn69Dm4uBh0OnIsBoArCFBoPDHgNqoAvBL4YvCb4JeBnxiCslkDogvRNQVGGALrBVobwBfAMNXoMTigsHDINHAAIvJGIdGn9ro4FBscNMANpF4LoBm4DChq1BFAJDBDobmMMIgvDA4UULwKHBMoLlBG4MynBeBCYQfFF56MBoAbDMAKzBnETm7oBGoM4hxeCQoJfCcJC/KAgIvFMAMNAASNBsQ1BLwVqFwIeELppCBF4dq")) diff --git a/apps/floralclk/app.js b/apps/floralclk/app.js new file mode 100644 index 000000000..5fb9303a8 --- /dev/null +++ b/apps/floralclk/app.js @@ -0,0 +1,76 @@ +function getImg() { + return require("heatshrink").decompress(atob("2F0gdt23bAX4C/AWvYppB+2kAgM2IPuwgRB/2ESpJB/IIMmzYUN6EJIN1IgECChuAa9u0IIUApoUMgVAINsCoMkwBBMKYRBs0kAgMkyBBGwDOEIIUmDoqbOAS0EySDBII1sgMAIJmgLgJBithBLpMkYpmBkmBIMckyTFByQLFsBBGgRBGxJBlgmQIIOTBYtiII0AgDFEtkJkmAJQoCdgGSFAILGgRBD7QOBIIMAibUFyBBj22SpJxEtsG7cSIIfQH4QACBAMAiBBn7ZBFsEAghLBIIXAAgJBDhuBkgOCyBcFIMDFEYQRBHwDIBAQIDBIIcAIMsEAobCCII0ggA9BHQJBEyUAjZBx7TCCQYRBDtu0yVIgZBizdJgGbYpQRB2mAoBEBhuBIIlJIMWggEBkBBDsA+Bydt0gUEwFJ0wFB2CDowDrBIIltWwJBGQYIaESQZBBjZBhghBCEwmJIJGCIJNJrZBhEoMAkhBDtiDDklsgEApukIIjFCIIVATwhBggjsBkhBBOIcktEEwEN0j7EIIw+fAQWkyEIIINggEbsBBEsEkwCLBiZBJgBBi2matuEwS7BgdiII2QhMgagZBCyFIIMoCCwGAgJBJyRBG2kAgMwBgMGIM41BZANJghBGgGbC4nAhu2TQMmIMugiBBDgBBDtkAyEIIIxEDgI4coBfI2D7BgETBAUCIIKPBgBBByR3k23aUQJrH2mQBYIIDsFIIIL+BpEAwEBmxBmO4ZBEiUAgwIDYQMAAoPQoEEKAJBlfYQLHyQyIpu26VAkgOBcBBBcegJBIwVAQYgCChJBq7ZBBgVtgEbBYnApBBHgJBBgEkyEBSQ9sghBetEAiYLE7EJgAUGoLRBgMkgFJEY9AgGbILVIkECZA/aIJO0iCGBEZMAILiABgEJII8BkDOFTAM0yEJEZJZBkhBbtuAIITFE2kAIJMgwENIJSkBILmkIIQ4E0GSgEkgQOBYokEwFNUhEE6RBekiwBkAIEIINIIILUBR4cBgkAEBFAgmCILtpkh6CIIsSIILSBgCGBBYMAggFDAQqhBwBBBQDJBDyFJkwLE2mSNwJBBZARBCkkDIOe2d4JBBgIvBIIcgZYYCFCIUAEAzFYMROgyBBFgMgiQgKIIMEzZBatskyZBJ2BBCwS5DkEQgIgI0hBBgEbILZlDEBESIIMCIIcAyVAXJG0gAUBahKGWEAOkEYvCoEAgYICpEEyT7J2ECoJBg0mDIgI4DIJFAgmQgEGDo+AyTmBYrxBBwQjBXgYCB6FIeQkBkGAwBBHtkEydtkBBf2mSU4ImBBYfaIIObIIe0wmSII9gkgRBAQRBeiRBBEY1JgDyDhO28mSoAdGgMkHbgCGYoRBHkEDAoVNIIVBoEAJofYhKeGATvApEEBY1hkkABAlEbAWSgBNC4BTBgENIMPQpMmBY1AgmAQAIIBwA+BSwJBBwARBgAHBwBBjhJBG7EAIIIvBzdsgBBFyFN2kCIMvadgLOGBAOQgOwidgAwJBEyVN0ESgLFBSoYCfgJBHeoJBBgECsA+CIIqGBgAOBH0ACCsEgzZBHiAyBgFiHwIPBRoMEyFIgGABoMTfa8AgxBKkkbYo0AiUAHAJBFyUEwFAAQMAkx3X4CkBBxNoghoFKwJBBGoOSYoRBDRoUkQC4CCE4MAiEBmxBIwQIE7SAB7BBByBBDtLFBIAMBbowCERh5iBoAhBCg9BgBBFIgdIXINshIdBIgIgCagLpKgBNKAQWwEYRBBggOF6AuByFNDQ9JkEAtq9BIIpNBIJTZCIKGAgEbBwnSFwUJDQ9pIIW0IIggBpEGGRNBkmTIKACBpBBF4QKBiUBDREkIILjCDocCoE2IMEDBwnABQMCoIsIkmAAoMEyQwBDoJWBIJUBIJts5KnFgRBFhMgNxWkIIQaBgMECQMQTBJBQoA+DdIcNIIkAAANIOwIQBzYdD2mSQYcE6ATBwAdEGQ8kiZBLgQ+CVwJBCMonYFgPYhYYDCAJBGwmQg3bsBBM6QjBIJfYN4STB0jpBgTpH7VbAwhBD2xBCSoIXBoEEgFt0wyH0GSU4VNIJUSIItJiVBIIu0ywbGkxuDKAQKCGQQABa4gCBtjWBoEAyRuHQZZJBCIukGYNJk36BgVkGQm0AoXagMgIIUbIJdAL4aDJVYLFDTA0t3/SIIP+AQIqBIIkAgYFB6EJgAlBII9tkmQIIUAIJPaIIYCCpETCItptu3+RBDkgMBLAJxCgECAoOAhBBDYoyVBHwMAiDFK7dghJBFMQ1rAYKACIITaB2QOCtAtBAoMApB0BIIIyJoDIBWAwCFxJBMpdt03/IImaIIImCsEEyFN2kCWwVISgS2HoDCJWYMkTYOxIIlAIIvardt0nf5JBF2xBDDIMN2BBCiUJWxJBMpEEIJDmF7QDB0mf9MnIIXfII9NwESZQMSWw/aBAPSoBNJ7YoBII2Qgj3BCIfWAYMkyf5IIQCBFQPJCINoQYWAiEBGRPAgEENoIOI2nahJBC2AkBMYMAG4JBGv+kIILFCn/yIIlsF4MNgGQhJxHAQOApMgagJBIwEAhMgfwO0QAS7CIIv27f//2Sv8k7VJ33SpJBDtpBBhEAwENIJOCV4MCSROEwEJgD+CJAMmIIWSIIubv//6V9a4W2AQRBDwmQWwOARwJBLiUBBxEETwMABY5BG22bpO//1NkiABIgU27JrC0DgCgQ+HAQXCDIMQIJOAyQeBBY1AVoJBF7dJk//5M3/5BI2AiBfAJBKLYWQIJOgIJD3BGIIID2hBCkn//M///pII+0gEBkETIJQfBkGQhoOIDocbIJwJCzf/IIP5m/+IIu2wDpBEYrvIwBBJDoIuBL4pBM71tIIQCB/+27MmDQXAhEAzZBMwhBLiQuBL4vYhMgyVNYo03/VJYoR+BIQP5QYZBCHxQCCgmABZO0iVBghfF7TOBII9//3SpMm/6TCpO/IItIghBMthBL2AlBkgLGoIxBTYZBD+hLBHwQCCm3ZIIeAoEGIJ0CIJYaKIIm2AYNpHYNt0hBKgVAkxBMtEkgVICJGEyAaKXYM2aIQJBHYVvkmTI4VJ2xBD2kCgE2yQyJAQNgkEAoEABYmkyZBBwRBLZAKeCIIl/IIP/ZYJRB5JBC2ESgE0yTILoJBBpEANYQLBghBCwZBQyxBCyd9IIX/SYO2IIsBKQNIIJUBkDFBgEbBAVsgmSpkEIKPS7a/Byf9GYNN//+ppBE0GQIJvahMAIISDDIIVIkkDIJpQCIINtIIP5GYNJm//BgPJC4WAyEAFIRBJ7BBBAAMEBYgXBoBBPa4JBEzZBDknf9pBEgmAIJvQIIOaToPQgARCwESIIMTIJYOBTYdbtukz5BEBgQpBVQRBDgEmQAwIB4FIQAcACQMbcAKMBkgFBIJVIEwlLtu0IIoMCIIwABgM2EYvABQOAIIewEANIPoXTOgJBStIDC9JBCBgWyBQVokhBDhIjGwEBkEAaIexIIUDCIVgIJnadg0tIoQgBBIYEDMoJACboYCEJQIOBoBbCIIVJg5BY0oDBXoQJDyYECgMkgQjKwBBBgRBHghZBjZfBLhBBK2gSHDoi2BIJfAIIc2IIoCCR4MkzVJGoo4DbIILG6QGF7BrCIIcTIJZ3BIIm5II0AkkQgEEDo+gIIILG7VJAwitDIJ/aGYMSgJBCbYJBEkBBBgVIgAdHgjZCBY1pkgDBgmQFIYHBhLsBIJXbtBBEsDMBIIkkIIMSIJFsCASPI22SoBsBhILEIJyqBIIdCHYObtukIItAGo9sQYSPIVoJBBgQLFIII+KIIq7BgRBGYoRpBgzFKIJVILI5BQyUAG4MSIJTsFAQeAWwIsJ6RBIhDaJIIuQgMkwBBGpEDsEkVQx3FIJSDJUhJBNydtkiDBiZBBiZBgA")); +} +var IMAGEWIDTH = 176; +var IMAGEHEIGHT = 109; + + +Graphics.prototype.setFontDancingScript = function() { + // Actual height 44 (44 - 1) + var widths = atob("DBIhFB4bGRoeFhweDQ=="); + var font = atob("AAAAAAAAAAAAAAMAAAAAAAHgAAAAAAD4AAAAAAA+AAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAD8AAAAAAD+AAAAAAD/AAAAAAD/gAAAAAH/gAAAAAH/wAAAAAH/gAAAAAP/gAAAAAP/gAAAAAf/AAAAAAf/AAAAAA/+AAAAAA/+AAAAAB/+AAAAAB/8AAAAAB/8AAAAAA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AAAAAf//4AAAAf///AAAAf///4AAAf////AAAf/8Af4AAP/gAA/AAP/AAADwAH+AAAAcAD+AAAAHAA+AAAABwAfAAAAAcAPAAAAAHADgAAAABwB4AAAAA8AcAAAAAOAHAAAAAHgBgAAAAHwAYAAAAD4AGAAAAD+ABwAAAD/AAcAAAD/gAHgAAH/wAA+AAP/wAAH+H//4AAB////4AAAP///4AAAA///4AAAAD//gAAAAAD8AAAAAAAAAAAGAAAAAAABwAAAAAAAcAAAAAAAHAAAAAAABwAAAAAAAcAAGAAAAPAADwAAA/wAA4AAB/8AAeAAP//AAPAD//7wAHwf//w8AB////gPAA///+ABwAf//4AAcAP/+AAADAH/wAAAAQB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAB8AAAAAAAfAAAAAAAGwAAAAAABsAAAAAAAfAAAAAAAHwAAP4AAB4AAP/AAAeAAH/wAAHAAD/+AAD4AB+AgAB+AA8AAAA/gAOAAAAf8AHAAAAPvABwAAAPzwAYAAAH4eAGAAAH8HgBgAAD+B8AcAAD/AfAHAAH/AHwB4AH/gB8AP4//gAfAD///wAHwAf//wAB8AB//gAAeAAP/gAAHAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAP+AAAAAAH/wAAAAAB/+AAAAAAcfgAAAAAEB8AB/AAAAPAA/4AAABwAf+AAAAcAP/gAAAHADwQAAABwAwABAAAcAcAAYAAHAHAAOAADwBgAHwAB4AYAD+AB+AHAB/4B/ABwB+///wAeB/P//4AH//h//8AA//4P/+AAH/4B/+AAA/8AH/AAAH8AAEAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAA4AAAAAAAeAAAAAAAPAAAAAAAHwAAAAAAD8AAAAAAB/AAAAAAA/wAAAAAAe8AAAAAAPPAAAAAAHjwAAAAADw8AAAAAB4PAAwAAA8DwP+AAAeA///wAAPAP//8AAHh////AAH3////wAD////AAAB///wAAAA//88AAAAf/gPAAAAf8ADgAAAHgAA4AAAAAAAMAAAAAAAAAfwAAAAAAP+AAAAAAH/wAAAAAB/+AAAAAA8PgAAAAwEA8AAAH8AAHAAAP/AABwAA//gAAcAD/4YAAHAH/gGAABwB/ABgAAcAfgAYAAPAD4AHAADwA+ABwAB4APgAeAB+AD4AH4B/AA+AA///wAPgAP//4AD4AB//8AB+AAP/+AAfgAB//AAHwAAH/AAB8AAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAf/4AAAAA///gAAAB///+AAAB////wAAB////+AAA//+B/gAA//8AD8AAf+OAAPAAP8HAABwAH8BgAAcAD8A4AAHAB+AOAABwAeADgAAcAPAA4AAHADgAOAADwBwADgAA8AcAA8AA+AGAAPgAfgBgAD+A/wAYAAf//4AGAAH//8ABwAA//+AAfgAH//AAD4AAf/AAAcAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAHAA/AAAABwB/wAwAA8B/8AeAAPB//AHwADx//gB8AA9//wAfAAP/4IAHwAD/wAAB8AB/wAAAPAB/gAAADwB/wAAAA8A/8AAAAPA/PAAAADw/DwAAAA8/A8AAAAP/AOAAAAD/ADgAAAB/gAwAAAAAAAAAAAAAAAAABAAAAAAAH/AAAAAAD/4AAAAAB//AAAAAA//4AAOAAfA+AAf8APgHwAP/wHgA8AH/+DwAHAD//48ABwA///eAAcAeAf/AAHAHAB/gABwBwAP4AA8AYAB/AAPAGAAf4AHgBgAP/AD4AcAHv8B8AHgD5///AA8H8H//gAP/+A//wAB/+AH/4AAP/AAf8AAA/AAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAP/gAAeAAP/+AAPgAH//wAD8AD//+AAPAB///gAAwAfwD8AAMAPwAfAADADwADwAAwB4AAeAAcAcAAHgAHAHAAA4ADgBwAAOAB4AcAADAB8AHAAAwA/ABwAAcA/gAeAAGB/wAHwADh/4AA+AB//8AAP+D//8AAB////+AAAP///+AAAB///+AAAAP//8AAAAAf/4AAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAA4AAAAAAAeAAAAADAHgAAAAB4BwAAAAAeAIAAAAAHgAAAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + var scale = 1; // size multiplier for this font + g.setFontCustom(font, 46, widths, 50+(scale<<8)+(1<<16)); +} + +// 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() { + var x = g.getWidth()/2; + var y = 50; + g.reset().clearRect(0,24,g.getWidth(),g.getHeight()-IMAGEHEIGHT); + if (g.getWidth() == IMAGEWIDTH) + g.drawImage(getImg(),0,g.getHeight()-IMAGEHEIGHT); + else { + let scale = g.getWidth()/IMAGEWIDTH; + y *= scale; + g.drawImage(getImg(),0,g.getHeight()-IMAGEHEIGHT*scale,{scale:scale}); + } + + var date = new Date(); + var dateStr = require("locale").date(date); + // draw time + g.setFont("DancingScript").setFontAlign(0,0).setColor("#f00"); + g.drawString(date.getHours(), x,y); + y += 43; + g.drawString(date.getMinutes().toString().padStart(2,0), x,y); + // draw date + y += 22; + g.setFontAlign(0,0).setFont("6x8"); + var p = g.getWidth()-60; + g.clearRect(p,y-4,g.getWidth()-p,y+3); // clear the background + g.drawString(dateStr,x,y); + // queue draw in one minute + queueDraw(); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); +// set background colour +g.setTheme({bg:"#0ff"}); +// 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(); diff --git a/apps/floralclk/app.png b/apps/floralclk/app.png new file mode 100644 index 000000000..a0284226e Binary files /dev/null and b/apps/floralclk/app.png differ diff --git a/apps/floralclk/screenshot_floral.png b/apps/floralclk/screenshot_floral.png new file mode 100644 index 000000000..7a8a389ba Binary files /dev/null and b/apps/floralclk/screenshot_floral.png differ diff --git a/apps/flow/README.md b/apps/flow/README.md new file mode 100644 index 000000000..caeaf92d9 --- /dev/null +++ b/apps/flow/README.md @@ -0,0 +1,12 @@ +# FLOW + +This is a game where you have to help a flow avoid white obstacles thing by tapping! +This is a demake of an app which I forgot the name of. +Press BTN(1) to restart. +See if you can get to 2500 score! + +## Screenshots + +![](screenshot1.png) +![](screenshot2.png) +![](screenshot3.png) diff --git a/apps/flow/app-icon.js b/apps/flow/app-icon.js new file mode 100644 index 000000000..969a608f4 --- /dev/null +++ b/apps/flow/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AwX48EHgEC1WgCQkVqoDBBfuqBQcBqoLagEqGAguBqALaGAOoAoQuEBbEAKgIMBBQNUBbgMCyoKHBbBVBBYIKGBbEBtNVrQLfOgNaT4gLagp0CPQOABbcBFwNAgEKBgILbitVqAFClWq0ALZFwTDFGAQLZFwYwDBfg")) diff --git a/apps/flow/app.js b/apps/flow/app.js new file mode 100644 index 000000000..5f4da8f35 --- /dev/null +++ b/apps/flow/app.js @@ -0,0 +1,220 @@ +const isB2 = process.env.HWVERSION === 2; + +// Bangle.js 1 runs just too fast in direct mode??? (also no getPixel) +if (!isB2) Bangle.setLCDMode("120x120"); + +const options = Bangle.getOptions(); + +options.lockTimeout = 0; +options.lcdPowerTimeout = 0; + +Bangle.setOptions(options); + +g.reset(); +g.setBgColor(0, 0, 0); +g.setColor(255, 255, 255); +g.clear(); +const h = g.getHeight(); + +function trigToCoord(ret) { + return ((ret + 1) * h) / 2; +} + +function trigToLen(ret) { + return (ret * h) / 2; +} + +let i = 0.2; +let speedCoef = 0.014; + +let flowFile = require("Storage").readJSON("flow.json"); + +let highestI = (flowFile && flowFile.hiscore) || 0.1; + +let colorA = [255, 255, 0]; +let colorB = [0, 255, 255]; + +let x = 0; +let xt = 0; +let safeMode = false; +let lost = false; + +function offsetRect(g, x, y, w) { + g.fillRect(x, y, x + w, y + w); +} + +function getColor(num) { + return [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [1, 1, 0], + [0, 1, 1], + [1, 0, 1], + [0.5, 0.5, 1], + [1, 0.5, 0], + [0, 1, 0.5], + [0.5, 0.5, 0.5], + ][num]; +} + +function calculateColor(num) { + colorA = getColor(Math.floor((num % 1) * 10)); + colorB = getColor(Math.floor((num % 10) - (num % 1))); +} + +calculateColor(highestI); + +Bangle.on("touch", () => (safeMode = !safeMode)); + +function resetGame() { + x = xt = 0; + safeMode = lost = false; + i = 0.2; + speedCoef = 0.014; + obstaclePeriod = 150; + obstacleMode = 1; + g.clear(); + shownScore = false; + intervalId = setInterval(draw); +} + +function checkCollision() { + lost = g.getPixel(trigToCoord(+x), (h * 2) / 3 - 4) !== 0; + if (lost) { + scoringI = i; + speedCoef = Math.min(speedCoef, 0.02); + g.setFont(isB2 ? "6x15" : "4x6", 3); + g.setColor(colorA[0], colorA[1], colorA[2]) + .drawString( + "Game over", + trigToCoord(0) - g.stringWidth("Game over") / 2, + trigToCoord(0) + ) + .setColor(1, 1, 1); + } +} + +function drawPlayer() { + if (!safeMode) xt = Math.cos(i * Math.PI * 4) / 7.5; + else xt = -Math.cos(i * Math.PI * 2) / 20 + 0.35; + x = x * 0.8 + xt * 0.2; + if (highestI > 250) calculateColor(i); + g.setColor(colorA[0], colorA[1], colorA[2]); + offsetRect(g, trigToCoord(+x), (h * 2) / 3, 3); + g.setColor(colorB[0], colorB[1], colorB[2]); + offsetRect(g, trigToCoord(-x), (h * 2) / 3, 3); +} + +let obstaclePeriod = 150; +let obstacleMode = 1; + +function drawObstracle() { + g.setColor(1, 1, 1); + switch (obstacleMode) { + case 0: + offsetRect(g, trigToCoord(-0.15), 0, trigToLen(0.3)); + break; + case 1: + offsetRect(g, trigToCoord(0.2), 0, trigToLen(0.2)); + offsetRect(g, trigToCoord(-0.4), 0, trigToLen(0.2)); + break; + case 2: + break; + } + obstaclePeriod--; + if (obstaclePeriod <= 0) { + // If we are off cooldown mode, pick a random actual mode + if (obstacleMode === 2) { + obstaclePeriod = Math.random() * 50 + 50; + obstacleMode = Math.round(Math.random()); + } else if (Math.random() > 0.5) { + // Give it a chance to repeat with no cooldown + obstaclePeriod = 25 + 2.5 * speedCoef; + obstacleMode = 2; + } + } +} + +let shownScore = false; +let scoringI = 0; + +function draw() { + if (!lost) { + drawPlayer(); + checkCollision(); + speedCoef *= 1.0005; + drawObstracle(); + } else { + speedCoef /= 1.05; + if (speedCoef <= 0.005) { + clearInterval(intervalId); + i -= speedCoef; + g.setFont(isB2 ? "6x15" : "4x6", 1); + const str = "Hiscore: " + Math.round(highestI * 10); + g.setColor( + scoringI > highestI ? 0 : 255, + 0, + scoringI > highestI ? 255 : 0 + ) + .drawString( + str, + trigToCoord(0) - g.stringWidth(str) / 2, + trigToCoord(0) + ) + .setColor(255, 255, 255); + if (scoringI > highestI) { + highestI = scoringI; + require("Storage").writeJSON("flow.json", { + hiscore: highestI, + }); + calculateColor(highestI); + } + setTimeout(resetGame, 3000); + } else if (speedCoef <= 0.01 && !shownScore) { + shownScore = true; + g.setFont(isB2 ? "6x15" : "4x6", 2); + const str = "Score: " + Math.round(scoringI * 10); + g.setColor(colorB[0], colorB[1], colorB[2]) + .drawString( + str, + trigToCoord(0) - g.stringWidth(str) / 2, + trigToCoord(0) + ) + .setColor(1, 1, 1); + } + } + i += speedCoef; + g.scroll(0, speedCoef * h); + g.flip(); +} + +let intervalId; + +if (BTN.read()) { + for (let i = 0; i < 10; i++) { + color = getColor(i); + g.setColor(color[0], color[1], color[2]); + g.fillRect((i / 10) * h, 0, ((i + 1) / 10) * h, h); + } + g.setColor(0); + g.setFont("Vector", 9); + let str = "Welcome to the debug screen!"; + g.drawString( + str, + trigToCoord(0) - g.stringWidth(str) / 2, + trigToCoord(0) - 9 + ); + str = "Don't hold BTN while opening to play!"; + g.drawString(str, trigToCoord(0) - g.stringWidth(str) / 2, trigToCoord(0)); + g.flip(); + setInterval(() => { + g.scroll(0, 0.014 * h); + i += 0.014; + calculateColor(i); + g.setColor(colorA[0], colorA[1], colorA[2]); + g.fillRect(0, 0, trigToCoord(0), 0.014 * h); + g.setColor(colorB[0], colorB[1], colorB[2]); + g.fillRect(trigToCoord(0), 0, trigToCoord(1), 0.014 * h); + }, 1000 / 30); +} else intervalId = setInterval(draw, 1000 / 30); diff --git a/apps/flow/app.png b/apps/flow/app.png new file mode 100644 index 000000000..b35c3ca77 Binary files /dev/null and b/apps/flow/app.png differ diff --git a/apps/flow/screenshot1.png b/apps/flow/screenshot1.png new file mode 100644 index 000000000..fd5dee427 Binary files /dev/null and b/apps/flow/screenshot1.png differ diff --git a/apps/flow/screenshot2.png b/apps/flow/screenshot2.png new file mode 100644 index 000000000..e29691b69 Binary files /dev/null and b/apps/flow/screenshot2.png differ diff --git a/apps/flow/screenshot3.png b/apps/flow/screenshot3.png new file mode 100644 index 000000000..3e1c80ba7 Binary files /dev/null and b/apps/flow/screenshot3.png differ diff --git a/apps/fontclock/ChangeLog b/apps/fontclock/ChangeLog new file mode 100644 index 000000000..d53df991b --- /dev/null +++ b/apps/fontclock/ChangeLog @@ -0,0 +1 @@ +0.01: Initial Release diff --git a/apps/fontclock/README.md b/apps/fontclock/README.md new file mode 100644 index 000000000..ecf6688b5 --- /dev/null +++ b/apps/fontclock/README.md @@ -0,0 +1,28 @@ +# Font Clock + +The Font Clock allows you to choose the font and clock style. + +![](app.png) + +## Usage + +### Choose the Clock Face from the selection + +Before uploading the upload page will ask which clock face you like to choose. Please choose using the provided pull down. As you look through the different selections a sample image will be shown to the right hand side. + +Once you have chosen your watch face press the upload button and the selection will be uploaded to the watch + +### Button 3 +Button 3 (bottom right button) is used to change the background colour. + +## Further Details + +For further details of design and working please visit [The Project Page](https://www.notion.so/adrianwkirk/Sweep-hand-clock-6aa5b6b3d1074d4e87fc947975b1e4b7) + +## Requests + +Reach out to adrian@adriankirk.com if you have feature requests or notice bugs. + +## Creator + +Made by [Adrian Kirk](mailto:adrian@adriankirk.com) \ No newline at end of file diff --git a/apps/fontclock/app.png b/apps/fontclock/app.png new file mode 100644 index 000000000..127b1af1e Binary files /dev/null and b/apps/fontclock/app.png differ diff --git a/apps/fontclock/custom.html b/apps/fontclock/custom.html new file mode 100644 index 000000000..6a013a003 --- /dev/null +++ b/apps/fontclock/custom.html @@ -0,0 +1,210 @@ + + + + + + +

Please select watch display

+ + + + + + +
+ + + +
+ +

Click

+ + + + + + diff --git a/apps/fontclock/display-01.png b/apps/fontclock/display-01.png new file mode 100644 index 000000000..e7100a25f Binary files /dev/null and b/apps/fontclock/display-01.png differ diff --git a/apps/fontclock/display-02.png b/apps/fontclock/display-02.png new file mode 100644 index 000000000..b7c8e81b1 Binary files /dev/null and b/apps/fontclock/display-02.png differ diff --git a/apps/fontclock/display-03.png b/apps/fontclock/display-03.png new file mode 100644 index 000000000..9cbe80544 Binary files /dev/null and b/apps/fontclock/display-03.png differ diff --git a/apps/fontclock/display-04.png b/apps/fontclock/display-04.png new file mode 100644 index 000000000..c8dbdeabb Binary files /dev/null and b/apps/fontclock/display-04.png differ diff --git a/apps/fontclock/display-05.png b/apps/fontclock/display-05.png new file mode 100644 index 000000000..b716443a9 Binary files /dev/null and b/apps/fontclock/display-05.png differ diff --git a/apps/fontclock/fontclock-icon.js b/apps/fontclock/fontclock-icon.js new file mode 100644 index 000000000..49431587b --- /dev/null +++ b/apps/fontclock/fontclock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lEowkA/4AvmUiAA0/CRHzkczAA0vExM/n/zn8zAIPzCZUi/8j+cvmUzAgI7JBQITHkY6JCwRNEIYITIDoQSEExXyDoQSDn4mKHQ4mKLoImRHQQmPMIYTDExY6HExY6HExQ6HYgISJHQ4TBAgbXOAAb3Ba5giBn8/H4zXHMYfzEww6I+cyPJAtEToizBNoQTFLo0yBAKMI+UikUjIwQSBJg61ICALGMPQgQBJhB6IbJjcGJhw6DCQJMMUIhMOHQavBCRo6CJh46DTJo6EJh5eCTJwADdwISQJiIAo")) diff --git a/apps/fontclock/fontclock.font.abril_ff50.js b/apps/fontclock/fontclock.font.abril_ff50.js new file mode 100644 index 000000000..3d5169c63 --- /dev/null +++ b/apps/fontclock/fontclock.font.abril_ff50.js @@ -0,0 +1,51 @@ +var NumeralFont = require("fontclock.font.js"); + +const DIM_30x38 = [30,38]; +const DIM_49x38 = [49,38]; + +class DigitNumeralFont extends NumeralFont{ + constructor(){ + super(); + // dimension map provides the dimensions of the character for + // each number for plotting and collision detection + this.widths = atob("DRIhFRwdHhsfGh8fDQ=="); + this.font = atob("AAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAD/AAAAAAB/4AAAAAAf+AAAAAAH/gAAAAAB/4AAAAAAf+AAAAAAD/AAAAAAA/gAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAPwAAAAAAf8AAAAAA/+AAAAAB/8AAAAAD/wAAAAAH/gAAAAAP/AAAAAAf+AAAAAA/8AAAAAB/4AAAAAD/wAAAAAH/gAAAAAP/AAAAAAH+AAAAAAB8AAAAAAAIAAAAAAAAAAAAAAAAAAH/8AAAAAP//8AAAAP///wAAAH///+AAAD////4AAB/////AAA/////wAAf////+AAH/////wAD/////8AA//////AAP/gAA/wADwAAAAeAA4AAAADgAMAAAAA4ADAAAAAOAAwAAAADgAMAAAAA4ADgAAAAeAA/gAAA/AAP/////wAD/////8AAf/////AAH/////gAA/////4AAH////8AAB////+AAAP////AAAA////gAAAD///gAAAAH//AAAAAAAAAAAAGAAAAAwABgAAAAMAAYAAAADAAGAAAAAwADgAAAAMAA//////AAP/////wAD/////8AA//////AAP/////wAD/////8AA//////AAP/////wAD/////8AA//////AAP/////wAAAAAAAMAAAAAAADAAAAAAAAwAAAAAAAMAAAAAAAAAAAAAAAAAAAHwAAD8AAH/AAB/AAD/wAA/wAA/+AAf8AAf/gAP/AAH/4AH/wAD/+AD/8AA//gB//AAOPwA//wADD4Aff8AAwAAPn/AAMAAPx/wADAAH8f8AA4AH+H/AAPgP/B/wAD///gf8AA///4H/AAP//8B/wAD//+Af8AAf//AH/AAH//wB/wAA//4A/8AAH/4Af/AAA/8A//wAAD8Af/8AAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAPwAAA/gAP+AAAf8AD/wAAP/gB/+AAH/4Af/wAB/+AH/8AAf/gB//AAP/4wP/wAD/8OD+OAAw/DgPDgAMDAwAA4ADAAcAAOAAwAHAADgAOAH4AA4AD///AAeAA///+A/AAP/////wAD/////8AA//9///AAP//f//gAB//n//4AAf/w//+AAD/8P//AAAf+B//gAAB+AP/wAAAAAB/4AAAAAADwAAAAAAAAAAAAAAAeAAAAAAAfgAAAAAAf4AAAAAAPmAAAAAAPhgAAAAAPwYAAAAAPwGAAAAAHwBgAAAAHwAYDAAAH4AGAwAAH4ABgMAAH4AAYDAAD4AAGAwAD/////8AA//////AAP/////wAD/////8AA//////AAP/////wAD/////8AA//////AAP/////wAD/////8AAAAAAYDAAAAAAGAwAAAAABgMAAAAAAYBAAAAAB/4AAAAAAf+AAAAAAAAAAAAAAAD4AAAAAAB/gAAAAAA/8AAP//wf/gAD//8H/4AA//3B//AAP8Bgf/wAD/A4D/8AAfwOA/jgAH8DAH44AB/gwAAOAAf4MAADgAH+DAAA4AB/g4AAeAAf8PwA/AAH/D///wAB/w///8AAf8P///AAD/j///wAA/4f//4AAP+H//+AAH/A///AAD/wP//gAA/gB//wAAAAAH/4AAAAAAfwAAAAAAAAAAAAAAAAAAAAAD//wAAAAH///AAAAH///8AAAD////gAAD////8AAB/////gAAf////4AAP/////AAH/////wAB/////8AA//////gAP8B4AD4AD4A4AAOAA4AMAADgAOAHAAA4ADABwAAOAAwAcAAHgAMAH4AP4ADD5///8AA5/f///AAP/////wAD/////8AAf/v//+AAH/7///AAA/+f//wAAH/H//4AAA/gf/4AAABgD/8AAAAAAH4AAAAAAAAAAAAAAAAAAAAf/wAAAAAP/8AAAAAD/8AAAAAA/8AAAAAAP+AAA/AAD/gAA/4AA/4AA//AAP+AAf/wAD/gAf/+AA/4AP//gAP+AH//4AD/gD//+AA/4B///gAP+B/+BwAD/g/8AAAA/4f8AAAAP+P8AAAAD/n8AAAAA/78AAAAAP/+AAAAAD/+AAAAAA/+AAAAAAP+AAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAB/gH/4AAA/8D//AAAf/h//wAAP/8f/+AAH//v//gAB//7//8AAf/////AAP/////wAD/////+AA//////gAP//+AB4ADgAeAAOAAwADgADgAMAA4AA4ADAAOAAOAA4AHwADgAP//+AD4AD/////+AA//////AAP/////wAD//7//8AAf/+///AAH//P//gAA//x//4AAH/4f/8AAA/8D/+AAAD8Af/AAAAAAB/AAAAAAAAAAAAA/gAAAAAA//APwAAA//8H+AAAf//j/wAAP//4/+AAD///f/gAB/////8AAf//+//AAP///v/wAD///7+eAA////PjgAPgAPwA4ADgAA4AOAAwAAOADgAMAADgA4ADAAA4AeAAwAAcAfgAPAAeA/wAD/////8AA//////AAP/////gAB/////wAAf////8AAD////+AAAf////AAAH////gAAAf///gAAAD///gAAAAH//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AH4AAAB/gD/AAAAf8B/4AAAP/Af+AAAD/wH/gAAA/8B/4AAAH/Af+AAAB/wD/AAAAP4A/gAAAAwABgAAAAAAAA=="); + var scale = 1; // size multiplier for this font + this.size = 50+(scale<<8)+(1<<16); + this.y_offset = -12; + + + } + getDimensions(hour){ + //return this.dimension_map[hour]; + switch (hour){ + case 10: + case 11: + case 12: + return DIM_49x38; + default: + return DIM_30x38; + + } + } + hour_txt(hour){ return hour.toString(); } + draw(hour_txt,x,y){ + /* going to leave this in here for future testing. + uncomment this so that it draws a box behind the string + so we can guess the digit dimensions*/ + /*var dim = [30,38]; + g.setColor(0.5,0,0); + g.fillPoly([x,y, + x+dim[0],y, + x+dim[0],y+dim[1], + x,y+dim[1] + ]); + g.setColor(1.0,1.0,1.0);*/ + g.setFontAlign(-1.0,-1.0,0); + g.setFontCustom(this.font, 46, this.widths, this.size); + g.drawString(hour_txt,x,y+this.y_offset ); + } + getName(){return "Digit";} +} + +module.exports = [DigitNumeralFont]; \ No newline at end of file diff --git a/apps/fontclock/fontclock.font.cpstc58.js b/apps/fontclock/fontclock.font.cpstc58.js new file mode 100644 index 000000000..6e91349ab --- /dev/null +++ b/apps/fontclock/fontclock.font.cpstc58.js @@ -0,0 +1,59 @@ +var NumeralFont = require("fontclock.font.js"); + +const DIM_20x58 = [20,58]; +const DIM_30x58 = [30,58]; +const DIM_40x58 = [40,58]; +const DIM_50x58 = [50,58]; +class DigitNumeralFont extends NumeralFont{ + constructor(){ + super(); + // dimension map provides the dimesions of the character for + // each number for plotting and collision detection + this.font = atob("AAAA/+AAAAAAB///wAAAAB////8AAAA/////+AAAP/////8AAD//////8AAf/8AAf/8AD/8AAAH/4Af+AAAAD/wD/gAAAAD/gf4AAAAAH+D/AAAAAAP8P4AAAAAAf5/AAAAAAA/n4AAAAAAB+/gAAAAAAH/+AAAAAAAf/wAAAAAAA//AAAAAAAD/8AAAAAAAP/4AAAAAAB//gAAAAAAH9+AAAAAAAfn8AAAAAAD+fwAAAAAAP4/gAAAAAB/D/AAAAAAP8H/AAAAAB/gP+AAAAAf8Af/AAAAH/gA//gAAD/8AB//8AH//gAB//////8AAB//////AAAB/////wAAAA////4AAAAAP//4AAAAAAAAAAAAAAGAAAAAAAAA8AAAAAAAAH8AAAAAAAA/wAAAAAAAH+AAAAAAAA/wAAAAAAAH+AAAAAAAA/wAAAAAAAH////////w/////////H////////8/////////3//////////////////8AAAAAAAAAAAAAAAAAADAAAAAAAAAcQAAAAAAAHzwAAAAAAA/PwAAAAAAH9/AAAAAAB/34AAAAAAP/fgAAAAAD//+AAAAAAf//wAAAAAH///AAAAAA///8AAAAAH///4AAAAB/4//gAAAAP/D/+AAAAD/wP34AAAAf+A/fwAAAD/wD8/gAAA/8APz/AAAH/gA/H+AAB/4AD8f8AAP/AAPw/8AD/wAA/B/+A/+AAD8D////wAAPwH///8AAA/AH///gAAD8AH//4AAAPwAH/+AAAAAAAAAAAAAAD8AAAAAAAAPwAAAAAAAA/AAgAAAAD/8AHAAAAAP/wB8AAAAA//APwAAAAD/8D/AAAAAP/wf8AAAAB//H/wAAAAH/8//gAAAAfv//+AAAAD+///8AAAAP7//fwAAAB/P/w/gAAAP8/+D/AAAB/j/wH+AAAP8P8AP8AAB/w/gAf8AAf+D4AA/8AH/wPAAB////+AwAAD////gCAAAH///8AAAAAH///AAAAAAD//wAAAAAAA/wAAAAAAAAAAAAAAAAAAAQAAAAAAAAHAAAAAAAAD8AAAAAAAA/wAAAAAAAP/AAAAAAAD/8AAAAAAB//wAAAAAAf//AAAAAAH//8AAAAAB//PwAAAAAf/w/AAAAAP/8D8AAAAD//APwAAAA//gA/AAAAP/4AD8AAAH/+AAPwAAB//gAA/AAAf/4AAD8AAH/8AAAPwAD//AAAA/AAP/wAAAD8AA/8AAAAPwAD/AAAAA/gAPgAAA/////4AAAD////+AAAAP////wAAAA/////AAAAD////8AAAAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAAAAAAAAA8AAAAAAAB/wAAAAAAD//AAAAAP///8AAAAA////wAAAAD////AAAAAP///8AAAAA//4PwAAAAH/8A/gAAAAf/wB+AAAAB+/AH4AAAAP78AfwAAAA/vwA/AAAAH8/AD+AAAA/z8AP8AAAD+PwAf4AAAf4/AA/wAAH/D8AD/gAA/4PwAH/gAf/A/AAP/8f/4AAAAf////AAAAAf///wAAAAA///8AAAAAAf//AAAAAAAH/gAAAAAAAAAAAAAAAAH/4AAAAAAP//8AAAAAD///+AAAAB////8AAAAf////8AAAH//8f/4AAA//8AD/wAAP//AAD/gAB//wAAH/AAP/+AAAH+AB//wAAAP4AP/+AAAAfwB//wAAAB/AP9/AAAAD+B/n4AAAAH4H8/gAAAAfg/j8AAAAB/H8PwAAAAH8fw/AAAAAPz+D8AAAAA/P4PwAAAAD9/A/AAAAAf38D8AAAAB/fgP4AAAAH5+A/gAAAAfv4B/AAAAD+/gH8AAAAPz+AP4AAAB/PwA/wAAAP8/AB/gAAB/gAAD/AAAP8AAAP+AAD/gAAAf+AA/8AAAA//gf/gAAAA////8AAAAB////gAAAAB///4AAAAAB//+AAAAAAA//AAAAAAAAAAAAPwAAAAAAAA/AAAAAAAAD8AAAAAAAAfwAAAAAAAP/AAAAAAAH/8AAAAAAD//wAAAAAD///AAAAAB///8AAAAA///vwAAAA///w/AAAAP//wD8AAAP//4APwAAH//8AA/AAD//+AAD8AD//+AAAPwB///AAAA/A///gAAAD8f//wAAAAP///4AAAAA///4AAAAAD//8AAAAAAP/+AAAAAAA/+AAAAAAAD/AAAAAAAAPgAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAH//wAAAAAB///wAAAAAf///wAAAAD////wAAAAf////gAAAH/wAf/AAAA/8AAP+AAAD/AAAf8AAAf4AAAf4AAD/AAAA/gB/P4AAAB/A///AAAAH8H//8AAAAP4///gAAAAfn//+AAAAB+f//wAAAAH/+B/AAAAAf/4H8AAAAA//APwAAAAD/8A/AAAAAP/4H8AAAAB/fw/wAAAAH9///gAAAAfj//+AAAAB+P//4AAAAP4P//wAAAA/gf//gAAAH8APD/AAAA/wAAH8AAAH+AAAf8AAA/wAAA/4AAH/AAAB/4AB/4AAAD/8A//AAAAH////wAAAAP///+AAAAAP///gAAAAAP//4AAAAAAH/+AAAAAAAAAAAAAAA/wAAAAAAA//8AAAAAAP//+AAAAAD///8AAAAA////8AAAAH////4AAAA/+AD/wAAAH/AAD/gAAA/4AAD/AAAH+AAAH+AAAfwAAAP8APz+AAAAfwA/P4AAAA/gH9/AAAAD+Af34AAAAH4B+fgAAAAfwH7+AAAAA/A/v4AAAAD8D+/AAAAAPwPz8AAAAA/B/PwAAAAD8P8/gAAAAPw/j+AAAAA/H8H4AAAAH8/wfgAAAAf3+B/AAAAB+fwH8AAAAP//AP4AAAB//4A/wAAAH//AB/gAAA//4AD/AAAP//AAH+AAB//wAAf+AAf/+AAAf/AP//gAAA/////8AAAB/////AAAAB////wAAAAB///4AAAAAB//4AAAAAAAAAAAAAAA="); + this.widths = atob("Jg8dGiAaKBsoKA=="); + } + getDimensions(hour){ + switch(hour){ + case 1: + return DIM_20x58; + case 2: + case 3: + case 4: + case 5: + case 7: + return DIM_30x58; + case 6: + case 8: + case 9: + case 11: + case 12: + return DIM_40x58; + case 10: + return DIM_50x58; + default: + return DIM_30x58; + } + } + hour_txt(hour){ return hour.toString(); } + draw(hour_txt,x,y){ + /* going to leave this in here for future testing. + uncomment this so that it draws a box behind the string + so we can guess the digit dimensions + dim = [50,58]; + g.setColor(0.5,0,0); + g.fillPoly([x,y, + x+dim[0],y, + x+dim[0],y+dim[1], + x,y+dim[1] + ]); + g.setColor(1.0,1.0,1.0);*/ + //g.setFontCopasetic40x58Numeric(); + //g.setFontAlign(-1,-1,0); + g.setFontAlign(-1,-1,0); + g.setFontCustom(this.font, 48, this.widths, 58); + g.drawString(hour_txt,x,y); + } + getName(){return "Digit";} +} + +module.exports = [DigitNumeralFont]; \ No newline at end of file diff --git a/apps/fontclock/fontclock.font.js b/apps/fontclock/fontclock.font.js new file mode 100644 index 000000000..10b063ca5 --- /dev/null +++ b/apps/fontclock/fontclock.font.js @@ -0,0 +1,26 @@ +/** + * We want to be able to change the font so we set up + * pure virtual for all fonts implementtions to use + */ +class NumeralFont { + /** + * The screen dimensions of what we are going to + * display for the given hour. + */ + getDimensions(hour){return [0,0];} + /** + * The characters that are going to be returned for + * the hour. + */ + hour_txt(hour){ return ""; } + /** + * method to draw text at the required coordinates + */ + draw(hour_txt,x,y){ return "";} + /** + * Called from the settings loader to identify the font + */ + getName(){return "";} +} + +module.exports = NumeralFont; \ No newline at end of file diff --git a/apps/fontclock/fontclock.font.json b/apps/fontclock/fontclock.font.json new file mode 100644 index 000000000..3f111ba1b --- /dev/null +++ b/apps/fontclock/fontclock.font.json @@ -0,0 +1,23 @@ +{ + "name": "Vector 4", + "numerals": [12,3,6,9], + "fonts": ["vector50"], + "radius": 75, + "color_schemes" : [ + { + "name": "black", + "background" : [0.0,0.0,0.0], + "second_hand": [1.0,0.0,0.0], + }, + { + "name": "red", + "background" : [1.0,0.0,0.0], + "second_hand": [1.0,1.0,0.0] + }, + { + "name": "grey", + "background" : [0.5,0.5,0.5], + "second_hand": [0.0,0.0,0.0] + } + ] +} \ No newline at end of file diff --git a/apps/fontclock/fontclock.font.mntn25.js b/apps/fontclock/fontclock.font.mntn25.js new file mode 100644 index 000000000..2aaeb4c9e --- /dev/null +++ b/apps/fontclock/fontclock.font.mntn25.js @@ -0,0 +1,60 @@ +var NumeralFont = require("fontclock.font.js"); + +const DIM_25x25 = [25,25]; +const DIM_10x25 = [10,25]; +const DIM_20x25 = [20,25]; +const DIM_31x25 = [31,25]; +const DIM_15x25 = [15,25]; + +class DigitNumeralFont extends NumeralFont{ + constructor(){ + super(); + // dimension map provides the dimensions of the character for + // each number for plotting and collision detection + this.widths = atob("BgsVCw8PEBEUEBQUBw=="); + this.font = atob("AAAAAAAAAAAAp9bgAAAAAAAAAAAADr+vAAAAAAAAAAAAAOv68AAAAAAAAAAAAA6/rwAAAAAAAAAAAADr+fAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAXwAAAAAAAAAAAAXz//wAAAAAAAAAXz//7q+AAAAAAAXz//8q+//wAAAAXz//8q+//yr3wAXz//8q+//yr3//ZAL/8q+//yr3//ZMAAAW+//2q3//ZQAAAAAC/2q3//pQAAAAAAAAF3//pQAAAAAAAAAAAnpQAAAAAAAAAAAAAAAAABL3//tgQAAAAAAAAj//su9//0gAAAAAC7/vv///73/gAAAAB/8/9u7u7/+34AAAA78/r/////c/+9QAAf9/P/LvLu/+///AADu/++//+7/7Pv79QAv37+/sQAAb/7978AF/f3vwAAAAD+/v+4Aj9779QAAAADs+/vwCP3vv1AAAAAOz7+/AF/P3fsAAAAC+/v94AL9+/v5AAAD/9/f/QAP3+/8/9ze/7+/v2AAj9+//Lztu+/P//AAAP/f3P////6//fcAAAL/z/y7u7vv7PoAAAAD/+z////9z/oAAAAAAK//3LvO/+QAAAAAAAAH3///6zAAAAAAAAAAAAAAAAAAAAAAC96fQAAAAAAAAAAAAL769AAAAAAAAAAAAAvvr5ZmZmZmZmZmAAC++v//////////8AAL76/bu7u7u7u7uwAAvvr/7u7u7u7u7uAAC++v/u7u7u7u7u4AAL76/KqqqqqqqqqgAAvvr///////////AAAjQlVVVVVVVVVVUAAAAAAAAAAAAAAAAAAAhTAAAAAAAAAyUlAAD7+udQAAAAHfv68AAPv777AAAAX/+/rwAC+/y/kAAAn+77+vAAb8/q9gAB79//v68ACf7frzAF/9/t+/rwAH/d+/QK/u/P/7+vAAX8/t/u/f/P7Pv68AAvv7+uzv3vz/+/rwAA/P3///z/v/Pr+vAACPv9u67939EOv68AAA/6///7/3AA6/rwAAAv/Ku+/iAADr+fAAAACu//5gAAAAAAAAAAAAAAAAAAAAAAAAAAhTAAAAAAAAAAIrIAD7+udgAAAAGo379gAPr7/rAAAAAfv935AB+vvvcjMkFQ/Pv+sAT6/d9K/r+vDs+/3QB/zvvzr+v68Nz7+/AJ/d+/Ov6/rx3Pv78Ab779+I/N/PX8+/zwAvv8/P/5/v7/z7/+AA+/z+m/r7/Kv9/PkACvv7///8+//7398gAB/9/bvvzvy8/89wAABv++/93+nf/b+gAAAALv/e/9//3v+wAAAAAASd21AVrcogAAAAAAAAAAAb753JAAAAAAAAAALP/frusAAAAAAAAE3/zO+u6wAAAAAABe/7z/367rAAAAAAf/+9/7vvrusAAAAG/+rv+s/9+u6wAAAAra//rf+5367rAAAABv/q7/q//vrusAAAAK2v/5z/w1+u6wAAAAb/6d/7IAX67rAAAACsr/+AL//vru//4AAG/+YAAaqr+u7aqgAAnUAAAD///67v//AAAAAAAABVWPruxVUAAAAAAAAAACtphgAAAAAAAAAAAAAAAAAAAAA0U3d3d3d1AADMAAAL76//////0ACr9AAAvvr9zMzMyABd/vAAC++v7u7u7qAPz79QAL76//////wPz975AAvvr8rN7Oyw+vv+wAC++vQN37/ODs+/vgAL769A6/v9wN37+/AAvvr0Dq+/3Q/Pv78AC++vQN38/vv8+/3QAL769Ar8397+7/36AAvvr0Bfv8/s/7/PMAC++vQA77+9/a+fwAAL769ABP3f///f8gAAVnSRAAb/mrzP8wAAAAAAAAAC3///wQAAAAAAAAAAAAJiAAAAAAAAAAA2ZmZiAAAAAAAAAK7//////+YAAAAAA//Lu7u7up77AAAABP+//+7u7v/5/QAAAP7fyN///+y/+/cAAH+/n/2qqqvv7vzwAA/f3+r/////v8+/cAD7+/v82rye38/e6wBPv9zrn9388fv7/NAI/O+va+/Pzw3Pv68Aj93689z7/dDc+vrwBPv+v06/z90Pz7++AA+/3vnO/Pz+/PvuwAD7+/o4/O/56/38+QAN/v5QP8/f//7PzxAAP89QAL+f3czfj6AAAK9wAAH/v///z/EAAADQAAAC79q879EAAAAAAAAAAJ3//YAAAAAAAAAAAAAAAAAAAAAL3p9AAAAAAAAAAAAAvvr0AAAAAAAAAAAAC++vQAAAAAAAAAAAAL769AAAAAAAAFrgAAvvr0AAAAAFrv/9AAC++vQAAFvv/9u98AAL769Wvv/8u9//6gAArN7//8u+//67z/AACv/8u+//27z//roAAFu+//273//rvP/wAAv/273//rvP//xxAABb3//rvP//xxAAAAAL/rvP//thAAAAAAAAXP/+thAAAAAAAAAACutgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGt/qYCvv2kAAAAAAb/69/+/+vP/AAAAAT+z//q/8//6/8QAAP8/7u9/P+7z/v7AADv36//+/v///778gAfv7/Lr/7++3/vz6AG+/7+/9+/v//Pz+0Aj8789d77+/Ds+/zwCf3Pryvuv68N36+vAJ/c+vK+6/rwzfr68An9z68r7r+vDN+vrwCPzvz1v+z78ez6+/AE+/7//fzv3+/Pv98AD7+/ne+/v57e/e/QAO7v3//9/u/v+vr2AAT9/7u9+v68uvz/AAAM/O////v///z/EAAACv+6vP/+u6z/IAAAAATP//1H3//7IAAAAAAAABAAAAEAAAAAAAAABJkwAAAAAAAAAAAAr///+gAAAAEQAAAB783dvP0QAADNAAAA37/93/n8AAC79QAAT5/c/9358wBt/vAADu/v+8/+79Afz79QAPn7+//Pv58Pv975AD+f7/zP3fjw+vv+wAf7789T+/6vTs+/vgCf3fvyP8/789z7+/AG+++/SP7Pvw+fr74AL5/e+837+/X3+v3AAPv7+//d3d797/35AA3+7/vMzMzK75+/MAA/r97//////r/fwAAAz5/7uqqqqt/d8gAAAe/N//////6v9gAAAACv/bqqqr3/4gAAAAAAOM/////aUAAAAAAAAAAAAAAAAAAAAAAAAAAQEQABARAAAAAAAABvvuoF+u6wAAAAAAAG++6gX67rAAAAAAAAb77qBfrusAAAAAAABvvuoF+u6wAAAAAAAE16pwPXunAAAAAAAAAAAAAAAAAAAA=="); + var scale = 1; // size multiplier for this font + this.size = 25+(scale<<8)+(4<<16); + this.y_offset = 0; + + } + getDimensions(hour){ + //return this.dimension_map[hour]; + switch(hour){ + case 0: + case 12: + return DIM_25x25; + case 1: + return DIM_10x25; + case 6: + case 8: + case 9: + case 11: + return DIM_20x25; + case 10: + return DIM_31x25; + default: + return DIM_15x25; + } + } + hour_txt(hour){ return hour.toString(); } + draw(hour_txt,x,y){ + /* going to leave this in here for future testing. + uncomment this so that it draws a box behind the string + so we can guess the digit dimensions*/ + /*var dim = [30,25]; + g.setColor(0.5,0,0); + g.fillPoly([x,y, + x+dim[0],y, + x+dim[0],y+dim[1], + x,y+dim[1] + ]); + g.setColor(1.0,1.0,1.0);*/ + g.setFontAlign(-1.0,-1.0,0); + g.setFontCustom(this.font, 46, this.widths, this.size); + g.drawString(hour_txt,x,y+this.y_offset ); + } + getName(){return "Digit";} +} + +module.exports = [DigitNumeralFont]; \ No newline at end of file diff --git a/apps/fontclock/fontclock.font.mntn50.js b/apps/fontclock/fontclock.font.mntn50.js new file mode 100644 index 000000000..650c0b1af --- /dev/null +++ b/apps/fontclock/fontclock.font.mntn50.js @@ -0,0 +1,46 @@ +var NumeralFont = require("fontclock.font.js"); + +const DIM_30x47 = [30,47]; +const DIM_49x47 = [49,47]; +const DIM_37x47 = [37,47]; +class DigitNumeralFont extends NumeralFont{ + constructor(){ + super(); + this.widths = atob("DRYqFR0fHyMnICgnDQ=="); + this.font = atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAChkoAAAAAAAAAAAAAvP7wAAAAAAAAAAAAC8/vAAAAAAAAAAAAALz+8AAAAAAAAAAAAAvP7wAAAAAAAAAAAAC8/vAAAAAAAAAAAAALz+8AAAAAAAAAAAAAvP7wAAAAAAAAAAAAC8/vAAAAAAAAAAAAALz+8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAH/AAAAAAAAAAAAAB//8AAAAAAAAAAAAf///gAAAAAAAAAAH///5LAAAAAAAAAB///+S/8AAAAAAAAf///kv//wAAAAAAH///5L///4AAAAAB///+S///+G8AAAAf///kv///hv/wAAH///5L///4b///AAP//+S///+G///5AAA//kv///hv//+QAAAD5L///4b///kAAAAAC///+G///5AAAAAAA///hv//+QAAAAAAAD/4b///kAAAAAAAAAKG///5AAAAAAAAAAAv//+QAAAAAAAAAAAD//kAAAAAAAAAAAAAP5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAAAAAAAAAAAAAC////4AAAAAAAAAAL//////gAAAAAAAAP//6Vr//8AAAAAAAH/9Gv/pH/9AAAAAAD/4v////4v/AAAAAA/8///////P/AAAAAP9v/4BUC//n/AAAAD/f/m///+b/3/AAAAv3/n/////2/3+AAAH9/3//+r//9/39AAA/v9/+G/+S/9/78AAD9/f+f///9v9/fwAAvf3/f/////f9/fgAD9/vz/4AAv/P78/AAf/39/0AAAH/f3/9AC9/f78AAAAD+/f34ALz+9/QAAAAH9+/PgA/Pvz8AAAAAPz+8/AD99/PgAAAAAvP338AP739+AAAAAC9/ffwA/ff34AAAAAL399/AD8+/PwAAAAA/P7z8APz/9/AAAAAD9//PgAvf37/AAAAA//39+AB+/Pz/AAAAP8/P70AD9/v3/wAAP/f79/AAP39/3/////3/f38AAf79/2////5/3+/QAA/f9/9r//p/9/78AAB/f9//5Rb//f9/QAAD/f9v/////n/f8AAAD/f/b////n/3/AAAAD/f/4WqlL/9/wAAAAH/X//////9f9AAAAAH/1/////9f/QAAAAAC/+H///0v/gAAAAAAB//+QAb//0AAAAAAAAf//////0AAAAAAAAAB/////QAAAAAAAAAAAAa6QAAAAAAAAAAAAAAAAAAAAAAAAClopAAAAAAAAAAAAAPvz4AAAAAAAAAAAAA+/PgAAAAAAAAAAAAD78+AAAAAAAAAAAAAPvz4AAAAAAAAAAAAA+/PgAAAAAAAAAAAAD78///////////wAAPvz///////////AAA+/P6qqqqqqqqqoAAD78///////////gAAPvz///////////AAA+/P//////////8AAD78+AAAAAAAAAAAAAPvz///////////AAA+/P//////////8AAD78/qqqqqqqqqqgAAPvz//////////+AAA+/P//////////8AAD68///////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAA/PQAAAAAAAG8+vAAD9+/ZAAAAAC/z+8AAP/3+9AAAAA//P7wAB+/PvwAAAAf/8/vAAL3+9/AAAAL/bz+8AA/P/z8AAAD/r/P7wAD9+/PgAAB/7/8/vAAP379+AAAv+//z+8AA/vf30AAP+/9vP7wAD+9/vQAH/v+v8/vAAP7399AD/7/f/z+8AA/fv30A/7/3//P7wAD9+/Pgv+/7/28/vAALz+9/v/v9/9/z+8AAvP3+//v/v+v/P7wAB+/f3/7/v/f/8/vAAD+/v4H/3/3/bz+8AAP39///7/2/0vP7wAAf79v/9/9/8C8/vAAA/f9G5/+v+ALz+8AAC/f/7//f/QAvP7wAAD/P///r/wAC8/vAAAH/n//n/0AALz+8AAAH/5Ab/8AAAvP7wAAAH////9AAAB8tfAAAAC///9AAAAAAAAAAAAAK/kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyQAAAAAAAAABj4AA/P7gAAAAAAAu/PwAD9+/fQAAAAH3+9/AAP73+8AAAAAfv378AB+/PvwAAAAA//P70AL3+9/AAAAAD+9/fgA/P7z8KWiloPz78+AD8+/Pg/vP7wvP/z8APz78+D+8/vC8/vPwA/fv34P7z+8L3+8/AD99/fQ/vP7wvf738AP3399D+8/vB9/ffwA/ff30P7z+8H3+9/AD9+/fg/vf30vP738APz/8/D9+/Pj8+/PwA/P77//z/9//778+AC+/f3//f3+/9/f74AD+/v3/+/v6/f7+/AAPz9/5b9/f+b/fz8AAv79////+v//3+/gAA/f9///3/f/9/38AAC/f+RR/3/Rlv9/gAAD/f////3////f8AAAD/b////3///3/AAAAH/2//X/5//n/0AAAAD/+lr///lb/8AAAAAC////9////+AAAAAAAv//9Af//+AAAAAAAABaQAAAaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB7z58AAAAAAAAAAAB//P7wAAAAAAAAAAC//8/vAAAAAAAAAAC//rz+8AAAAAAAAAD//X/P7wAAAAAAAAH//X/8/vAAAAAAAAH//b//z+8AAAAAAAH//L/+vP7wAAAAAAL/+P/9f8/vAAAAAAL/+f/9f/z+8AAAAAL/9f/8v//P7wAAAAD/9f/4v/68/vAAAAAP9v/4//4vz+8AAAAA8v/5//0v/P7wAAAAAv/1//1//8/vAAAAAP/1//x///z+8AAAAA/y//i//wvP7wAAAADi//i//gC8/vAAAAAD//X//gALz+8AAAAA//X//QAAvP7wAAAAD/L//Af//8/vv/gAAOL/+AC///z+///AAAP/+AAH///P7//8AAD/9AAAAAC8/vAAAAAP9AAAB///z+///AAA4AAAAL///P7//8AAAAAAAAKqr8/vaqgAAAAAAAAAALz+8AAAAAAAAAAAAAvPrwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAA8AAAD14//////0AAL8AAAPvz//////gACf8AAA+/P/////+AAvfwAAD78+AAAAAAAD/vwAAPvz//////gA39/AAA+/P/////+AP3++AAD78/qqqqqgA/fz8AAPvz//////Qt/vvwAA+/P/////+D78/vQAD78//////4P739+AAPvz4A9ufPQ/Pvz8AA+/PgH3+9+C8+vPwAD78+Avf/z0L3+9/AAPvz4C8//fAff338AA+/PgLz7+8B9/vfwAD78+AvPv7wL3+9/AAPvz4D8+/fg/P/z8AA+/PgLz+8/D9+/PwAD78+AvP77///39+AAPvz4B+/P3/9/v/wAA+/PgD9+/3/f39/AAD78+AP38/0L/f78AAPvz4Av79///39/QAA+/PgA/f8//9//8AAD78+AB/f9L5f9/QAAPvz4AD/f/6//f8AAA+/PgAD/P///3/AAAD289AAH/j//y/wAAAAAAAAAH/9AH/8AAAAAAAAAAD////+AAAAAAAAAAAB///+AAAAAAAAAAAAAGvpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa////6QAAAAAAAAB///////+AAAAAAAB/////////gAAAAAA//kAAAABv/0AAAAAP/X//////n/0AAAAD/b///////6/0AAAA/3//6qqqv/9/wAAAL9/9a////kv9/wAAB/P+v//////P9/QAAP6/f///////P+/AAA/P3/5AAAAf/fz8AAP3+/2/////3/v74AA/f79//////79/fwAD/+/f/6qqr/3++/AAvf39/Pz799//3+9AD8/vfw/vf/w/fv38APz/8/D/9+/D8+/PwA/fvz4ff734L3/9/AD99/fS8//fQff738AP3399Lz799B9/ffwA/ff30vPv7wH3+9/AD9+/Pj8+/vQvP7z8APz68+Lz79+D8+/PwA/P338vP7z8f779+AB+/f/x+/f//+/f70AD+9/fD+9/f/7/+/AAP3+/QP3+/b2/f38AA/fz8Av/3///7//gAB/v3QA/f3///v38AAD9/gAC/v2//b+/gAAH9+AAD+/+AL/r8AAAP+wAAH+v///6/QAAAP0AAAP/f//9/4AAAAfQAAAP/lvlv+AAAAAkAAAAL//r//QAAAAAAAAAAD////wAAAAAAAAAAAAv//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPrz4AAAAAAAAAAAAA+/PgAAAAAAAAAAAAD78+AAAAAAAAAAAAAPvz4AAAAAAAAAAAAA+/PgAAAAAAAAAAAAD78+AAAAAAAAAAAAAPvz4AAAAAAAAAGAAA+/PgAAAAAAAB/8AAD78+AAAAAAAv//wAAPvz4AAAAAL///4AAA+/PgAAAC///+G8AAD78+AAAv///hv/wAAPvz4AL///4b///AAA+/Pm///+G///9EAAD79////Rv//+R/wAAPr///0f///kv//AAA///9H///5L///4AAD//R///+S///+GwAAPkf///kv///hv/AAAL///5L///4b//8AAD//+W///+G///9AAAP/hv///Rv//+QAAAAob///0f///kAAAAAC///9H///5AAAAAAAP//R///+QAAAAAAAA/kf///kAAAAAAAAAAL///5AAAAAAAAAAAP//+QAAAAAAAAAAAA//kAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//0AL//9AAAAAAAf///+L////QAAAAAP/+r////r//wAAAAD/0v5f/4r6b/wAAAA/6///n////1/wAAAP9////3////9/gAAC/f/gG/3/kB/9/AAAP7/b/9/3//+f+/AAC/f3//9/v///f79AAP39/+/9/f///f38AA/v/9uf+/v/n+/vwAL79/f/v38//38/vQA/P77//f7z///78/AD8//Pz9/vfw/Prz8AP379+H79+/D8/vfwA/vf30Pvz68L399/AD+9/fQ+/Prwff378AP7399D78+vB9/fvwA/vf30Pvz68H39+/AD+9/fQ+/Prwff378AP7399D78+vB9/fvwA/vf30Pvz+8H39+/AD9+/fg/vP7wvf338APz/8/D+8/vT8/vfwA/P77/v339//378/AC9/f7//P77/+/f34AH++/r/9/f3/r9/vQAP39/0H++v4D/f38AA/v+///39///+/fwAA/f9///39//+v39AAD/v9b5/v+b+P+/wAAD/f/6//v/6//r8AAAH/f////7///2/gAAAL/X//3/3//5/4AAAAH/5FC//9BB/+AAAAAH//////////QAAAAAB////A////QAAAAAAAK/5AAG/5QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//QAAAAAAAAAAAAv///4AAAAAAAAAAAf////9AAAABQAAAAH/0aR/9AAAAPQAAAB/2//+v9AAAC/AAAAf9////f8AAA3/AAAD/f/Qf/f4AAL38AAAv3/P/n/fwAAv78AAD+/r//3/fwAd/fwAAv7+///7+/AD9/vgAD9/f8pv3+/Af39/AAP7//v//v38Lf778AB+/f3/9/f/w//P70AL3++/r+9/vT99/fgA/P7z8Pz79+Pz78/AD9+/Pgvf/z4vPrz8AP3799B9/vPi9/vfwA/vf30D7+8+H399/AD9+/fQff/z4ff338AP3799C8+/fS8/vfwA/P7z4P73+8Pz/8/AD8/vfw//fvw/fvz8AL39//n3+8/P79/fgAP/39/6VVVv/f7/8AA/f7+//////3+/fwAD+/f9/////9/79/AAH9/f+FVVVRv9/P0AAP3+f///////f9/AAAf3/f//////7/P0AAA/3/W////+j/3/AAAA/7/+lVVVr/9/wAAAB/3///////+f9AAAAB/9//////+L/QAAAAB//QVVVVQv/wAAAAAA/////////4AAAAAAAf///////4AAAAAAAABv////+gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB89fAHz18AAAAAAAAPz78AvP7wAAAAAAAA/PvwC8/vAAAAAAAAD8+/ALz+8AAAAAAAAPz78AvP7wAAAAAAAA/PvwC8/vAAAAAAAAD8+/ALz+8AAAAAAAAPz78AvP7wAAAAAAAA/PvwC8/vAAAAAAAAC8+vALz68AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); + var scale = 1; // size multiplier for this font + this.size = 50+(scale<<8)+(2<<16); + this.y_offset = -2; + + } + getDimensions(hour){ + switch(hour){ + case 3: + return DIM_30x47; + case 12: + return DIM_49x47; + default: + return DIM_37x47; + } + } + hour_txt(hour){ return hour.toString(); } + draw(hour_txt,x,y){ + /* going to leave this in here for future testing. + uncomment this so that it draws a box behind the string + so we can guess the digit dimensions + var dim = [37,47]; + g.setColor(0.5,0,0); + g.fillPoly([x,y, + x+dim[0],y, + x+dim[0],y+dim[1], + x,y+dim[1] + ]); + g.setColor(1.0,1.0,1.0);*/ + g.setFontAlign(-1.0,-1.0,0); + g.setFontCustom(this.font, 46, this.widths, this.size); + g.drawString(hour_txt,x,y+this.y_offset ); + } + getName(){return "Digit";} +} + +module.exports = [DigitNumeralFont]; \ No newline at end of file diff --git a/apps/fontclock/fontclock.font.vector25.js b/apps/fontclock/fontclock.font.vector25.js new file mode 100644 index 000000000..95b23d040 --- /dev/null +++ b/apps/fontclock/fontclock.font.vector25.js @@ -0,0 +1,39 @@ +var NumeralFont = require("fontclock.font.js"); + +const DIM_14x22 = [14,22]; +const DIM_27x22 = [27,22]; +class DigitNumeralFont extends NumeralFont{ + constructor(){ + super(); + } + getDimensions(hour){ + if (hour < 10){ + return DIM_14x22; + } else { + return DIM_27x22; + } + } + hour_txt(hour){ return hour.toString(); } + draw(hour_txt,x,y){ + if(hour_txt == null) + return; + + /* going to leave this in here for future testing. + uncomment this so that it draws a box behind the string + so we can guess the digit dimensions + var dim = [14,22]; + g.setColor(0.5,0,0); + g.fillPoly([x,y, + x+dim[0],y, + x+dim[0],y+dim[1], + x,y+dim[1] + ]); + g.setColor(1.0,1.0,1.0);*/ + g.setFontAlign(-1,-1,0); + g.setFont("Vector",25); + g.drawString(hour_txt,x,y); + } + getName(){return "Digit";} +} + +module.exports = [DigitNumeralFont]; \ No newline at end of file diff --git a/apps/fontclock/fontclock.font.vector50.js b/apps/fontclock/fontclock.font.vector50.js new file mode 100644 index 000000000..ccc4599fd --- /dev/null +++ b/apps/fontclock/fontclock.font.vector50.js @@ -0,0 +1,91 @@ +var NumeralFont = require("fontclock.font.js"); + +const DIM_28x44 = [28,44]; +const DIM_54x44 = [54,44]; + +class DigitNumeralFont extends NumeralFont{ + constructor(){ + super(); + } + getDimensions(hour){ + if (hour < 10){ + return DIM_28x44; + } else { + return DIM_54x44; + } + } + hour_txt(hour){ return hour.toString(); } + draw(hour_txt,x,y){ + if(hour_txt == null) + return; + + /* going to leave this in here for future testing. + uncomment this so that it draws a box behind the string + so we can guess the digit dimensions + var dim = [14,22]; + g.setColor(0.5,0,0); + g.fillPoly([x,y, + x+dim[0],y, + x+dim[0],y+dim[1], + x,y+dim[1] + ]); + g.setColor(1.0,1.0,1.0);*/ + g.setFontAlign(-1,-1,0); + g.setFont("Vector",50); + g.drawString(hour_txt,x,y); + } + getName(){return "Digit";} +} + +const DIM_50x40 = [50,40]; +const DIM_70x40 = [70,40]; +class RomanNumeralFont extends NumeralFont{ + constructor(){ + super(); + } + getText(hour){ + switch (hour){ + case 1 : return 'I'; + case 2 : return 'II'; + case 3 : return 'III'; + case 4 : return 'IV'; + case 5 : return 'V'; + case 6 : return 'VI'; + case 7 : return 'VII'; + case 8 : return 'VIII'; + case 9 : return 'IX'; + case 10: return 'X'; + case 11: return 'XI'; + case 12: return 'XII'; + default: return ''; + } + } + getDimensions(hour){ + switch (hour){ + case 3: + case 6: + case 9: + return DIM_50x40; + case 12: + return DIM_70x40; + default: + return DIM_70x40; + } + } + hour_txt(hour){ return this.getText(hour); } + draw(hour_txt,x,y){ + /*var dim = DIM_70x40; + g.setColor(0.5,0,0); + g.fillPoly([x,y, + x+dim[0],y, + x+dim[0],y+dim[1], + x,y+dim[1] + ]);*/ + g.setFontAlign(-1,-1,0); + g.setFont("Vector",50); + g.drawString(hour_txt,x,y); + } + getName(){return "Roman";} +} + +module.exports = [DigitNumeralFont,RomanNumeralFont]; \ No newline at end of file diff --git a/apps/fontclock/fontclock.hand.js b/apps/fontclock/fontclock.hand.js new file mode 100644 index 000000000..c5ab2e769 --- /dev/null +++ b/apps/fontclock/fontclock.hand.js @@ -0,0 +1,10 @@ +class Hand { + /** + * Pure virtual class for all Hand classes to extend. + * a hand class will have 1 main function + * moveTo which will move the hand to the given angle. + */ + moveTo(angle){} +} + +module.exports = Hand; \ No newline at end of file diff --git a/apps/fontclock/fontclock.hourscriber.js b/apps/fontclock/fontclock.hourscriber.js new file mode 100644 index 000000000..eaddbab4e --- /dev/null +++ b/apps/fontclock/fontclock.hourscriber.js @@ -0,0 +1,137 @@ + +const TWO_PI = 2* Math.PI; + +// The problem with the trig inverse functions on +// a full circle is that the sector information will be lost +// Choosing to use arcsin because you can get back the +// sector with the help of the original coordinates +function reifyasin(x,y,asin_angle){ + if(x >= 0 && y >= 0){ + return asin_angle; + } else if(x >= 0 && y < 0){ + return Math.PI - asin_angle; + } else if(x < 0 && y < 0){ + return Math.PI - asin_angle; + } else { + return TWO_PI + asin_angle; + } +} + +// rebase and angle so be between -pi and pi +// rather than 0 to 2PI +function rebaseNegative(angle){ + if(angle > Math.PI){ + return angle - TWO_PI; + } else { + return angle; + } +} + +// rebase an angle so that it is between 0 to 2pi +// rather than -pi to pi +function rebasePositive(angle){ + if(angle < 0){ + return angle + TWO_PI; + } else { + return angle; + } +} + +/** + * The Hour Scriber is responsible for drawing the numeral + * on the screen at the requested angle. + * It allows for the font to be changed on the fly. + */ +class HourScriber { + constructor(radius, numeral_font, draw_test, bg_colour_supplier, numeral_colour_supplier, hour){ + this.radius = radius; + this.numeral_font = numeral_font; + this.draw_test = draw_test; + this.curr_numeral_font = numeral_font; + this.bg_colour_supplier = bg_colour_supplier; + this.numeral_colour_supplier = numeral_colour_supplier; + this.hours = hour; + this.curr_hour_x = -1; + this.curr_hour_y = -1; + this.curr_hours = -1; + this.curr_hour_str = null; + this.last_draw_time = null; + } + setNumeralFont(numeral_font){ + this.numeral_font = numeral_font; + } + toString(){ + return "HourScriber{numeralfont=" + this.numeral_font.getName() + ",hours=" + this.hours + "}"; + } + draw(){ + var changed = false; + if(this.curr_hours != this.hours || this.curr_numeral_font !=this.numeral_font){ + var background = this.bg_colour_supplier(); + g.setColor(background[0],background[1],background[2]); + this.curr_numeral_font.draw(this.curr_hour_str, + this.curr_hour_x, + this.curr_hour_y); + //console.log("erasing old hour display:" + this.curr_hour_str + " color:" + background); + var hours_frac = this.hours / 12; + var angle = TWO_PI*hours_frac; + var dimensions = this.numeral_font.getDimensions(this.hours); + // we set the radial coord to be in the middle + // of the drawn text. + var width = dimensions[0]; + var height = dimensions[1]; + var delta_center_x = this.radius*Math.sin(angle) - width/2; + var delta_center_y = this.radius*Math.cos(angle) + height/2; + this.curr_hour_x = screen_center_x + delta_center_x; + this.curr_hour_y = screen_center_y - delta_center_y; + this.curr_hour_str = this.numeral_font.hour_txt(this.hours); + // now work out the angle of the beginning and the end of the + // text box so we know when to redraw + // bottom left angle + var x1 = delta_center_x; + var y1 = delta_center_y; + var r1 = Math.sqrt(x1*x1 + y1*y1); + var angle1 = reifyasin(x1,y1,Math.asin(x1/r1)); + // bottom right angle + var x2 = delta_center_x; + var y2 = delta_center_y - height; + var r2 = Math.sqrt(x2*x2 + y2*y2); + var angle2 = reifyasin(x2,y2,Math.asin(x2/r2)); + // top left angle + var x3 = delta_center_x + width; + var y3 = delta_center_y; + var r3 = Math.sqrt(x3*x3 + y3*y3); + var angle3 = reifyasin(x3,y3, Math.asin(x3/r3)); + // top right angle + var x4 = delta_center_x + width; + var y4 = delta_center_y - height; + var r4 = Math.sqrt(x4*x4 + y4*y4); + var angle4 = reifyasin(x4,y4,Math.asin(x4/r4)); + if(Math.min(angle1,angle2,angle3,angle4) < Math.PI && Math.max(angle1,angle2,angle3,angle4) > 1.5*Math.PI){ + angle1 = rebaseNegative(angle1); + angle2 = rebaseNegative(angle2); + angle3 = rebaseNegative(angle3); + angle3 = rebaseNegative(angle4); + this.angle_from = rebasePositive( Math.min(angle1,angle2,angle3,angle4) ); + this.angle_to = rebasePositive( Math.max(angle1,angle2,angle3,angle4) ); + } else { + this.angle_from = Math.min(angle1,angle2,angle3,angle4); + this.angle_to = Math.max(angle1,angle2,angle3,angle4); + } + //console.log(angle1 + "/" + angle2 + " / " + angle3 + " / " + angle4); + //console.log( this.angle_from + " to " + this.angle_to); + this.curr_hours = this.hours; + this.curr_numeral_font = this.numeral_font; + changed = true; + } + if(changed || + this.draw_test(this.angle_from, this.angle_to, this.last_draw_time) ){ + var numeral_color = this.numeral_colour_supplier(); + g.setColor(numeral_color[0],numeral_color[1],numeral_color[2]); + this.numeral_font.draw(this.curr_hour_str,this.curr_hour_x,this.curr_hour_y); + this.last_draw_time = new Date(); + //console.log("redraw digit:" + this.hours); + } + } +} + +module.exports = HourScriber; \ No newline at end of file diff --git a/apps/fontclock/fontclock.js b/apps/fontclock/fontclock.js new file mode 100644 index 000000000..bd6ba16b7 --- /dev/null +++ b/apps/fontclock/fontclock.js @@ -0,0 +1,436 @@ +/** +* Adrian Kirk 2021-03 +* Simple Clock showing 1 numeral for the hour +* with a smooth sweep second. +*/ + +var ThinHand = require("fontclock.thinhand.js"); +var ThickHand = require("fontclock.thickhand.js"); +var HourScriber = require("fontclock.hourscriber.js"); + +const screen_center_x = g.getWidth()/2; +const screen_center_y = 10 + (g.getHeight()+10)/2; +const TWO_PI = 2* Math.PI; + + +SETTING_PREFIX = "fontclock"; +// load the date formats and languages required +const FONTS_FILE = SETTING_PREFIX +".font.json"; +const DEFAULT_FONTS = [ "cpstc58" ]; +const DEFAULT_NUMERALS = [12,3,6,9]; +const DEFAULT_RADIUS = 70; +var color_schemes = [ + { + name: "black", + background : [0.0,0.0,0.0], + } +]; +var fonts = DEFAULT_NUMERALS; +var numerals = DEFAULT_NUMERALS; +var radius = DEFAULT_RADIUS; + +var fonts_info = null; +try { + fonts_info = require("Storage").readJSON(FONTS_FILE); +} catch(e){ + console.log("failed to load fonts file:" + FONTS_FILE + e); +} +if(fonts_info != null){ + console.log("loaded font:" + JSON.stringify(fonts_info)); + fonts = fonts_info.fonts; + numerals = fonts_info.numerals; + radius = fonts_info.radius; + color_schemes = fonts_info.color_schemes; +} else { + fonts = DEFAULT_FONTS; + numerals = DEFAULT_NUMERALS; + radius = DEFAULT_RADIUS; + console.log("no fonts loaded defaulting to:" + fonts); +} + +if(fonts == null || fonts.length == 0){ + fonts = DEFAULT_FONTS; + console.log("defaulting fonts to locale:" + fonts); +} + +let color_scheme_index = 0; + +// The force draw is set to true to force all objects to redraw themselves +let force_redraw = true; +let bg_colour_supplier = ()=>color_schemes[color_scheme_index].background; +var WHITE = [1.0,1.0,1.0]; +function default_white(color){ + if(color == null){ + return WHITE + } else { + return color; + } +} + +// The seconds hand is the main focus and is set to redraw on every cycle +let seconds_hand = new ThinHand(screen_center_x, + screen_center_y, + 95, + 0, + (angle, last_draw_time) => false, + bg_colour_supplier, + ()=>default_white(color_schemes[color_scheme_index].second_hand)); + +// The minute hand is set to redraw at a 250th of a circle, +// when the second hand is ontop or slighly overtaking +// or when a force_redraw is called +const minute_hand_angle_tolerance = TWO_PI/25 +let minutes_hand_redraw = function(angle, last_draw_time){ + return force_redraw || (seconds_hand.angle > angle && + Math.abs(seconds_hand.angle - angle) < minute_hand_angle_tolerance && + new Date().getTime() - last_draw_time.getTime() > 500); +}; + +let minutes_hand = new ThinHand(screen_center_x, + screen_center_y, + 80, minute_hand_angle_tolerance, + minutes_hand_redraw, + bg_colour_supplier, + ()=>default_white(color_schemes[color_scheme_index].minute_hand)); +// The hour hand is a thick hand so we have to redraw when the minute hand +// overlaps from its behind angle coverage to its ahead angle coverage. +let hour_hand_redraw = function(angle_from, angle_to, last_draw_time){ + return force_redraw || (seconds_hand.angle >= angle_from && + seconds_hand.angle <= angle_to && + new Date().getTime() - last_draw_time.getTime() > 500); +}; +let hours_hand = new ThickHand(screen_center_x, + screen_center_y, + 40, + TWO_PI/600, + hour_hand_redraw, + bg_colour_supplier, + () => default_white(color_schemes[color_scheme_index].hour_hand), + 5, + 4); + +function draw_clock(){ + var date = new Date(); + draw_background(); + draw_hour_digits(); + draw_seconds(date); + draw_mins(date); + draw_hours(date); + force_redraw = false; +} +// drawing the second the millisecond as we need the fine gradation +// for the sweep second hand. +function draw_seconds(date){ + var seconds = date.getSeconds() + date.getMilliseconds()/1000; + var seconds_frac = seconds / 60; + var seconds_angle = TWO_PI*seconds_frac; + seconds_hand.moveTo(seconds_angle); +} +// drawing the minute includes the second and millisec to make the +// movement as continuous as possible. +function draw_mins(date,seconds_angle){ + var mins = date.getMinutes() + date.getSeconds()/60 + date.getMilliseconds()/(60*1000); + var mins_frac = mins / 60; + var mins_angle = TWO_PI*mins_frac; + var redraw = minutes_hand.moveTo(mins_angle); + if(redraw){ + //console.log("redraw mins"); + } +} + +function draw_hours(date){ + var hours = (date.getHours() % 12) + date.getMinutes()/60 + date.getSeconds()/3600; + var hours_frac = hours / 12; + var hours_angle = TWO_PI*hours_frac; + var redraw = hours_hand.moveTo(hours_angle); + if(redraw){ + //console.log("redraw hours"); + } +} + + + +let numeral_fonts = []; +for(var i=0; i< fonts.length; i++) { + var file = SETTING_PREFIX +".font." + fonts[i] + ".js" + console.log("loading font set:" + fonts[i] + "->" + file); + var loaded_fonts = require(file); + for (var j = 0; j < loaded_fonts[j]; j++) { + var loaded_font = new loaded_fonts[j]; + numeral_fonts.push(loaded_font); + console.log("loaded font name:" + loaded_font.getName()) + } +} + +let numeral_fonts_index = 0; +const ONE_POINT_FIVE_PI = 1.5*Math.PI; +/** +* predicate for deciding when the digit has to be redrawn +*/ +let hour_numeral_redraw = function(angle_from, angle_to, last_draw_time){ + var seconds_hand_angle = seconds_hand.angle; + // we have to cope with the 12 problem where the + // left side of the box has a value almost 2PI and the right + // side has a small positive value. The values are rebased so + // that they can be compared + if(angle_from > angle_to && angle_from > ONE_POINT_FIVE_PI){ + angle_from = angle_from - TWO_PI; + if(seconds_hand_angle > Math.PI) + seconds_hand_angle = seconds_hand_angle - TWO_PI; + } + //console.log("initial:" + angle_from + "/" + angle_to + " seconds " + seconds_hand_angle); + var redraw = force_redraw || + (seconds_hand_angle >= angle_from && seconds_hand_angle <= angle_to && seconds_hand.last_draw_time.getTime() > last_draw_time.getTime()) || + (minutes_hand.last_draw_time.getTime() > last_draw_time.getTime()); + if(redraw){ + //console.log(angle_from + "/" + angle_to + " seconds " + seconds_hand_angle); + } + return redraw; +}; + +// now add the numbers to the clock face +var numeral_colour_supplier = () => default_white(color_schemes[color_scheme_index].numeral); +var hour_scribers = []; +console.log("numerals:" + numerals + " length:" + numerals.length) +console.log("radius:" + radius) +for(var digit_idx=0; digit_idx" + scriber); +} +//console.log("hour_scribers:" + hour_scribers ); + +/** +* Called from button 1 to change the numerals that are +* displayed on the clock face +*/ +function next_font() { + var curr_font = numeral_fonts_index; + numeral_fonts_index = numeral_fonts_index + 1; + if (numeral_fonts_index >= numeral_fonts.length) { + numeral_fonts_index = 0; + } + + if (curr_font != numeral_fonts_index) { + console.log("numeral font changed") + for (var i = 0; i < hour_scribers.length; i++) { + hour_scribers[i].setNumeralFont( + numeral_fonts[numeral_fonts_index]); + } + force_redraw = true; + return true; + } else { + return false; + } +} + +const hour_zone_angle = hour_scribers.length/TWO_PI; +function draw_hour_digits() { + if(force_redraw){ + for(var i=0; i" + scriber); + scriber.draw(); + } + } else { + var hour_scriber_idx = (0.5 + (seconds_hand.angle * hour_zone_angle)) | 0; + if (hour_scriber_idx >= hour_scribers.length) + hour_scriber_idx = 0; + + //console.log("angle:" + seconds_hand.angle + " idx:" + hour_scriber_idx); + if (hour_scriber_idx >= 0) { + hour_scribers[hour_scriber_idx].draw(); + } + } +} + + + +function draw_background(){ + if(force_redraw){ + background = color_schemes[color_scheme_index].background; + g.setColor(background[0],background[1],background[2]); + g.fillPoly([0,25, + 0,240, + 240,240, + 240,25 + ]); + } +} + +function next_colorscheme(){ + var prev_color_scheme_index = color_scheme_index; + color_scheme_index += 1; + color_scheme_index = color_scheme_index % color_schemes.length; + //console.log("color_scheme_index=" + color_scheme_index); + force_redraw = true; + if(prev_color_scheme_index == color_scheme_index){ + return false; + } else { + return true; + } +} + +/** +* called from load_settings on startup to +* set the color scheme to named value +*/ +function set_colorscheme(colorscheme_name){ + console.log("setting color scheme:" + colorscheme_name); + for (var i=0; i < color_schemes.length; i++) { + if(color_schemes[i].name == colorscheme_name){ + color_scheme_index = i; + force_redraw = true; + console.log("match"); + break; + } + } +} + +/** +* called from load_settings on startup +* to set the font to named value +*/ +function set_font(font_name){ + console.log("setting font:" + font_name); + for (var i=0; i < numeral_fonts.length; i++) { + if(numeral_fonts[i].getName() == font_name) { + numeral_fonts_index = i; + force_redraw = true; + console.log("match"); + for (var j = 0; j < hour_scribers.length; j++) { + hour_scribers[j].setNumeralFont(numeral_fonts[numeral_fonts_index]); + } + break; + } + } +} + +/** +* Called on startup to set the watch to the last preference settings +*/ +function load_settings(){ + try{ + var file = SETTING_PREFIX + ".settings.json"; + settings = require("Storage").readJSON(file); + if(settings != null){ + console.log(file + " loaded:" + JSON.stringify(settings)); + if(settings.color_scheme != null){ + set_colorscheme(settings.color_scheme); + } + if(settings.font != null){ + set_font(settings.font); + } + } else { + console.log(file + " not found - no settings to load"); + } + } catch(e){ + console.log("failed to load settings:" + e); + } +} + +/** +* Called on button press to save down the last preference settings +*/ +function save_settings(){ + var settings = { + font : numeral_fonts[numeral_fonts_index].getName(), + color_scheme : color_schemes[color_scheme_index].name, + }; + var file = SETTING_PREFIX + ".settings.json"; + console.log(file + ": saving:" + JSON.stringify(settings)); + require("Storage").writeJSON(file,settings); +} + +// Boiler plate code for setting up the clock, +// below +let intervalRef = null; + +function clearTimers(){ + if(intervalRef) { + clearInterval(intervalRef); + intervalRef = null; + } +} + +function startTimers(){ + setTimeout(scheduleDrawClock,100); + draw_clock(); +} + +// The clock redraw is set to 100ms. This is the smallest number +// that give the (my) human eye the illusion of a continious sweep +// second hand. +function scheduleDrawClock(){ + if(intervalRef) clearTimers(); + intervalRef = setInterval(draw_clock, 100); + draw_clock(); +} + +function reset_clock(){ + force_redraw = true; +} + +Bangle.on('lcdPower', (on) => { + if (on) { + console.log("lcdPower: on"); + reset_clock(); + startTimers(); + } else { + console.log("lcdPower: off"); + reset_clock(); + clearTimers(); + } +}); + +Bangle.on('faceUp',function(up){ + console.log("faceUp: " + up + " LCD: " + Bangle.isLCDOn()); + if (up && !Bangle.isLCDOn()) { + //console.log("faceUp and LCD off"); + clearTimers(); + Bangle.setLCDPower(true); + } +}); + +g.clear(); +load_settings(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +startTimers(); + +function button1pressed() { + if (next_font()) { + save_settings(); + } +} + +function button2pressed() { + clearTimers(); + // the clock is being unloaded so we clear out the big + // data structures for the launcher + hour_scribers = []; + Bangle.showLauncher(); +} + +function button3pressed(){ + if(next_colorscheme()) { + save_settings(); + } +} + +// Handle button 1 being pressed +setWatch(button1pressed, BTN1,{repeat:true,edge:"falling"}); + +// Handle button 1 being pressed +setWatch(button2pressed, BTN2,{repeat:true,edge:"falling"}); + +// Handle button 3 being pressed +setWatch(button3pressed, BTN3,{repeat:true,edge:"falling"}); + diff --git a/apps/fontclock/fontclock.png b/apps/fontclock/fontclock.png new file mode 100644 index 000000000..97377413c Binary files /dev/null and b/apps/fontclock/fontclock.png differ diff --git a/apps/fontclock/fontclock.thickhand.js b/apps/fontclock/fontclock.thickhand.js new file mode 100644 index 000000000..d28f91aef --- /dev/null +++ b/apps/fontclock/fontclock.thickhand.js @@ -0,0 +1,103 @@ +var Hand = require("fontclock.hand.js"); + +class ThickHand extends Hand { + /** + * The thick hand is created from a filled polygone, so its slower to + * draw so to be used sparingly with few redraws + */ + constructor(centerX, + centerY, + length, + tolerance, + draw_test, + color_bg_supplier, + color_fg_supplier, + base_height, + thickness){ + super(); + this.centerX = centerX; + this.centerY = centerY; + this.length = length; + this.color_bg_supplier = color_bg_supplier; + this.color_fg_supplier = color_fg_supplier; + this.base_height = base_height; + // angle from the center to the top corners of the rectangle + this.delta_top = Math.atan(thickness/(2*length)); + // angle from the center to the bottom corners of the rectangle + this.delta_base = Math.atan(thickness/(2*base_height)); + // the radius that the bottom corners of the rectangle move through + this.vertex_radius_base = Math.sqrt( (thickness*thickness/4) + base_height * base_height); + // the radius that the top corners of the rectangle move through + this.vertex_radius_top = Math.sqrt( (thickness*thickness/4) + length * length); + // last records the last plotted values (so we don't have to keep recalculating + this.last_x1 = centerX; + this.last_y1 = centerY; + this.last_x2 = centerX; + this.last_y2 = centerY; + this.last_x3 = centerX; + this.last_y3 = centerY; + this.last_x4 = centerX; + this.last_y4 = centerY; + // The change in angle from the last plotted angle before we actually redraw + this.tolerance = tolerance; + // predicate test that is called if the hand is not going to redraw to see + // if there is an externally defined reason for redrawing (like another hand) + this.draw_test = draw_test; + this.angle = -1; + this.last_draw_time = null; + } + // method to move the hand to a new angle + moveTo(angle){ + if(Math.abs(angle - this.angle) > this.tolerance || this.draw_test(this.angle - this.delta_base,this.angle + this.delta_base ,this.last_draw_time) ){ + //var background = color_schemes[color_scheme_index].background; + var background = this.color_bg_supplier; + g.setColor(background[0],background[1],background[2]); + g.fillPoly([this.last_x1, + this.last_y1, + this.last_x2, + this.last_y2, + this.last_x3, + this.last_y3, + this.last_x4, + this.last_y4 + ]); + // bottom left + var x1 = this.centerX + + this.vertex_radius_base*Math.sin(angle - this.delta_base); + var y1 = this.centerY - this.vertex_radius_base*Math.cos(angle - this.delta_base); + // bottom right + var x2 = this.centerX + + this.vertex_radius_base*Math.sin(angle + this.delta_base); + var y2 = this.centerY - this.vertex_radius_base*Math.cos(angle + this.delta_base); + // top right + var x3 = this.centerX + this.vertex_radius_top*Math.sin(angle + this.delta_top); + var y3 = this.centerY - this.vertex_radius_top*Math.cos(angle + this.delta_top); + // top left + var x4 = this.centerX + this.vertex_radius_top*Math.sin(angle - this.delta_top); + var y4 = this.centerY - this.vertex_radius_top*Math.cos(angle - this.delta_top); + //var hand_color = color_schemes[color_scheme_index][this.color_theme]; + var hand_color = this.color_fg_supplier(); + g.setColor(hand_color[0],hand_color[1],hand_color[2]); + g.fillPoly([x1,y1, + x2,y2, + x3,y3, + x4,y4 + ]); + this.last_x1 = x1; + this.last_y1 = y1; + this.last_x2 = x2; + this.last_y2 = y2; + this.last_x3 = x3; + this.last_y3 = y3; + this.last_x4 = x4; + this.last_y4 = y4; + this.angle = angle; + this.last_draw_time = new Date(); + return true; + } else { + return false; + } + } +} + +module.exports = ThickHand; \ No newline at end of file diff --git a/apps/fontclock/fontclock.thinhand.js b/apps/fontclock/fontclock.thinhand.js new file mode 100644 index 000000000..cf58d451a --- /dev/null +++ b/apps/fontclock/fontclock.thinhand.js @@ -0,0 +1,67 @@ +var Hand = require("fontclock.hand.js"); + +class ThinHand extends Hand { + /** + * The thin hand is created from a simple line, so its easy and fast + * to draw. + */ + constructor(centerX, + centerY, + length, + tolerance, + draw_test, + color_bg_supplier, + color_fg_supplier){ + super(); + this.centerX = centerX; + this.centerY = centerY; + this.length = length; + this.color_bg_supplier = color_bg_supplier; + this.color_fg_supplier = color_fg_supplier; + // The last x and y coordinates (not the centre) of the last draw + this.last_x = centerX; + this.last_y = centerY; + // tolerance is the angle tolerance (from the last draw) + // in radians for a redraw to be called. + this.tolerance = tolerance; + // draw test is a predicate (angle, time). This is called + // when the hand thinks that it does not have to draw (from its internal tests) + // to see if it has to draw because of another object. + this.draw_test = draw_test; + // The current angle of the hand. Set to -1 initially + this.angle = -1; + this.last_draw_time = null; + this.active = false; + } + // method to move the hand to a new angle + moveTo(angle){ + // first test to see of the angle called is beyond the tolerance + // for a redraw + if(Math.abs(angle - this.angle) > this.tolerance || + // and then call the predicate to see if a redraw is needed + this.draw_test(this.angle,this.last_draw_time) ){ + // rub out the old hand line + var background = this.color_bg_supplier(); + g.setColor(background[0],background[1],background[2]); + g.drawLine(this.centerX, this.centerY, this.last_x, this.last_y); + // Now draw the new hand line + var hand_color = this.color_fg_supplier(); + g.setColor(hand_color[0],hand_color[1],hand_color[2]); + var x2 = this.centerX + this.length*Math.sin(angle); + var y2 = this.centerY - this.length*Math.cos(angle); + g.drawLine(this.centerX, this.centerY, x2, y2); + // and store the last draw details for the next call + this.last_x = x2; + this.last_y = y2; + this.angle = angle; + this.last_draw_time = new Date(); + this.active = true; + return true; + } else { + this.active = false; + return false; + } + } +} + +module.exports = ThinHand; \ No newline at end of file diff --git a/apps/fwupdate/ChangeLog b/apps/fwupdate/ChangeLog new file mode 100644 index 000000000..96e7e4e9b --- /dev/null +++ b/apps/fwupdate/ChangeLog @@ -0,0 +1,4 @@ +0.01: Initial version +0.02: Add support for ZIPs + Find and download ZIPs direct from the Espruino website + Take 'beta' tag off diff --git a/apps/fwupdate/app.png b/apps/fwupdate/app.png new file mode 100644 index 000000000..1fabf06a2 Binary files /dev/null and b/apps/fwupdate/app.png differ diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html new file mode 100644 index 000000000..8c2008e54 --- /dev/null +++ b/apps/fwupdate/custom.html @@ -0,0 +1,361 @@ + + + + + +

THIS IS CURRENTLY BETA - PLEASE USE THE NORMAL FIRMWARE UPDATE + INSTRUCTIONS FOR BANGLE.JS 1 AND BANGLE.JS 2. For usage on Bangle.js 2 you'll likely need to have an updated bootloader.

+
+

Firmware updates using the App Loader are only possible on + Bangle.js 2. For firmware updates on Bangle.js 1 please + see the Bangle.js 1 instructions

+
+

Your current firmware version is unknown

+ + +

Firmware updates via this tool work differently to the NRF Connect method mentioned on + the Bangle.js page. Firmware + is uploaded to a file on the Bangle. Once complete the Bangle reboots and the bootloader copies + the new firmware into internal Storage.

+ +

+
+    
+    
+    
+
+    
+  
+
diff --git a/apps/gallifr/ChangeLog b/apps/gallifr/ChangeLog
index c785cbd67..0e1f45042 100644
--- a/apps/gallifr/ChangeLog
+++ b/apps/gallifr/ChangeLog
@@ -1 +1,2 @@
 0.01: First released version
+0.02: Changed setWatch to Bangle.setUI
diff --git a/apps/gallifr/app.js b/apps/gallifr/app.js
index 281988ad7..d327bcdc1 100644
--- a/apps/gallifr/app.js
+++ b/apps/gallifr/app.js
@@ -243,5 +243,5 @@ startTimers();
 Bangle.loadWidgets();
 drawAll();
 
-// Show launcher when middle button pressed
-setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
+// Show launcher when button pressed
+Bangle.setUI("clock");
diff --git a/apps/gallifr/screenshot_time.png b/apps/gallifr/screenshot_time.png
new file mode 100644
index 000000000..2754138c4
Binary files /dev/null and b/apps/gallifr/screenshot_time.png differ
diff --git a/apps/gbdebug/ChangeLog b/apps/gbdebug/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/gbdebug/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/gbdebug/README.md b/apps/gbdebug/README.md
new file mode 100644
index 000000000..47b1525b8
--- /dev/null
+++ b/apps/gbdebug/README.md
@@ -0,0 +1,26 @@
+# Gadgetbridge Debug
+
+This is useful if your Bangle isn't responding to the Gadgetbridge
+Android app properly.
+
+This app disables all existing Gadgetbridge handlers and then displays the
+messages that come from Gadgetbridge on the screen
+of the watch. It also saves the last 10 messages in a variable
+called `history`.
+
+More info on Gadgetbridge at http://www.espruino.com/Gadgetbridge
+
+## Usage
+
+* Run the `GB Debug` app on your Bangle
+* Connect your Bangle to Gadgetbridge
+* Do whatever was causing you problems (eg receiving a call)
+* The Gadgetbridge message should now be displayed on-screen
+
+If you want to get the *actual* data rather than copying it from the screen.
+
+* Ensure the `GB Debug` app is kept running after the above steps
+* Disconnect Gadgetbridge from the Bangle
+* Connect the Web IDE on your PC
+* Type `show()` on the left-hand side of the IDE and the
+last 10 messages from Gadgetbridge will be shown.
diff --git a/apps/gbdebug/app-icon.js b/apps/gbdebug/app-icon.js
new file mode 100644
index 000000000..a701ef3a9
--- /dev/null
+++ b/apps/gbdebug/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA="))
diff --git a/apps/gbdebug/app.js b/apps/gbdebug/app.js
new file mode 100644
index 000000000..ee5e46999
--- /dev/null
+++ b/apps/gbdebug/app.js
@@ -0,0 +1,21 @@
+E.showMessage("Waiting for message");
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+var history = [];
+
+GB = function(e) {
+  if (history.length > 10)
+    history = history.slice(history.length-10);
+  history.push(e);
+
+  var s = JSON.stringify(e,null,2);
+
+  g.reset().clear(Bangle.appRect);
+  g.setFont("6x8").setFontAlign(-1,0);
+  g.drawString(s, 10, g.getHeight()/2);
+};
+
+function show() {
+  print(JSON.stringify(history,null,2));
+}
diff --git a/apps/gbdebug/app.png b/apps/gbdebug/app.png
new file mode 100644
index 000000000..f70bce7ad
Binary files /dev/null and b/apps/gbdebug/app.png differ
diff --git a/apps/gbmusic/ChangeLog b/apps/gbmusic/ChangeLog
index ecbca5fb6..316b98a84 100644
--- a/apps/gbmusic/ChangeLog
+++ b/apps/gbmusic/ChangeLog
@@ -2,4 +2,7 @@
 0.02: Increase text brightness, improve controls, (try to) reduce memory usage
 0.03: Only auto-start if active app is a clock, auto close after 1 hour of inactivity
 0.04: Setting to disable touch controls, minor bugfix
-0.05: Setting to disable double/triple press control, remove touch controls setting, reduce fadeout flicker
\ No newline at end of file
+0.05: Setting to disable double/triple press control, remove touch controls setting, reduce fadeout flicker
+0.06: Bangle.js 2 support
+0.07: Fix "previous" button image
+0.08: Fix scrolling title background color
diff --git a/apps/gbmusic/README.md b/apps/gbmusic/README.md
index 4bad9b8c8..5d06164c2 100644
--- a/apps/gbmusic/README.md
+++ b/apps/gbmusic/README.md
@@ -3,7 +3,9 @@
 If you have an Android phone with Gadgetbridge, this app allows you to view
 and control music playback.
 
-![Screenshot: playing](screenshot.png) ![Screenshot: paused](screenshot_2.png)
+| Bangle.js 1                                | Bangle.js 2                                |
+|:-------------------------------------------|:-------------------------------------------|
+| ![Screenshot: Bangle 1](screenshot_v1.png) | ![Screenshot: Bangle 2](screenshot_v2.png) |
 
 Download the [latest Gadgetbridge for Android here](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/).
 
@@ -23,25 +25,27 @@ Automatically load the app when you play music and close when the music stops.
 (If the app opened automatically, it closes after music has been paused for 5 minutes.)
 
 **Simple button**:
-Disable double/triple pressing Button 2: always simply toggle play/pause.
+Disable double/triple pressing Middle Button: always simply toggle play/pause.
 (For music players which handle multiple button presses themselves.)
 
 ## Controls
 
 ### Buttons
-* Button 1: Volume up
-* Button 2: 
-  - Single press: toggle play/pause 
-  - Double press: next song
-  - Triple press: previous song
+* Button 1 (*Bangle.js 1*): Volume up
+* Middle Button: 
+  - Single press: Toggle play/pause 
+  - Double press: Next song
+  - Triple press: Previous song
   - Long-press: open application launcher 
-* Button 3: Volume down
+* Button 3 (*Bangle.js 1*): Volume down
 
 ### Touch
-* Left: pause/previous song
-* Right: next song/resume
-* Center: toggle play/pause
-* Swipe: next/previous song
+* Left: Pause/previous song
+* Right: Next song/resume
+* Center: Toggle play/pause
+* Swipe left/right: Next/previous song
+* Swipe up/down (*Bangle.js 2*): Volume up/down
+
 
 ## Creator
 
diff --git a/apps/gbmusic/app.js b/apps/gbmusic/app.js
index 5f95868bb..1bddf70f7 100644
--- a/apps/gbmusic/app.js
+++ b/apps/gbmusic/app.js
@@ -4,77 +4,9 @@
  **/
 let auto = false; // auto close if opened automatically
 let stat = "";
-let info = {
-  artist: "",
-  album: "",
-  track: "",
-  n: 0,
-  c: 0,
-};
 const POUT = 300000; // auto close timeout when paused: 5 minutes (in ms)
 const IOUT = 3600000; // auto close timeout for inactivity: 1 hour (in ms)
-
-///////////////////////
-// Self-repeating timeouts
-///////////////////////
-
-// Clock
-let tock = -1;
-function tick() {
-  if (!Bangle.isLCDOn()) {
-    return;
-  }
-  const now = new Date;
-  if (now.getHours()*60+now.getMinutes()!==tock) {
-    drawDateTime();
-    tock = now.getHours()*60+now.getMinutes();
-  }
-  setTimeout(tick, 1000); // we only show minute precision anyway
-}
-
-// Fade out while paused and auto closing
-let fade = null;
-function fadeOut() {
-  if (!Bangle.isLCDOn() || !fade) {
-    return;
-  }
-  drawMusic(false); // don't clear: draw over existing text to prevent flicker
-  setTimeout(fadeOut, 500);
-}
-function brightness() {
-  if (!fade) {
-    return 1;
-  }
-  return Math.max(0, 1-((Date.now()-fade)/POUT));
-}
-
-// Scroll long track names
-// use an interval to get smooth movement
-let offset = null, // scroll Offset: null = no scrolling
-  iScroll;
-function scroll() {
-  offset += 10;
-  drawScroller();
-}
-function scrollStart() {
-  if (offset!==null) {
-    return; // already started
-  }
-  offset = 0;
-  if (Bangle.isLCDOn()) {
-    if (!iScroll) {
-      iScroll = setInterval(scroll, 200);
-    }
-    drawScroller();
-  }
-}
-function scrollStop() {
-  if (iScroll) {
-    clearInterval(iScroll);
-    iScroll = null;
-  }
-  offset = null;
-}
+const BANGLE2 = process.env.HWVERSION===2;
 
 /**
  * @param {string} text
@@ -85,21 +17,22 @@ function fitText(text) {
     return Infinity;
   }
   // make a guess, then shrink/grow until it fits
-  const test = (s) => g.setFont("Vector", s).stringWidth(text);
-  let best = Math.floor(24000/test(100));
-  if (test(best)===240) { // good guess!
+  const w = Bangle.appRect.w,
+    test = (s) => g.setFont("Vector", s).stringWidth(text);
+  let best = Math.floor(100*w/test(100));
+  if (test(best)===w) { // good guess!
     return best;
   }
-  if (test(best)<240) {
+  if (test(best) 240
+  // width > w
   do {
     best--;
-  } while(test(best)>240);
+  } while(test(best)>w);
   return best;
 }
 
@@ -115,14 +48,6 @@ function textCode(text) {
   }
   return code%360;
 }
-// dark magic
-function hsv2rgb(h, s, v) {
-  const f = (n) => {
-    const k = (n+h/60)%6;
-    return v-v*s*Math.max(Math.min(k, 4-k, 1), 0);
-  };
-  return {r: f(5), g: f(3), b: f(1)};
-}
 function f2hex(f) {
   return ("00"+(Math.round(f*255)).toString(16)).substr(-2);
 }
@@ -131,38 +56,218 @@ function f2hex(f) {
  * @return {string} Semi-random color to use for given info
  */
 function infoColor(name) {
-  let h, s, v;
-  if (name==="num") {
-    // always white
-    h = 0;
-    s = 0;
-  } else {
-    // make color depend deterministically on info
-    let code = textCode(info[name]);
-    switch(name) {
-      case "track": // also use album
-        code += textCode(info.album);
-      // fallthrough
-      case "album": // also use artist
-        code += textCode(info.artist);
-    }
-    h = code%360;
-    s = 0.7;
+  // make color depend deterministically on info
+  let code = textCode(layout[name].label);
+  switch(name) {
+    case "title": // also use album and artist
+      code += textCode(layout.album.label);
+    // fallthrough
+    case "album": // also use artist
+      code += textCode(layout.artist.label);
   }
-  v = brightness();
-  const rgb = hsv2rgb(h, s, v);
-  return "#"+f2hex(rgb.r)+f2hex(rgb.g)+f2hex(rgb.b);
+  let rgb;
+  if (g.getBPP()===3) {
+    // only pick 3-bit colors, always at full brightness
+    rgb = [code&1, (code&2)/2, (code&4)/4];
+    if (g.setColor(rgb[0], rgb[1], rgb[2]).getColor()===g.theme.bg) {
+      // avoid picking the bg color
+      rgb = rgb.map(c => 1-c);
+    }
+    return "#"+f2hex(rgb[0])+f2hex(rgb[1])+f2hex(rgb[2]);
+  } else {
+    // pick any hue, adjust for brightness
+    const h = code%360, s = 0.7, b = brightness();
+    return E.HSBtoRGB(h/360, s, b);
+  }
+}
+
+/**
+ * Render scrolling title
+ * @param l
+ */
+function rScroller(l) {
+  g.setFont("Vector", Math.round(g.getHeight()*l.fsz.slice(0, -1)/100));
+  const w = g.stringWidth(l.label)+40,
+    y = l.y+l.h/2;
+  l.offset = l.offset%w;
+  g.setClipRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1)
+    .setColor(l.col).setBgColor(l.bgCol) // need to set colors: iScroll calls this function outside Layout
+    .setFontAlign(-1, 0) // left center
+    .clearRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1)
+    .drawString(l.label, l.x-l.offset+40, y)
+    .drawString(l.label, l.x-l.offset+40+w, y);
 }
 /**
- * Remember track color until info changes
- * Because we need this every time we move the scroller
- * @return {string}
+ * Render title
+ * @param l
  */
-function trackColor() {
-  if (!("track_color" in info) || fade) {
-    info.track_color = infoColor("track");
+function rTitle(l) {
+  if (l.offset!==null) {
+    rScroller(l); // already scrolling
+    return;
   }
-  return info.track_color;
+  let size = fitText(l.label);
+  if (sizel.h) {
+    size = l.h;
+  }
+  g.setFont("Vector", size)
+    .setFontAlign(0, -1) // center top
+    .drawString(l.label, l.x+l.w/2, l.y);
+}
+/**
+ * Render icon
+ * @param l
+ */
+function rIcon(l) {
+  const x2 = l.x+l.w-1,
+    y2 = l.y+l.h-1;
+  switch(l.icon) {
+    case "pause":
+      const w13 = l.w/3;
+      g.drawRect(l.x, l.y, l.x+w13, y2);
+      g.drawRect(l.x+l.w-w13, l.y, x2, y2);
+      break;
+    case "play":
+      g.drawPoly([
+        l.x, l.y,
+        x2, l.y+l.h/2,
+        l.x, y2,
+      ], true);
+      break;
+    case "previous":
+      const w15 = l.w*1/5;
+      g.drawPoly([
+        x2, l.y,
+        l.x+w15, l.y+l.h/2,
+        x2, y2,
+      ], true);
+      g.drawRect(l.x, l.y, l.x+w15, y2);
+      break;
+    case "next":
+      const w45 = l.w*4/5;
+      g.drawPoly([
+        l.x, l.y,
+        l.x+w45, l.y+l.h/2,
+        l.x, y2,
+      ], true);
+      g.drawRect(l.x+w45, l.y, x2, y2);
+      break;
+    default: // red X
+      console.log(`Unknown icon: ${l.icon}`);
+      g.setColor("#f00")
+        .drawRect(l.x, l.y, x2, y2)
+        .drawLine(l.x, l.y, x2, y2)
+        .drawLine(l.x, y2, x2, l.y);
+  }
+}
+let layout;
+function makeUI() {
+  global.gbmusic_active = true; // we don't need our widget (needed for <2.09 devices)
+  Bangle.loadWidgets();
+  Bangle.drawWidgets();
+  delete (global.gbmusic_active);
+  const Layout = require("Layout");
+  layout = new Layout({
+    type: "v", c: [
+      {
+        type: "h", fillx: 1, c: [
+          {id: "time", type: "txt", label: "88:88", valign: -1, halign: -1, font: "8%", bgCol: g.theme.bg},
+          {fillx: 1},
+          {id: "num", type: "txt", label: "88:88", valign: -1, halign: 1, font: "12%", bgCol: g.theme.bg},
+          BANGLE2 ? {} : {id: "up", type: "txt", label: " +", font: "6x8:2"},
+        ],
+      },
+      {id: "title", type: "custom", label: "", fillx: 1, filly: 2, offset: null, font: "Vector:20%", render: rTitle, bgCol: g.theme.bg},
+      {id: "artist", type: "custom", label: "", fillx: 1, filly: 1, size: 30, render: rInfo, bgCol: g.theme.bg},
+      {id: "album", type: "custom", label: "", fillx: 1, filly: 1, size: 20, render: rInfo, bgCol: g.theme.bg},
+      {height: 10},
+      {
+        type: "h", c: [
+          {width: 3},
+          {id: "prev", type: "custom", height: 15, width: 15, icon: "previous", render: rIcon, bgCol: g.theme.bg},
+          {id: "date", type: "txt", halign: 0, valign: 1, label: "", font: "8%", fillx: 1, bgCol: g.theme.bg},
+          {id: "next", type: "custom", height: 15, width: 15, icon: "next", render: rIcon, bgCol: g.theme.bg},
+          BANGLE2 ? {width: 3} : {id: "down", type: "txt", label: " -", font: "6x8:2"},
+        ],
+      },
+      {height: 10},
+    ],
+  }, {lazy: true});
+  layout.render();
+}
+
+///////////////////////
+// Self-repeating timeouts
+///////////////////////
+
+// Clock
+let tock = -1;
+function tick() {
+  if (!BANGLE2 && !Bangle.isLCDOn()) {
+    return;
+  }
+  const now = new Date();
+  if (now.getHours()*60+now.getMinutes()!==tock) {
+    drawDateTime();
+    tock = now.getHours()*60+now.getMinutes();
+  }
+  setTimeout(tick, 1000); // we only show minute precision anyway
+}
+
+// Fade out while paused and auto closing
+let fade = null;
+function fadeOut() {
+  if (BANGLE2 || !Bangle.isLCDOn() || !fade) {
+    return;
+  }
+  layout.render();
+  setTimeout(fadeOut, 500);
+}
+function brightness() {
+  if (!fade) {
+    return 1;
+  }
+  return Math.max(0, 1-((Date.now()-fade)/POUT));
+}
+
+// Scroll long track names
+// use an interval to get smooth movement
+let iScroll;
+function scroll() {
+  layout.title.offset += 10;
+  rScroller(layout.title);
+}
+function scrollStart() {
+  if (layout.title.offset!==null) {
+    return; // already started
+  }
+  layout.title.offset = 0;
+  if (BANGLE2 || Bangle.isLCDOn()) {
+    if (!iScroll) {
+      iScroll = setInterval(scroll, 200);
+    }
+    rScroller(layout.title);
+  }
+}
+function scrollStop() {
+  if (iScroll) {
+    clearInterval(iScroll);
+    iScroll = null;
+  }
+  layout.title.offset = null;
 }
 
 ////////////////////
@@ -172,10 +277,9 @@ function trackColor() {
  * Draw date and time
  */
 function drawDateTime() {
-  const now = new Date;
+  const now = new Date();
   const l = require("locale");
   const is12 = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
-  let time;
   if (is12) {
     const d12 = new Date(now.getTime());
     const hour = d12.getHours();
@@ -184,29 +288,35 @@ function drawDateTime() {
     } else if (hour>12) {
       d12.setHours(hour-12);
     }
-    time = l.time(d12, true)+l.meridian(now);
+    layout.time.label = l.time(d12, true)+l.meridian(now);
   } else {
-    time = l.time(now, true);
+    layout.time.label = l.time(now, true);
   }
-  g.reset();
-  g.setFont("Vector", 24)
-    .setFontAlign(-1, -1) // top left
-    .clearRect(10, 30, 119, 54)
-    .drawString(time, 10, 30);
-
-  const date = require("locale").date(now, true);
-  g.setFont("Vector", 16)
-    .setFontAlign(0, 1) // bottom center
-    .setClipRect(35, 198, 199, 214)
-    .clearRect(31, 198, 199, 214)
-    .drawString(date, 119, 240-26);
+  layout.date.label = require("locale").date(now, true);
+  layout.render();
 }
 
+function drawControls() {
+  let l = layout;
+  const cc = a => (a ? "#f00" : "#0f0"); // control color: red for active, green for inactive
+  if (!BANGLE2) {
+    l.up.col = cc("volumeup" in tCommand);
+    l.down.col = cc("volumedown" in tCommand);
+  }
+  l.prev.icon = (stat==="play") ? "pause" : "previous";
+  l.prev.col = cc("prev" in tCommand || "pause" in tCommand);
+  l.next.icon = (stat==="play") ? "next" : "play";
+  l.next.col = cc("next" in tCommand || "play" in tCommand);
+  layout.render();
+}
+
+////////////////////////
+// GB event handlers
+///////////////////////
 /**
- * Draw track number and total count
- * @param {boolean} clr - Clear area before redrawing?
+ * Mangle track number and total count for display
  */
-function drawNum(clr) {
+function formatNum(info) {
   let num = "";
   if ("n" in info && info.n>0) {
     num = "#"+info.n;
@@ -214,198 +324,26 @@ function drawNum(clr) {
       num += "/"+info.c;
     }
   }
-  g.reset();
-  g.setFont("Vector", 30)
-    .setFontAlign(1, -1); // top right
-  if (clr) {
-    g.clearRect(225, 30, 120, 60);
-  }
-  g.drawString(num, 225, 30);
-}
-/**
- * Clear rectangle used by track title
- */
-function clearTrack() {
-  g.clearRect(0, 60, 239, 119);
-}
-/**
- * Draw track title
- * @param {boolean} clr - Clear area before redrawing?
- */
-function drawTrack(clr) {
-  let size = fitText(info.track);
-  if (size<25) {
-    // the title is too long: start the scroller
-    scrollStart();
-    return;
-  } else {
-    scrollStop();
-  }
-  // stationary track
-  if (size>40) {
-    size = 40;
-  }
-  g.reset();
-  g.setFont("Vector", size)
-    .setFontAlign(0, 1) // center bottom
-    .setColor(trackColor());
-  if (clr) {
-    clearTrack();
-  }
-  g.drawString(info.track, 119, 109);
-}
-/**
- * Draw scrolling track title
- */
-function drawScroller() {
-  g.reset();
-  g.setFont("Vector", 40);
-  const w = g.stringWidth(info.track)+40;
-  offset = offset%w;
-  g.setFontAlign(-1, 1) // left bottom
-    .setColor(trackColor());
-  clearTrack();
-  g.drawString(info.track, -offset+40, 109)
-    .drawString(info.track, -offset+40+w, 109);
+  return num;
 }
 
-/**
- * Draw track artist and album
- * @param {boolean} clr - Clear area before redrawing?
- */
-function drawArtistAlbum(clr) {
-  // we just use small enough fonts to make these always fit
-  // calculate stuff before clear+redraw
-  const aCol = infoColor("artist");
-  const bCol = infoColor("album");
-  let aSiz = fitText(info.artist);
-  if (aSiz>30) {
-    aSiz = 30;
-  }
-  let bSiz = fitText(info.album);
-  if (bSiz>20) {
-    bSiz = 20;
-  }
-  g.reset();
-  if (clr) {
-    g.clearRect(0, 120, 240, 189);
-  }
-  let top = 124;
-  if (info.artist) {
-    g.setFont("Vector", aSiz)
-      .setFontAlign(0, -1) // center top
-      .setColor(aCol)
-      .drawString(info.artist, 119, top);
-    top += aSiz+4; // fit album neatly under artist
-  }
-  if (info.album) {
-    g.setFont("Vector", bSiz)
-      .setFontAlign(0, -1) // center top
-      .setColor(bCol)
-      .drawString(info.album, 119, top);
-  }
-}
-
-/**
- *
- * @param {string} icon Icon name
- * @param {number} x
- * @param {number} y
- * @param {number} s Icon size
- */
-function drawIcon(icon, x, y, s) {
-  ({
-    pause: function(x, y, s) {
-      const w1 = s/3;
-      g.drawRect(x, y, x+w1, y+s);
-      g.drawRect(x+s-w1, y, x+s, y+s);
-    },
-    play: function(x, y, s) {
-      g.drawPoly([
-        x, y,
-        x+s, y+s/2,
-        x, y+s,
-      ], true);
-    },
-    previous: function(x, y, s) {
-      const w2 = s*1/5;
-      g.drawPoly([
-        x+s, y,
-        x+w2, y+s/2,
-        x+s, y+s,
-      ], true);
-      g.drawRect(x, y, x+w2, y+s);
-    },
-    next: function(x, y, s) {
-      const w2 = s*4/5;
-      g.drawPoly([
-        x, y,
-        x+w2, y+s/2,
-        x, y+s,
-      ], true);
-      g.drawRect(x+w2, y, x+s, y+s);
-    },
-  })[icon](x, y, s);
-}
-function controlColor(ctrl) {
-  return (ctrl in tCommand) ? "#ff0000" : "#008800";
-}
-function drawControl(ctrl, x, y) {
-  g.setColor(controlColor(ctrl));
-  const s = 20;
-  if (stat!==controlState) {
-    g.clearRect(x, y, x+s, y+s);
-  }
-  drawIcon(ctrl, x, y, s);
-}
-let controlState;
-function drawControls() {
-  g.reset();
-  if (stat==="play") {
-    // left touch
-    drawControl("pause", 10, 190);
-    // right touch
-    drawControl("next", 200, 190);
-  } else {
-    drawControl("previous", 10, 190);
-    drawControl("play", 200, 190);
-  }
-  g.setFont("6x8", 2);
-  // BTN1
-  g.setFontAlign(1, -1);
-  g.setColor(controlColor("volumeup"));
-  g.drawString("+", 240, 30);
-  // BTN2
-  g.setFontAlign(1, 1);
-  g.setColor(controlColor("volumedown"));
-  g.drawString("-", 240, 210);
-  controlState = stat;
-}
-
-/**
- * @param {boolean} [clr=true] Clear area before redrawing?
- */
-function drawMusic(clr) {
-  clr = !(clr===false); // undefined means yes
-  drawNum(clr);
-  drawTrack(clr);
-  drawArtistAlbum(clr);
-}
-
-////////////////////////
-// GB event handlers
-///////////////////////
 /**
  * Update music info
- * @param {Object} e - Gadgetbridge musicinfo event
+ * @param {Object} info - Gadgetbridge musicinfo event
  */
-function musicInfo(e) {
-  info = e;
-  delete (info.t);
-  offset = null;
-  if (Bangle.isLCDOn()) {
-    drawMusic();
-  }
+function musicInfo(info) {
+  scrollStop();
+  layout.title.label = info.track || "";
+  layout.album.label = info.album || "";
+  layout.artist.label = info.artist || "";
+  // color depends on all labels
+  layout.title.col = infoColor("title");
+  layout.album.col = infoColor("album");
+  layout.artist.col = infoColor("artist");
+  layout.num.label = formatNum(info);
+  layout.render();
+  rTitle(layout.title); // force redraw of title, or scroller might break
+  // reset auto exit interval
   if (tIxt) {
     clearTimeout(tIxt);
     tIxt = null;
@@ -435,7 +373,6 @@ function musicState(e) {
     tIxt = null;
   }
   fade = null;
-  delete info.track_color;
   if (auto) { // auto opened -> auto close
     switch(stat) {
       case "stop": // never actually happens with my phone :-(
@@ -444,7 +381,7 @@ function musicState(e) {
       case "play":
         // if inactive for double song duration (or an hour if unknown), load the clock
         // i.e. phone finished playing without bothering to notify the watch
-        tIxt = setTimeout(load, (info.dur*2000) || IOUT);
+        tIxt = setTimeout(load, (e.dur*2000) || IOUT);
         break;
       case "pause":
       default:
@@ -456,8 +393,7 @@ function musicState(e) {
         break;
     }
   }
-  if (Bangle.isLCDOn()) {
-    drawMusic(false); // redraw in case we were fading out but resumed play
+  if (BANGLE2 || Bangle.isLCDOn()) {
     drawControls();
   }
 }
@@ -473,30 +409,34 @@ function musicState(e) {
  */
 let tPress, nPress = 0;
 function startButtonWatches() {
-  // BTN1/3: volume control
-  // Wait for falling edge to avoid messing with volume while long-pressing BTN3
-  // to reload the watch (and same for BTN2 for consistency)
-  setWatch(() => { sendCommand("volumeup"); }, BTN1, {repeat: true, edge: "falling"});
-  setWatch(() => { sendCommand("volumedown"); }, BTN3, {repeat: true, edge: "falling"});
+  let btn = BTN1;
+  if (!BANGLE2) {
+    // BTN1/3: volume control
+    // Wait for falling edge to avoid messing with volume while long-pressing BTN3
+    // to reload the watch (and same for BTN2 for consistency)
+    setWatch(() => { sendCommand("volumeup"); }, BTN1, {repeat: true, edge: "falling"});
+    setWatch(() => { sendCommand("volumedown"); }, BTN3, {repeat: true, edge: "falling"});
+    btn = BTN2;
+  }
 
-  // BTN2: long-press for launcher, otherwise depends on number of presses
+  // middle button: long-press for launcher, otherwise depends on number of presses
   setWatch(() => {
     if (nPress===0) {
       tPress = setTimeout(() => {Bangle.showLauncher();}, 3000);
     }
-  }, BTN2, {repeat: true, edge: "rising"});
+  }, btn, {repeat: true, edge: "rising"});
   const s = require("Storage").readJSON("gbmusic.json", 1) || {};
   if (s.simpleButton) {
     setWatch(() => {
       clearTimeout(tPress);
       togglePlay();
-    }, BTN2, {repeat: true, edge: "falling"});
+    }, btn, {repeat: true, edge: "falling"});
   } else {
     setWatch(() => {
       nPress++;
       clearTimeout(tPress);
       tPress = setTimeout(handleButton2Press, 500);
-    }, BTN2, {repeat: true, edge: "falling"});
+    }, btn, {repeat: true, edge: "falling"});
   }
 }
 function handleButton2Press() {
@@ -524,7 +464,7 @@ let tCommand = {};
  */
 function sendCommand(command) {
   Bluetooth.println(JSON.stringify({t: "music", n: command}));
-  // for controlColor
+  // for control color
   if (command in tCommand) {
     clearTimeout(tCommand[command]);
   }
@@ -539,18 +479,29 @@ function sendCommand(command) {
 function togglePlay() {
   sendCommand(stat==="play" ? "pause" : "play");
 }
-function startTouchWatches() {
+function pausePrev() {
+  sendCommand(stat==="play" ? "pause" : "previous");
+}
+function nextPlay() {
+  sendCommand(stat==="play" ? "next" : "play");
+}
+
+/**
+ * Setup touch+swipe for Bangle.js 1
+ */
+function touch1() {
   Bangle.on("touch", side => {
     if (!Bangle.isLCDOn()) {return;} // for <2v10 firmware
     switch(side) {
       case 1:
-        sendCommand(stat==="play" ? "pause" : "previous");
+        pausePrev();
         break;
       case 2:
-        sendCommand(stat==="play" ? "next" : "play");
+        nextPlay();
         break;
-      case 3:
+      default:
         togglePlay();
+        break;
     }
   });
   Bangle.on("swipe", dir => {
@@ -558,16 +509,56 @@ function startTouchWatches() {
     sendCommand(dir===1 ? "previous" : "next");
   });
 }
+/**
+ * Setup touch+swipe for Bangle.js 2
+ */
+function touch2() {
+  Bangle.on("touch", (side, xy) => {
+    const ar = Bangle.appRect;
+    if (xy.xar.x+ar.w*2/3) {
+      nextPlay();
+    } else {
+      togglePlay();
+    }
+  });
+  // swiping
+  let drag;
+  Bangle.on("drag", e => {
+    if (!drag) { // start dragging
+      drag = {x: e.x, y: e.y};
+    } else if (!e.b) { // released
+      const dx = e.x-drag.x, dy = e.y-drag.y;
+      drag = null;
+      if (Math.abs(dx)>Math.abs(dy)+10) {
+        // horizontal
+        sendCommand(dx>0 ? "previous" : "next");
+      } else if (Math.abs(dy)>Math.abs(dx)+10) {
+        // vertical
+        sendCommand(dy>0 ? "volumedown" : "volumeup");
+      }
+    }
+  });
+}
+function startTouchWatches() {
+  if (BANGLE2) {
+    touch2();
+  } else {
+    touch1();
+  }
+}
 function startLCDWatch() {
+  if (BANGLE2) {
+    return; // always keep drawing
+  }
   Bangle.on("lcdPower", (on) => {
     if (on) {
       // redraw and resume scrolling
       tick();
-      drawMusic();
-      drawControls();
+      layout.render();
       fadeOut();
-      if (offset!==null) {
-        drawScroller();
+      if (offset.offset!==null) {
         if (!iScroll) {
           iScroll = setInterval(scroll, 200);
         }
@@ -585,15 +576,10 @@ function startLCDWatch() {
 /////////////////////
 // Startup
 /////////////////////
-// check for saved music stat (by widget) to load
 g.clear();
-global.gbmusic_active = true; // we don't need our widget (needed for <2.09 devices)
-Bangle.loadWidgets();
-Bangle.drawWidgets();
-delete (global.gbmusic_active);
 
 function startEmulator() {
-  if (typeof Bluetooth==="undefined") { // emulator!
+  if (typeof Bluetooth==="undefined" || typeof Bluetooth.println==="undefined") { // emulator!
     Bluetooth = {
       println: (line) => {console.log("Bluetooth:", line);},
     };
@@ -609,6 +595,7 @@ function startWatches() {
 }
 
 function start() {
+  makeUI();
   // start listening for music updates
   const _GB = global.GB;
   global.GB = (event) => {
@@ -628,43 +615,39 @@ function start() {
         return;
     }
   };
-  drawMusic();
-  drawControls();
   startWatches();
   tick();
   startEmulator();
 }
 
 function init() {
+  // check for saved music status (by widget) to load
   let saved = require("Storage").readJSON("gbmusic.load.json", true);
   require("Storage").erase("gbmusic.load.json");
   if (saved) {
     // autoloaded: load state was saved by widget
-    info = saved.info;
-    stat = saved.state;
-    delete saved;
     auto = true;
     start();
-  } else {
-    delete saved;
-    let s = require("Storage").readJSON("gbmusic.json", 1) || {};
-    if (!("autoStart" in s)) {
-      // user opened the app, but has not picked a setting yet
-      // ask them about autoloading now
-      E.showPrompt(
-        "Automatically load\n"+
-        "when playing music?\n",
-      ).then(choice => {
-        s.autoStart = choice;
-        require("Storage").writeJSON("gbmusic.json", s);
-        delete s;
-        setTimeout(start, 0);
-      });
-    } else {
-      delete s;
-      start();
-    }
+    musicInfo(saved.info);
+    musicState(saved.state);
+    return;
   }
-}
-init();
 
+  let s = require("Storage").readJSON("gbmusic.json", 1) || {};
+  if ("autoStart" in s) {
+    start();
+    return;
+  }
+
+  // user opened the app, but has not picked a autoStart setting yet
+  // ask them about autoloading now
+  E.showPrompt(
+    "Automatically load\n"+
+    "when playing music?\n"
+  ).then(choice => {
+    s.autoStart = choice;
+    require("Storage").writeJSON("gbmusic.json", s);
+    setTimeout(start, 0);
+  });
+}
+init();
\ No newline at end of file
diff --git a/apps/gbmusic/screenshot.png b/apps/gbmusic/screenshot.png
deleted file mode 100644
index 569a6a2c5..000000000
Binary files a/apps/gbmusic/screenshot.png and /dev/null differ
diff --git a/apps/gbmusic/screenshot_2.png b/apps/gbmusic/screenshot_2.png
deleted file mode 100644
index f19f8f428..000000000
Binary files a/apps/gbmusic/screenshot_2.png and /dev/null differ
diff --git a/apps/gbmusic/screenshot_v1.png b/apps/gbmusic/screenshot_v1.png
new file mode 100644
index 000000000..3b290e459
Binary files /dev/null and b/apps/gbmusic/screenshot_v1.png differ
diff --git a/apps/gbmusic/screenshot_v2.png b/apps/gbmusic/screenshot_v2.png
new file mode 100644
index 000000000..b89b5022e
Binary files /dev/null and b/apps/gbmusic/screenshot_v2.png differ
diff --git a/apps/gbridge/ChangeLog b/apps/gbridge/ChangeLog
index f4837d60a..67d421f33 100644
--- a/apps/gbridge/ChangeLog
+++ b/apps/gbridge/ChangeLog
@@ -22,3 +22,7 @@
 0.20: Reduce memory usage
 0.21: Fix HRM setting
 0.22: Respect Quiet Mode
+0.23: Allow notification dismiss to remove from phone too
+0.24: tag HRM power requests to allow this to work alongside other widgets/apps (fix #799)
+0.25: workaround call notification
+	  Fix inflated step number
diff --git a/apps/gbridge/README.md b/apps/gbridge/README.md
index 867b736ab..03bf883d7 100644
--- a/apps/gbridge/README.md
+++ b/apps/gbridge/README.md
@@ -52,3 +52,62 @@ Activity reporting
 You'll need a Gadgetbridge release *after* version 0.50.0 for Actvity Reporting to be enabled.
 
 By default heart rate isn't reported, but it can be enabled from `Settings`, `App/Widget Settings`, `Gadgetbridge`, `Record HRM`
+
+
+## Troubleshooting
+
+1. Switch to using one of the stock watch faces like s7clk or wave on a Bangle 2.
+
+2. Check that the battery charge level is being seen by the Gadgetbridge App on the phone.  This proves that data is getting to your phone.
+
+### You can test the notifications on the Bangle
+
+First disconnect from Gadgetbridge on your phone. Then connect
+through the IDE and enter the following code.  You should get a pop
+up screen on your Bangle.  This proves that the watch is correctly
+setup for notifications.
+
+
+    GB({"t":"notify","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"})
+
+
+NOTE: On a Bangle 2, this will fail if you have not installed 'Notifications Fullscreen'.
+
+### Check that notifications are getting through to your Bangle
+
+* Disconnect your Bangle from Gadgetbridge on your phone.
+* Connect through the IDE
+* Run the following bit of code
+
+        var log = [];
+        function GB(d) {
+          log.push(JSON.stringify(d));
+        }
+
+* Disconnect from the IDE
+* Connect your Bangle to Gadgetbridge
+* Call your phone to get a missed call
+* Disonnect your Bangle to Gadgetbridge
+* Connect through the IDE
+* Run the following bit of code
+
+        log;
+
+If notifications are getting through then you should see something like.
+
+
+        >log
+        =[
+          "{\"t\":\"call\",\"cmd\"" ... "r\":\"0191xxxxxxx\"}",
+          "{\"t\":\"call\",\"cmd\"" ... "r\":\"0191xxxxxxx\"}"
+         ]
+
+
+IMPORTANT: Now reset your Bangle using a BTN3 long press so that the GB() function is restored.
+
+## References
+
+[Bangle Gadgetbridge Page](https://www.espruino.com/Gadgetbridge)
+
+[Gadgetbridge Project Home](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Home)
+
diff --git a/apps/gbridge/sample_messages.js b/apps/gbridge/sample_messages.js
index be33a25b8..046ffa9e4 100644
--- a/apps/gbridge/sample_messages.js
+++ b/apps/gbridge/sample_messages.js
@@ -15,7 +15,7 @@ GB({"t":"notify","id":1592721714,"src":"ALARMCLOCKRECEIVER"})
 GB({"t":"notify-","id":1592721714})
 
 // Weather update (doesn't show a notification, not handled by gbridge app: see weather app)
-GB({"t":"weather","temp":288,"hum":94,"txt":"Light rain","wind":0,"loc":"Test City"})
+GB({"t":"weather","temp":288,"hum":94,"txt":"Light rain","wind":0,"wdir":120,"loc":"Test City"})
 
 // Nextcloud updated a file
 GB({"t":"notify","id":1594184421,"src":"Nextcloud","title":"Downloaded","body":"test.file downloaded"})
diff --git a/apps/gbridge/settings.js b/apps/gbridge/settings.js
index afd0be4fb..f9c7cde90 100644
--- a/apps/gbridge/settings.js
+++ b/apps/gbridge/settings.js
@@ -23,6 +23,7 @@
   }
   var mainmenu = {
     "" : { "title" : "Gadgetbridge" },
+    "< Back" : back,
     "Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" },
     "Show Icon" : {
       value: settings().showIcon,
@@ -34,8 +35,7 @@
       value: !!settings().hrm,
       format: v => v?"Yes":"No",
       onchange: v => updateSetting('hrm', v)
-    },
-    "< Back" : back,
+    }    
   };
 
   var findPhone = {
diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js
index b4ce71907..7cb7147ec 100644
--- a/apps/gbridge/widget.js
+++ b/apps/gbridge/widget.js
@@ -1,4 +1,7 @@
 (() => {
+  // Current shown notification, saved for dismissing.
+  var currentNot = null;
+
   // Music handling
   const state = {
     music: "stop",
@@ -125,7 +128,7 @@
     if (activityInterval)
       clearInterval(activityInterval);
     activityInterval = undefined;
-    if (s.hrm) Bangle.setHRMPower(1);
+    if (s.hrm) Bangle.setHRMPower(1,"gbr");
     if (s.hrm) {
       if (realtime) {
         // if realtime reporting, leave HRM on and use that to trigger events
@@ -135,7 +138,7 @@
         hrmTimeout = 5;
         activityInterval = setInterval(function() {
           hrmTimeout = 5;
-          Bangle.setHRMPower(1);
+          Bangle.setHRMPower(1,"gbr");
         }, interval*1000);
       }
     } else {
@@ -151,16 +154,23 @@
   global.GB = (event) => {
     switch (event.t) {
       case "notify":
-      case "notify-":
-        if (event.t === "notify") {
-          require("notify").show(prettifyNotificationEvent(event));
-          if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) {
-            Bangle.buzz();
-          }
-        } else { // notify-
-          require("notify").hide(event);
+        currentNot = prettifyNotificationEvent(event);
+        currentNot.onHide = function() {
+          // when notification hidden, remove from phone
+          gbSend({ t:"notify", n:"DISMISS", id:currentNot.id });
+        };
+        require("notify").show(currentNot);
+        if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) {
+          Bangle.buzz();
         }
         break;
+      case "notify-":
+        currentNot.t = "notify";
+        currentNot.n = "DISMISS";
+        gbSend(currentNot);
+        currentNot = null;
+        require("notify").hide(event);
+        break;
       case "musicinfo":
         state.musicInfo = event;
         updateMusic({on: false});
@@ -174,7 +184,7 @@
       case "call":
         var note = { size: 55, title: event.name, id: "call",
                      body: event.number, icon:require("heatshrink").decompress(atob("jEYwIMJj4CCwACJh4CCCIMOAQMGAQMHAQMDAQMBCIMB4PwgHz/EAn4CBj4CBg4CBgACCAAw="))}
-        if (event.cmd === "incoming") {
+        if (event.cmd === "incoming" || event.cmd === "") {
           require("notify").show(note);
           if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) {
             Bangle.buzz();
@@ -252,7 +262,7 @@
   // Send a summary of activity to Gadgetbridge
   function sendActivity(hrm) {
     var steps = currentSteps - lastSentSteps;
-    lastSentSteps = 0;
+    lastSentSteps = currentSteps;
     gbSend({ t: "act", stp: steps, hrm:hrm });
   }
 
@@ -271,7 +281,7 @@
     if (hrmTimeout!==undefined) hrmTimeout--;
     if (ok || hrmTimeout<=0) {
       if (hrmTimeout!==undefined)
-        Bangle.setHRMPower(0);
+        Bangle.setHRMPower(0,"gbr");
       sendActivity(hrm.confidence>20 ? hrm.bpm : -1);
     }
   });
diff --git a/apps/gbtwist/ChangeLog b/apps/gbtwist/ChangeLog
new file mode 100644
index 000000000..ec66c5568
--- /dev/null
+++ b/apps/gbtwist/ChangeLog
@@ -0,0 +1 @@
+0.01: Initial version
diff --git a/apps/gbtwist/README.md b/apps/gbtwist/README.md
new file mode 100644
index 000000000..7e9dbcbe5
--- /dev/null
+++ b/apps/gbtwist/README.md
@@ -0,0 +1,15 @@
+# Gadgetbridge Twist Control
+
+Control your music app (e.g. MortPlayer Music [a folder based, not tag based player] ) that handles multiple play-commands (same as using a single-button-headset's button to change songs) on your Gadgetbridge-connected phone.
+- Activate counting for 4 seconds with a twist (beeps at start and end of counting) 
+- twist multiple times for:
+ play/pause (1),
+ next song (2),
+ prev. song (3),
+ next folder (4),
+ prev. folder (5),
+ reset counter (6) 
+- the command to be sent is shown in green
+- Volume up/down is controlled by BTN1/BTN3 presses
+
+![screenshot1](https://user-images.githubusercontent.com/84921310/119907374-65bb6180-bf50-11eb-9073-f29f7e333e00.jpg)
diff --git a/apps/gbtwist/app-icon.js b/apps/gbtwist/app-icon.js
new file mode 100644
index 000000000..b28bbe664
--- /dev/null
+++ b/apps/gbtwist/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwIYVhAFEjgFEh4FEg+AAocD4AME8ADCgPAvAFCj/8nkQAoN//8enAQB///44FBgYFB8f4FoIFB+IFBh/+n/4AocH/AXBj/+gP8FIIFDFwM//0x/wFDAIIFNv4FB/4FNEaIFFj/gn5HCj+AAoUEh4FBMgUP4AFDw/gv/wAoPDPoKhBjnxAoKtBjl4TYLICninBagUPWYLJPFoIADZIYABnj6KABIA="))
diff --git a/apps/gbtwist/app.js b/apps/gbtwist/app.js
new file mode 100644
index 000000000..4bd495277
--- /dev/null
+++ b/apps/gbtwist/app.js
@@ -0,0 +1,97 @@
+//   just a watch, to fill an empty screen
+
+function drwClock() {
+  var d = new Date();
+  var h = d.getHours(), m = d.getMinutes();
+  var time = ("0"+h).substr(-2) + ":" + ("0"+m).substr(-2);
+  g.reset();
+  g.setFont('6x8',7);
+  g.setFontAlign(-1,-1);
+  g.drawString(time,20,80);
+}
+
+g.clear();
+drwClock();
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+/////////////////////////////////////////////////////////////
+//   control music by twist/buttons
+
+var counter = 0; //stores your counted your twists
+var tstate = false; //are you ready to count the twists?
+
+function playx() {
+  Bluetooth.println(JSON.stringify({t:"music", n:"play"}));
+}
+
+function volup() {
+  Bluetooth.println(JSON.stringify({t:"music", n:"volumeup"}));
+}
+
+function voldn() {
+  Bluetooth.println(JSON.stringify({t:"music", n:"volumedown"}));
+}
+
+function sendCmd() {
+  print (counter);
+  Bangle.beep(200,3000);
+  if (tstate==false && counter>0){
+  do {playx(); counter--;}
+  while (counter >= 1);
+  }
+}
+
+function twistctrl() {
+  if (tstate==false){
+    tstate=true;
+    setTimeout('tstate=false',4000);
+    setTimeout(sendCmd,4100);
+    Bangle.beep(200,3000);
+  }
+  else{
+  g.clearRect(10,140,230,200);
+  if (tstate==true){
+  if (counter < 5){
+    counter++;
+    drwCmd();
+    Bangle.buzz(100,2);
+    }
+  else {
+    counter = 0;
+    Bangle.buzz(400);
+       }
+  }
+  }
+}
+
+function drwCmd() {
+  g.setFont('6x8',6);
+  g.setColor(0.3,1,0.3);
+  g.clearRect(10,140,230,200);
+switch (counter){
+  case 1:
+  g.drawString('play',50,150);
+  break;
+  case 2:
+  g.drawString('next',50,150);
+  break;
+  case 3:
+  g.drawString('prev',50,150);
+  break;
+  case 4:
+  g.drawString('nx f',50,150);
+  break;
+  case 5:
+  g.drawString('pr f',50,150);
+  break;
+  case 0:
+  g.clearRect(10,140,230,200);
+  break;
+}
+}
+
+setWatch(volup,BTN1,{repeat:true});
+setWatch(voldn,BTN3,{repeat:true});
+Bangle.on('twist',twistctrl);
+setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
\ No newline at end of file
diff --git a/apps/gbtwist/app.png b/apps/gbtwist/app.png
new file mode 100644
index 000000000..2379c76f0
Binary files /dev/null and b/apps/gbtwist/app.png differ
diff --git a/apps/geissclk/ChangeLog b/apps/geissclk/ChangeLog
index bd718a5b1..7458fadee 100644
--- a/apps/geissclk/ChangeLog
+++ b/apps/geissclk/ChangeLog
@@ -1,2 +1,3 @@
 0.01: New App!
 0.02: BTN2->launcher, use smaller text to allow "20:00" to fit on screen
+0.03: Changed setWatch to Bangle.setUI
diff --git a/apps/geissclk/clock.js b/apps/geissclk/clock.js
index 7d63b815e..f14ea5f39 100644
--- a/apps/geissclk/clock.js
+++ b/apps/geissclk/clock.js
@@ -148,4 +148,5 @@ Bangle.drawWidgets();
 iterate();
 animInterval = setInterval(iterate, 50);
 
-setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
+// Show launcher when button pressed
+Bangle.setUI("clock");
diff --git a/apps/getup/bangle1-get-up-screenshot.png b/apps/getup/bangle1-get-up-screenshot.png
new file mode 100644
index 000000000..3bd950280
Binary files /dev/null and b/apps/getup/bangle1-get-up-screenshot.png differ
diff --git a/apps/golfscore/ChangeLog b/apps/golfscore/ChangeLog
new file mode 100644
index 000000000..4995dd59a
--- /dev/null
+++ b/apps/golfscore/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: multiple player score support
\ No newline at end of file
diff --git a/apps/golfscore/README.md b/apps/golfscore/README.md
new file mode 100644
index 000000000..68552ad4b
--- /dev/null
+++ b/apps/golfscore/README.md
@@ -0,0 +1,37 @@
+# Golf Score
+
+Lets you keep track of strokes during a game of Golf.
+
+![](mainmenu.png)
+![](setupmenu.png)
+![](scorecard.png)
+![](holemenu.png)
+
+## Usage
+
+1. Open the app,
+1. scroll to setup
+2. set the number of holes (18 by default, but can be configured)
+3. set the number of players (4 by default, but can be 1-20)
+4. click back
+5. scroll to a hole (hole 1)
+6. scroll to a player and set the number of strokes they took (repeat as needed)
+7. click next hole and repeat #6 and #7 as needed; or click back
+8. at any time, check the score card for a sum total of all the strokes for each player
+
+## Features
+
+Track strokes for multiple players (1-20)
+Set number of holes on course
+
+## Controls
+
+N/A
+
+## Requests
+
+Michael Salaverry (github.com/barakplasma)
+
+## Creator
+
+Michael Salaverry
diff --git a/apps/golfscore/app-icon.js b/apps/golfscore/app-icon.js
new file mode 100644
index 000000000..238001688
--- /dev/null
+++ b/apps/golfscore/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwIEBgOABQcD4AFDg1wAokYDokOAokDDwkBDwkADwn4nAFD/geDgP8gYFEDwn8gFgDocA+AFCkE/A4IABg//Aoc//4RDn/+Goc/8AFJj4FLEQYFGh4FLIAYFGg4FKh5sBApEfnhTEAok+Aol8vihEAon4AocB+F4ZQYFF8AFDg/AAocPAouAKYcfXQQFHjzEEhjvDA"))
diff --git a/apps/golfscore/app.js b/apps/golfscore/app.js
new file mode 100644
index 000000000..7c5c2d0e8
--- /dev/null
+++ b/apps/golfscore/app.js
@@ -0,0 +1,113 @@
+// @ts-check
+// @ts-ignore
+const menu = require("graphical_menu");
+/**
+ * @type {{showMenu: (config) => void}}
+ */
+let E;
+/**
+ * @type {{clear: () => void}}
+ */
+let g;
+
+let holes_count = 18;
+let player_count = 4;
+/**
+ * @type {number[][]}
+ */
+let course = new Array(holes_count).map(() => new Array(player_count).fill(0));
+
+const main_menu = {
+  "": {
+    "title": "-- Golf --"
+  },
+  "Setup": function () { E.showMenu(setup_menu); },
+  "Score Card": function () {
+    calculate_score();
+    E.showMenu(score_card);
+  },
+};
+
+function calculate_score() {
+  let scores = course.reduce((acc, hole) => {
+    hole.forEach((stroke_count, player) => {
+      acc[player] = acc[player]+stroke_count;
+    });
+    return acc;
+  }, new Array(player_count).fill(0));
+
+  score_card = {
+    "": {
+      "title": "score card"
+    },
+    "< Back": function () { E.showMenu(main_menu); },
+  };
+
+  for (let player = 0; player < player_count; player++) {
+    score_card["Player - " + (player + 1)] = {
+      value: scores[player]
+    };
+  }
+}
+
+let score_card = {};
+
+const setup_menu = {
+  "": {
+    "title": "-- Golf Setup --"
+  },
+  "Holes": {
+    value: holes_count,
+    min: 1, max: 20, step: 1, wrap: true,
+    onchange: v => { holes_count = v; add_holes(); }
+  },
+  "Players": {
+    value: player_count,
+    min: 1, max: 10, step: 1, wrap: true,
+    onchange: v => { player_count = v; }
+  },
+  "< Back": function () { E.showMenu(main_menu); },
+};
+
+function inc_hole(i, player) { return function (v) { course[i][player] = v; }; }
+
+function add_holes() {
+  for (let j = 0; j < 20; j++) {
+    delete main_menu["Hole - " + (j + 1)];
+  }
+  for (let i = 0; i < holes_count; i++) {
+    course[i] = new Array(player_count).fill(0);
+    main_menu["Hole - " + (i + 1)] = goto_hole_menu(i);
+  }
+  E.showMenu(main_menu);
+}
+
+function goto_hole_menu(i) {
+  return function () {
+    E.showMenu(hole_menu(i));
+  };
+}
+
+function hole_menu(i) {
+  let menu = {
+    "": {
+      "title": `-- Hole ${i + 1}--`
+    },
+    "Next hole": goto_hole_menu(i + 1),
+    "< Back": function () { E.showMenu(main_menu); },
+  };
+
+  for (let player = 0; player < player_count; player++) {
+    menu[`player - ${player + 1}`] = {
+      value: course[i][player],
+      min: 1, max: 20, step: 1, wrap: true,
+      onchange: inc_hole(i, player)
+    };
+  }
+
+  return menu;
+}
+
+// @ts-ignore
+g.clear();
+add_holes();
\ No newline at end of file
diff --git a/apps/golfscore/app.png b/apps/golfscore/app.png
new file mode 100644
index 000000000..fc5d51557
Binary files /dev/null and b/apps/golfscore/app.png differ
diff --git a/apps/golfscore/holemenu.png b/apps/golfscore/holemenu.png
new file mode 100644
index 000000000..ac214f182
Binary files /dev/null and b/apps/golfscore/holemenu.png differ
diff --git a/apps/golfscore/mainmenu.png b/apps/golfscore/mainmenu.png
new file mode 100644
index 000000000..3ebeb0ca7
Binary files /dev/null and b/apps/golfscore/mainmenu.png differ
diff --git a/apps/golfscore/scorecard.png b/apps/golfscore/scorecard.png
new file mode 100644
index 000000000..9e7ff1130
Binary files /dev/null and b/apps/golfscore/scorecard.png differ
diff --git a/apps/golfscore/setupmenu.png b/apps/golfscore/setupmenu.png
new file mode 100644
index 000000000..13158e2e7
Binary files /dev/null and b/apps/golfscore/setupmenu.png differ
diff --git a/apps/gpsinfo/ChangeLog b/apps/gpsinfo/ChangeLog
index ceff7011e..381412c16 100644
--- a/apps/gpsinfo/ChangeLog
+++ b/apps/gpsinfo/ChangeLog
@@ -1,3 +1,4 @@
 0.02: Ensure screen doesn't display garbage at startup
 0.03: Show number of satellites while waiting for fix
 0.04: Add Maidenhead readout of GPS location
+0.05: Refactor to use 'layout' library for multi-device support
diff --git a/apps/gpsinfo/gps-info.js b/apps/gpsinfo/gps-info.js
index 1a8cb2fd1..df888651a 100644
--- a/apps/gpsinfo/gps-info.js
+++ b/apps/gpsinfo/gps-info.js
@@ -1,11 +1,14 @@
-var img = require("heatshrink").decompress(atob("mEwghC/AH4AKg9wC6t3u4uVC6wWBI6t3uJeVuMQCqcBLisAi4XLxAABFxAXKgc4DBAuBRhQXEDAq7MmYXEwBHEXZYXFGAOqAAKDMmczC4mIC62CC50PC4JIBkQABiIvRmURAAUSjQXSFwMoxGKC6CRFwUSVYgXLPIgXXwMYegoXLJAYXCGBnzGA0hPQIwMgYwGC6gwCC4ZIMC4gYBC604C4ZISmcRVgapQAAMhC6GIJIwXCMBcIxGDDBAuLC4IwGAARGMAAQWGmAXPJQoWMC4pwCCpoXJAB4XXAH4A/ABQA="))
+function satelliteImage() {
+  return require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4AGnE4F1wvsF34wgFldcLdyMYsoACF1WJF4YxPFzOtF4wxNFzAvKSiIvU1ovIGAkJAAQucF5QxCFwYwbF4QwLrwvjYIVfrwABrtdq9Wqwvkq4oCAAtXmYvi1teE4NXrphCrxoCGAbvdSIoAHNQNeFzQvGeRQvCsowrYYNfF8YwHZQQFCF8QwGF4owjeYovBroHEMERhEF8IwNrtWryYFF8YwCq4vhGBeJF5AwaxIwKwVXFwwvandfMJeJF8M6nZiLGQIvdstfGAVlGBZkCxJeZJQIwCGIRjMFzYACGIc6r/+FsIvGGIYABEzYvPGQYvusovkAH4A/AH4A/ACo="));
+}
 
-Bangle.setGPSPower(1);
-Bangle.setLCDMode("doublebuffered");
+var Layout = require("Layout");
+var layout;
+Bangle.setGPSPower(1, "app");
 E.showMessage("Loading..."); // avoid showing rubbish on screen
 
 var lastFix = {
-  fix: 0,
+  fix: -1,
   alt: 0,
   lat: 0,
   lon: 0,
@@ -31,14 +34,14 @@ function getMaidenHead(param1,param2){
   lon = param2;
 
   lon = lon + 180;
-  t = lon/20;
+  var t = lon/20;
   fLon = Math.floor(t);
   t = (t % fLon)*10;
   sqLon = Math.floor(t);
-  t=(t-sqLon)*24;
+  t = (t-sqLon)*24;
   subLon = Math.floor(t);
   extLon = Math.floor((t-subLon)*10);
-  
+
   lat = lat + 90;
   t = lat/10;
   fLat = Math.floor(t);
@@ -51,43 +54,58 @@ function getMaidenHead(param1,param2){
   return U[fLon]+U[fLat]+sqLon+sqLat+L[subLon]+L[subLat]+extLon+extLat;
 }
 function onGPS(fix) {
+  if (lastFix.fix != fix.fix) {
+    // if fix is different, change the layout
+    if (fix.fix) {
+      layout = new Layout( {
+        type:"v", c: [
+          {type:"txt", font:"6x8:2", label:"GPS Info" },
+          {type:"img", src:satelliteImage, pad:4 },
+          {type:"txt", font:"6x8", label:"", fillx:true, id:"alt"  },
+          {type:"txt", font:"6x8", label:"", fillx:true, id:"lat" },
+          {type:"txt", font:"6x8", label:"", fillx:true, id:"lon" },
+          {type:"txt", font:"6x8", label:"", fillx:true, id:"speed" },
+          {type:"txt", font:"6x8", label:"", fillx:true, id:"time" },
+          {type:"txt", font:"6x8", label:"", fillx:true, id:"sat" },
+          {type:"txt", font:"6x8", label:"", fillx:true, id:"maidenhead" },
+        ]},{lazy:true});
+    } else {
+      layout = new Layout( {
+        type:"v", c: [
+          {type:"txt", font:"6x8:2", label:"GPS Info" },
+          {type:"img", src:satelliteImage, pad:4 },
+          {type:"txt", font:"6x8", label:"Waiting for GPS" },
+          {type:"h", c: [
+            {type:"txt", font:"10%", label:fix.satellites, pad:2, id:"sat" },
+            {type:"txt", font:"6x8", pad:3, label:"Satellites" }
+          ]},
+          {type:"txt", font:"6x8", label:"", id:"progress" }
+        ]},{lazy:true});
+    }
+    g.clearRect(0,24,g.getWidth(),g.getHeight());
+    layout.render();
+  }
   lastFix = fix;
-  g.clear();
-  g.setFontAlign(-1, -1);
-  g.drawImage(img, 20, -12);
-  g.setFont("6x8");
-  g.setFontVector(22);
-  g.drawString("GPS Info", 70, 0);
   if (fix.fix) {
     nofix = 0;
-    var alt = fix.alt;
-    var lat = fix.lat;
-    var lon = fix.lon;
-    var speed = fix.speed;
-    var time = formatTime(fix.time);
+    var locale = require("locale");
     var satellites = fix.satellites;
-    var maidenhead = getMaidenHead(lat,lon);
-    var s = 15;
-    g.setFontVector(s);
-    g.drawString("Altitude: "+alt+" m",10,36);
-    g.drawString("Lat: "+lat,10,54);
-    g.drawString("Lon: "+lon,10,72);
-    g.drawString("Speed: "+speed.toFixed(1)+" km/h",10,90);
-    g.drawString("Time: "+time,10,108);
-    g.drawString("Satellites: "+satellites,10,126);
-    g.drawString("Maidenhead: "+maidenhead,10,144);
+    var maidenhead = getMaidenHead(fix.lat,fix.lon);
+    layout.alt.label = "Altitude: "+locale.distance(fix.alt);
+    layout.lat.label = "Lat: "+fix.lat.toFixed(6);
+    layout.lon.label = "Lon: "+fix.lon.toFixed(6);
+    layout.speed.label = "Speed: "+locale.speed(fix.speed);
+    layout.time.label = "Time: "+formatTime(fix.time);
+    layout.sat.label = "Satellites: "+satellites;
+    layout.maidenhead.label = "Maidenhead: "+maidenhead;
   } else {
-    g.setFontAlign(0, 1);
-    g.setFont("6x8", 2);
-    g.drawString("Waiting for GPS", 120, 80);
+    layout.sat.label = fix.satellites;
     nofix = (nofix+1) % 4;
-    g.drawString(".".repeat(nofix) + " ".repeat(4-nofix), 120, 120);
-    // Show number of satellites:
-    g.setFontAlign(0,0);
-    g.setFont("6x8");
-    g.drawString(fix.satellites+" satellites", 120, 100);
+    layout.progress.label = ".".repeat(nofix) + " ".repeat(4-nofix);
   }
-  g.flip();
+  layout.render();
 }
 
+Bangle.loadWidgets();
+Bangle.drawWidgets();
 Bangle.on('GPS', onGPS);
diff --git a/apps/gpsrec/ChangeLog b/apps/gpsrec/ChangeLog
index 412dbe9d3..365405846 100644
--- a/apps/gpsrec/ChangeLog
+++ b/apps/gpsrec/ChangeLog
@@ -21,3 +21,11 @@
 0.17: Disable recording if storage is full (fix #574)
 0.18: Period counter now uses GPS time rather than counting packets (allows use with GPS Setup)
 0.19: Fix memory usage issues inside track viewer app
+0.20: Add documentation to explain time needed for getting a time fix
+0.21: Fix issue where a period of 1s recorded every 2s, 5s every 6s, and so on
+0.22: Ensure Bangle.setGPSPower uses 'gpsrec' as a tag
+0.23: Fix issue where tracks wouldn't record when running from OpenStMap if a period hadn't been set up first
+0.24: Better support for Bangle.js 2, avoid widget area for Graphs, smooth graphs more
+0.25: Fix issue where if Bangle.js 2 got a GPS fix but no reported time, errors could be caused by the widget (fix #935)
+0.26: Multiple bugfixes
+0.27: Map drawing with light theme (fix #1023) 
diff --git a/apps/gpsrec/README.md b/apps/gpsrec/README.md
new file mode 100644
index 000000000..71b934111
--- /dev/null
+++ b/apps/gpsrec/README.md
@@ -0,0 +1,13 @@
+# GPS Recorder
+
+![icon](app.png)
+
+This app allows you to record a GPS track. It can run in background. The data can later be exported as KML or GPX files via the BangleJS app store.
+
+## Tips
+
+When you turn on recording, a widget badge that looks like a satellite will appear immediately at the top of the screen. However, the recording does not begin immediately. It usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). You will notice a blinking question mark at the lower left of the badge indicating currently getting a fix. The badge will change when a GPS fix is achieved and that is when the app actually starts writing data to the log file. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix.
+
+## Viewing a track
+
+![](screenshot.png)
diff --git a/apps/gpsrec/app.js b/apps/gpsrec/app.js
index 29594289d..833a816ea 100644
--- a/apps/gpsrec/app.js
+++ b/apps/gpsrec/app.js
@@ -102,7 +102,8 @@ function getTrackInfo(fn) {
   var lfactor = Math.cos(minLat*Math.PI/180);
   var ylen = (maxLat-minLat);
   var xlen = (maxLong-minLong)* lfactor;
-  var scale = xlen>ylen ? 200/xlen : 200/ylen;
+  var screenSize = g.getHeight()-48; // 24 for widgets, plus a border
+  var scale = xlen>ylen ? screenSize/xlen : screenSize/ylen;
   return {
     fn : fn,
     filename : filename,
@@ -110,6 +111,7 @@ function getTrackInfo(fn) {
     records : nl,
     minLat : minLat, maxLat : maxLat,
     minLong : minLong, maxLong : maxLong,
+    lat : (minLat+maxLat)/2, lon : (minLong+maxLong)/2,
     lfactor : lfactor,
     scale : scale,
     duration : Math.round(duration/1000)
@@ -180,31 +182,32 @@ function plotTrack(info) {
     getMapXY = osm.latLonToXY.bind(osm);
   } else {
     getMapXY = function(lat, lon) { "ram"
-      var ix = 30 + Math.round((long - info.minLong)*info.lfactor*info.scale);
-      var iy = 210 - Math.round((lat - info.minLat)*info.scale);
-      return {x:ix, y:iy};
+      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("Drawing...","GPS Track "+info.fn);
+  g.flip(); // on buffered screens, draw a not saying we're busy
+  g.clear(1);
   var s = require("Storage");
   var cx = g.getWidth()/2;
-  var cy = g.getHeight()/2;
+  var cy = 24 + (g.getHeight()-24)/2;
   g.setColor(1,0.5,0.5);
   g.setFont("Vector",16);
   g.drawString("Track"+info.fn.toString()+" - Loading",10,220);
-  g.setColor(0,0,0);
+  g.setColor(g.theme.bg);
   g.fillRect(0,220,239,239);
   if (!info.qOSTM) {
     g.setColor(1, 0, 0);
     g.fillRect(9,80,11,120);
     g.fillPoly([9,60,19,80,0,80]);
-    g.setColor(1,1,1);
+    g.setColor(g.theme.fg);
     g.drawString("N",2,40);
-    g.setColor(1,1,1);
   } else {
-    osm.lat = (info.minLat+info.maxLat)/2;
-    osm.lon = (info.minLong+info.maxLong)/2;
+    osm.lat = info.lat;
+    osm.lon = info.lon;
     osm.draw();
     g.setColor(0, 0, 0);
   }
@@ -224,7 +227,7 @@ function plotTrack(info) {
   g.setColor(0,1,0);
   g.fillCircle(mp.x,mp.y,5);
   if (info.qOSTM) g.setColor(1,0,0.55);
-  else g.setColor(1,1,1);
+  else g.setColor(g.theme.fg);
   l = f.readLine(f);
   while(l!==undefined) {
     c = l.split(",");
@@ -244,14 +247,15 @@ function plotTrack(info) {
   g.setColor(1,0,0);
   g.fillCircle(ox,oy,5);
   if (info.qOSTM) g.setColor(0, 0, 0);
-  else g.setColor(1,1,1);
-  g.drawString(require("locale").distance(dist),120,220);
+  else g.setColor(g.theme.fg);
+  g.drawString(require("locale").distance(dist),g.getWidth() / 2, g.getHeight() - 20);
   g.setFont("6x8",2);
   g.setFontAlign(0,0,3);
-  g.drawString("Back",230,200);
+  g.drawString("Back",g.getWidth() - 10, g.getHeight()/2);
   setWatch(function() {
     viewTrack(info.fn, info);
-  }, BTN3);
+  }, global.BTN3||BTN1);
+  Bangle.drawWidgets();
   g.flip();
 }
 
@@ -260,8 +264,8 @@ function plotGraph(info, style) {
   E.showMenu(); // remove menu
   E.showMessage("Calculating...","GPS Track "+info.fn);
   var filename = getFN(info.fn);
-  var infn = new Float32Array(200);
-  var infc = new Uint16Array(200);
+  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
@@ -278,7 +282,7 @@ function plotGraph(info, style) {
     title = "Altitude (m)";
     while(l!==undefined) {
       ++nl;c=l.split(",");
-      i = Math.round(200*(c[0]/1000 - strt)/dur);
+      i = Math.round(80*(c[0]/1000 - strt)/dur);
       infn[i]+=+c[3];
       infc[i]++;
       l = f.readLine(f);
@@ -289,7 +293,7 @@ function plotGraph(info, style) {
     var t,dx,dy,d,lt = c[0]/1000;
     while(l!==undefined) {
       ++nl;c=l.split(",");
-      i = Math.round(200*(c[0]/1000 - strt)/dur);
+      i = Math.round(80*(c[0]/1000 - strt)/dur);
       t = c[0]/1000;
       p = Bangle.project({lat:c[1],lon:c[2]});
       dx = p.x-lp.x;
@@ -320,21 +324,21 @@ function plotGraph(info, style) {
   // draw
   g.clear(1).setFont("6x8",1);
   var r = require("graph").drawLine(g, infn, {
-    x:4,y:0,
+    x:4,y:24,
     width: g.getWidth()-24,
-    height: g.getHeight()-8,
+    height: g.getHeight()-(24+8),
     axes : true,
     gridy : grid,
-    gridx : 50,
+    gridx : infn.length / 3,
     title: title,
     xlabel : x=>Math.round(x*dur/(60*infn.length))+" min" // minutes
   });
   g.setFont("6x8",2);
   g.setFontAlign(0,0,3);
-  g.drawString("Back",230,200);
+  g.drawString("Back",g.getWidth() - 10, g.getHeight() - 40);
   setWatch(function() {
     viewTrack(info.fn, info);
-  }, BTN3);
+  }, global.BTN3||BTN1);
   g.flip();
 }
 
diff --git a/apps/gpsrec/interface.html b/apps/gpsrec/interface.html
index 11b53164f..4c7270f0a 100644
--- a/apps/gpsrec/interface.html
+++ b/apps/gpsrec/interface.html
@@ -95,6 +95,7 @@ function getTrackList() {
   Util.showModal("Loading Tracks...");
   domTracks.innerHTML = "";
   Puck.write(`\x10(function() {
+    Bluetooth.println("");
     for (var n=0;n<36;n++) {
       var f = require("Storage").open(".gpsrc"+n.toString(36),"r");
       var l = f.readLine();
diff --git a/apps/gpsrec/screenshot.png b/apps/gpsrec/screenshot.png
new file mode 100644
index 000000000..f6e001749
Binary files /dev/null and b/apps/gpsrec/screenshot.png differ
diff --git a/apps/gpsrec/widget.js b/apps/gpsrec/widget.js
index 8e4286db5..995f5f73b 100644
--- a/apps/gpsrec/widget.js
+++ b/apps/gpsrec/widget.js
@@ -4,7 +4,7 @@
   var fixToggle = false; // toggles once for each reading
   var gpsTrack; // file for GPS track
   var gpsOn = false;
-  var lastFixTime;
+  var lastFixTime = Date.now();
 
   // draw your widget
   function draw() {
@@ -26,10 +26,9 @@
     fixToggle = !fixToggle;
     WIDGETS["gpsrec"].draw();
     if (hasFix) {
-      var period = 1000000;
-      if (lastFixTime!==undefined)
-        period = fix.time.getTime() - lastFixTime;
-      if (period > settings.period*1000) {
+      if (fix.time===undefined) fix.time = new Date(); // Bangle.js 2 can provide a fix before time it seems
+      var period = fix.time.getTime() - lastFixTime;
+      if (period+500 > settings.period*1000) { // round up
         lastFixTime = fix.time.getTime();
         try {
           if (gpsTrack) gpsTrack.write([
@@ -69,7 +68,7 @@
       gpsTrack = undefined;
     }
     if (gOn != gpsOn) {
-      Bangle.setGPSPower(gOn);
+      Bangle.setGPSPower(gOn,"gpsrec");
       gpsOn = gOn;
     }
   }
@@ -78,8 +77,7 @@
     reload();
     Bangle.drawWidgets(); // relayout all widgets
   },plotTrack:function(m) { // m=instance of openstmap module
-    settings = require("Storage").readJSON("gpsrec.json",1)||{};
-    settings.file |= 0;
+    // if we're here, settings was already loaded
     var n = settings.file.toString(36);
     var f = require("Storage").open(".gpsrc"+n,"r");
     var l = f.readLine(f);
diff --git a/apps/gpsservice/ChangeLog b/apps/gpsservice/ChangeLog
deleted file mode 100644
index 9b415e6a7..000000000
--- a/apps/gpsservice/ChangeLog
+++ /dev/null
@@ -1,4 +0,0 @@
-0.01: New App
-0.02: Restore to SuperE mode on power off.
-0.03: dont reset to SuperE mode on power, as it prevents its general use
-0.04: Only turn GPS off if it was previously on (stops other apps/widgets that use GPS getting broken)
diff --git a/apps/gpsservice/README.md b/apps/gpsservice/README.md
deleted file mode 100644
index b1e3e60d4..000000000
--- a/apps/gpsservice/README.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# GPS Service
-
-A configurable, low power GPS widget that runs in the background.
-
-## Goals
-
-To develop a low power GPS widget that runs in the background and to
-facilitate an OS grid reference display in a watch face.
-
-
-* An app that turns on the GPS and constantly displays the screen
-  will use around 75mA, the battery will last between 3-4 hours.
-
-* Using the GPS in a Widget in Super-E Power Saving Mode (PSM) with
-  the screen off most of the time, will consume around 35mA and you
-  might get 10hrs before a recharge.
-  
-* Using the GPS in Power Saving Mode On/Off (PSMOO) with suitable
-  settings can reduce the average consumption to around 15mA.  A
-  simple test using a 120s update period, 6s search period was still
-  running with 45% battery 20 hours after it started.
-
-
-## Settings
-
-The Settings App enables you set the following options for the GPS
-Service.  Go to Settings, select App/Widgets and then 'GPS Service'.
-
-- GPS - On/Off.  When this value is changed the GPS Service will be
-  powered on or off and the GPS Widget will be displayed.
-
-- Power Mode:
-
-   - SuperE - the factory default setup for the GPS. The recommended
-   power saving mode.
-
-   - PSMOO - On/Off power saving mode. Configured by interval and
-   search time. Choose this mode if you are happy to get a GPS
-   position update less often (say every 1 or 2 minutes). The longer
-   the interval the more time the GPS will spend sleeping in low
-   power mode (7mA) between obtaining fixes (35mA).  For walking in
-   open country an update once every 60 seconds is adequate to put
-   you within a 6 digit grid refernce sqaure.
-
-- update - the time between two position fix attempts.
-
-- search - the time between two acquisition attempts if the receiver
-  is unable to get a position fix.
-
-
-
-## Screenshots
-### GPS Watch face
-
-* The Age value is the number of seconds since the last position fix was received.
-
-![](gps_face.jpg)
-
-### Grid Reference Watch face
-
-* The time shown is the timestamp of the last position fix.
-* The age value is shown at the bottom of the screen. 
-
-![](osref_face.jpg)
-
-## Interface for Apps
-
-The code below demonstrates how you can setup and start the gpsservice from your own App.
-
-```js
-function test_gps_on() {
-
-  var settings = WIDGETS.gpsservice.gps_get_settings();
-
-  // change the settings to what you require
-  settings.gpsservice = true;
-  settings.update = 65;
-  settings.search = 5;
-  settings.power_mode = "PSMOO";
-  
-  WIDGETS.gpsservice.gps_set_settings(settings);
-  WIDGETS.gpsservice.reload(); // will power on
-}
-```
-
-In your app can retrieve the last fix as and when required.
-
-```js
-var fix = {
-  fix: 0,
-  alt: 0,
-  lat: 0,
-  lon: 0,
-  speed: 0,
-  time: 0,
-  satellites: 0
-};
-
-// only attempt to get gps fix if gpsservice is loaded
-if (WIDGETS.gpsservice !== undefined) {
-  fix = WIDGETS.gpsservice.gps_get_fix();
-  gps_on = WIDGETS.gpsservice.gps_get_status();
-}
-
-if (fix.fix) {
-  var time = formatTime(fix.time);
-  var age = timeSince(time);
-```
-
-When done you can turn the gpsservice off using the code below.
-
-```js
-function test_gps_off() {
-
-  var settings = WIDGETS.gpsservice.gps_get_settings();
-
-  settings.gpsservice = false;
-  settings.power_mode = "SuperE";
-  
-  WIDGETS.gpsservice.gps_set_settings(settings);
-  WIDGETS.gpsservice.reload(); // will power off
-}
-```
-
-## To Do List
-* add a logging option with options for interval between log points
-* add graphics and icons to the watch faces to make them look nicer
-
-
-## References
-
-* [UBLOX M8 Receiver Data Sheet](https://www.u-blox.com/sites/default/files/products/documents/u-blox8-M8_ReceiverDescrProtSpec_%28UBX-13003221%29.pdf)
-
-* [UBLOX Power Management App Note](https://www.u-blox.com/sites/default/files/products/documents/PowerManagement_AppNote_%28UBX-13005162%29.pdf)
-
-* Some useful code on Github and be found [here](https://portal.u-blox.com/s/question/0D52p0000925T00CAE/ublox-max-m8q-getting-stuck-when-sleeping-with-extint-pin-control)
-and [here](https://github.com/thasti/utrak/blob/master/gps.c)
-
diff --git a/apps/gpsservice/app.js b/apps/gpsservice/app.js
deleted file mode 100644
index 14bc48938..000000000
--- a/apps/gpsservice/app.js
+++ /dev/null
@@ -1,70 +0,0 @@
-Bangle.loadWidgets();
-Bangle.drawWidgets();
-
-const SETTINGS_FILE = "gpsservice.settings.json";
-let settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
-
-
-
-function updateSettings() {
-  require("Storage").write(SETTINGS_FILE, settings);
-}
-
-function reloadWidget() {
-  if (WIDGETS.gpsservice)
-    WIDGETS.gpsservice.reload();
-}
-
-function showMainMenu() {
-  var power_options = ["SuperE","PSMOO"];
-
-  const mainmenu = {
-    '': { 'title': 'GPS Service' },
-    '< Exit': ()=>{load();},
-    'GPS': {
-      value: !!settings.gpsservice,
-      format: v =>v?'On':'Off',
-      onchange: v => {
-        settings.gpsservice = v;
-	updateSettings();
-	reloadWidget();  // only when we change On/Off status
-      },
-    },
-
-    'Power Mode': {
-      value: 0 | power_options.indexOf(settings.power_mode),
-      min: 0, max: 1,
-      format: v => power_options[v],
-      onchange: v => {
-        settings.power_mode = power_options[v];
-	updateSettings();
-      },
-    },
-
-    'Update (s)': {
-      value: settings.update,
-      min: 10,
-      max: 1800,
-      step: 10,
-      onchange: v => {
-	settings.period =v;
-	updateSettings();
-      }
-    },
-    'Search (s)': {
-      value: settings.search,
-      min: 1,
-      max: 65,
-      step: 1,
-      onchange: v => {
-	settings.search = v;
-	updateSettings();
-      }
-    },
-    '< Back': ()=>{load();}
-  };
-
-  return E.showMenu(mainmenu);
-}
-
-showMainMenu();
diff --git a/apps/gpsservice/gps_face.jpg b/apps/gpsservice/gps_face.jpg
deleted file mode 100644
index 839d86895..000000000
Binary files a/apps/gpsservice/gps_face.jpg and /dev/null differ
diff --git a/apps/gpsservice/gpsservice-icon.js b/apps/gpsservice/gpsservice-icon.js
deleted file mode 100644
index b3f2dd3d4..000000000
--- a/apps/gpsservice/gpsservice-icon.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("mEwghC/AH4AKg9wC6t3u4uVC6wWBI6t3uJeVuMQCqcBLisAi4XLxAABFxAXKgc4DBAuBRhQXEDAq7MmYXEwBHEXZYXFGAOqAAKDMmczC4mIC62CC50PC4JIBkQABiIvRmURAAUSjQXSFwMoxGKC6CRFwUSVYgXLPIgXXwMYegoXLJAYXCGBnzGA0hPQIwMgYwGC6gwCC4ZIMC4gYBC604C4ZISmcRVgapQAAMhC6GIJIwXCMBcIxGDDBAuLC4IwGAARGMAAQWGmAXPJQoWMC4pwCCpoXJAB4XXAH4A/ABQA="))
diff --git a/apps/gpsservice/gpsservice.png b/apps/gpsservice/gpsservice.png
deleted file mode 100644
index 970e85139..000000000
Binary files a/apps/gpsservice/gpsservice.png and /dev/null differ
diff --git a/apps/gpsservice/osref_face.jpg b/apps/gpsservice/osref_face.jpg
deleted file mode 100644
index ff1cece7c..000000000
Binary files a/apps/gpsservice/osref_face.jpg and /dev/null differ
diff --git a/apps/gpsservice/settings.js b/apps/gpsservice/settings.js
deleted file mode 100644
index 471c23163..000000000
--- a/apps/gpsservice/settings.js
+++ /dev/null
@@ -1,4 +0,0 @@
-(function(back) {
-  // just go right to our app
-  load("gpsservice.app.js");
-})();
diff --git a/apps/gpsservice/settings.json b/apps/gpsservice/settings.json
deleted file mode 100644
index 3d687f9c2..000000000
--- a/apps/gpsservice/settings.json
+++ /dev/null
@@ -1 +0,0 @@
-{"gpsservice":false, "power_mode":"SuperE", "update":120, "search":6}
diff --git a/apps/gpsservice/test-bed.js b/apps/gpsservice/test-bed.js
deleted file mode 100644
index 0713cab9b..000000000
--- a/apps/gpsservice/test-bed.js
+++ /dev/null
@@ -1,259 +0,0 @@
-
-/*
-
-test bed for working out lowest power consumption, with workable GPS
-Load into IDE and upload code to RAM when connected to watch
-
-*/
-
-
-Bangle.on('GPS-raw',function (d) {
-  if (d[0]=="$") return;
-  if (d.startsWith("\xB5\x62\x05\x01")) print("GPS ACK");
-  else if (d.startsWith("\xB5\x62\x05\x00")) print("GPS NACK");
-  // 181,98 sync chars  
-  else print("GPS",E.toUint8Array(d).join(","));
-});
-
-function writeGPScmd(cmd) {
-  var d = [0xB5,0x62]; // sync chars
-  d = d.concat(cmd);
-  var a=0,b=0;
-  for (var i=2;i {
-  var settings = {};
-  var fixToggle = false; // toggles once for each reading
-  var have_fix = false;
-  var debug = false;
-  var gpsPowerEnabled = false;
-
-  var last_fix = {
-    fix: 0,
-    alt: 0,
-    lat: 0,
-    lon: 0,
-    speed: 0,
-    time: 0,
-    satellites: 0
-  };
-
-  function gps_get_fix() { return last_fix; }
-  function gps_get_status() { return WIDGETS.gpsservice.width === 24 ? true : false;}
-  function gps_get_version() { return "0.03"; }
-
-  function log_debug(o) {
-    if (debug) console.log(o);
-  }
-
-  function gps_set_debug(v) {
-    debug = v;
-  }
-
-  // Called by the GPS widget settings to reload settings and decide what to do
-  function reload() {
-    settings = gps_get_settings();
-    log_debug(settings);
-    Bangle.removeListener('GPS',onGPS);
-
-    if (settings.gpsservice) {
-       gps_power_on();
-    } else {
-       gps_power_off();
-    }
-  }
-
-  // retrieve the settings from Storage, can be called by external apps
-  function gps_get_settings() {
-    var sets = require("Storage").readJSON("gpsservice.settings.json",1)||{};
-    sets.gpsservice = sets.gpsservice||false;
-    sets.update = sets.update||120;
-    sets.search = sets.search||5;
-    sets.power_mode = sets.power_mode||"SuperE";
-    return sets;
-  }
-
-  // pass in the required settings, can be called by external apps
-  function gps_set_settings(sets) {
-    settings.gpsservice = sets.gpsservice||false;
-    settings.update = sets.update||120;
-    settings.search = sets.search||5;
-    settings.power_mode = sets.power_mode||"SuperE";
-    require("Storage").write("gpsservice.settings.json", settings);
-  }
-
-  // issue: currently possible to call this without having set settings.gpsservice in settings file
-  function gps_power_on() {
-    have_fix = false;
-    fixToggle = false;
-    setupGPS();
-    WIDGETS.gpsservice.width = 24;
-  }
-
-  function gps_power_off() {
-    //setupSuperE();  // return to expected setup for other apps
-    if (gpsPowerEnabled) {
-      gpsPowerEnabled = false;
-      Bangle.setGPSPower(0);
-    }
-    have_fix = false;
-    fixToggle = false;
-    last_fix.fix = 0;
-    WIDGETS.gpsservice.width = 0;
-  }
-
-  // quick hack
-  function wait(ms){
-    var start = new Date().getTime();
-    var end = start;
-    while(end < start + ms) {
-      end = new Date().getTime();
-    }
-  }
-
-  function setupGPS() {
-    Bangle.setGPSPower(1);
-    gpsPowerEnabled = true;
-
-    if (settings.power_mode === "PSMOO") {
-      setupPSMOO();
-    } else {
-      setupSuperE();
-    }
-    Bangle.on('GPS',onGPS);
-  }
-
-  function setupPSMOO() {
-    log_debug("setupGPS() PSMOO");
-    UBX_CFG_RESET();
-    wait(100);
-
-    UBX_CFG_PM2(settings.update, settings.search);
-    wait(20);
-
-    UBX_CFG_RXM();
-    wait(20);
-
-    UBX_CFG_SAVE();
-    wait(20);
-  }
-
-  function setupSuperE() {
-    log_debug("setupGPS() Super-E");
-    UBX_CFG_RESET();
-    wait(100);
-
-    UBX_CFG_PMS();
-    wait(20);
-
-    UBX_CFG_SAVE();
-    wait(20);
-  }
-
-  function writeGPScmd(cmd) {
-    var d = [0xB5,0x62]; // sync chars
-    d = d.concat(cmd);
-    var a=0,b=0;
-    for (var i=2;i>8;
-    } while (i);
-
-    return bytes;
-  }
-
-
-  /*
-   * Extended Power Management
-   * update and search are in milli seconds
-   * settings are loaded little endian, lsb first
-   *
-   * https://github.com/thasti/utrak/blob/master/gps.c
-   */
-  function UBX_CFG_PM2(update,search) {
-
-    var u = int_2_bytes(update*1000);
-    var s = int_2_bytes(search*1000);
-
-    writeGPScmd([0x06, 0x3B,                /* class id */
-     44, 0,                      /* length */
-     0x01, 0x00, 0x00, 0x00,    /* v1, reserved 1..3 */
-     0x00, 0x10, 0x00, 0x00,    /* on/off-mode, update ephemeris */
-     u[3], u[2], u[1], u[0],    /* update period, ms, 120s=00 01 D4 C0, 30s= 00 00 75 30 */
-     s[3], s[2], s[1], s[0],    /* search period, ms, 120s, 20s = 00 00 4E 20, 5s = 13 88 */
-     0x00, 0x00, 0x00, 0x00,    /* grid offset */
-     0x00, 0x00,              /* on-time after first fix */
-     0x01, 0x00,                /* minimum acquisition time */
-     0x00, 0x00, 0x00, 0x00,    /* reserved 4,5 */
-     0x00, 0x00, 0x00, 0x00,    /* reserved 6 */
-     0x00, 0x00, 0x00, 0x00,    /* reserved 7 */
-     0x00, 0x00, 0x00, 0x00,    /* reserved 8,9,10 */
-     0x00, 0x00, 0x00, 0x00]);  /* reserved 11 */
-  }
-
-  // enable power saving mode, after configured with PM2
-  function UBX_CFG_RXM() {
-    writeGPScmd([0x06, 0x11,      /* UBX-CFG-RXM */
-     2, 0,            /* length */
-     0x08, 0x01]);    /* reserved, enable power save mode */
-  }
-
-
-  /*
-   * Save configuration otherwise it will reset when the GPS wakes up
-   *
-   */
-  function UBX_CFG_SAVE() {
-    writeGPScmd([0x06, 0x09,   // class id
-     0x0D, 0x00,   // length
-     0x00, 0x00, 0x00, 0x00,  // clear mask
-     0xFF, 0xFF, 0x00, 0x00,  // save mask
-     0x00, 0x00, 0x00, 0x00,  // load mask
-     0x07]);                  // b2=eeprom b1=flash b0=bat backed ram
-  }
-
-  /*
-   * Reset to factory settings using clear mask in UBX_CFG_CFG
-   * https://portal.u-blox.com/s/question/0D52p0000925T00CAE/ublox-max-m8q-getting-stuck-when-sleeping-with-extint-pin-control
-   */
-  function UBX_CFG_RESET() {
-    writeGPScmd([0x06, 0x09,   // class id
-                 0x0D, 0x00,
-     0xFF, 0xFB, 0x00, 0x00,  // clear mask
-     0x00, 0x00, 0x00, 0x00,  // save mask
-     0xFF, 0xFF, 0x00, 0x00,  // load mask
-     0x17]);
-  }
-
-  // draw the widget
-  function draw() {
-    if (!settings.gpsservice) return;
-    g.reset();
-    g.drawImage(atob("GBgCAAAAAAAAAAQAAAAAAD8AAAAAAP/AAAAAAP/wAAAAAH/8C9AAAB/8L/QAAAfwv/wAAAHS//wAAAAL//gAAAAf/+AAAAAf/4AAAAL//gAAAAD/+DwAAAB/Uf8AAAAfA//AAAACAf/wAAAAAH/0AAAAAB/wAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),this.x,this.y);
-    if (gps_get_status() === true && have_fix) {
-      g.setColor("#00FF00");
-      g.drawImage(fixToggle ? atob("CgoCAAAAA0AAOAAD5AAPwAAAAAAAAAAAAAAAAA==") : atob("CgoCAAABw0AcOAHj5A8PwHwAAvgAB/wABUAAAA=="),this.x,this.y+14);
-    } else {
-      g.setColor("#0000FF");
-      if (fixToggle) g.setFont("6x8").drawString("?",this.x,this.y+14);
-    }
-  }
-
-  function onGPS(fix) {
-    fixToggle = !fixToggle;
-    WIDGETS.gpsservice.draw();
-    log_debug(fix);
-    last_fix.satellites = fix.satellites;
-
-    /*
-     * If we have a fix record it, we will get another soon. Apps
-     * will see the timestamp of the last fix and be able to work out
-     * if it is stale. This means an App will always have the last
-     * known fix, and we avoid saying no fix all the time.
-     *
-     */
-    if (fix.fix) {
-      last_fix.fix = fix.fix;
-      last_fix.alt = fix.alt;
-      last_fix.lat = fix.lat;
-      last_fix.lon = fix.lon;
-      last_fix.speed = fix.speed;
-      last_fix.time = fix.time;
-    }
-  }
-
-  // redraw when the LCD turns on
-  Bangle.on('lcdPower', function(on) {
-    if (on) WIDGETS.gpsservice.draw();
-  });
-
-  // add the widget
-  WIDGETS.gpsservice = {
-    area:"tl",
-    width:24,
-    draw:draw,
-    gps_power_on:gps_power_on,
-    gps_power_off:gps_power_off,
-    gps_get_status:gps_get_status,
-    gps_get_fix:gps_get_fix,
-    gps_get_version:gps_get_version,
-    gps_get_settings:gps_get_settings,
-    gps_set_settings:gps_set_settings,
-    gps_set_debug:gps_set_debug,
-    reload:function() {
-      reload();
-      Bangle.drawWidgets(); // relayout all widgets
-    }};
-
-  // load settings, set correct widget width
-  reload();
-
-})();
diff --git a/apps/gpstime/ChangeLog b/apps/gpstime/ChangeLog
index a3bd6351e..4d9bbc8a2 100644
--- a/apps/gpstime/ChangeLog
+++ b/apps/gpstime/ChangeLog
@@ -1,2 +1,3 @@
 0.03: Fix time output on new firmwares when no GPS time set (fix #104)
-0.04: Fix shown UTC time zone sign
\ No newline at end of file
+0.04: Fix shown UTC time zone sign
+0.05: Use new 'layout library for Bangle2, fix #764 by adding a back button
diff --git a/apps/gpstime/gpstime-icon.js b/apps/gpstime/gpstime-icon.js
index 665c8d5f6..99998c6c4 100644
--- a/apps/gpstime/gpstime-icon.js
+++ b/apps/gpstime/gpstime-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mEwghC/AH8A1QWVhWq0AuVAAIuVAAIwT1WinQwTFwMzmQwTCYMjlUqGCIuBlWi0UzC6JdBIoMjC4UDmAuOkYXBPAWgmczLp2ilUiVAUDC4IwLFwIUBLoJ2BFwQwM1WjCgJ1DFwQwLFwJ1B0SQCkQWDGBQXBCgK9BDgKQBAAgwJOwUzRgIDBC54wCkZdGPBwACRgguDBIIwLFxEJBQIwLFxGaBYQwKFxQwLgAWGmQuBcAQwJC48ifYYwJgUidgsyC4L7DGBIXBdohnBCgL7BcYIXIGAqMCIoL7DL5IwERgIUBLoL7BO5QXBGAK7DkWiOxQXGFwOjFoUyFxZhDgBdCCgJ1CCxYxCgBABkcqOwIuNGAQXC0S9BLpgAFXoIwBmYuPAAYwCLp4wHFyYwDFyYwDFygwCCyoA/AFQA="))
+require("heatshrink").decompress(atob("mEw4UA////G161hyd8Jf4ALlQLK1WABREC1WgBZEK32oFxPW1QuJ7QwIFwOqvQLHhW31NaBY8qy2rtUFoAuG3W61EVqALF1+qr2gqtUHQu11dawNVqo6F22q9XFBYIwEhWqz2r6oLBGAheBqwuBBYx2CFwQLGlWqgoLCMAsKLoILChR6EgQuDqkqYYsBFweqYYoLDoWnYYoLD/WVYYv8FwXqPoIwEn52BqGrPoILEh/1FwOl9SsBBYcD/pdB2uq/QvEh/8LoOu1xHFh8/gGp9WWL4oMBgWltXeO4owBgWt1ReFYYh2GYYmXEQzDD3wiHegYKIGAJRGAAguJAH4AC"))
diff --git a/apps/gpstime/gpstime.js b/apps/gpstime/gpstime.js
index a061d2e23..8c80953fa 100644
--- a/apps/gpstime/gpstime.js
+++ b/apps/gpstime/gpstime.js
@@ -1,68 +1,75 @@
-var img = require("heatshrink").decompress(atob("mEwghC/AH8A1QWVhWq0AuVAAIuVAAIwT1WinQwTFwMzmQwTCYMjlUqGCIuBlWi0UzC6JdBIoMjC4UDmAuOkYXBPAWgmczLp2ilUiVAUDC4IwLFwIUBLoJ2BFwQwM1WjCgJ1DFwQwLFwJ1B0SQCkQWDGBQXBCgK9BDgKQBAAgwJOwUzRgIDBC54wCkZdGPBwACRgguDBIIwLFxEJBQIwLFxGaBYQwKFxQwLgAWGmQuBcAQwJC48ifYYwJgUidgsyC4L7DGBIXBdohnBCgL7BcYIXIGAqMCIoL7DL5IwERgIUBLoL7BO5QXBGAK7DkWiOxQXGFwOjFoUyFxZhDgBdCCgJ1CCxYxCgBABkcqOwIuNGAQXC0S9BLpgAFXoIwBmYuPAAYwCLp4wHFyYwDFyYwDFygwCCyoA/AFQA="));
+function satelliteImage() {
+  return require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4AGnE4F1wvsF34wgFldcLdyMYsoACF1WJF4YxPFzOtF4wxNFzAvKSiIvU1ovIGAkJAAQucF5QxCFwYwbF4QwLrwvjYIVfrwABrtdq9Wqwvkq4oCAAtXmYvi1teE4NXrphCrxoCGAbvdSIoAHNQNeFzQvGeRQvCsowrYYNfF8YwHZQQFCF8QwGF4owjeYovBroHEMERhEF8IwNrtWryYFF8YwCq4vhGBeJF5AwaxIwKwVXFwwvandfMJeJF8M6nZiLGQIvdstfGAVlGBZkCxJeZJQIwCGIRjMFzYACGIc6r/+FsIvGGIYABEzYvPGQYvusovkAH4A/AH4A/ACo="));
+}
+
+var fix;
 
 Bangle.setLCDPower(1);
 Bangle.setLCDTimeout(0);
+var Layout = require("Layout");
+Bangle.setGPSPower(1, "app");
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+E.showMessage("Loading..."); // avoid showing rubbish on screen
 
-g.clear();
-
-var fix;
-Bangle.setGPSPower(1);
-Bangle.on('GPS',function(f) {
-  fix = f;
-  g.reset(1);
-  g.setFont("6x8",2);
-  g.setFontAlign(0,0);
-  g.clearRect(90,30,239,90);
-  if (fix.fix) {
-    g.drawString("GPS",170,40);
-    g.drawString("Acquired",170,60);
+function setGPSTime() {
+  if (fix.time!==undefined) {
+    setTime(fix.time.getTime()/1000);
+    E.showMessage("System time set", {img:require("heatshrink").decompress(atob("lEo4UBvvv///vEFBYNVAAWq1QFDBAgKGrQJD0oJDtQJD1IICqwGBFoIDByocDwAJBgQeDtWoJwcqDwWq0EAgfAgEKHoQcCBIQeBGAQaBBIQzBytaEwQJDlWlrQmBBIkK0tqBI+ptRNCBIcCBKhECBIh6CAgUL8AJHl/4BI8+3gJRl/8GJH/BI8Ah6MDLIZQB+BjGAAIoBBI84BIaVCAAaVBVIYJEWYLkEXobRDAAbRBcoYACcoT5DEwYJCtQoElWpBINaDwYcB0oJBGQIzCAYIwBDwQGBAAIcCDwYACDgQACBIYIEBQYFDA="))});
   } else {
-    g.drawString("Waiting for",170,40);
-    g.drawString("GPS Fix",170,60);
+    E.showMessage("No GPS time to set");
   }
-  g.setFont("6x8");
-  g.drawString(fix.satellites+" satellites",170,80);
 
-  g.clearRect(0,100,239,239);
-  var t = ["","","","---",""];
-  if (fix.time!==undefined)
+  Bangle.removeListener('GPS',onGPS);
+  setTimeout(function() {
+    fix = undefined;
+    layout.forgetLazyState(); // redraw all next time
+    Bangle.on('GPS',onGPS);
+  }, 2000);
+}
+
+var layout = new Layout( {
+  type:"v", c: [
+    {type:"h", c:[
+      {type:"img", src:satelliteImage },
+      { type:"v", fillx:1, c: [
+       {type:"txt", font:"6x8:2", label:"Waiting\nfor GPS", id:"status" },
+       {type:"txt", font:"6x8", label:"---", id:"sat" },
+     ]},
+    ]},
+    {type:"txt", fillx:1, filly:1, font:"6x8:2", label:"---", id:"gpstime" }
+  ]},{lazy:true, btns: [
+    { label : "Set", cb : setGPSTime},
+    { label : "Back", cb : ()=>load() }
+  ]});
+
+
+function onGPS(f) {
+  if (fix===undefined) {
+    g.clear();
+    Bangle.drawWidgets();
+  }
+  fix = f;
+  if (fix.fix) {
+    layout.status.label = "GPS\nAcquired";
+  } else {
+    layout.status.label = "Waiting\nfor GPS";
+  }
+  layout.sat.label = fix.satellites+" satellites";
+
+  var t = ["","---",""];
+  if (fix.time!==undefined) {
     t = fix.time.toString().split(" ");
-    /*
- [
-  "Sun",
-  "Nov",
-  "10",
-  "2019",
-  "15:55:35",
-  "GMT+0100"
- ]
-  */
-  //g.setFont("6x8",2);
-  //g.drawString(t[0],120,110); // day
-  g.setFont("6x8",3);
-  g.drawString(t[1]+" "+t[2],120,135); // date
-  g.setFont("6x8",2);
-  g.drawString(t[3],120,160); // year
-  g.setFont("6x8",3);
-  g.drawString(t[4],120,185); // time
-  if (fix.time) {
-    // timezone
     var tz = (new Date()).getTimezoneOffset()/-60;
     if (tz==0) tz="UTC";
     else if (tz>0) tz="UTC+"+tz;
     else tz="UTC"+tz;
-    g.setFont("6x8",2);
-    g.drawString(tz,120,210); // gmt
-    g.setFontAlign(0,0,3);
-    g.drawString("Set",230,120);
-    g.setFontAlign(0,0);
-  }
-});
 
-setInterval(function() {
-  g.drawImage(img,48,48,{scale:1.5,rotate:Math.sin(getTime()*2)/2});
-},100);
-setWatch(function() {
-  if (fix.time!==undefined)
-    setTime(fix.time.getTime()/1000);
-}, BTN2, {repeat:true});
+    t = [t[1]+" "+t[2],t[3],t[4],t[5],tz];
+  }
+
+  layout.gpstime.label = t.join("\n");
+  layout.render();
+}
+
+Bangle.on('GPS',onGPS);
diff --git a/apps/gpstouch/Changelog b/apps/gpstouch/Changelog
new file mode 100644
index 000000000..7f837e50e
--- /dev/null
+++ b/apps/gpstouch/Changelog
@@ -0,0 +1 @@
+0.01: First version
diff --git a/apps/gpstouch/README.md b/apps/gpstouch/README.md
new file mode 100644
index 000000000..172b5da57
--- /dev/null
+++ b/apps/gpstouch/README.md
@@ -0,0 +1,18 @@
+# GPS Touch
+
+- A touch controlled GPS watch for Bangle JS 2
+- Key feature is the conversion of Lat/Lon into Ordinance Servey Grid Reference
+- Swipe left and right to change the display
+- Select GPS and switch the GPS On or Off by touching twice in the top half of the display
+- Select LOGGER and switch the GPS Recorder On or Off by touching twice in the top half of the display
+- Displays the GPS time in the bottom half of the screen when the GPS is powered on, otherwise 00:00:00 
+- Select display of Course, Speed, Altitude, Longitude, Latitude, Ordinance Servey Grid Reference
+
+## Screenshots
+
+![](screenshot1.png)
+![](screenshot2.png)
+![](screenshot3.png)
+![](screenshot4.png)
+
+Written by: [Hugh Barney](https://github.com/hughbarney)  For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)
diff --git a/apps/gpstouch/geotools.js b/apps/gpstouch/geotools.js
new file mode 100644
index 000000000..5adc57872
--- /dev/null
+++ b/apps/gpstouch/geotools.js
@@ -0,0 +1,128 @@
+/**
+ * 
+ * A module of Geo functions for use with gps fixes
+ *
+ * let geo = require("geotools");
+ * let os = geo.gpsToOSGrid(fix);
+ * let ref = geo.gpsToOSMapRef(fix);
+ *
+ */
+
+Number.prototype.toRad = function() { return this*Math.PI/180; };
+/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
+/*  Ordnance Survey Grid Reference functions  (c) Chris Veness 2005-2014                          */
+/*   - www.movable-type.co.uk/scripts/gridref.js                                                  */
+/*   - www.movable-type.co.uk/scripts/latlon-gridref.html                                         */
+/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
+function OsGridRef(easting, northing) {
+  this.easting = 0|easting;
+  this.northing = 0|northing;
+}
+OsGridRef.latLongToOsGrid = function(point) {
+  var lat = point.lat.toRad();
+  var lon = point.lon.toRad();
+
+  var a = 6377563.396, b = 6356256.909;          // Airy 1830 major & minor semi-axes
+  var F0 = 0.9996012717;                         // NatGrid scale factor on central meridian
+  var lat0 = (49).toRad(), lon0 = (-2).toRad();  // NatGrid true origin is 49�N,2�W
+  var N0 = -100000, E0 = 400000;                 // northing & easting of true origin, metres
+  var e2 = 1 - (b*b)/(a*a);                      // eccentricity squared
+  var n = (a-b)/(a+b), n2 = n*n, n3 = n*n*n;
+
+  var cosLat = Math.cos(lat), sinLat = Math.sin(lat);
+  var nu = a*F0/Math.sqrt(1-e2*sinLat*sinLat);              // transverse radius of curvature
+  var rho = a*F0*(1-e2)/Math.pow(1-e2*sinLat*sinLat, 1.5);  // meridional radius of curvature
+  var eta2 = nu/rho-1;
+
+  var Ma = (1 + n + (5/4)*n2 + (5/4)*n3) * (lat-lat0);
+  var Mb = (3*n + 3*n*n + (21/8)*n3) * Math.sin(lat-lat0) * Math.cos(lat+lat0);
+  var Mc = ((15/8)*n2 + (15/8)*n3) * Math.sin(2*(lat-lat0)) * Math.cos(2*(lat+lat0));
+  var Md = (35/24)*n3 * Math.sin(3*(lat-lat0)) * Math.cos(3*(lat+lat0));
+  var M = b * F0 * (Ma - Mb + Mc - Md);              // meridional arc
+
+  var cos3lat = cosLat*cosLat*cosLat;
+  var cos5lat = cos3lat*cosLat*cosLat;
+  var tan2lat = Math.tan(lat)*Math.tan(lat);
+  var tan4lat = tan2lat*tan2lat;
+
+  var I = M + N0;
+  var II = (nu/2)*sinLat*cosLat;
+  var III = (nu/24)*sinLat*cos3lat*(5-tan2lat+9*eta2);
+  var IIIA = (nu/720)*sinLat*cos5lat*(61-58*tan2lat+tan4lat);
+  var IV = nu*cosLat;
+  var V = (nu/6)*cos3lat*(nu/rho-tan2lat);
+  var VI = (nu/120) * cos5lat * (5 - 18*tan2lat + tan4lat + 14*eta2 - 58*tan2lat*eta2);
+
+  var dLon = lon-lon0;
+  var dLon2 = dLon*dLon, dLon3 = dLon2*dLon, dLon4 = dLon3*dLon, dLon5 = dLon4*dLon, dLon6 = dLon5*dLon;
+
+  var N = I + II*dLon2 + III*dLon4 + IIIA*dLon6;
+  var E = E0 + IV*dLon + V*dLon3 + VI*dLon5;
+
+  return new OsGridRef(E, N);
+};
+
+/*
+ * converts northing, easting to standard OS grid reference.
+ *
+ * [digits=10] - precision (10 digits = metres)
+ *   to_map_ref(8, 651409, 313177); => 'TG 5140 1317'
+ *   to_map_ref(0, 651409, 313177); => '651409,313177'
+ *
+ */
+function to_map_ref(digits, easting, northing) {
+  if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision '${digits}'`); // eslint-disable-line comma-spacing
+
+  let e = easting;
+  let n = northing;
+
+  // use digits = 0 to return numeric format (in metres) - note northing may be >= 1e7
+  if (digits == 0) {
+    const format = { useGrouping: false,  minimumIntegerDigits: 6, maximumFractionDigits: 3 };
+    const ePad = e.toLocaleString('en', format);
+    const nPad = n.toLocaleString('en', format);
+    return `${ePad},${nPad}`;
+  }
+
+  // get the 100km-grid indices
+  const e100km = Math.floor(e / 100000), n100km = Math.floor(n / 100000);
+
+  // translate those into numeric equivalents of the grid letters
+  let l1 = (19 - n100km) - (19 - n100km) % 5 + Math.floor((e100km + 10) / 5);
+  let l2 = (19 - n100km) * 5 % 25 + e100km % 5;
+
+  // compensate for skipped 'I' and build grid letter-pairs
+  if (l1 > 7) l1++;
+  if (l2 > 7) l2++;
+  const letterPair = String.fromCharCode(l1 + 'A'.charCodeAt(0), l2 + 'A'.charCodeAt(0));
+
+  // strip 100km-grid indices from easting & northing, and reduce precision
+  e = Math.floor((e % 100000) / Math.pow(10, 5 - digits / 2));
+  n = Math.floor((n % 100000) / Math.pow(10, 5 - digits / 2));
+
+  // pad eastings & northings with leading zeros
+  e = e.toString().padStart(digits/2, '0');
+  n = n.toString().padStart(digits/2, '0');
+
+  return `${letterPair} ${e} ${n}`;
+}
+
+/**
+ * 
+ * Module exports section, example code below
+ *
+ * let geo = require("geotools");
+ * let os = geo.gpsToOSGrid(fix);
+ * let ref = geo.gpsToOSMapRef(fix);
+ */
+
+// get easting and northings
+exports.gpsToOSGrid = function(gps_fix) {
+  return OsGridRef.latLongToOsGrid(gps_fix);
+}
+
+// string with an OS Map grid reference
+exports.gpsToOSMapRef = function(gps_fix) {
+  let os = OsGridRef.latLongToOsGrid(last_fix);
+  return to_map_ref(6, os.easting, os.northing);
+}
diff --git a/apps/gpstouch/gpstouch.app.js b/apps/gpstouch/gpstouch.app.js
new file mode 100644
index 000000000..4e49dd1e5
--- /dev/null
+++ b/apps/gpstouch/gpstouch.app.js
@@ -0,0 +1,246 @@
+const h = g.getHeight();
+const w = g.getWidth();
+let geo = require("geotools");
+let last_fix;
+let listennerCount = 0;
+
+function log_debug(o) {
+  //console.log(o);
+}
+
+function resetLastFix() {
+  last_fix = {
+    fix: 0,
+    alt: 0,
+    lat: 0,
+    lon: 0,
+    speed: 0,
+    time: 0,
+    course: 0,
+    satellites: 0
+  };
+}
+
+function processFix(fix) {
+  last_fix.time = fix.time;
+  log_debug(fix);
+
+  if (fix.fix) {
+    if (!last_fix.fix) {
+      // we dont need to suppress this in quiet mode as it is user initiated
+      Bangle.buzz(1500); // buzz on first position
+    }
+    last_fix = fix;
+  }
+}
+
+function draw() {
+  var d = new Date();
+  var da = d.toString().split(" ");
+  var time = da[4].substr(0,5);
+  var hh = da[4].substr(0,2);
+  var mm = da[4].substr(3,2);
+
+  g.reset();
+  drawTop(d,hh,mm);
+  drawInfo();
+}
+
+function drawTop(d,hh,mm) {
+  g.setFont("Vector", w/3);
+  g.setFontAlign(0, 0);
+  g.setColor(g.theme.bg);
+  g.fillRect(0, 24, w, ((h-24)/2) + 24);
+  g.setColor(g.theme.fg);
+
+  g.setFontAlign(1,0);  // right aligned
+  g.drawString(hh, (w/2) - 6, ((h-24)/4) + 24);
+  g.setFontAlign(-1,0); // left aligned
+  g.drawString(mm, (w/2) + 6, ((h-24)/4) + 24);
+
+  // for the colon
+  g.setFontAlign(0,0); // centre aligned
+  if (d.getSeconds()&1) g.drawString(":", w/2, ((h-24)/4) + 24);
+}
+
+function drawInfo() {
+  if (infoData[infoMode] && infoData[infoMode].calc) {
+    g.setFont("Vector", w/7);
+    g.setFontAlign(0, 0);
+
+    if (infoData[infoMode].get_color)
+      g.setColor(infoData[infoMode].get_color());
+    else
+      g.setColor("#0ff");
+    g.fillRect(0, ((h-24)/2) + 24 + 1, w, h);
+
+    if (infoData[infoMode].is_control)
+      g.setColor("#fff");
+    else
+      g.setColor("#000");
+
+    g.drawString((infoData[infoMode].calc()), w/2, (3*(h-24)/4) + 24);
+  }
+}
+
+const infoData = {
+  ID_LAT: {
+    calc: () => 'Lat: ' + last_fix.lat.toFixed(4),
+  },
+  ID_LON: {
+    calc: () => 'Lon: ' + last_fix.lon.toFixed(4),
+  },
+  ID_SPEED: {
+    calc: () => 'Speed: ' + last_fix.speed.toFixed(1),
+  },
+  ID_ALT: {
+    calc: () => 'Alt: ' + last_fix.alt.toFixed(0),
+  },
+  ID_COURSE: {
+    calc: () => 'Course: '+ last_fix.course.toFixed(0),
+  },
+  ID_SATS: {
+    calc: () => 'Satelites: ' + last_fix.satellites,
+  },
+  ID_TIME: {
+    calc: () => formatTime(last_fix.time),
+  },
+  OS_REF: {
+    calc: () => !last_fix.fix ? "OO 000 000" : geo.gpsToOSMapRef(last_fix),
+  },
+  GPS_POWER: {
+    calc: () => (Bangle.isGPSOn()) ? 'GPS On' : 'GPS Off',
+    action: () => toggleGPS(),
+    get_color: () => Bangle.isGPSOn() ? '#f00' : '#00f',
+    is_control: true,
+  },
+  GPS_LOGGER: {
+    calc: () => 'Logger ' + loggerStatus(),
+    action: () => toggleLogger(),
+    get_color: () => loggerStatus() == "ON" ? '#f00' : '#00f',
+    is_control: true,
+  },
+};
+
+function toggleGPS() {
+  if (loggerStatus() == "ON")
+    return;
+
+  Bangle.setGPSPower(Bangle.isGPSOn() ? 0 : 1, 'gpstouch');
+  // add or remove listenner
+  if (Bangle.isGPSOn()) {
+    if (listennerCount == 0) {
+      Bangle.on('GPS', processFix);
+      listennerCount++;
+      log_debug("listennerCount=" + listennerCount);
+    }
+  } else {
+    if (listennerCount > 0) {
+      Bangle.removeListener("GPS", processFix);
+      listennerCount--;
+      log_debug("listennerCount=" + listennerCount);
+    }
+  }
+  resetLastFix();
+}
+
+function loggerStatus() {
+  var settings = require("Storage").readJSON("gpsrec.json",1)||{};
+  if (settings == {}) return "Install";
+  return settings.recording ? "ON" : "OFF";
+}
+
+function toggleLogger() {
+  var settings = require("Storage").readJSON("gpsrec.json",1)||{};
+  if (settings == {}) return;
+
+  settings.recording = !settings.recording;
+  require("Storage").write("gpsrec.json", settings);
+
+  if (WIDGETS["gpsrec"])
+    WIDGETS["gpsrec"].reload();
+
+  if (settings.recording && listennerCount == 0) {
+    Bangle.on('GPS', processFix);
+    listennerCount++;
+    log_debug("listennerCount=" + listennerCount);
+  }
+}
+
+function formatTime(now) {
+  try {
+    var fd = now.toUTCString().split(" ");
+    return fd[4];
+  } catch (e) {
+    return "00:00:00";
+  }
+}
+
+const infoList = Object.keys(infoData).sort();
+let infoMode = infoList[0];
+
+function nextInfo() {
+  let idx = infoList.indexOf(infoMode);
+  if (idx > -1) {
+    if (idx === infoList.length - 1) infoMode = infoList[0];
+    else infoMode = infoList[idx + 1];
+  }
+}
+
+function prevInfo() {
+  let idx = infoList.indexOf(infoMode);
+  if (idx > -1) {
+    if (idx === 0) infoMode = infoList[infoList.length - 1];
+    else infoMode = infoList[idx - 1];
+  }
+}
+
+Bangle.on('swipe', dir => {
+  if (dir == 1) prevInfo(); else nextInfo();
+  draw();
+});
+
+let prevTouch = 0;
+
+Bangle.on('touch', function(button, xy) {
+  let dur = 1000*(getTime() - prevTouch);
+  prevTouch = getTime();
+
+  if (dur <= 1000 && xy.y < h/2 && infoData[infoMode].is_control) {
+    Bangle.buzz();
+    if (infoData[infoMode] && infoData[infoMode].action) {
+      infoData[infoMode].action();
+      draw();
+    }
+  }
+});
+
+// Stop updates when LCD is off, restart when on
+Bangle.on('lcdPower', on => {
+  if (secondInterval)
+    clearInterval(secondInterval);
+  secondInterval = undefined;
+  if (on)
+    secondInterval = setInterval(draw, 1000);
+  draw();
+});
+
+resetLastFix();
+
+// add listenner if already powered on, plus tag app
+if (Bangle.isGPSOn() || loggerStatus() == "ON") {
+  Bangle.setGPSPower(1, 'gpstouch');
+  if (listennerCount == 0) {
+    Bangle.on('GPS', processFix);
+    listennerCount++;
+    log_debug("listennerCount=" + listennerCount);
+  }
+}
+
+g.clear();
+var secondInterval = setInterval(draw, 1000);
+draw();
+// Show launcher when button pressed
+Bangle.setUI("clock");
+Bangle.loadWidgets();
+Bangle.drawWidgets();
diff --git a/apps/gpstouch/gpstouch.icon.js b/apps/gpstouch/gpstouch.icon.js
new file mode 100644
index 000000000..c4cf85676
--- /dev/null
+++ b/apps/gpstouch/gpstouch.icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4UA///j+EAYO/uYDB//wCYcPBA4AFh/ABZMDBbkX6gLIgtX6tQBY9VBYNVBY0BBYdABYsFqoACEgQLDitVtWpqtUBYtVq2q1WVGAQLErQLB0oLFHQNqBYIkBHgMDIwYKBAAJIDIweqz/2BYJtDBYI6Bv/9HgILHYwILGh4gBBYWfbooLF6AjPBYW//wLGL4Wv/RfGNZaDIBYibEBYizIBYjLDBYzXBd4TXCBZ60BBYRqEBZpUBBYRSFJAQLCA4b7BHgQLFgYLGIwYLEgoLBHQYLEgILBHQYLEgALBAoYLFi/UBZMHBZUD6ALKApQAFBbHwBZMP/4ABBwgIDA="))
diff --git a/apps/gpstouch/gpstouch.png b/apps/gpstouch/gpstouch.png
new file mode 100644
index 000000000..c411356ae
Binary files /dev/null and b/apps/gpstouch/gpstouch.png differ
diff --git a/apps/gpstouch/screenshot1.png b/apps/gpstouch/screenshot1.png
new file mode 100644
index 000000000..03cb1e2a9
Binary files /dev/null and b/apps/gpstouch/screenshot1.png differ
diff --git a/apps/gpstouch/screenshot2.png b/apps/gpstouch/screenshot2.png
new file mode 100644
index 000000000..a05794b34
Binary files /dev/null and b/apps/gpstouch/screenshot2.png differ
diff --git a/apps/gpstouch/screenshot3.png b/apps/gpstouch/screenshot3.png
new file mode 100644
index 000000000..9e3115d72
Binary files /dev/null and b/apps/gpstouch/screenshot3.png differ
diff --git a/apps/gpstouch/screenshot4.png b/apps/gpstouch/screenshot4.png
new file mode 100644
index 000000000..924371f5f
Binary files /dev/null and b/apps/gpstouch/screenshot4.png differ
diff --git a/apps/grocery/ChangeLog b/apps/grocery/ChangeLog
index 5560f00bc..906046782 100644
--- a/apps/grocery/ChangeLog
+++ b/apps/grocery/ChangeLog
@@ -1 +1,2 @@
 0.01: New App!
+0.02: Refactor code to store grocery list in separate file
diff --git a/apps/grocery/app.js b/apps/grocery/app.js
new file mode 100644
index 000000000..481efc3d9
--- /dev/null
+++ b/apps/grocery/app.js
@@ -0,0 +1,29 @@
+var filename = 'grocery_list.json';
+var settings = require("Storage").readJSON(filename,1)|| { products: [] };
+
+function updateSettings() {
+  require("Storage").writeJSON(filename, settings);
+  Bangle.buzz();
+}
+
+function twoChat(n){
+  if(n<10) return '0'+n;
+  return ''+n;
+}
+
+const mainMenu = settings.products.reduce(function(m, p, i){
+  const name = twoChat(p.quantity)+' '+p.name;
+  m[name] = {
+    value: p.ok,
+    format: v => v?'[x]':'[ ]',
+    onchange: v => {
+      settings.products[i].ok = v;
+      updateSettings();
+    }
+  };
+  return m;
+}, {
+  '': { 'title': 'Grocery list' }
+});
+mainMenu['< Back'] = ()=>{load();};
+E.showMenu(mainMenu);
diff --git a/apps/grocery/grocery.html b/apps/grocery/grocery.html
index 14c406d75..e717dee2e 100644
--- a/apps/grocery/grocery.html
+++ b/apps/grocery/grocery.html
@@ -105,56 +105,9 @@
       }
 
       document.getElementById("upload").addEventListener("click", function() {
-
-
-        var app = `
-var newTime = ${Date.now()}
-var products = ${JSON.stringify(products)}
-var newTime = newTime;
-var filename = 'grocery';
-var settings = require("Storage").readJSON(filename,1)|| null;
-function getSettings(){
-  return {
-    products : products,
-    date: newTime
-  };
-}
-if(!settings || !settings.date || settings.date < newTime){
-  settings = getSettings();
-  Bangle.buzz(500);
-}
-function updateSettings() {
-  require("Storage").writeJSON(filename, settings);
-  Bangle.buzz();
-}
-function twoChat(n){
-  if(n<10) return '0'+n;
-  return ''+n;
-}
-const mainMenu = settings.products.reduce(function(m, p, i){
-  const name = twoChat(p.quantity)+' '+p.name;
-  m[name] = {
-    value: p.ok,
-    format: v => v?'[x]':'[ ]',
-    onchange: v => {
-      settings.products[i].ok = v;
-      updateSettings();
-    }
-  };
-  return m;
-}, {
-  '': { 'title': 'Grocery list' }
-});
-mainMenu['< Back'] = ()=>{load();};
-E.showMenu(mainMenu);
-`;
-
-        var icon = `require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AQ0QACF1nGAAIxpFoYwqFwwwnRggwGB4eFAggACLzwHCMAeF1WGAgOGw2x2IGCLzYGEF4YpBwotCFwJfWFwo1GSAYtBAIIABRq4vFMhAwBzoAFdzIuKAAOc4IAGGC4qEMZOiF44wXFxovleBYvIGCwmB0WjE4V/AgfG1IvCzujFQOjwoECF6WFwovBDYOFEwN/AgIwCAgOFBwYrBBAQEBzodCF6AAHww1CBpIODAAYvRDAWG2IEBAYYJFBxICCF6Ox1WxAAQfBAYQlCAAIOJAQIvUADQvn1WGR4RfbP4gAFBwgFCF7a5EdwQADF46/cL9wAQF94AGF85bB1TvmF47vdJ4bvFF8qPRFgLv/L7jPCaQq/fYYrvgJgoAGd/7v/F/4v/F5oAdF54weFyAA/AH4A3A="))`;
         sendCustomizedApp({
           storage:[
-            {name:"grocery.app.js", url:"app.js", content:app},
-            {name:"grocery.img", content:icon, evaluate:true},
-            {name:"grocery"}
+            { name:"grocery_list.json", content: JSON.stringify({products: products}) }
           ]
         });
       });
diff --git a/apps/hcclock/ChangeLog b/apps/hcclock/ChangeLog
new file mode 100644
index 000000000..aaa55d01a
--- /dev/null
+++ b/apps/hcclock/ChangeLog
@@ -0,0 +1,2 @@
+0.01: base code
+0.02: saved settings when switching color scheme
\ No newline at end of file
diff --git a/apps/hcclock/README.md b/apps/hcclock/README.md
new file mode 100644
index 000000000..328f1fe03
--- /dev/null
+++ b/apps/hcclock/README.md
@@ -0,0 +1,13 @@
+# Hi-Contrast Clock
+
+A High-contrast, black-on-white or white-on-black clock displaying huge pixel digits. It is purposed for being both elegant and readable in high luminosity environments. The goal is to keep the clock as simple and efficient as possible.
+
+## Usage
+
+* BTN 1 switches between the two modes : black-on-white or white-on-black
+* That's it!
+
+## Issues and Requests
+
+If you have issues, feel free to contact me at https://github.com/peeweek/
+
diff --git a/apps/hcclock/bangle1-high-contrast-clock-screenshot.png b/apps/hcclock/bangle1-high-contrast-clock-screenshot.png
new file mode 100644
index 000000000..f3cd85e70
Binary files /dev/null and b/apps/hcclock/bangle1-high-contrast-clock-screenshot.png differ
diff --git a/apps/hcclock/hcclock-icon.js b/apps/hcclock/hcclock-icon.js
new file mode 100644
index 000000000..2486c6500
--- /dev/null
+++ b/apps/hcclock/hcclock-icon.js
@@ -0,0 +1 @@
+E.toArrayBuffer(atob("MDAB////////////////////////////////////////////////////////////////////////////////4AABgAAH4AABgAAH///5n//n///5n//n4AABn//n4AABn//n5///n//n5///n//n4AABgAAH4AABgAAH/////////////////////////015urF//3d+vZt//1V5uNV/////////////////5//5gAAH5//5gAAH5//5n//n5//5n//n4AABgAAH4AABgAAH///5n//n///5n//n///5gAAH///5gAAH////////////////////////////////////////////////////////////////////////////////"))
diff --git a/apps/hcclock/hcclock-icon.png b/apps/hcclock/hcclock-icon.png
new file mode 100644
index 000000000..5d5506249
Binary files /dev/null and b/apps/hcclock/hcclock-icon.png differ
diff --git a/apps/hcclock/hcclock.app.js b/apps/hcclock/hcclock.app.js
new file mode 100644
index 000000000..4664dd763
--- /dev/null
+++ b/apps/hcclock/hcclock.app.js
@@ -0,0 +1,243 @@
+
+//////////////////////////////////////////////////////
+// Numbers Rect order (left, top, right, bottom)
+// Each number defines a set of rects to draw
+
+const numbers = 
+[
+  [// Zero
+    [0, 0, 1, 0.2],
+    [0, 0.8, 1, 1],
+    [0, 0, 0.1, 1],
+    [0.9, 0, 1, 1]
+  ],
+  [// One
+    [0.7, 0, 1, 0.2],
+    [0.9, 0, 1, 1]
+  ],
+  [// Two
+    [0, 0, 1, 0.2],
+    [0, 0.4, 1, 0.6],
+    [0, 0.8, 1, 1],
+    [0, 0.4, 0.1, 1],
+    [0.9, 0, 1, 0.6]
+  ],
+  [// Three
+    [0, 0, 1, 0.2],
+    [0.5, 0.4, 1, 0.6],
+    [0, 0.8, 1, 1],
+    [0.9, 0, 1, 1]
+  ],
+  [// Four
+    [0, 0.4, 1, 0.6],
+    [0, 0, 0.1, 0.6],
+    [0.9, 0, 1, 1]
+  ],
+  [// Five
+    [0, 0, 1, 0.2],
+    [0, 0.4, 1, 0.6],
+    [0, 0.8, 1, 1],
+    [0, 0, 0.1, 0.6],
+    [0.9, 0.4, 1, 1]
+  ],
+  [// Six
+    [0, 0, 1, 0.2],
+    [0, 0.4, 1, 0.6],
+    [0, 0.8, 1, 1],
+    [0, 0, 0.1, 1.0],
+    [0.9, 0.4, 1, 1]
+  ],
+  [// Seven
+    [0.0, 0, 1, 0.2],
+    [0.9, 0, 1, 1]
+  ],
+  [// Eight
+    [0, 0, 1, 0.2],
+    [0, 0.4, 1, 0.6],
+    [0, 0.8, 1, 1],
+    [0, 0, 0.1, 1],
+    [0.9, 0, 1, 1]
+  ],
+  [// Nine
+    [0, 0, 1, 0.2],
+    [0, 0.4, 1, 0.6],
+    [0, 0.8, 1, 1],
+    [0, 0, 0.1, 0.6],
+    [0.9, 0, 1, 1]
+  ] 
+];
+
+const months = [ "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" ];
+
+const interval = 1000; // in ms
+const top = 32;
+
+let ampm = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
+
+let bg = 255;
+let fg = 0;
+
+let mins = -1;
+let hour = -1;
+let day = -1;
+
+function redraw() {
+  mins = -1;
+  hour = -1;
+  day = -1;
+  refresh();
+}
+
+function refresh() {
+  g.setColor(bg,bg,bg);
+  g.fillRect(0,45,240,210);
+  Bangle.drawWidgets();
+  updateTime();
+}
+
+function updateTime()
+{
+  let now = new Date();
+  let m = now.getMinutes();
+  let h = now.getHours();
+  let mo = now.getMonth();
+  let y = now.getFullYear();
+  let d = now.getDate();
+  
+  if(h != hour)
+  {
+    hour = h;
+    g.setColor(bg,bg,bg);
+    g.fillRect(0,60,240,110);
+    g.setColor(fg,fg,fg);
+    if(ampm)
+      h = h%12;
+    drawDigits(60, h);
+  }
+  if(m != mins)
+  {
+    mins = m;
+    g.setColor(bg,bg,bg);
+    g.fillRect(0,145,240,195);
+    g.setColor(fg,fg,fg);
+    drawDigits(145, mins);
+  }
+  if(d != day)
+  {
+    day = d;
+    g.setFont("6x8", 2);
+    g.setFontAlign(0, -1, 0);
+    g.drawString(fmtDate(d,mo,y,hour), 120, 120); 
+  }
+}
+
+function drawDigits(x, value)
+{
+  if(!Bangle.isLCDOn()) // No need to draw when LCD Off
+    return;
+  
+  drawChar(Math.floor(value/10),  15, x, 115, x+50);
+  if(value%10 == 1)
+    drawChar(value%10, 55, x, 155, x+50);
+  else
+    drawChar(value%10, 125, x, 225, x+50);
+}
+
+function drawChar(i, xMin, yMin, xMax, yMax)
+{
+   numbers[i].forEach(rect => {
+     r = place(rect, xMin, yMin, xMax, yMax);
+     g.setColor(fg,fg,fg);
+     g.fillRect(r[0], r[1], r[2], r[3]);
+    });
+}
+
+function place(array, xMin, yMin, xMax, yMax)
+{
+  return [
+   lerp(xMin,xMax,array[0]),
+   lerp(yMin,yMax,array[1]),
+   lerp(xMin,xMax,array[2]),
+   lerp(yMin,yMax,array[3])
+  ];
+}
+
+function lerp(a,b,t)
+{
+  return a + t*(b-a);
+}
+
+function fmtDate(day,month,year,hour)
+{
+  if(ampm)
+  {
+    let ap = "(AM)";
+    if(hour == 0 || hour > 12)
+      ap = "(PM)";
+    return months[month] + " " + day + " " + year + " "+ ap;
+  }
+  else
+    return months[month] + ". " + day + " " + year;
+}
+
+
+//////////////////////////////////////////
+//
+//  HANDLE COLORS + SETTINGS
+//
+
+function getColorScheme()
+{
+    let settings = require('Storage').readJSON("hcclock.json", true) || {};
+    if (!("scheme" in settings)) {
+      settings.scheme = 0;
+    }
+    return settings.scheme;
+}
+
+function setColorScheme(value)
+{
+    let settings = require('Storage').readJSON("hcclock.json", true) || {};
+    settings.scheme = value;
+    require('Storage').writeJSON('hcclock.json', settings);
+
+    if(value == 0) // White
+    {
+      bg = 255;
+      fg = 0;
+    }
+    else // Black
+    {
+      bg = 0;
+      fg = 255;
+    }
+    redraw();
+}
+
+function flipColors()
+{
+  if(getColorScheme() == 0)
+      setColorScheme(1);
+  else
+      setColorScheme(0);
+}
+
+//////////////////////////////////////////
+//
+//   MAIN FUNCTION()
+//
+
+// Initialize
+g.clear();
+Bangle.loadWidgets();
+setColorScheme(getColorScheme());
+
+// Define Refresh Interval
+setInterval(updateTime, interval);
+
+// Handle Button Press
+setWatch(flipColors, BTN1, true);
+setWatch(Bangle.showLauncher, BTN2, false);
+
+// Handle redraw on LCD on / fullscreen notifications dismissed
+Bangle.on('lcdPower', (on) => { if(on) redraw(); });
diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog
new file mode 100644
index 000000000..c65cc3ab4
--- /dev/null
+++ b/apps/health/ChangeLog
@@ -0,0 +1,10 @@
+0.01: New App!
+0.02: Modified data format to include daily summaries
+0.03: Settings to turn HRM on
+0.04: Add HRM graph view
+      Don't restart HRM when changing apps if we've already got a good BPM value
+0.05: Fix daily summary calculation
+0.06: Fix daily health summary for movement (a line got deleted!)
+0.07: Added coloured bar charts
+0.08: Suppress bleed through of E.showMenu's when displaying bar charts
+0.09: Fix file naming so months are 1-based (not 0) (fix #1119)
diff --git a/apps/health/README.md b/apps/health/README.md
new file mode 100644
index 000000000..c69e2e45b
--- /dev/null
+++ b/apps/health/README.md
@@ -0,0 +1,45 @@
+# Health Tracking
+
+Logs health data to a file every 10 minutes, and provides an app to view it
+
+**BETA - requires firmware 2v11**
+
+## Usage
+
+Once installed, health data is logged automatically.
+
+To view data, run the `Health` app from your watch.
+
+## Features
+
+Stores:
+
+* Heart rate
+* Step count
+* Movement
+
+## Settings
+
+* **Heart Rt** - Whether to monitor heart rate or not
+  * **Off** - Don't turn HRM on, but record heart rate if the HRM was turned on by another app/widget
+  * **10 Min** - Turn HRM on every 10 minutes (for each heath entry) and turn it off after 2 minutes, or when a good reading is found
+  * **Always** - Keep HRM on all the time (more accurate recording, but reduces battery life to ~36 hours)
+
+
+## Technical Info
+
+Once installed, the `health.boot.js` hooks onto the `Bangle.health` event and
+writes data to a binary file (one per month).
+
+A library (that can be used with `require("health").readXYZ` can then be used
+to grab historical health info.
+
+## TODO
+
+* `interface` page for desktop to allow data to be viewed and exported in common formats
+* More features in app:
+  * Step counting goal (ensure pedometers use this)
+  * Calendar view showing steps per day
+  * Yearly view
+  * Heart rate 'zone' graph
+  * .. other
diff --git a/apps/health/app-icon.js b/apps/health/app-icon.js
new file mode 100644
index 000000000..d522d9a9a
--- /dev/null
+++ b/apps/health/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4UA///8H5AYM7/5L/ACsBqtQAgMFqtABYcVqtVAgIDBqgLDAwITBDYNVrQiEAANQEQNVtWAFIYfCE4Xq0AuEAAdX1W0BZFe1XUHQgADvWrJogAE9WtBYl66ouD2oLEtQGBFwQQBBYgeBFwYjFA4QuCBYgfCFwYLCL4IICFwacCPwetEwYLCR4QJBFwbFCU4QhBFwbMDNAYuCHQQwFFwowFFwowFFwwwEFwzNGFwjxFFwowEFw7aFBQwwDFwwwEFwwwEFw4wDBRAwBFxAwCFxAwCFxIA/AB4A="))
diff --git a/apps/health/app.js b/apps/health/app.js
new file mode 100644
index 000000000..08d6ead17
--- /dev/null
+++ b/apps/health/app.js
@@ -0,0 +1,257 @@
+function getSettings() {
+  return require("Storage").readJSON("health.json",1)||{};
+}
+
+function setSettings(s) {
+  require("Storage").writeJSON("health.json",s);
+}
+
+function menuMain() {
+  swipe_enabled = false;
+  clearButton();
+  E.showMenu({
+    "":{title:"Health Tracking"},
+    "< Back":()=>load(),
+    "Step Counting":()=>menuStepCount(),
+    "Movement":()=>menuMovement(),
+    "Heart Rate":()=>menuHRM(),
+    "Settings":()=>menuSettings()
+  });
+}
+
+function menuSettings() {
+  swipe_enabled = false;
+  clearButton();
+  var s=getSettings();
+  E.showMenu({
+    "":{title:"Health Tracking"},
+    "< Back":()=>menuMain(),
+    "Heart Rt":{
+      value : 0|s.hrm,
+      min : 0, max : 2,
+      format : v=>["Off","10 mins","Always"][v],
+      onchange : v => { s.hrm=v;setSettings(s); }
+    }
+  });
+}
+
+function menuStepCount() {
+  swipe_enabled = false;
+  clearButton();
+  E.showMenu({
+    "":{title:"Step Counting"},
+    "< Back":()=>menuMain(),
+    "per hour":()=>stepsPerHour(),
+    "per day":()=>stepsPerDay()
+  });
+}
+
+function menuMovement() {
+  swipe_enabled = false;
+  clearButton();
+  E.showMenu({
+    "":{title:"Movement"},
+    "< Back":()=>menuMain(),
+    "per hour":()=>movementPerHour(),
+    "per day":()=>movementPerDay(),
+  });
+}
+
+function menuHRM() {
+  swipe_enabled = false;
+  clearButton();
+  E.showMenu({
+    "":{title:"Heart Rate"},
+    "< Back":()=>menuMain(),
+    "per hour":()=>hrmPerHour(),
+    "per day":()=>hrmPerDay(),
+  });
+}
+
+
+function stepsPerHour() {
+  E.showMessage("Loading...");
+  var data = new Uint16Array(24);
+  require("health").readDay(new Date(), h=>data[h.hr]+=h.steps);
+  g.clear(1);
+  Bangle.drawWidgets();
+  g.reset();
+  setButton(menuStepCount);
+  barChart("HOUR", data);
+}
+
+function stepsPerDay() {
+  E.showMessage("Loading...");
+  var data = new Uint16Array(31);
+  require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps);
+  g.clear(1);
+  Bangle.drawWidgets();
+  g.reset();
+  setButton(menuStepCount);
+  barChart("DAY", data);
+}
+
+function hrmPerHour() {
+  E.showMessage("Loading...");
+  var data = new Uint16Array(24);
+  var cnt = new Uint8Array(23);
+  require("health").readDay(new Date(), h=>{
+    data[h.hr]+=h.bpm;
+    if (h.bpm) cnt[h.hr]++;
+  });
+  data.forEach((d,i)=>data[i] = d/cnt[i]);
+  g.clear(1);
+  Bangle.drawWidgets();
+  g.reset();
+  setButton(menuHRM);
+  barChart("HOUR", data);
+}
+
+function hrmPerDay() {
+  E.showMessage("Loading...");
+  var data = new Uint16Array(31);
+  var cnt = new Uint8Array(31);
+  require("health").readDailySummaries(new Date(), h=>{
+    data[h.day]+=h.bpm;
+    if (h.bpm) cnt[h.day]++;
+  });
+  data.forEach((d,i)=>data[i] = d/cnt[i]);
+  g.clear(1);
+  Bangle.drawWidgets();
+  g.reset();
+  setButton(menuHRM);
+  barChart("DAY", data);
+}
+
+function movementPerHour() {
+  E.showMessage("Loading...");
+  var data = new Uint16Array(24);
+  require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
+  g.clear(1);
+  Bangle.drawWidgets();
+  g.reset();
+  setButton(menuMovement);
+  barChart("HOUR", data);
+}
+
+function movementPerDay() {
+  E.showMessage("Loading...");
+  var data = new Uint16Array(31);
+  require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement);
+  g.clear(1);
+  Bangle.drawWidgets();
+  g.reset();
+  setButton(menuMovement);
+  barChart("DAY", data);
+}
+
+// Bar Chart Code
+
+const w = g.getWidth();
+const h = g.getHeight();
+
+var data_len;
+var chart_index;
+var chart_max_datum;
+var chart_label;
+var chart_data;
+var swipe_enabled = false;
+var btn;
+
+// find the max value in the array, using a loop due to array size 
+function max(arr) {
+  var m = -Infinity;
+
+  for(var i=0; i< arr.length; i++)
+    if(arr[i] > m) m = arr[i];
+  return m;
+}
+
+// find the end of the data, the array might be for 31 days but only have 2 days of data in it
+function get_data_length(arr) {
+  var nlen = arr.length;
+  
+  for(var i = arr.length - 1; i > 0 && arr[i] == 0;  i--)
+    nlen--;
+  
+  return nlen;
+}
+
+function barChart(label, dt) {
+  data_len = get_data_length(dt);
+  chart_index = Math.max(data_len - 5, -5);  // choose initial index that puts the last day on the end
+  chart_max_datum = max(dt);                 // find highest bar, for scaling
+  chart_label = label;
+  chart_data = dt;
+  drawBarChart();
+  swipe_enabled = true;
+}
+
+function drawBarChart() {
+  const bar_bot = 140;
+  const bar_width = (w - 2) / 9;  // we want 9 bars, bar 5 in the centre
+  var bar_top;
+  var bar;
+  
+  g.setColor(g.theme.bg);
+  g.fillRect(0,24,w,h);
+  
+  for (bar = 1; bar < 10; bar++) {
+    if (bar == 5) {
+      g.setFont('6x8', 2);
+      g.setFontAlign(0,-1)
+      g.setColor(g.theme.fg);
+      g.drawString(chart_label + " " + (chart_index + bar -1) + "   " + chart_data[chart_index + bar - 1], g.getWidth()/2, 150);
+      g.setColor("#00f");
+    } else {
+      g.setColor("#0ff");
+    }
+
+    // draw a fake 0 height bar if chart_index is outside the bounds of the array
+    if ((chart_index + bar - 1) >= 0 && (chart_index + bar - 1) < data_len) 
+      bar_top = bar_bot - 100 * (chart_data[chart_index + bar - 1]) / chart_max_datum;
+    else
+      bar_top = bar_bot;
+
+    g.fillRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
+    g.setColor(g.theme.fg);
+    g.drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
+  }
+}
+
+function next_bar() {
+  chart_index = Math.min(data_len - 5, chart_index + 1);
+}
+
+function prev_bar() {
+  // HOUR data starts at index 0, DAY data starts at index 1
+  chart_index = Math.max((chart_label == "DAY") ? -3 : -4, chart_index - 1);
+}
+
+Bangle.on('swipe', dir => {
+  if (!swipe_enabled) return;
+  if (dir == 1) prev_bar(); else next_bar();
+  drawBarChart();
+});
+
+// use setWatch() as Bangle.setUI("updown",..) interacts with swipes
+function setButton(fn) {
+  // cancel callback, otherwise a slight up down movement will show the E.showMenu()
+  Bangle.setUI("updown", undefined);
+  
+  if (process.env.HWVERSION == 1)
+    btn = setWatch(fn, BTN2);
+  else
+    btn = setWatch(fn, BTN1);
+}
+
+function clearButton() {
+  if (btn !== undefined) {
+    clearWatch(btn);
+    btn = undefined;
+  }
+}
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+menuMain();
diff --git a/apps/health/app.png b/apps/health/app.png
new file mode 100644
index 000000000..04f1fee5e
Binary files /dev/null and b/apps/health/app.png differ
diff --git a/apps/health/boot.js b/apps/health/boot.js
new file mode 100644
index 000000000..c72e62b41
--- /dev/null
+++ b/apps/health/boot.js
@@ -0,0 +1,84 @@
+(function(){
+   var settings = require("Storage").readJSON("health.json",1)||{};
+   var hrm = 0|settings.hrm;
+   if (hrm==1) {
+     function onHealth() {
+       Bangle.setHRMPower(1, "health");
+       setTimeout(()=>Bangle.setHRMPower(0, "health"),2*60000); // give it 2 minutes
+     }
+     Bangle.on("health", onHealth);
+     Bangle.on('HRM', h => {
+       if (h.confidence>80) Bangle.setHRMPower(0, "health");
+     });
+     if (Bangle.getHealthStatus().bpmConfidence) return;
+     onHealth();
+   } else Bangle.setHRMPower(hrm!=0, "health");
+})();
+
+Bangle.on("health", health => {
+  // ensure we write health info for *last* block
+  var d = new Date(Date.now() - 590000);
+
+  const DB_RECORD_LEN = 4;
+  const DB_RECORDS_PER_HR = 6;
+  const DB_RECORDS_PER_DAY = DB_RECORDS_PER_HR*24 + 1/*summary*/;
+  const DB_RECORDS_PER_MONTH = DB_RECORDS_PER_DAY*31;
+  const DB_HEADER_LEN = 8;
+  const DB_FILE_LEN = DB_HEADER_LEN + DB_RECORDS_PER_MONTH*DB_RECORD_LEN;
+
+  function getRecordFN(d) {
+    return "health-"+d.getFullYear()+"-"+(d.getMonth()+1)+".raw";
+  }
+  function getRecordIdx(d) {
+    return (DB_RECORDS_PER_DAY*(d.getDate()-1)) +
+           (DB_RECORDS_PER_HR*d.getHours()) +
+           (0|(d.getMinutes()*DB_RECORDS_PER_HR/60));
+  }
+  function getRecordData(health) {
+    return String.fromCharCode(
+      health.steps>>8,health.steps&255, // 16 bit steps
+      health.bpm, // 8 bit bpm
+      Math.min(health.movement / 8, 255)); // movement
+  }
+
+  var rec = getRecordIdx(d);
+  var fn = getRecordFN(d);
+  var f = require("Storage").read(fn);
+  if (f) {
+    var dt = f.substr(DB_HEADER_LEN+(rec*DB_RECORD_LEN), DB_RECORD_LEN);
+    if (dt!="\xFF\xFF\xFF\xFF") {
+      print("HEALTH ERR: Already written!");
+      return;
+    }
+  } else {
+    require("Storage").write(fn, "HEALTH1\0", 0, DB_FILE_LEN); // header
+  }
+  var recordPos = DB_HEADER_LEN+(rec*DB_RECORD_LEN);
+  require("Storage").write(fn, getRecordData(health), recordPos, DB_FILE_LEN);
+  if (rec%DB_RECORDS_PER_DAY != DB_RECORDS_PER_DAY-2) return;
+  // we're at the end of the day. Read in all of the data for the day and sum it up
+  var sumPos = recordPos + DB_RECORD_LEN; // record after the current one is the sum
+  if (f.substr(sumPos, DB_RECORD_LEN)!="\xFF\xFF\xFF\xFF") {
+    print("HEALTH ERR: Daily summary already written!");
+    return;
+  }
+  health = { steps:0, bpm:0, movement:0, movCnt:0, bpmCnt:0};
+  var records = DB_RECORDS_PER_HR*24;
+  for (var i=0;i
+  
+    
+  
+  
+    
+ + + + + diff --git a/apps/health/lib.js b/apps/health/lib.js new file mode 100644 index 000000000..2e3e0c002 --- /dev/null +++ b/apps/health/lib.js @@ -0,0 +1,84 @@ +const DB_RECORD_LEN = 4; +const DB_RECORDS_PER_HR = 6; +const DB_RECORDS_PER_DAY = DB_RECORDS_PER_HR*24 + 1/*summary*/; +const DB_RECORDS_PER_MONTH = DB_RECORDS_PER_DAY*31; +const DB_HEADER_LEN = 8; +const DB_FILE_LEN = DB_HEADER_LEN + DB_RECORDS_PER_MONTH*DB_RECORD_LEN; + +function getRecordFN(d) { + return "health-"+d.getFullYear()+"-"+(d.getMonth()+1)+".raw"; +} +function getRecordIdx(d) { + return (DB_RECORDS_PER_DAY*(d.getDate()-1)) + + (DB_RECORDS_PER_HR*d.getHours()) + + (0|(d.getMinutes()*DB_RECORDS_PER_HR/60)); +} + +// Read all records from the given month +exports.readAllRecords = function(d, cb) { + var fn = getRecordFN(d); + var f = require("Storage").read(fn); + if (f===undefined) return; + var idx = DB_HEADER_LEN; + for (var day=0;day<31;day++) { + for (var hr=0;hr<24;hr++) { // actually 25, see below + for (var m=0;m (https://ionicabizau.net) + +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. \ No newline at end of file diff --git a/apps/hebrew_calendar/README.md b/apps/hebrew_calendar/README.md new file mode 100644 index 000000000..7a96a97db --- /dev/null +++ b/apps/hebrew_calendar/README.md @@ -0,0 +1,26 @@ +# Hebrew Calendar + +Displays the current hebrew calendar date +Add screen shots (if possible) to the app folder and link then into this file with ![](.png) + +## Usage + +Open the app, and it shows a menu with the date components + +## Features + +Shows the hebrew date, month, and year; alongside the gregorian date + +## Controls + +Name the buttons and what they are used for + +## Requests + +Michael Salaverry (github.com/barakplasma) + +## Creator + +Michael Salaverry +with help from https://github.com/IonicaBizau/hebrew-date (MIT license) + \ No newline at end of file diff --git a/apps/hebrew_calendar/app-icon.js b/apps/hebrew_calendar/app-icon.js new file mode 100644 index 000000000..372033d58 --- /dev/null +++ b/apps/hebrew_calendar/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cBz3v///s/immVkf27Xu+ec5PikmSpMt21JAoNbtgIBkm27IOCAQVt23JkmW7dt2VJku27YREpdkyQaBy3JkoRBloLDF4YCDHAQCBHAQ7DyVeAQN2km8BAPsBwfYAQMlDwYbGK4VdkmJlMk2fpCIVn6QRC5+kC4WRCIO774ICu+/CIXvvwRDAQOXt5gCl3eQIIyB3qNEAQP7CIUl+wRCpftCIwCglbmBABvRkzQBZoICLEoMAAB0JCIUUgNogXQgughdAi2AlARF2WBsEC4EEwEJoESwNlCLGLssW5UsylZtGy6IRFgiNLwARXrMk2VJbYPJkrYBCIsFyWLksWpRHClGyqNk0BrIoBrCgB9KssC5QRBrNABAQRIgFJomC5AIECIkSgAEBrMsy1KgGQgMgCI8sagJ6BqEFygRHi1QgmUCgICCwELdIxoBQAOALIMBPQJuBCI3KKYImBEAMlJoNACIpoBoEWpJZBpMogRuBCI0JHwLIByRrBiwiCCIovCLIY7CCI5TBBwJHBCgJfBCI7+CLIb4CCJAvBLIY7CCJMC7JZBpZXDCJA4CGQgRbgXbtmW7Y1MLIJHBLIkC6ARGBwIRCBQYRigu2CI4AHCIOACJxrEQ44AHpIA=")) diff --git a/apps/hebrew_calendar/app.js b/apps/hebrew_calendar/app.js new file mode 100644 index 000000000..399d124f3 --- /dev/null +++ b/apps/hebrew_calendar/app.js @@ -0,0 +1,26 @@ +g.clear(); + +let now = new Date(); + +let today = require('hebrewDate').hebrewDate(now); + +var mainmenu = { + "": { + "title": "Hebrew Date" + }, + greg: { + // @ts-ignore + value: require('locale').date(now, 1), + }, + date: { + value: today.date, + }, + month: { + value: today.month_name, + }, + year: { + value: today.year, + } +}; +// @ts-ignore +E.showMenu(mainmenu); \ No newline at end of file diff --git a/apps/hebrew_calendar/app.png b/apps/hebrew_calendar/app.png new file mode 100644 index 000000000..ad9ec9af7 Binary files /dev/null and b/apps/hebrew_calendar/app.png differ diff --git a/apps/hebrew_calendar/hebrewDate.js b/apps/hebrew_calendar/hebrewDate.js new file mode 100644 index 000000000..da0c9cf50 --- /dev/null +++ b/apps/hebrew_calendar/hebrewDate.js @@ -0,0 +1,311 @@ +/*! + * This script was taked from this page http://www.shamash.org/help/javadate.shtml and ported to Node.js by Ionică Bizău in https://github.com/IonicaBizau/hebrew-date + * + * This script was adapted from C sources written by + * Scott E. Lee, which contain the following copyright notice: + * + * Copyright 1993-1995, Scott E. Lee, all rights reserved. + * Permission granted to use, copy, modify, distribute and sell so long as + * the above copyright and this permission statement are retained in all + * copies. THERE IS NO WARRANTY - USE AT YOUR OWN RISK. + * + * Bill Hastings + * RBI Software Systems + * bhastings@rbi.com + */ +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; +var GREG_SDN_OFFSET = 32045, DAYS_PER_5_MONTHS = 153, DAYS_PER_4_YEARS = 1461, DAYS_PER_400_YEARS = 146097; +var HALAKIM_PER_HOUR = 1080, HALAKIM_PER_DAY = 25920, HALAKIM_PER_LUNAR_CYCLE = 29 * HALAKIM_PER_DAY + 13753, HALAKIM_PER_METONIC_CYCLE = HALAKIM_PER_LUNAR_CYCLE * (12 * 19 + 7); +var HEB_SDN_OFFSET = 347997, NEW_MOON_OF_CREATION = 31524, NOON = 18 * HALAKIM_PER_HOUR, AM3_11_20 = 9 * HALAKIM_PER_HOUR + 204, AM9_32_43 = 15 * HALAKIM_PER_HOUR + 589; +var SUN = 0, MON = 1, TUES = 2, WED = 3, THUR = 4, FRI = 5, SAT = 6; +function weekdayarr(d0, d1, d2, d3, d4, d5, d6) { + this[0] = d0; + this[1] = d1; + this[2] = d2; + this[3] = d3; + this[4] = d4; + this[5] = d5; + this[6] = d6; +} +function gregmontharr(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11) { + this[0] = m0; + this[1] = m1; + this[2] = m2; + this[3] = m3; + this[4] = m4; + this[5] = m5; + this[6] = m6; + this[7] = m7; + this[8] = m8; + this[9] = m9; + this[10] = m10; + this[11] = m11; +} +function hebrewmontharr(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13) { + this[0] = m0; + this[1] = m1; + this[2] = m2; + this[3] = m3; + this[4] = m4; + this[5] = m5; + this[6] = m6; + this[7] = m7; + this[8] = m8; + this[9] = m9; + this[10] = m10; + this[11] = m11; + this[12] = m12; + this[13] = m13; +} +function monthsperyeararr(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17, m18) { + this[0] = m0; + this[1] = m1; + this[2] = m2; + this[3] = m3; + this[4] = m4; + this[5] = m5; + this[6] = m6; + this[7] = m7; + this[8] = m8; + this[9] = m9; + this[10] = m10; + this[11] = m11; + this[12] = m12; + this[13] = m13; + this[14] = m14; + this[15] = m15; + this[16] = m16; + this[17] = m17; + this[18] = m18; +} +var gWeekday = new weekdayarr("Sun", "Mon", "Tues", "Wednes", "Thurs", "Fri", "Satur"), gMonth = new gregmontharr("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"), hMonth = new hebrewmontharr("Tishri", "Heshvan", "Kislev", "Tevet", "Shevat", "AdarI", "AdarII", "Nisan", "Iyyar", "Sivan", "Tammuz", "Av", "Elul"), mpy = new monthsperyeararr(12, 12, 13, 12, 12, 13, 12, 13, 12, 12, 13, 12, 12, 13, 12, 12, 13, 12, 13); +/** + * hebrewDate + * Convert the Gregorian dates into Hebrew calendar dates. + * + * @name hebrewDate + * @function + * @param {Date|Number} inputDate The date object (representing the Gregorian date) or the year. + * @return {Object} An object containing: + * + * - `year`: The Hebrew year. + * - `month`: The Hebrew month. + * - `month_name`: The Hebrew month name. + * - `date`: The Hebrew date. + */ +function hebrewDate(inputDateOrYear) { + var inputMonth, inputDate; + var hebrewMonth = 0, hebrewDate = 0, hebrewYear = 0, metonicCycle = 0, metonicYear = 0, moladDay = 0, moladHalakim = 0; + function GregorianToSdn(inputYear, inputMonth, inputDay) { + var year = 0, month = 0, sdn = void 0; + // Make year a positive number + if (inputYear < 0) { + year = inputYear + 4801; + } + else { + year = inputYear + 4800; + } + // Adjust the start of the year + if (inputMonth > 2) { + month = inputMonth - 3; + } + else { + month = inputMonth + 9; + year--; + } + sdn = Math.floor(Math.floor(year / 100) * DAYS_PER_400_YEARS / 4); + sdn += Math.floor(year % 100 * DAYS_PER_4_YEARS / 4); + sdn += Math.floor((month * DAYS_PER_5_MONTHS + 2) / 5); + sdn += inputDay - GREG_SDN_OFFSET; + return sdn; + } + function SdnToHebrew(sdn) { + var tishri1 = 0, tishri1After = 0, yearLength = 0, inputDay = sdn - HEB_SDN_OFFSET; + FindTishriMolad(inputDay); + tishri1 = Tishri1(metonicYear, moladDay, moladHalakim); + if (inputDay >= tishri1) { + // It found Tishri 1 at the start of the year. + hebrewYear = metonicCycle * 19 + metonicYear + 1; + if (inputDay < tishri1 + 59) { + if (inputDay < tishri1 + 30) { + hebrewMonth = 1; + hebrewDate = inputDay - tishri1 + 1; + } + else { + hebrewMonth = 2; + hebrewDate = inputDay - tishri1 - 29; + } + return; + } + // We need the length of the year to figure this out,so find Tishri 1 of the next year. + moladHalakim += HALAKIM_PER_LUNAR_CYCLE * mpy[metonicYear]; + moladDay += Math.floor(moladHalakim / HALAKIM_PER_DAY); + moladHalakim = moladHalakim % HALAKIM_PER_DAY; + tishri1After = Tishri1((metonicYear + 1) % 19, moladDay, moladHalakim); + } + else { + // It found Tishri 1 at the end of the year. + hebrewYear = metonicCycle * 19 + metonicYear; + if (inputDay >= tishri1 - 177) { + // It is one of the last 6 months of the year. + if (inputDay > tishri1 - 30) { + hebrewMonth = 13; + hebrewDate = inputDay - tishri1 + 30; + } + else if (inputDay > tishri1 - 60) { + hebrewMonth = 12; + hebrewDate = inputDay - tishri1 + 60; + } + else if (inputDay > tishri1 - 89) { + hebrewMonth = 11; + hebrewDate = inputDay - tishri1 + 89; + } + else if (inputDay > tishri1 - 119) { + hebrewMonth = 10; + hebrewDate = inputDay - tishri1 + 119; + } + else if (inputDay > tishri1 - 148) { + hebrewMonth = 9; + hebrewDate = inputDay - tishri1 + 148; + } + else { + hebrewMonth = 8; + hebrewDate = inputDay - tishri1 + 178; + } + return; + } + else { + if (mpy[(hebrewYear - 1) % 19] == 13) { + hebrewMonth = 7; + hebrewDate = inputDay - tishri1 + 207; + if (hebrewDate > 0) + return; + hebrewMonth--; + hebrewDate += 30; + if (hebrewDate > 0) + return; + hebrewMonth--; + hebrewDate += 30; + } + else { + hebrewMonth = 6; + hebrewDate = inputDay - tishri1 + 207; + if (hebrewDate > 0) + return; + hebrewMonth--; + hebrewDate += 30; + } + if (hebrewDate > 0) + return; + hebrewMonth--; + hebrewDate += 29; + if (hebrewDate > 0) + return; + // We need the length of the year to figure this out,so find Tishri 1 of this year. + tishri1After = tishri1; + FindTishriMolad(moladDay - 365); + tishri1 = Tishri1(metonicYear, moladDay, moladHalakim); + } + } + yearLength = tishri1After - tishri1; + moladDay = inputDay - tishri1 - 29; + if (yearLength == 355 || yearLength == 385) { + // Heshvan has 30 days + if (moladDay <= 30) { + hebrewMonth = 2; + hebrewDate = moladDay; + return; + } + moladDay -= 30; + } + else { + // Heshvan has 29 days + if (moladDay <= 29) { + hebrewMonth = 2; + hebrewDate = moladDay; + return; + } + moladDay -= 29; + } + // It has to be Kislev. + hebrewMonth = 3; + hebrewDate = moladDay; + } + function FindTishriMolad(inputDay) { + // Estimate the metonic cycle number. Note that this may be an under + // estimate because there are 6939.6896 days in a metonic cycle not + // 6940,but it will never be an over estimate. The loop below will + // correct for any error in this estimate. + metonicCycle = Math.floor((inputDay + 310) / 6940); + // Calculate the time of the starting molad for this metonic cycle. + MoladOfMetonicCycle(); + // If the above was an under estimate,increment the cycle number until + // the correct one is found. For modern dates this loop is about 98.6% + // likely to not execute,even once,because the above estimate is + // really quite close. + while (moladDay < inputDay - 6940 + 310) { + metonicCycle++; + moladHalakim += HALAKIM_PER_METONIC_CYCLE; + moladDay += Math.floor(moladHalakim / HALAKIM_PER_DAY); + moladHalakim = moladHalakim % HALAKIM_PER_DAY; + } + // Find the molad of Tishri closest to this date. + for (metonicYear = 0; metonicYear < 18; metonicYear++) { + if (moladDay > inputDay - 74) + break; + moladHalakim += HALAKIM_PER_LUNAR_CYCLE * mpy[metonicYear]; + moladDay += Math.floor(moladHalakim / HALAKIM_PER_DAY); + moladHalakim = moladHalakim % HALAKIM_PER_DAY; + } + } + function MoladOfMetonicCycle() { + var r1 = void 0, r2 = void 0, d1 = void 0, d2 = void 0; + // Start with the time of the first molad after creation. + r1 = NEW_MOON_OF_CREATION; + // Calculate gMetonicCycle * HALAKIM_PER_METONIC_CYCLE. The upper 32 + // bits of the result will be in r2 and the lower 16 bits will be in r1. + r1 += metonicCycle * (HALAKIM_PER_METONIC_CYCLE & 0xFFFF); + r2 = r1 >> 16; + r2 += metonicCycle * (HALAKIM_PER_METONIC_CYCLE >> 16 & 0xFFFF); + // Calculate r2r1 / HALAKIM_PER_DAY. The remainder will be in r1,the + // upper 16 bits of the quotient will be in d2 and the lower 16 bits + // will be in d1. + d2 = Math.floor(r2 / HALAKIM_PER_DAY); + r2 -= d2 * HALAKIM_PER_DAY; + r1 = r2 << 16 | r1 & 0xFFFF; + d1 = Math.floor(r1 / HALAKIM_PER_DAY); + r1 -= d1 * HALAKIM_PER_DAY; + moladDay = d2 << 16 | d1; + moladHalakim = r1; + } + function Tishri1(metonicYear, moladDay, moladHalakim) { + var tishri1 = moladDay, dow = tishri1 % 7, leapYear = metonicYear == 2 || metonicYear == 5 || metonicYear == 7 || metonicYear == 10 || metonicYear == 13 || metonicYear == 16 || metonicYear == 18, lastWasLeapYear = metonicYear == 3 || metonicYear == 6 || metonicYear == 8 || metonicYear == 11 || metonicYear == 14 || metonicYear == 17 || metonicYear == 0; + // Apply rules 2,3 and 4 + if (moladHalakim >= NOON || !leapYear && dow == TUES && moladHalakim >= AM3_11_20 || lastWasLeapYear && dow == MON && moladHalakim >= AM9_32_43) { + tishri1++; + dow++; + if (dow == 7) + dow = 0; + } + // Apply rule 1 after the others because it can cause an additional delay of one day. + if (dow == WED || dow == FRI || dow == SUN) { + tishri1++; + } + return tishri1; + } + var inputYear = inputDateOrYear; + if ((typeof inputYear === "undefined" ? "undefined" : _typeof(inputYear)) === "object") { + inputMonth = inputDateOrYear.getMonth() + 1; + inputDate = inputDateOrYear.getDate(); + inputYear = inputDateOrYear.getFullYear(); + } + SdnToHebrew(GregorianToSdn(inputYear, inputMonth, inputDate)); + return { + year: hebrewYear, + month: hebrewMonth, + date: hebrewDate, + month_name: hMonth[hebrewMonth - 1] + }; +} + +exports.hebrewDate = hebrewDate; diff --git a/apps/hidmsicswipe/changelog b/apps/hidmsicswipe/changelog new file mode 100644 index 000000000..df3737358 --- /dev/null +++ b/apps/hidmsicswipe/changelog @@ -0,0 +1 @@ +0.01: Core functionnality based entirely on hidmsic diff --git a/apps/hidmsicswipe/hidmsicswipe-icon.js b/apps/hidmsicswipe/hidmsicswipe-icon.js new file mode 100644 index 000000000..6a0c64b9c --- /dev/null +++ b/apps/hidmsicswipe/hidmsicswipe-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A5xGICquZzAVUAAIXQCogXQCoxHPCox0BxIXNxIVFBAQXPUAwXPBw4XowAvuC/4X/C9sIC6kIxGZzIXSFgIWBC6QWEC6RECAAOJwAXQFwoXLxAqBC4MICweZCxhWEC4mICxxuDA4I3BCxQ/FQxpyEK6AucC4idMI5OICyQwBQpgA/AH4Au")) diff --git a/apps/hidmsicswipe/hidmsicswipe.js b/apps/hidmsicswipe/hidmsicswipe.js new file mode 100644 index 000000000..e0fc760a4 --- /dev/null +++ b/apps/hidmsicswipe/hidmsicswipe.js @@ -0,0 +1,93 @@ +var storage = require('Storage'); + +const settings = storage.readJSON('setting.json',1) || { HID: false }; + +var sendHid, next, prev, toggle, up, down, profile; +var lasty = 0; +var lastx = 0; + +if (settings.HID=="kbmedia") { + profile = 'Music'; + sendHid = function (code, cb) { + try { + NRF.sendHIDReport([1,code], () => { + NRF.sendHIDReport([1,0], () => { + if (cb) cb(); + }); + }); + } catch(e) { + print(e); + } + }; + next = function (cb) { sendHid(0x01, cb); }; + prev = function (cb) { sendHid(0x02, cb); }; + toggle = function (cb) { sendHid(0x10, cb); }; + up = function (cb) {sendHid(0x40, cb); }; + down = function (cb) { sendHid(0x80, cb); }; +} else { + E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) { + if (enable) { + settings.HID = "kbmedia"; + require("Storage").write('setting.json', settings); + setTimeout(load, 1000, "hidmsicswipe.app.js"); + } else setTimeout(load, 1000); + }); +} + +function drawApp() { + g.clear(); + if(Bangle.isLocked()==false) E.showMessage('Swipe', 'Music'); + else E.showMessage('Locked', 'Music'); +} + +if (next) { + setWatch(function(e) { + var len = e.time - e.lastTime; + E.showMessage('lock'); + setTimeout(drawApp, 1000); + Bangle.setLocked(true); + }, BTN1, { edge:"falling",repeat:true,debounce:50}); + Bangle.on('drag', function(e) { + if(!e.b){ + //console.log(lasty); + //console.log(lastx); + if(lasty > 40){ + E.showMessage('down'); + setTimeout(drawApp, 1000); + down(() => {}); + } + else if(lasty < -40){ + E.showMessage('up'); + setTimeout(drawApp, 1000); + up(() => {}); + } else if(lastx < -40){ + E.showMessage('prev'); + setTimeout(drawApp, 1000); + prev(() => {}); + } else if(lastx > 40){ + E.showMessage('next'); + setTimeout(drawApp, 1000); + next(() => {}); + } else if(lastx==0 && lasty==0){ + E.showMessage('play/pause'); + setTimeout(drawApp, 1000); + toggle(() => {}); + } + lastx = 0; + lasty = 0; + } + else{ + lastx = lastx + e.dx; + lasty = lasty + e.dy; + } + }); + + Bangle.on("lock", function(on) { + if(!on){ + E.showMessage('unlock'); + setTimeout(drawApp, 1000); + } + }); + + drawApp(); +} diff --git a/apps/hidmsicswipe/hidmsicswipe.png b/apps/hidmsicswipe/hidmsicswipe.png new file mode 100644 index 000000000..923b5aa0e Binary files /dev/null and b/apps/hidmsicswipe/hidmsicswipe.png differ diff --git a/apps/hourstrike/ChangeLog b/apps/hourstrike/ChangeLog index 73b8cb168..09eb45b36 100644 --- a/apps/hourstrike/ChangeLog +++ b/apps/hourstrike/ChangeLog @@ -5,3 +5,4 @@ 0.05: Add display for the next strike time 0.06: Move the next strike time to the first row of display 0.07: Change the boot function to avoid reloading the entire watch +0.08: Default to no strikes. Fix file-not-found issue during the first boot. Add data file. diff --git a/apps/hourstrike/app.js b/apps/hourstrike/app.js index c70fa2d41..7dc62d440 100644 --- a/apps/hourstrike/app.js +++ b/apps/hourstrike/app.js @@ -1,25 +1,10 @@ const storage = require('Storage'); -let settings; +var settings = storage.readJSON('hourstrike.json', 1); function updateSettings() { storage.write('hourstrike.json', settings); } -function resetSettings() { - settings = { - interval: 3600, - start: 9, - end: 21, - vlevel: 0.5, - next_hour: -1, - next_minute: -1, - }; - updateSettings(); -} - -settings = storage.readJSON('hourstrike.json', 1); -if (!settings) resetSettings(); - function showMainMenu() { var mode_txt = ['Off','1 min','5 min','10 min','1/4 h','1/2 h','1 h']; var mode_interval = [-1,60,300,600,900,1800,3600]; diff --git a/apps/hourstrike/boot.js b/apps/hourstrike/boot.js index 8ddad31af..027b8bb5b 100644 --- a/apps/hourstrike/boot.js +++ b/apps/hourstrike/boot.js @@ -1,6 +1,6 @@ (function() { function setup () { - var settings = require('Storage').readJSON('hourstrike.json',1)||[]; + var settings = require('Storage').readJSON('hourstrike.json',1); var t = new Date(); var t_min_sec = t.getMinutes()*60+t.getSeconds(); var wait_msec = settings.interval>0?(settings.interval-t_min_sec%settings.interval)*1000:-1; diff --git a/apps/hourstrike/hourstrike.json b/apps/hourstrike/hourstrike.json new file mode 100644 index 000000000..09b17dc8e --- /dev/null +++ b/apps/hourstrike/hourstrike.json @@ -0,0 +1 @@ +{"interval":-1,"start":9,"end":21,"vlevel":0.5,"next_hour":-1,"next_minute":-1} diff --git a/apps/hrings/bangle1-hypno-rings-screenshot.png b/apps/hrings/bangle1-hypno-rings-screenshot.png new file mode 100644 index 000000000..66f8bcba2 Binary files /dev/null and b/apps/hrings/bangle1-hypno-rings-screenshot.png differ diff --git a/apps/hrm/ChangeLog b/apps/hrm/ChangeLog index 1efe78c07..9b390b63e 100644 --- a/apps/hrm/ChangeLog +++ b/apps/hrm/ChangeLog @@ -1,3 +1,6 @@ 0.01: New App! 0.02: Use HRM data and calculations from Bangle.js (don't access hardware directly) 0.03: Fix timing issues, and use 1/2 scale to keep graph on screen +0.04: Update for new firmwares that have a 'HRM-raw' event +0.05: Tweaks for 'HRM-raw' handling +0.06: Add widgets diff --git a/apps/hrm/heartrate-icon.js b/apps/hrm/heartrate-icon.js index cadbc7dfa..20c9b15f7 100644 --- a/apps/hrm/heartrate-icon.js +++ b/apps/hrm/heartrate-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwghC/AH4AThnMAAXABJoMHBwgJJAAYMFAAIJLFxImCBJIuLABYuI4gXNNZFCC6AIFkZIQA4szC6vEmdMC60sC6nDmc8C6RDBC4irLC4gTBocymgGBoYXO4UyUwNEAYKrMC4ZEBUwNMVAR7LC4dDCoYBBSYJ7DoZQCC4kCmczkc0JIVM4UzmgaBAAQWD4AXBggJBJAIkBocs4c0BAQXJJARBD4c8oc8HAKZCI4gWCVAYXEJIJoCOovNC4cMUIQPB4RFBTAYAFIwapEC4JyCZAalHGAvCJYZYCVAYuIMIhjE5heGCwxhDMYTtIFw4wFoYsGFxIwF4YuRGAh7DFxxhGFyIYKCxqrGIpwwKFx4YGCyJJFCyQYDCygA/AH4AFA=")) +require("heatshrink").decompress(atob("mEw4UA///g3yrv/7f+Jf4AJgNVoAEGAANVAAIEGCIQABoAEEBYMFAwVQAggLBioGCqgEEFIgAGFwdXBYw1Dr4LKrwLHIIVaBYxNDvXVBanVteVBZGVt+VKooLBq+19u1JItQgNW0vlBYIxEL4Ne1u18taGIN9BYUD1XvBYN62+q1a0D1d7ytttYLEWYV6BYNt93VEYKzCita6t59vqX4sFIgN70tqa4pUBTgO1vbvFgB0BKQNZawYACdYNeytdFwgwCBYJ2DFwQwCqoxBFwwABBYoKEGAKyDFwgwDFw4kDERBVDEQ4kEEQ4kDBRAYBERBuCNAoA/AA4=")) diff --git a/apps/hrm/heartrate.js b/apps/hrm/heartrate.js index 1ec0a31d2..a47251010 100644 --- a/apps/hrm/heartrate.js +++ b/apps/hrm/heartrate.js @@ -3,23 +3,33 @@ Bangle.setLCDTimeout(0); Bangle.setHRMPower(1); var hrmInfo, hrmOffset = 0; var hrmInterval; +var btm = g.getHeight()-1; +var lastHrmPt = []; // last xy coords we draw a line to + function onHRM(h) { - // this is the first time we're called if (counter!==undefined) { + // the first time we're called remove + // the countdown counter = undefined; - g.clear(); + g.clearRect(0,24,g.getWidth(),g.getHeight()); } hrmInfo = h; - hrmOffset = 0; + /* On 2v09 and earlier firmwares the only solution for realtime + HRM was to look at the 'raw' array that got reported. If you timed + it right you could grab the data pretty much as soon as it was written. + In new firmwares, '.raw' is not available. */ if (hrmInterval) clearInterval(hrmInterval); hrmInterval = undefined; - setTimeout(function() { - hrmInterval = setInterval(readHRM,41); - }, 40); + if (hrmInfo.raw) { + hrmOffset = 0; + setTimeout(function() { + hrmInterval = setInterval(readHRM,41); + }, 40); + } var px = g.getWidth()/2; g.setFontAlign(0,0); - g.clearRect(0,24,239,90); + g.clearRect(0,24,g.getWidth(),80); g.setFont("6x8").drawString("Confidence "+hrmInfo.confidence+"%", px, 75); var str = hrmInfo.bpm; g.setFontVector(40).drawString(str,px,45); @@ -28,13 +38,40 @@ function onHRM(h) { g.drawString("BPM",px+15,45); } Bangle.on('HRM', onHRM); +/* On newer (2v10) firmwares we can subscribe to get +HRM events as they happen */ +Bangle.on('HRM-raw', function(v) { + hrmOffset++; + if (hrmOffset>g.getWidth()) { + hrmOffset=0; + g.clearRect(0,80,g.getWidth(),g.getHeight()); + lastHrmPt = [-100,0]; + } + + y = E.clip(btm-v.filt/4,btm-10,btm); + g.setColor(1,0,0).fillRect(hrmOffset,btm, hrmOffset, y); + y = E.clip(170 - (v.raw/2),80,btm); + g.setColor(g.theme.fg).drawLine(lastHrmPt[0],lastHrmPt[1],hrmOffset, y); + lastHrmPt = [hrmOffset, y]; + if (counter !==undefined) { + counter = undefined; + g.clearRect(0,24,g.getWidth(),g.getHeight()); + } +}); // It takes 5 secs for us to get the first HRM event var counter = 5; function countDown() { - E.showMessage("Please wait...\n"+counter--); - if (counter) setTimeout(countDown, 1000); + if (counter) { + g.drawString(counter--,g.getWidth()/2,g.getHeight()/2, true); + setTimeout(countDown, 1000); + } } +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +g.reset().setFont("6x8",2).setFontAlign(0,0); +g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16); countDown(); @@ -47,14 +84,14 @@ function readHRM() { if (!hrmInfo) return; if (hrmOffset==0) { - g.clearRect(0,100,239,239); - g.moveTo(-100,0); + g.clearRect(0,100,g.getWidth(),g.getHeight()); + lastHrmPt = [-100,0]; } for (var i=0;i<2;i++) { var a = hrmInfo.raw[hrmOffset]; hrmOffset++; y = E.clip(170 - (a*2),100,230); - g.setColor(1,1,1); - g.lineTo(hrmOffset, y); + g.setColor(g.theme.fg).drawLine(lastHrmPt[0],lastHrmPt[1],hrmOffset, y); + lastHrmPt = [hrmOffset, y]; } } diff --git a/apps/imgclock/ChangeLog b/apps/imgclock/ChangeLog index 20906fb87..01a6a4248 100644 --- a/apps/imgclock/ChangeLog +++ b/apps/imgclock/ChangeLog @@ -5,4 +5,5 @@ Scaling for background images <240px wide 0.05: Fix memory/interval leak when LCD turns on 0.06: Support 12 hour time -0.07: Don't cut off wide date formats \ No newline at end of file +0.07: Don't cut off wide date formats +0.08: Use Bangle.setUI for button/launcher handling diff --git a/apps/imgclock/app.js b/apps/imgclock/app.js index 751647a69..0e4435638 100644 --- a/apps/imgclock/app.js +++ b/apps/imgclock/app.js @@ -84,5 +84,5 @@ Bangle.on('lcdPower',on=>{ draw(); } }); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); +// Show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/impwclock/ChangeLog b/apps/impwclock/ChangeLog index c6974d37c..7bc119426 100644 --- a/apps/impwclock/ChangeLog +++ b/apps/impwclock/ChangeLog @@ -1,2 +1,4 @@ 0.01: New App! 0.02: Stopped watchface from flashing every interval +0.03: Move to Bangle.setUI to launcher support +0.04: Tweaks for compatibility with BangleJS2 diff --git a/apps/impwclock/README.md b/apps/impwclock/README.md index 30e42c95e..ac1341097 100644 --- a/apps/impwclock/README.md +++ b/apps/impwclock/README.md @@ -1,4 +1,4 @@ # Imprecise Word Clock -This clock tells time in very rough approximation, as in "Late morning" or "Early afternoon." Good for vacations and weekends. Press button 1 to see the time in accurate, digital form. But do you really need to know the exact time? +This clock tells time in very rough approximation, as in "Late morning" or "Early afternoon." Good for vacations and weekends. Touch the screen to see the time in accurate, digital form. But do you really need to know the exact time? diff --git a/apps/impwclock/bangle1-impercise-word-clock-screenshot.png b/apps/impwclock/bangle1-impercise-word-clock-screenshot.png new file mode 100644 index 000000000..9521c06a0 Binary files /dev/null and b/apps/impwclock/bangle1-impercise-word-clock-screenshot.png differ diff --git a/apps/impwclock/clock-impword.js b/apps/impwclock/clock-impword.js index 94b92b778..8bb5da6ba 100644 --- a/apps/impwclock/clock-impword.js +++ b/apps/impwclock/clock-impword.js @@ -2,7 +2,7 @@ A remix of word clock by Gordon Williams https://github.com/gfwilliams - Changes the representation of time to be more general -- Shows accurate digital time when button 1 is pressed +- Toggles showing of accurate digital time when screen touched. */ /* jshint esversion: 6 */ @@ -34,23 +34,24 @@ const timeOfDay = { }; +var big = g.getWidth()>200; // offsets and increments -const xs = 35; -const ys = 31; -const dy = 22; -const dx = 25; +const xs = big ? 35 : 20; +const ys = big ? 31 : 28; +const dx = big ? 25 : 20; +const dy = big ? 22 : 16; + // font size and color -const fontSize = 3; // "6x8" +const fontSize = big ? 3 : 2; // "6x8" const passivColor = 0x3186 /*grey*/ ; const activeColorNight = 0xF800 /*red*/ ; const activeColorDay = 0xFFFF /* white */; var hidxPrev; +var showDigitalTime = false; function drawWordClock() { - - // get time var t = new Date(); var h = t.getHours(); @@ -116,6 +117,8 @@ function drawWordClock() { // check whether we need to redraw the watchface if (hidx !== hidxPrev) { + // Turn off showDigitalTime + showDigitalTime = false; // draw allWords var c; var y = ys; @@ -139,15 +142,14 @@ function drawWordClock() { hidxPrev = hidx; } - // Display digital time while button 1 is pressed - g.clearRect(0, 215, 240, 240); - if (BTN1.read()){ + // Display digital time when button is pressed or screen touched + g.clearRect(0, big ? 215 : 160, big ? 240 : 176, big ? 240 : 176); + if (showDigitalTime){ g.setColor(activeColor); - g.drawString(time, 120, 215); + g.drawString(time, big ? 120 : 90, big ? 215 : 160); } } - Bangle.on('lcdPower', function(on) { if (on) drawWordClock(); }); @@ -158,8 +160,17 @@ Bangle.drawWidgets(); setInterval(drawWordClock, 1E4); drawWordClock(); -// Show digital time while top button is pressed -setWatch(drawWordClock, BTN1, {repeat:true,edge:"both"}); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); +// If LCD pressed, toggle drawing digital time +Bangle.on('touch',e=>{ + if (showDigitalTime){ + showDigitalTime = false; + drawWordClock(); + } else { + showDigitalTime = true; + drawWordClock(); + } +}); + +// Show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/intervalTimer/ChangeLog b/apps/intervalTimer/ChangeLog new file mode 100644 index 000000000..d62860265 --- /dev/null +++ b/apps/intervalTimer/ChangeLog @@ -0,0 +1 @@ +0.01: First Release \ No newline at end of file diff --git a/apps/intervalTimer/README.md b/apps/intervalTimer/README.md new file mode 100644 index 000000000..d57c16e9c --- /dev/null +++ b/apps/intervalTimer/README.md @@ -0,0 +1,34 @@ +# Interval Timer + +An interval timer for workouts and whatever else! + +## Usage + +First set the active time (i.e. the number of seconds to perform exercises). + +![Set Active Time](images/set-active.png) + +Next set the rest time (i.e. number of seconds to rest between exercises). + +![Set Rest Time](images/set-rest.png) + +Finally choose the number of sets to perform. + +![Set Number Sets](images/set-sets.png) + +Active time will be shown in red, rest time in green. The watch will buzz whenever active or rest time gets to 0. + +![Timer (active)](images/timer1.png) +![Timer (rest)](images/timer2.png) + +You can press the physical button during timer countdown to pause the timer. + +![Paused](images/pause.png) + +View after all sets are completed. Press menu to change settings or restart to start timer again with the same settings. + +![Completed view](images/done.png) + +## Creator + +James Gough diff --git a/apps/intervalTimer/app-icon.js b/apps/intervalTimer/app-icon.js new file mode 100644 index 000000000..1ca594050 --- /dev/null +++ b/apps/intervalTimer/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwg96hWq1WgDCgXWxGZzOICqQABC4QABCyIXFDBsICIeJyfznAFBwAWPC4Of///mYYMCwgXBl4XB/4xCFxwABn4XCDAQwICw2ICwf/+YwJxGDHoQXHGARGIn/4C5QwBJAwQDC5QLCIw6GEC5BIGIwQLBJAgXGJAwXEJAgXPHgoXIEYIXFLwRIFC484C4h2DJAoIFPA+Ix4MGAAJoDHYgXKf4QXUJAYJGC5p5CF6hIBO44XNABIXGEw4AIU4rXFC5jvFc5AAHxAXGQwwAHQAIXcPCB2FC4RgOB4IXFJBxGHJB5GHJAYwKFwIXIJAIwKFwJGHGAYYICwIuIGAeImYWFmYJBFxIYEwZjC+YtCCxZJDAA4WMDBIWODIwVRAH4AXA==")) \ No newline at end of file diff --git a/apps/intervalTimer/app.js b/apps/intervalTimer/app.js new file mode 100644 index 000000000..fd57dbe2b --- /dev/null +++ b/apps/intervalTimer/app.js @@ -0,0 +1,306 @@ +/** + +Interval Timer + +An app for the Bangle.js watch + +*/ + +var Layout = require("Layout"); + +// Globals +var timerMode; // 'active' || 'rest' +var numSets = 1; +var activeTime = 20; +var restTime = 10; +var counter; +var setsRemaining; +var counterInterval; +var outOfTimeTimeout; +var timerIsPaused; +var timerLayout; + +/** Called to initialize the timer layout */ +function initTimerLayout() { + timerLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"40%", pad: 10, label:"00:00", id:"time" }, + {type:"txt", font:"6x8:2", label:"0", id:"set" } + ] + }, {btns: [ + {label: "Stop", cb: l => { + if (timerIsPaused){ + timerIsPaused = false; + resumeTimer(); + } + else{ + timerIsPaused = true; + pauseTimer(); + } + } + } + ] + }); +} + +/** Pauses the timer by clearing the counterInterval */ +function pauseTimer() { + if (counterInterval){ + clearTimeout(counterInterval); + counterInterval = undefined; + } + // update layout to display "Paused" + timerLayout.clear(timerLayout.time); + timerLayout.time.label = "||"; + timerLayout.clear(timerLayout.set); + timerLayout.set.label = "Paused"; + timerLayout.render(); +} + +/** Reumes the timer by setting the counterInterval again */ +function resumeTimer() { + if (!counterInterval){ + counterInterval = setInterval(countDown, 1000); + } + // display the timer values again. + timerLayout.clear(timerLayout.time); + timerLayout.time.label = counter; + timerLayout.clear(timerLayout.set); + timerLayout.set.label = `Sets: ${setsRemaining}`; + timerLayout.render(); +} + +/** Display 'Done' view, called when all sets are completed */ +function outOfTime() { + var stopLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"30%", label:"Done!", id:"time" }, + ] + }, {btns: [ + // menu button allows user to modify times and sets + {label:"Menu", cb: l=> { + if (outOfTimeTimeout){ + clearTimeout(outOfTimeTimeout); + outOfTimeTimeout = undefined; + } + //stopLayout.remove(); + setup(); + } + }, + // restart button runs timer again with the same settings + {label:"Restart", cb: l=> { + if (outOfTimeTimeout){ + clearTimeout(outOfTimeTimeout); + outOfTimeTimeout = undefined; + } + //stopLayout.remove(); + timerMode = 'active'; + startTimer(); + } + } + ]}); + + if (counterInterval) return; + setsRemaining = numSets; + g.clear(); + stopLayout.render(); + Bangle.buzz(500); + Bangle.beep(200, 4000) + .then(() => new Promise(resolve => setTimeout(resolve,200))) + .then(() => Bangle.beep(200, 3000)); +} + +/** Function called by the counterInterval at each second. + Updates the timer display values. +*/ +function countDown() { + // Out of time + if (counter<=0) { + if(timerMode === 'active'){ + timerMode = 'rest'; + startTimer(); + return; + } + else{ + --setsRemaining; + if (setsRemaining === 0){ + clearInterval(counterInterval); + counterInterval = undefined; + //setWatch(startTimer, (process.env.HWVERSION==2) ? BTN1 : BTN2); + outOfTime(); + return; + } + timerMode = 'active'; + startTimer(); + return; + } + } + + timerLayout.clear(timerLayout.time); + timerLayout.time.label = counter; + timerLayout.render(); + counter--; +} + +/** Start the interval timer. */ +function startTimer() { + timerIsPaused = false; + g.clear(); + if(timerMode === 'active'){ + counter = activeTime; + timerLayout.time.col = '#f00'; + } + else{ + counter = restTime; + timerLayout.time.col = '#0f0'; + } + + timerLayout.clear(timerLayout.set); + timerLayout.set.label = `Sets: ${setsRemaining}`; + timerLayout.render(); + Bangle.buzz(); + countDown(); + if (!counterInterval){ + counterInterval = setInterval(countDown, 1000); + } +} + +/** Menu step in which user sets the number of sets to be performed. */ +function setNumSets(){ + g.clear(); + var menuLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"6x8:2", label:"Number Sets", id:"title" }, + {type:"txt", font:"30%", pad: 20, label: numSets, id:"value" }, + {type:"btn", font:"6x8:2", label:"Back", cb: l => { + setRestTime(); + } + } + ] + }, {btns: [ + {label:"+", cb: l=> { + incrementNumSets(); + }}, + {label:"Go", cb: l=> { + setsRemaining = numSets; + initTimerLayout(); + startTimer(); + }}, + {label:"-", cb: l=>{ + decrementNumSets(); + }} + ]}); + menuLayout.render(); + + const incrementNumSets = () => { + ++numSets; + menuLayout.clear(menuLayout.numSets); + menuLayout.value.label = numSets; + menuLayout.render(); + }; + + const decrementNumSets = () => { + if(numSets === 1){ + return; + } + --numSets; + menuLayout.clear(menuLayout.numSets); + menuLayout.value.label = numSets; + menuLayout.render(); + }; +} + +/** Menu step in which user sets the number of seconds of rest time for each set. */ +function setRestTime(){ + g.clear(); + var menuLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"6x8:2", label:"Rest Time", id:"title" }, + {type:"txt", font:"30%", pad: 20, label: restTime, id:"value" }, + {type:"btn", font:"6x8:2", label:"Back", cb: l => { + setActiveTime(); + } + } + ] + }, {btns: [ + {label:"+", cb: l=> { + incrementRestTime(); + }}, + {label:"OK", cb: l=>setNumSets()}, + {label:"-", cb: l=>{ + decrementRestTime(); + }} + ]}); + menuLayout.render(); + + const incrementRestTime = () => { + restTime += 5; + menuLayout.clear(menuLayout.restTime); + menuLayout.value.label = restTime; + menuLayout.render(); + }; + + const decrementRestTime = () => { + if(restTime === 0){ + return; + } + restTime -= 5; + menuLayout.clear(menuLayout.restTime); + menuLayout.value.label = restTime; + menuLayout.render(); + }; +} + +/** Menu step in which user sets the number of seconds of active time for each set. */ +function setActiveTime(){ + g.clear(); + var menuLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"6x8:2", label:"Active Time", id:"title" }, + {type:"txt", font:"30%", pad: 20, label: activeTime, id:"value" } + ] + }, {btns: [ + {font:"20%", label:"+", fillx:1, cb: l=> { + incrementActiveTime(); + }}, + {label:"OK", cb: l => setRestTime()}, + {type:"btn", font:"20%", label:"-", fillx:1, cb: l=> { + decrementActiveTime(); + } + } + ]}); + menuLayout.render(); + + const incrementActiveTime = () => { + activeTime += 5; + menuLayout.clear(menuLayout.activeTime); + menuLayout.value.label = activeTime; + menuLayout.render(); + }; + + const decrementActiveTime = () => { + if(activeTime === 0){ + return; + } + activeTime -= 5; + menuLayout.clear(menuLayout.activeTime); + menuLayout.value.label = activeTime; + menuLayout.render(); + }; +} + +/** Start the setup menu, walks through setting active time, rest time, and number of sets. */ +function setup(){ + if (timerLayout){ + // remove timerLayout, otherwise it's pause button callback will still be registered + timerLayout.remove(timerLayout); + timerLayout = undefined; + } + Bangle.setUI(); // remove all existing input handlers + timerMode = 'active'; + setActiveTime(); +} + +// this keeps the watch LCD lit up +Bangle.setLCDPower(1); +setup(); \ No newline at end of file diff --git a/apps/intervalTimer/app.png b/apps/intervalTimer/app.png new file mode 100644 index 000000000..782c449b3 Binary files /dev/null and b/apps/intervalTimer/app.png differ diff --git a/apps/intervalTimer/images/done.png b/apps/intervalTimer/images/done.png new file mode 100644 index 000000000..d210540d1 Binary files /dev/null and b/apps/intervalTimer/images/done.png differ diff --git a/apps/intervalTimer/images/pause.png b/apps/intervalTimer/images/pause.png new file mode 100644 index 000000000..727380799 Binary files /dev/null and b/apps/intervalTimer/images/pause.png differ diff --git a/apps/intervalTimer/images/set-active.png b/apps/intervalTimer/images/set-active.png new file mode 100644 index 000000000..75b86150b Binary files /dev/null and b/apps/intervalTimer/images/set-active.png differ diff --git a/apps/intervalTimer/images/set-rest.png b/apps/intervalTimer/images/set-rest.png new file mode 100644 index 000000000..e33c9eb02 Binary files /dev/null and b/apps/intervalTimer/images/set-rest.png differ diff --git a/apps/intervalTimer/images/set-sets.png b/apps/intervalTimer/images/set-sets.png new file mode 100644 index 000000000..3d5a9107f Binary files /dev/null and b/apps/intervalTimer/images/set-sets.png differ diff --git a/apps/intervalTimer/images/timer1.png b/apps/intervalTimer/images/timer1.png new file mode 100644 index 000000000..3d1cb6350 Binary files /dev/null and b/apps/intervalTimer/images/timer1.png differ diff --git a/apps/intervalTimer/images/timer2.png b/apps/intervalTimer/images/timer2.png new file mode 100644 index 000000000..026774ba2 Binary files /dev/null and b/apps/intervalTimer/images/timer2.png differ diff --git a/apps/ios/ChangeLog b/apps/ios/ChangeLog new file mode 100644 index 000000000..5e60068aa --- /dev/null +++ b/apps/ios/ChangeLog @@ -0,0 +1,10 @@ +0.01: New App! +0.02: Remove messages on disconnect +0.03: Handling of message actions (ok/clear) +0.04: Added common bundleId's +0.05: Added more bundleId's (app-id's which can be used to + determine a friendly app name in the notifications) +0.06: Fix (not) popupping up old messages +0.07: Added more details from music (instead of Undefined) + Added more app identifiers + diff --git a/apps/ios/README.md b/apps/ios/README.md new file mode 100644 index 000000000..b4c2c6ac9 --- /dev/null +++ b/apps/ios/README.md @@ -0,0 +1,31 @@ +# iOS integration app + +This is the iOS integration app for Bangle.js. This app allows you to receive +notifications from your iPhone. The Apple Notification Center Service (ANCS) +sends all the messages to your watch. + +You can allow this if you connect your Bangle to your iPhone. It will be +prompted for immediatly after you connect the Bangle to the iPhone. + +### Connecting your Bangle(2).js to your iPhone +The Bangle watches are Bluetooth Low Energy (BLE) devices. Sometimes they +will not be seen/detected by the Bluetooth scanner in your iPhone settings +menu. + +To resolve this, you can download numerous apps who can actually scan +for BLE devices. There are great ones out there, free and paid. + +We really like WebBLE, which we also recommend to load apps on your +watch with your iOS device, as Safari does not support WebBluetooth +for now. It's just a few bucks/pounds/euro's. + +If you like to try a free app first, you can always use NRF Toolbox or +Bluetooth BLE Device Finder to find and connect your Bangle. + +## Requests + +Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=ios%20app + +## Creator + +Gordon Williams diff --git a/apps/ios/app-icon.js b/apps/ios/app-icon.js new file mode 100644 index 000000000..b74048750 --- /dev/null +++ b/apps/ios/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwZC/AGEB/4AGwARHv4RH/wQGj4QHAAP4CIoQJAAIRWg4RL8ARVn4RL/gR/CJv9BIP934DFEZH+v/0AgMv+wRK+YCBz/7C4PfCJOfAQO//JHMCIX3/d/CJ//t4RJF4JlCCIP/koRKEYh+DCIxlBCIQADCJQgCn4DCCJSbBHIIDBXYQRI/+Sp4DB7ZsCfdQRzg4RL8ARVgARLCAgRSj4QJ/ARFgF/CA/+CA0AgIRHwARHAH4AnA")) diff --git a/apps/ios/app.js b/apps/ios/app.js new file mode 100644 index 000000000..b210886fd --- /dev/null +++ b/apps/ios/app.js @@ -0,0 +1,2 @@ +// Config app not implemented yet +setTimeout(()=>load("messages.app.js"),10); diff --git a/apps/ios/app.png b/apps/ios/app.png new file mode 100644 index 000000000..79aa78f3a Binary files /dev/null and b/apps/ios/app.png differ diff --git a/apps/ios/boot.js b/apps/ios/boot.js new file mode 100644 index 000000000..8ccfb617d --- /dev/null +++ b/apps/ios/boot.js @@ -0,0 +1,185 @@ +bleServiceOptions.ancs = true; +Bangle.ancsMessageQueue = []; + +/* Handle ANCS events coming in, and fire off 'notify' events +when we actually have all the information we need */ +E.on('ANCS',msg=>{ + /* eg: + { + event:"add", + uid:42, + category:4, + categoryCnt:42, + silent:true, + important:false, + preExisting:true, + positive:false, + negative:true + } */ + + //console.log("ANCS",msg.event,msg.id); + // don't need info for remove events - pass these on + if (msg.event=="remove") + return E.emit("notify", msg); + + // not a remove - we need to get the message info first + function ancsHandler() { + var msg = Bangle.ancsMessageQueue[0]; + NRF.ancsGetNotificationInfo( msg.uid ).then( info => { + + if(msg.preExisting === true){ + info.new = false; + } else { + info.new = true; + } + + E.emit("notify", Object.assign(msg, info)); + Bangle.ancsMessageQueue.shift(); + if (Bangle.ancsMessageQueue.length) + ancsHandler(); + }); + } + Bangle.ancsMessageQueue.push(msg); + // if this is the first item in the queue, kick off ancsHandler, + // otherwise ancsHandler will handle the rest + if (Bangle.ancsMessageQueue.length==1) + ancsHandler(); +}); + +// Handle ANCS events with all the data +E.on('notify',msg=>{ +/* Info from ANCS event plus + "uid" : int, + "appId" : string, + "title" : string, + "subtitle" : string, + "message" : string, + "messageSize" : string, + "date" : string, + "new" : boolean, + "posAction" : string, + "negAction" : string, + "name" : string, +*/ + var appNames = { + "com.apple.facetime": "FaceTime", + "com.apple.mobilecal": "Calendar", + "com.apple.mobilemail": "Mail", + "com.apple.mobilephone": "Phone", + "com.apple.MobileSMS": "SMS Message", + "com.apple.Passbook": "iOS Wallet", + "com.apple.podcasts": "Podcasts", + "com.apple.reminders": "Reminders", + "com.apple.shortcuts": "Shortcuts", + "com.atebits.Tweetie2": "Twitter", + "com.burbn.instagram" : "Instagram", + "com.facebook.Facebook": "Facebook", + "com.facebook.Messenger": "Messenger", + "com.google.Chromecast" : "Google Home", + "com.google.Gmail" : "GMail", + "com.google.hangouts" : "Hangouts", + "com.google.ios.youtube" : "YouTube", + "com.hammerandchisel.discord" : "Discord", + "com.ifttt.ifttt" : "IFTTT", + "com.jumbo.app" : "Jumbo", + "com.linkedin.LinkedIn" : "LinkedIn", + "com.microsoft.Office.Outlook" : "Outlook Mail", + "com.nestlabs.jasper.release" : "Nest", + "com.netflix.Netflix" : "Netflix", + "com.reddit.Reddit" : "Reddit", + "com.skype.skype": "Skype", + "com.skype.SkypeForiPad": "Skype", + "com.spotify.client": "Spotify", + "com.strava.stravaride": "Strava", + "com.tinyspeck.chatlyio": "Slack", + "com.toyopagroup.picaboo": "Snapchat", + "com.ubercab.UberClient": "Uber", + "com.ubercab.UberEats": "UberEats", + "com.vilcsak.bitcoin2": "Coinbase", + "com.wordfeud.free": "WordFeud", + "com.zhiliaoapp.musically": "TikTok", + "net.whatsapp.WhatsApp": "WhatsApp", + "nl.ah.Appie": "Albert Heijn", + "nl.postnl.TrackNTrace": "PostNL", + "ph.telegra.Telegraph": "Telegram", + "tv.twitch": "Twitch", + + // could also use NRF.ancsGetAppInfo(msg.appId) here + }; + var unicodeRemap = { + '2019':"'" + }; + var replacer = ""; //(n)=>print('Unknown unicode '+n.toString(16)); + //if (appNames[msg.appId]) msg.a + require("messages").pushMessage({ + t : msg.event, + id : msg.uid, + src : appNames[msg.appId] || msg.appId, + new : msg.new, + title : msg.title&&E.decodeUTF8(msg.title, unicodeRemap, replacer), + subject : msg.subtitle&&E.decodeUTF8(msg.subtitle, unicodeRemap, replacer), + body : msg.message&&E.decodeUTF8(msg.message, unicodeRemap, replacer) + }); + // TODO: posaction/negaction? +}); + +// Apple media service +E.on('AMS',a=>{ + function push(m) { + var msg = { t : "modify", id : "music", title:"Music" }; + if (a.id=="artist") msg.artist = m; + else if (a.id=="album") msg.album = m; + else if (a.id=="title") msg.track = m; + else if (a.id=="duration") msg.dur = m; + else return; + require("messages").pushMessage(msg); + } + if (a.truncated) NRF.amsGetMusicInfo(a.id).then(push) + else push(a.value); +}); + +// Music control +Bangle.musicControl = cmd => { + // play, pause, playpause, next, prev, volup, voldown, repeat, shuffle, skipforward, skipback, like, dislike, bookmark + NRF.amsCommand(cmd); +}; +// Message response +Bangle.messageResponse = (msg,response) => { + if (isFinite(msg.id)) return NRF.sendANCSAction(msg.id, response);//true/false + // error/warn here? +}; +// remove all messages on disconnect +NRF.on("disconnect", () => require("messages").clearAll()); + +/* +// For testing... + +NRF.ancsGetNotificationInfo = function(uid) { + print("ancsGetNotificationInfo",uid); + return Promise.resolve({ + "uid" : uid, + "appId" : "Hangouts", + "title" : "Hello", + "subtitle" : "There", + "message" : "Lots and lots of text", + "messageSize" : 100, + "date" : "...", + "posAction" : "ok", + "negAction" : "cancel", + "name" : "Fred", + }); +}; + +E.emit("ANCS", { + event:"add", + uid:42, + category:4, + categoryCnt:42, + silent:true, + important:false, + preExisting:true, + positive:false, + negative:true +}); + +*/ diff --git a/apps/isoclock/ChangeLog b/apps/isoclock/ChangeLog index cd3ceea5c..809091ce4 100644 --- a/apps/isoclock/ChangeLog +++ b/apps/isoclock/ChangeLog @@ -1 +1,2 @@ 0.01: Created app based on digiclock with some small tweaks. +0.02: Swap to Bangle.setUI for launcher/buttons diff --git a/apps/isoclock/isoclock.js b/apps/isoclock/isoclock.js index 5f63a1248..59f28e66e 100644 --- a/apps/isoclock/isoclock.js +++ b/apps/isoclock/isoclock.js @@ -1,82 +1,82 @@ //load fonts require("Font7x11Numeric7Seg").add(Graphics); -require("FontHaxorNarrow7x17").add(Graphics); +require("FontHaxorNarrow7x17").add(Graphics); //screen position -const X = 170; -const Y = 140; +const X = 170; +const Y = 140; function draw() { // Date Variables - var date = new Date(); - var h = date.getHours(); - var m = date.getMinutes(); - var day = date.getDay(); + var date = new Date(); + var h = date.getHours(); + var m = date.getMinutes(); + var day = date.getDay(); var month = date.getMonth()+1; - var dateNum = date.getDate(); - var year = date.getFullYear(); - var half = "AM"; - var time = ("0" + h).substr(-2) + ":" + ("0" + m).substr(-2); - - //convert day into string + var dateNum = date.getDate(); + var year = date.getFullYear(); + var half = "AM"; + var time = ("0" + h).substr(-2) + ":" + ("0" + m).substr(-2); + + //convert day into string switch (day) { case 0: - day = "Sunday"; - break; - + day = "Sunday"; + break; + case 1: - day = "Monday"; - break; - + day = "Monday"; + break; + case 2: - day = "Tuesday"; - break; - + day = "Tuesday"; + break; + case 3: - day = "Wednesday"; - break; - + day = "Wednesday"; + break; + case 4: - day = "Thursday"; - break; - + day = "Thursday"; + break; + case 5: - day = "Friday"; - break; - + day = "Friday"; + break; + case 6: - day = "Saturday"; - break; - + day = "Saturday"; + break; + default: - day = "ERROR"; - break; + day = "ERROR"; + break; } - + if (h > 12) { - half = "PM"; - h = h - 12; + half = "PM"; + h = h - 12; } //reset graphics - g.reset(); + g.reset(); //draw the time g.setFont("7x11Numeric7Seg", 5); g.setFontAlign(1,1); g.drawString(time, X+10, Y, true /*clear background*/); - g.setFont("7x11Numeric7Seg", 3); - g.drawString(("0"+date.getSeconds()).substr(-2), X+55, Y, true /*clear background*/); + g.setFont("7x11Numeric7Seg", 3); + g.drawString(("0"+date.getSeconds()).substr(-2), X+55, Y, true /*clear background*/); g.setFontAlign(0,1); g.setFont("HaxorNarrow7x17", 3); g.drawString(day, X-60, Y+53, true); - g.drawString(year+"-"+month+"-"+dateNum, X-55, Y-55, true); - - + g.drawString(year+"-"+month+"-"+dateNum, X-55, Y-55, true); + + } //clear screen at startup -g.clear(); +g.clear(); //draw immediatly -draw(); +draw(); var secondInterval = setInterval(draw, 1000); // Stop updates when LCD is off, restart when on @@ -92,4 +92,5 @@ Bangle.on('lcdPower',on=>{ Bangle.loadWidgets(); Bangle.drawWidgets(); -setWatch(Bangle.showLauncher, BTN2, {repeat : false, edge: "falling"}); +// Show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/jbm8b_IT/ChangeLog b/apps/jbm8b_IT/ChangeLog new file mode 100644 index 000000000..b7b783924 --- /dev/null +++ b/apps/jbm8b_IT/ChangeLog @@ -0,0 +1 @@ +0.01: Cloning Magic 8 Ball and make it speak italian \ No newline at end of file diff --git a/apps/jbm8b_IT/app-icon.js b/apps/jbm8b_IT/app-icon.js new file mode 100644 index 000000000..09bf032a6 --- /dev/null +++ b/apps/jbm8b_IT/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhBC/AGMrq2B1gAEwNWlYthq2s64AKGYIydFpoAEGLUrFqIADqxcXFqhiDFymBFy7GCF1owTRjCSVlYudeiGsF7/XlaNqSKBeP1mBwJxQMBReO1gaEleBMDBLN1hAC1hhBAoIwNCwQAGlZINqxvFGAIXOSBAXQN4hPBC5yQIVBxfBCAgvQSBC+NFAYRDMwJHOF654DqxkBYooALF6+sbIhkEF8Z3CRIWBR6AvXFAzvQF6wnIYQJgNd5AWNdoLoGBBAvPO5pfYH4IvUUwS/GVBzXBYCpHCq2s1mBDwKOWDwRgNPAwVVMCRLCwIABCZ6OJJSAATLxZgRACJeLAAMrFz9WFxiRgRpoADwIub1guQGDmsXhqSfRiL0G1jqkMRYxRwKLUGK2sFryVEq2B1gAEwNWFkIA/AH4A/AH4AQ")) \ No newline at end of file diff --git a/apps/jbm8b_IT/app.js b/apps/jbm8b_IT/app.js new file mode 100644 index 000000000..13ab3d39d --- /dev/null +++ b/apps/jbm8b_IT/app.js @@ -0,0 +1,79 @@ +const affirmative = [ + 'È certo.', + 'È decisamente\ncosì.', + 'Senza alcun\ndubbio.', + 'Sì,\nsenza dubbio.', + 'Ci puoi\ncontare.', + 'Da quanto\nvedo,\nsì.', + 'Molto\nprobabilmente.', + 'Le prospettive\nsono buone.', + 'Sì.', + 'I segni\nindicano\ndi sì.' +]; +const nonCommittal = [ + 'È difficile\ndirlo,\nprova di nuovo.', + 'Rifai la domanda\npiù tardi.', + 'Meglio non\nrisponderti\nadesso.', + 'Non posso\npredirlo ora.', + 'Concentrati e\nrifai la\ndomanda.' +]; +const negative = [ + 'Non ci\ncontare.', + 'La mia\nrisposta\nè no.', + 'Le mie\nfonti dicono\ndi no.', + 'Le prospettive\nnon sono\nbuone.', + 'È molto\ndubbio.' +]; +const title = 'Magic 8 Ball'; + +const answers = [affirmative, nonCommittal, negative]; + +function getRandomArbitrary(min, max) { + return Math.random() * (max - min) + min; +} + +function predict() { + // affirmative, negative or non-committal + let max = answers.length; + const a = Math.floor(getRandomArbitrary(0, max)); + // sets max compared to answer category + max = answers[a].length; + const b = Math.floor(getRandomArbitrary(0, max)); + // get the answer + const response = answers[a][b]; + return response; +} + +function draw(msg) { + // console.log(msg); + g.clear(); + E.showMessage(msg, title); +} + +function reply(button) { + const theButton = (typeof button === 'undefined' || isNaN(button)) ? 1 : button; + const timer = Math.floor(getRandomArbitrary(0, theButton) * 1000); + // Thinking... + draw('...'); + setTimeout('draw(predict());', timer); +} + +function ask() { + draw('Ponimi una\ndomanda\nSì/No e\ntocca lo\nschermo'); +} + +g.clear(); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +ask(); + +// Event Handlers + +Bangle.on('touch', (button) => reply(button)); + +setWatch(ask, BTN1, { repeat: true, edge: "falling" }); +setWatch(reply, BTN3, { repeat: true, edge: "falling" }); + +// Back to launcher +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); \ No newline at end of file diff --git a/apps/jbm8b_IT/app.png b/apps/jbm8b_IT/app.png new file mode 100644 index 000000000..24c3013de Binary files /dev/null and b/apps/jbm8b_IT/app.png differ diff --git a/apps/jbm8b_IT/bangle1-magic-8-ball-italiano-screenshot.png b/apps/jbm8b_IT/bangle1-magic-8-ball-italiano-screenshot.png new file mode 100644 index 000000000..8bc2c7e9b Binary files /dev/null and b/apps/jbm8b_IT/bangle1-magic-8-ball-italiano-screenshot.png differ diff --git a/apps/kitchen/ChangeLog b/apps/kitchen/ChangeLog index 71548ec30..3767a9548 100644 --- a/apps/kitchen/ChangeLog +++ b/apps/kitchen/ChangeLog @@ -8,3 +8,6 @@ 0.08: Improved error handling for missing firmware features, added template app.kit.js 0.09: Added heart rate monitor app 0.10: Converted Stepo to use direct screen writes, added a Trip Counter feature to stepo +0.11: Detect when waypoints.json is not present, error E-WPT +0.12: Added stepo2 as a replacement for stepo and digi +0.13: Added long press BTN2 toggle gpsrec status in GPS clock diff --git a/apps/kitchen/README.md b/apps/kitchen/README.md index a829a39b0..2a1b148fd 100644 --- a/apps/kitchen/README.md +++ b/apps/kitchen/README.md @@ -41,9 +41,25 @@ The following buttons depend on which face is currently in use - Waypointer : select next waypoint +## Stepo2 +![](screenshot_stepo2.jpg) + +- Requires one of the pedominter widgets to be installed +- Stepo2 is a combination of Stepo and Digi and now replaces them +- Displays the time in large font +- Display current step count in a doughnut gauge +- The gauge show percentage of steps out of a goal of 10000 steps +- When the battery is less than 25% the doughnut turns red +- Use BTN1 to switch to the Trip Counter, use long press to reset Trip Counter +- Use BTN1 to cycle through the displays of Day,Date, Trip Counter, Battery %, Mem % and Firmware +- Use BTN3 to switch to the next app + + + ## Stepo ![](screenshot_stepo.jpg) +- now replaced by Stepo2 but still available if you install manually - Requires one of the pedominter widgets to be installed - Displays the time in large font - Display current step count in a doughnut gauge @@ -62,11 +78,13 @@ The following buttons depend on which face is currently in use ## Digi ![](screenshot_digi.jpg) +- now replaced by Stepo2 but still available if you install manually - Displays the time in large font - Display day and date - Use BTN1 to switch between display of battery and memory %. - Use BTN3 to switch to the next app. + ## Swatch ![](screenshot_swatch.jpg) - A simple stopwatch @@ -76,6 +94,9 @@ The following buttons depend on which face is currently in use ## Heart ![](screenshot_heart.jpg) - A simple heart rate monitor, at present the app is just showing the raw value from HRM.bpm +- This is an experimental app and not installed by default. The + heart.kit.js file can be uploaded via the Espruino IDE if you want + to try it out. Then reload the App. - BTN1, long press, turn heart rate monitor on / off ## Waypointer @@ -226,12 +247,12 @@ I have settled on directly writing to the screen using the Graphics object (g.) for the compass App. This creates a bit of flicker when the arrow moves but is more reliable than using the ArrayBuffer. -v0.09: Since adding the heart rate monitor I have noticed that I can -sometimes can a memory error when switch through the Apps back to the -Stepo App. I think this can be cured by statically allocating the -ArrayBuffer for stepo rather than using new everytime you switch back -into the stepo watch face. The problem is that the bangle memory -management / defragmentation is quite slow to run. +v0.09: Since adding the heart rate monitor I have sometimes observed +a low memory error when switching through the Apps back to the Stepo +App. I think this can be cured by statically allocating the +ArrayBuffer for stepo rather than using 'new' everytime you switch +back into the stepo watch face. The problem is that the bangle +memory management / defragmentation is quite slow to run. v0.10: Revisited having a display buffer for the stepo part of the App. Now use direct screen writing as it means less memory allocation and @@ -241,16 +262,21 @@ reduces chance of getting a memory error on switching watch faces. The following error codes will be displayed if one of the dependancies is not met. -* E-STEPS - no pedomintor widget has been installed, please install the widpedom or the activepedom widgets -* E-CALIB - no compass calibration data was found, see 'Compass Calibration' -* E-FW - require firmware 2v08.187 or later to detect gps and compass power status +* E-STEPS - no pedomintor widget has been installed, please install + the widpedom or the activepedom widgets +* E-CALIB - no compass calibration data was found, see 'Compass + Calibration' +* E-FW - require firmware 2v08.187 or later to detect gps and compass + power status +* E-WPT - missing waypoints.json file ### Issues / Future enhancements +* Add a settings app so that 'Kitchen' based clocks can be enabled/disabled * GPS time display shows GMT and not BST, needs localising -* Occassional buzzing after 2-3 days of use, seems to disappear after - a reset to the launcher menu. Needs investigation * Automatically switch the GPS power setting from Super-E to PSMOO 10 seconds after the LCD goes off. At present I just rely on using the GPSSetup app and set the GPS power mode that I want. * Add a small graph to the heart rate monitor app +* Add a facility to call the Arrow calibration process +* Maybe create waypoints.json file if missing diff --git a/apps/kitchen/annex.js b/apps/kitchen/annex.js new file mode 100644 index 000000000..d789f5d0c --- /dev/null +++ b/apps/kitchen/annex.js @@ -0,0 +1,29 @@ +// annexed code that might be worth keeping + +/***************************************************************************** + +Screen Buffer Object that can be shared between faces + +Making into a Class like this means we allocate the memory once +and avoid fragmenting the memory when we switch in and out of faces + +******************************************************************************/ + +function BUF() { + this.pal4color = new Uint16Array([0x0000,0xFFFF,0x7BEF,0xAFE5],0,2); // b,w,grey,greenyellow + this.pal4red = new Uint16Array([0x0000,0xFFFF,0xF800,0xAFE5],0,2); // b,w,red,greenyellow + this.buf = Graphics.createArrayBuffer(120,120,2,{msb:true}); +} + +BUF.prototype.flip = function(x,y) { + g.drawImage({width:120,height:120,bpp:2, buffer:this.buf.buffer, palette:this.pal4color}, x, y); + this.buf.clear(); +} + +BUF.prototype.flip_red = function(x,y) { + g.drawImage({width:120,height:120,bpp:2, buffer:this.buf.buffer, palette:this.pal4red}, x, y); + this.buf.clear(); +} + +let bufObj = new BUF(); + diff --git a/apps/kitchen/compass.kit.js b/apps/kitchen/compass.kit.js index 530ba021c..0113e513f 100644 --- a/apps/kitchen/compass.kit.js +++ b/apps/kitchen/compass.kit.js @@ -17,13 +17,12 @@ } function init(gps,sw, hrm) { - showMem("compass init() START"); gpsObject = gps; intervalRefSec = undefined; bearing = 0; // always point north if GPS is off heading = 0; oldHeading = 0; - previous = {hding:"-", bs:"-", dst:"-", wp_name:"-", course:999}; + resetPrevious(); loc = require("locale"); CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; getWaypoint(); @@ -34,12 +33,9 @@ */ if (!Bangle.isCompassOn()) Bangle.setCompassPower(1); gps.determineGPSState(); - - showMem("compass init() END"); } function freeResources() { - showMem("compass freeResources() START"); gpsObject = undefined; intervalRefSec = undefined; previous = undefined; @@ -50,7 +46,6 @@ CALIBDATA = undefined; wp = undefined; if (Bangle.isCompassOn !== undefined && Bangle.isCompassOn()) Bangle.setCompassPower(0); - showMem("compass freeResources() END"); } function startTimer() { @@ -67,12 +62,6 @@ if (Bangle.isCompassOn !== undefined && Bangle.isCompassOn()) Bangle.setCompassPower(0); } - function showMem(msg) { - var val = process.memory(); - var str = msg + " " + Math.round(val.usage*100/val.total) + "%"; - log_debug(str); - } - function onButtonShort(btn) { log_debug("onButtonShort()"); if (gpsObject.getState() !== gpsObject.GPS_RUNNING) return; @@ -96,14 +85,19 @@ function onButtonLong(btn) { log_debug("markWaypoint()"); - if (btn !== 1) return; - if (gpsObject.getState() !== gpsObject.GPS_RUNNING) return; - log_debug("markWaypoint()"); + if (btn === 1) { + if (gpsObject.getState() !== gpsObject.GPS_RUNNING) return; + log_debug("markWaypoint()"); - gpsObject.markWaypoint(); - resetPrevious(); - getWaypoint(); - drawGPSData(); + gpsObject.markWaypoint(); + resetPrevious(); + getWaypoint(); + drawGPSData(); + return; + } + + if (btn === 2) + Bangle.showLauncher(); } function getWaypoint() { @@ -206,12 +200,12 @@ drawCompass(dir, 0xFFC0); // yellow oldHeading = dir; } - + if (gpsObject.getState() === gpsObject.GPS_RUNNING) { drawGPSData(); } else { drawCompassHeading(); - } + } } // only used when acting as compass with GPS off diff --git a/apps/kitchen/digi.kit.js b/apps/kitchen/digi.kit.js index 91ae70905..c3b12baac 100644 --- a/apps/kitchen/digi.kit.js +++ b/apps/kitchen/digi.kit.js @@ -55,7 +55,10 @@ if (btn === 1) cycleInfoMode(); } - function onButtonLong(btn) {} + function onButtonLong(btn) { + if (btn === 2) Bangle.showLauncher(); + } + function getGPSfix() { return undefined; } function setGPSfix(f) {} diff --git a/apps/kitchen/gps.kit.js b/apps/kitchen/gps.kit.js index d6c936226..3bda43876 100644 --- a/apps/kitchen/gps.kit.js +++ b/apps/kitchen/gps.kit.js @@ -45,7 +45,15 @@ } function onButtonLong(btn) { - if (btn === 1) toggleGPSPower(); + switch(btn) { + case 1: + toggleGPSPower(); + return; + case 2: + if (gpsObject.getState() === gpsObject.GPS_RUNNING) + gpsObject.toggleGpsLogging(); + return; + } } function draw(){ @@ -142,8 +150,9 @@ g.clearRect(0, Y_ACTIVITY, 239, Y_MODELINE - 1); g.drawString(activityStr, 120, Y_ACTIVITY); g.setFont("6x8",2); - g.setColor(1,1,1); - g.drawString(age, 120, Y_ACTIVITY + 46); + g.setColor(1,1,1); + var age_and_logging = age + " logging " + gpsObject.loggingStatus(); + g.drawString(age_and_logging, 120, Y_ACTIVITY + 46); } } diff --git a/apps/kitchen/kitchen.app.js b/apps/kitchen/kitchen.app.js index c3f7bd74d..5564b2807 100644 --- a/apps/kitchen/kitchen.app.js +++ b/apps/kitchen/kitchen.app.js @@ -2,7 +2,7 @@ var FACES = []; var STOR = require("Storage"); STOR.list(/\.kit\.js$/).forEach(face=>FACES.push(eval(require("Storage").read(face)))); -var iface = STOR.list(/\.kit\.js$/).indexOf("stepo.kit.js"); +var iface = STOR.list(/\.kit\.js$/).indexOf("stepo2.kit.js"); var face = FACES[iface](); var firstPress var pressTimer; @@ -33,10 +33,10 @@ function nextFace(){ // when you feel the buzzer you know you have done a long press function longPressCheck() { Bangle.buzz(); - debug_log("long PressCheck() buzz"); + debug_log("BUZZ, long press"); if (pressTimer) { clearInterval(pressTimer); - debug_log("clear pressTimer 2"); + debug_log("CLEAR pressTimer 2"); pressTimer = undefined; } } @@ -48,10 +48,10 @@ function buttonPressed(btn) { } else { firstPress = getTime(); if (pressTimer) { - debug_log("clear pressTimer 1"); + debug_log("CLEAR pressTimer 1"); clearInterval(pressTimer); } - debug_log("set pressTimer 1"); + debug_log("SET pressTimer 1"); pressTimer = setInterval(longPressCheck, 1500); } } @@ -60,7 +60,7 @@ function buttonPressed(btn) { function buttonReleased(btn) { var dur = getTime() - firstPress; if (pressTimer) { - debug_log("clear pressTimer 3"); + debug_log("CLEAR pressTimer 3"); clearInterval(pressTimer); pressTimer = undefined; } @@ -71,7 +71,8 @@ function buttonReleased(btn) { face.onButtonLong(btn); break; case 2: - Bangle.showLauncher(); + face.onButtonLong(btn); + //Bangle.showLauncher(); break; case 3: // do nothing @@ -94,8 +95,8 @@ function setButtons(){ } Bangle.on('kill',()=>{ - Bangle.setCompassPower(0); - Bangle.setGPSPower(0); + Bangle.setCompassPower(0,'kitchen'); + Bangle.setGPSPower(0,'kitchen'); }); Bangle.on('lcdPower',function(on) { @@ -214,7 +215,7 @@ GPS.prototype.toggleGPSPower = function() { this.log_debug("toggleGPSPower()"); this.gpsPowerState = Bangle.isGPSOn(); this.gpsPowerState = !this.gpsPowerState; - Bangle.setGPSPower(this.gpsPowerState ? 1 : 0); + Bangle.setGPSPower((this.gpsPowerState ? 1 : 0), 'kitchen'); this.resetLastFix(); this.determineGPSState(); @@ -256,7 +257,7 @@ GPS.prototype.processFix = function(fix) { this.gpsState = this.GPS_RUNNING; if (!this.last_fix.fix && !(require("Storage").readJSON("setting.json", 1) || {}).quiet) { Bangle.buzz(); // buzz on first position - debug_log("GPS fix buzz"); + debug_log("BUZZ - gps fix"); } this.last_fix = fix; } @@ -303,7 +304,7 @@ GPS.prototype.getWPdistance = function() { //log_debug(this.last_fix); //log_debug(this.wp_current); - if (this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) + if (this.wp_current.name === "E-WPT" || this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) return 0; else return this.calcDistance(this.last_fix, this.wp_current); @@ -313,14 +314,14 @@ GPS.prototype.getWPbearing = function() { //log_debug(this.last_fix); //log_debug(this.wp_current); - if (this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) + if (this.wp_current.name === "E-WPT" || this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) return 0; else return this.calcBearing(this.last_fix, this.wp_current); } GPS.prototype.loadFirstWaypoint = function() { - var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; + var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}]; this.wp_index = 0; this.wp_current = waypoints[this.wp_index]; log_debug(this.wp_current); @@ -332,7 +333,7 @@ GPS.prototype.getCurrentWaypoint = function() { } GPS.prototype.waypointHasLocation = function() { - if (this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) + if (this.wp_current.name === "E-WPT" || this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) return false; else return true; @@ -340,12 +341,12 @@ GPS.prototype.waypointHasLocation = function() { GPS.prototype.markWaypoint = function() { - if(this.wp_current.name === "NONE") + if(this.wp_current.name === "E-WPT" || this.wp_current.name === "NONE") return; log_debug("GPS::markWaypoint()"); - var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; + var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}]; this.wp_current = waypoints[this.wp_index]; if (this.waypointHasLocation()) { @@ -360,7 +361,7 @@ GPS.prototype.markWaypoint = function() { } GPS.prototype.nextWaypoint = function(inc) { - var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; + var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}]; this.wp_index+=inc; if (this.wp_index>=waypoints.length) this.wp_index=0; if (this.wp_index<0) this.wp_index = waypoints.length-1; @@ -369,6 +370,26 @@ GPS.prototype.nextWaypoint = function(inc) { return this.wp_current; } +GPS.prototype.toggleGpsLogging = function() { + var settings = require("Storage").readJSON("gpsrec.json",1)||{}; + if (settings == {}) return false; + + settings.recording = !settings.recording; + require("Storage").write("gpsrec.json", settings); + + if (WIDGETS["gpsrec"]) + WIDGETS["gpsrec"].reload(); + + return true; +} + +GPS.prototype.loggingStatus = function() { + var settings = require("Storage").readJSON("gpsrec.json",1)||{}; + if (settings == {}) return "E-LOG"; + if (settings.recording) return "ON"; + return "OFF"; +} + var gpsObj = new GPS(); @@ -731,14 +752,14 @@ function TRIP() { TRIP.prototype.resetTrip = function(steps) { this.tripStart = (0 + steps); - console.log("resetTrip starting=" + this.tripStart); + log_debug("resetTrip starting=" + this.tripStart); } TRIP.prototype.getTrip = function(steps) { let tripSteps = (0 + steps) - this.tripStart; - console.log("getTrip steps=" + steps); - console.log("getTrip tripStart=" + this.tripStart); - console.log("getTrip=" + tripSteps); + log_debug("getTrip steps=" + steps); + log_debug("getTrip tripStart=" + this.tripStart); + log_debug("getTrip=" + tripSteps); return tripSteps; } @@ -758,7 +779,6 @@ Debug Object ******************************************************************************/ -/* function DEBUG() { this.logfile = require("Storage").open("debug.log","a"); } @@ -770,7 +790,6 @@ DEBUG.prototype.log = function(msg) { } debugObj = new DEBUG(); -*/ function debug_log(m) { //debugObj.log(m); diff --git a/apps/kitchen/screenshot_stepo2.jpg b/apps/kitchen/screenshot_stepo2.jpg new file mode 100644 index 000000000..acff792b0 Binary files /dev/null and b/apps/kitchen/screenshot_stepo2.jpg differ diff --git a/apps/kitchen/stepo.kit.js b/apps/kitchen/stepo.kit.js index 9fa34e8ab..2b60a9d0d 100644 --- a/apps/kitchen/stepo.kit.js +++ b/apps/kitchen/stepo.kit.js @@ -19,11 +19,16 @@ } function onButtonLong(btn) { - trip.resetTrip(getSteps()); - trip.setTripState(true); - drawStepText(); + if (btn === 1) { + trip.resetTrip(getSteps()); + trip.setTripState(true); + drawStepText(); + return; + } + + if (btn === 2) Bangle.showLauncher(); } - + function radians(a) { return a*Math.PI/180; } diff --git a/apps/kitchen/stepo2.kit.js b/apps/kitchen/stepo2.kit.js new file mode 100644 index 000000000..9eaa784da --- /dev/null +++ b/apps/kitchen/stepo2.kit.js @@ -0,0 +1,262 @@ +(() => { + function getFace(){ + var intervalRefSec; + var trip; + var prevSteps; + var prevTopText1; + var prevTopText2; + var prevBottomText; + var prevMins; + var infoMode; + + const INFO_DATE = 0; + const INFO_TRIP = 1; + const INFO_BATT = 2; + const INFO_MEM = 3; + const INFO_FW = 4; + + function init(g,sw,hrm,tr) { + trip = tr; + infoMode = INFO_DATE; + forceRedraw(); + } + + function freeResources() { + trip = undefined; + } + + function forceRedraw() { + prevStepsText = ''; + prevSteps = -1; + prevTopText1 = ''; + prevTopText2 = ''; + prevBottomText = ''; + prevMins = ''; + } + + function cycleInfoMode() { + switch(infoMode) { + case INFO_DATE: + infoMode = INFO_TRIP; + break; + case INFO_TRIP: + infoMode = INFO_BATT; + break; + case INFO_BATT: + infoMode = INFO_MEM + break; + case INFO_MEM: + infoMode = INFO_FW + break; + case INFO_FW: + default: + infoMode = INFO_DATE; + break; + } + } + + function onButtonShort(btn) { + cycleInfoMode(); + forceRedraw(); + draw(); + } + + function onButtonLong(btn) { + if (btn === 1) { + trip.resetTrip(getSteps()); + infoMode = INFO_TRIP; + forceRedraw(); + draw(); + return; + } + + if (btn === 2) Bangle.showLauncher(); + } + + function radians(a) { + return a*Math.PI/180; + } + + function startTimer() { + draw(); + intervalRefSec = setInterval(draw, 5000); + } + + function stopTimer() { + if(intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} + } + + function draw() { + var d = new Date(); + var da = d.toString().split(" "); + var hh = da[4].substr(0,2); + var mm = da[4].substr(3,2); + var day = da[0]; + var day_month = da[2] + " " + da[1]; + + g.setColor(1,1,1); // white + + if (prevMins != mm) { + prevMins = mm; + // hours and minutes + g.clearRect(0, 24, 149, 239); + g.setFontAlign(-1, -1); + g.setFont("Vector", 104); + g.drawString(hh, 20, 30, true); + g.drawString(mm, 20, 120, true); + } + + /* + * if our trip count is greater than todays steps then we have + * rolled over to the next day so we should reset the trip counter + */ + var steps = getSteps(); + if (trip.getTrip(steps) < 0) + trip.resetTrip(steps); + + drawTopText(day,day_month); + drawBottomText(); + drawSteps(); + } + + function drawTopText(dy, dm) { + var topText1 = ""; + var topText2 = ""; + + switch(infoMode) { + case INFO_DATE: + topText1 = dy.toUpperCase(); + topText2 = dm.toUpperCase(); + break; + case INFO_TRIP: + topText2 = "TRIP"; + break; + case INFO_BATT: + topText2 = "BATT"; + break; + case INFO_MEM: + topText2 = "MEM"; + break; + case INFO_FW: + topText2 = "F/W"; + break; + } + + if (prevTopText1 !== topText1 || prevTopText2 !== topText2) { + prevTopText1 = topText1; + prevTopText2 = topText2; + + // day, date + g.setFont("Vector", 24); + g.setFontAlign(0, -1); + g.clearRect(150, 30, 239, 75); + g.drawString(topText1, 195, 30, true); + g.drawString(topText2, 195, 55, true); + } + } + + function drawBottomText() { + var bottomText = ""; + var steps = getSteps(); + + switch(infoMode) { + case INFO_DATE: + bottomText = "" + steps; + break; + case INFO_TRIP: + bottomText = "" + trip.getTrip(steps); + break; + case INFO_BATT: + bottomText = "" + E.getBattery() + "%"; + break; + case INFO_MEM: + var val = process.memory(); + bottomText = "" + Math.round(val.usage*100/val.total) + "%"; + break; + case INFO_FW: + bottomText = process.env.VERSION; + break; + } + + if (prevBottomText !== bottomText) { + prevBottomText = bottomText; + g.clearRect(148, 190, 239, 239); + g.setColor(1,1,1); // white + g.setFont("Vector", 24); + g.setFontAlign(0, -1); + g.drawString(bottomText, 195, 190); + } + } + + function drawSteps() { + var i = 0; + var cx = 150 + 45; + var cy = 130; + var r = 34; + + var steps = getSteps(); + + if (trip.getTripState() == true) + steps = trip.getTrip(steps); + + if (prevSteps == steps) + return; + + prevSteps = steps; + + var percent = steps / 10000; + + if (percent > 1) percent = 1; + + var startrot = 0 - 180; + var midrot = -180 - (360 * percent); + var endrot = -360 - 180; + + //g.setColor(0x07FF); // light cyan + g.setColor(0xFFC0); // yellow + + // draw guauge + for (i = startrot; i > midrot; i -= 3) { + x = cx + r * Math.sin(radians(i)); + y = cy + r * Math.cos(radians(i)); + g.fillCircle(x,y,3); + } + + // change the remaining color to RED if battery is below 25% + if (E.getBattery() > 25) { + g.setColor(0x7BEF); // grey + //g.setColor(0x000D); // dark navy + } else { + g.setColor(0xF800); // red + } + + // draw remainder of guage in grey or red + for (i = midrot - 12; i > endrot + 12; i -= 3) { + x = cx + r * Math.sin(radians(i)); + y = cy + r * Math.cos(radians(i)); + g.fillCircle(x,y,3); + } + } + + function getSteps() { + if (stepsWidget() === undefined) + return "E-STEPS"; + + return stepsWidget().getSteps(); + } + + function stepsWidget() { + if (WIDGETS.activepedom !== undefined) { + return WIDGETS.activepedom; + } else if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom; + } + return undefined; + } + + return {init:init, freeResources:freeResources, startTimer:startTimer, stopTimer:stopTimer, + onButtonShort:onButtonShort, onButtonLong:onButtonLong}; + } + + return getFace; +})(); diff --git a/apps/kitchen/swatch.kit.js b/apps/kitchen/swatch.kit.js index b88518cbd..8c24f34b8 100644 --- a/apps/kitchen/swatch.kit.js +++ b/apps/kitchen/swatch.kit.js @@ -31,7 +31,9 @@ } } - function onButtonLong(btn) {} + function onButtonLong(btn) { + if (btn === 2) Bangle.showLauncher(); + } return {init:init, freeResources:freeResources, startTimer:startTimer, stopTimer:stopTimer, onButtonShort:onButtonShort, onButtonLong:onButtonLong}; diff --git a/apps/lapcounter/ChangeLog b/apps/lapcounter/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/lapcounter/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/lapcounter/README.md b/apps/lapcounter/README.md new file mode 100644 index 000000000..8866955e4 --- /dev/null +++ b/apps/lapcounter/README.md @@ -0,0 +1,19 @@ +# Lap Counter + +Click button to count laps (e.g. in a swimming pool). +Also shows total duration snapshot (like a stopwatch, but laid back). + +![Screenshot](screenshot.png) + +## Usage + +* Click BTN1 to start counting. Counter becomes `0`, duration becomes `00:00.0` +* Each time you click BTN1, counter is incremented, and you see duration between first and last clicks. + +## Features + +Disables LCD timeout (so that you can be _sure_ what BTN1 would do). + +## Creator + +[Nimrod Kerrett](https://zzzen.com) diff --git a/apps/lapcounter/app-icon.js b/apps/lapcounter/app-icon.js new file mode 100644 index 000000000..a443b3a41 --- /dev/null +++ b/apps/lapcounter/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIA/AH4A/AAkQgEBAREAC6oABdZQXkI6wuKC5iPUFxoXIOpoX/C6QFCC6IsCC6ZEDC/4XcPooXOFgoXQIgwX/C7IUFC5wsIC5ouCC6hcJC5h1DF9YwBChCPOAH4A/AH4Ap")) diff --git a/apps/lapcounter/app.js b/apps/lapcounter/app.js new file mode 100644 index 000000000..215f6140a --- /dev/null +++ b/apps/lapcounter/app.js @@ -0,0 +1,53 @@ +const w = g.getWidth(); +const h = g.getHeight(); +const wid_h = 24; +let tStart; +let tNow; +let counter=-1; + +const icon = require("heatshrink").decompress(atob("mEwwkBiIA/AH4A/AAkQgEBAREAC6oABdZQXkI6wuKC5iPUFxoXIOpoX/C6QFCC6IsCC6ZEDC/4XcPooXOFgoXQIgwX/C7IUFC5wsIC5ouCC6hcJC5h1DF9YwBChCPOAH4A/AH4Ap")); + +function timeToText(t) { // Courtesy of stopwatch app + let hrs = Math.floor(t/3600000); + let mins = Math.floor(t/60000)%60; + let secs = Math.floor(t/1000)%60; + let tnth = Math.floor(t/100)%10; + let text; + + if (hrs === 0) + text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2) + "." + tnth; + else + text = ("0"+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2); + //log_debug(text); + return text; +} + +function doCounter() { + if (counter<0) { + tStart = Date.now(); + tNow = tStart; + } else { + tNow = Date.now(); + } + counter++; + let dT = tNow-tStart; + + g.clearRect(0,wid_h,w,h-wid_h); + g.setFontAlign(0,0); + g.setFont("Vector",72); + g.drawString(counter,w/2,h/2); + g.setFont("Vector",24); + g.drawString(timeToText(dT),w/2,h/2+50); +} + +setWatch(doCounter, BTN1, true); + +g.clear(true); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +Bangle.setLCDTimeout(0); +g.drawImage(icon,w/2-24,h/2-24); +g.setFontAlign(0,0); +require("Font8x12").add(Graphics); +g.setFont("8x12"); +g.drawString("Click button to count.", w/2, h/2+22); diff --git a/apps/lapcounter/app.png b/apps/lapcounter/app.png new file mode 100644 index 000000000..7d6ca8317 Binary files /dev/null and b/apps/lapcounter/app.png differ diff --git a/apps/lapcounter/screenshot.png b/apps/lapcounter/screenshot.png new file mode 100644 index 000000000..f3113d86e Binary files /dev/null and b/apps/lapcounter/screenshot.png differ diff --git a/apps/largeclock/ChangeLog b/apps/largeclock/ChangeLog index d06cc9edf..8c9b24be9 100644 --- a/apps/largeclock/ChangeLog +++ b/apps/largeclock/ChangeLog @@ -5,3 +5,6 @@ 0.05: Add support for 12 hour time 0.06: Allow to disable BTN1 and BTN3 buttons 0.07: Don't clear all intervals during initialisation +0.08: Use Bangle.setUI for button/launcher handling +0.09: fix font size for latest firmwares +0.10: Configure the side text direction based on the wrist on which you wear your watch diff --git a/apps/largeclock/README.md b/apps/largeclock/README.md index 5c2ad42c2..b6e6a640f 100644 --- a/apps/largeclock/README.md +++ b/apps/largeclock/README.md @@ -7,6 +7,7 @@ A readable and informational digital watch, with date, seconds and moon phase an - Readable - Informative: hours, minutes, secondsa, date, year and moon phase - Pairs nicely with any other apps: in setting > large clock any installed app can be assigned to BTN1 and BTN3 in order to open it easily directly from the watch, without the hassle of passing trough the launcher. For example BTN1 can be assigned to alarm and BTN3 to chronometer. +- Configure the text direction on the side depending on the wrist on which you wear your watch. ## How to use it diff --git a/apps/largeclock/bangle1-large-clock-screenshot.png b/apps/largeclock/bangle1-large-clock-screenshot.png new file mode 100644 index 000000000..756ae994b Binary files /dev/null and b/apps/largeclock/bangle1-large-clock-screenshot.png differ diff --git a/apps/largeclock/largeclock.js b/apps/largeclock/largeclock.js index 24127ac15..e1afd5949 100644 --- a/apps/largeclock/largeclock.js +++ b/apps/largeclock/largeclock.js @@ -14,6 +14,9 @@ const settings = require("Storage").readJSON("largeclock.json", 1)||{}; const BTN1app = settings.BTN1 || ""; const BTN3app = settings.BTN3 || ""; +const right_hand = !!settings.right_hand; +const rotation = right_hand ? 3 : 1; + function drawMoon(d) { const BLACK = 0, MOON = 0x41f, @@ -140,14 +143,14 @@ function drawTime(d) { g.clearRect(0, 24, moonX - moonR - 10, 239); g.setColor(1, 1, 1); g.setFontAlign(-1, -1); - g.setFont("Vector", 100); + g.setFont("Vector", 130); g.drawString(hours, 40, 24, true); g.setColor(1, 50, 1); - g.drawString(minutes, 40, 135, true); + g.drawString(minutes, 40, 130, true); g.setFont("Vector", 20); - g.setRotation(3); - g.drawString(`${dow} ${day} ${month}`, 50, 10, true); - g.drawString(year, is12Hour ? 46 : 75, 205, true); + g.setRotation(rotation); + g.drawString(`${dow} ${day} ${month}`, 60, right_hand?10:205, true); + g.drawString(year, is12Hour?(right_hand?56:120):(right_hand?85:115), right_hand?205:10, true); lastMinutes = minutes; } g.setRotation(0); @@ -179,9 +182,9 @@ Bangle.on("lcdPower", function(on) { Bangle.setLCDMode(); -// Show launcher when middle button pressed -clearWatch(); -setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); +// Show launcher when button pressed +Bangle.setUI("clock"); + if (BTN1app) setWatch( function() { load(BTN1app); diff --git a/apps/largeclock/largeclock.json b/apps/largeclock/largeclock.json index 58c981197..7fff2f438 100644 --- a/apps/largeclock/largeclock.json +++ b/apps/largeclock/largeclock.json @@ -1,4 +1,5 @@ { "BTN1": "", - "BTN3": "" + "BTN3": "", + "right_hand": false } diff --git a/apps/largeclock/settings.js b/apps/largeclock/settings.js index 293f66677..f996666ab 100644 --- a/apps/largeclock/settings.js +++ b/apps/largeclock/settings.js @@ -28,7 +28,8 @@ const settings = s.readJSON("largeclock.json", 1) || { BTN1: "", - BTN3: "" + BTN3: "", + right_hand: false }; function showApps(btn) { @@ -67,10 +68,19 @@ } const mainMenu = { - "": { title: "Large Clock Settings" }, + "": { title: "Large Clock" }, "< Back": back, "BTN1 app": () => showApps("BTN1"), - "BTN3 app": () => showApps("BTN3") + "BTN3 app": () => showApps("BTN3"), + "On right hand": { + value: !!settings.right_hand, + format: v=>v?"Yes":"No", + onchange: v=>{ + settings.right_hand = v; + s.writeJSON("largeclock.json", settings); + } + } }; + E.showMenu(mainMenu); }); diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog index 7e7ea65ab..0b2f134ad 100644 --- a/apps/launch/ChangeLog +++ b/apps/launch/ChangeLog @@ -2,3 +2,10 @@ 0.02: Only store relevant app data (saves RAM when many apps) 0.03: Allow scrolling to wrap around (fix #382) 0.04: Now displays widgets +0.05: Use g.theme for colours +0.06: Use Bangle.setUI for buttons +0.07: Theme colours fix +0.08: Merge Bangle.js 1 and 2 launchers +0.09: Bangle.js 2 - pressing the button goes back to clock (fix #971) + After 10s of being locked, the launcher goes back to the clock screen +0.10: added in selectable font in settings including scalable vector font diff --git a/apps/launch/app-bangle1.js b/apps/launch/app-bangle1.js new file mode 100644 index 000000000..f779f5de4 --- /dev/null +++ b/apps/launch/app-bangle1.js @@ -0,0 +1,75 @@ +var s = require("Storage"); +var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); +apps.sort((a,b)=>{ + var n=(0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; +}); +var selected = 0; +var menuScroll = 0; +var menuShowing = false; + +function drawMenu() { + g.reset().setFont("6x8",2).setFontAlign(-1,0); + var w = g.getWidth(); + var h = g.getHeight(); + var m = w/2; + var n = Math.floor((h-48)/64); + if (selected>=n+menuScroll) menuScroll = 1+selected-n; + if (selectedn+menuScroll) ? g.theme.fg : g.theme.bg); + g.fillPoly([m,h-7,m-14,h-21,m+14,h-21]); + // draw + g.setColor(g.theme.fg); + for (var i=0;i{ + if (dir) { + selected += dir; + if (selected<0) selected = apps.length-1; + if (selected>=apps.length) selected = 0; + drawMenu(); + } else { + if (!apps[selected].src) return; + if (require("Storage").read(apps[selected].src)===undefined) { + E.showMessage("App Source\nNot found"); + setTimeout(drawMenu, 2000); + } else { + E.showMessage("Loading..."); + load(apps[selected].src); + } + } +}); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +// 10s of inactivity goes back to clock +if (Bangle.setLocked) Bangle.setLocked(false); // unlock initially +var lockTimeout; +Bangle.on('lock', locked => { + if (lockTimeout) clearTimeout(lockTimeout); + lockTimeout = undefined; + if (locked) + lockTimeout = setTimeout(_=>load(), 10000); +}); diff --git a/apps/launch/app-bangle2.js b/apps/launch/app-bangle2.js new file mode 100644 index 000000000..156eecdf4 --- /dev/null +++ b/apps/launch/app-bangle2.js @@ -0,0 +1,77 @@ +var s = require("Storage"); +let fonts = g.getFonts(); +var scaleval = 1; +var vectorval = 20; +var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; +let settings = require('Storage').readJSON("launch.json", true) || {}; +if ("vectorsize" in settings) { + vectorval = parseInt(settings.vectorsize); +} +if ("font" in settings){ + if(settings.font == "Vector"){ + scaleval = vectorval/20; + font = "Vector"+(vectorval).toString(); + } + else{ + font = settings.font; + scaleval = (font.split('x')[1])/20; + } +} +var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); +apps.sort((a,b)=>{ + var n=(0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; +}); +apps.forEach(app=>{ + if (app.icon) + app.icon = s.read(app.icon); // should just be a link to a memory area +}); +// FIXME: check not needed after 2v11 +if (g.wrapString) { + g.setFont(font); + apps.forEach(app=>app.name = g.wrapString(app.name, g.getWidth()-64).join("\n")); +} + +function drawApp(i, r) { + var app = apps[i]; + if (!app) return; + g.clearRect((r.x),(r.y),(r.x+r.w-1), (r.y+r.h-1)); + g.setFont(font).setFontAlign(-1,0).drawString(app.name,64*scaleval,r.y+(32*scaleval)); + if (app.icon) try {g.drawImage(app.icon,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){} +} + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +E.showScroller({ + h : 64*scaleval, c : apps.length, + draw : drawApp, + select : i => { + var app = apps[i]; + if (!app) return; + if (!app.src || require("Storage").read(app.src)===undefined) { + E.showMessage("App Source\nNot found"); + setTimeout(drawMenu, 2000); + } else { + E.showMessage("Loading..."); + load(app.src); + } + } +}); + +// pressing button goes back +setWatch(_=>load(), BTN1, {edge:"falling"}); + +// 10s of inactivity goes back to clock +Bangle.setLocked(false); // unlock initially +var lockTimeout; +Bangle.on('lock', locked => { + if (lockTimeout) clearTimeout(lockTimeout); + lockTimeout = undefined; + if (locked) + lockTimeout = setTimeout(_=>load(), 10000); +}); diff --git a/apps/launch/app.js b/apps/launch/app.js deleted file mode 100644 index 9795d8901..000000000 --- a/apps/launch/app.js +++ /dev/null @@ -1,66 +0,0 @@ -var s = require("Storage"); -var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); -apps.sort((a,b)=>{ - var n=(0|a.sortorder)-(0|b.sortorder); - if (n) return n; // do sortorder first - if (a.nameb.name) return 1; - return 0; -}); -var selected = 0; -var menuScroll = 0; -var menuShowing = false; - -function drawMenu() { - g.setFont("6x8",2); - g.setFontAlign(-1,0); - var n = 3; - if (selected>=n+menuScroll) menuScroll = 1+selected-n; - if (selectedn+menuScroll) ? -1 : 0); - g.fillPoly([120,233,106,219,134,219]); - // draw - g.setColor(-1); - for (var i=0;i=apps.length) selected = 0; - drawMenu(); -}, BTN3, {repeat:true}); -setWatch(function() { // run - if (!apps[selected].src) return; - if (require("Storage").read(apps[selected].src)===undefined) { - E.showMessage("App Source\nNot found"); - setTimeout(drawMenu, 2000); - } else { - E.showMessage("Loading..."); - load(apps[selected].src); - } -}, BTN2, {repeat:true,edge:"falling"}); -Bangle.loadWidgets(); -Bangle.drawWidgets(); diff --git a/apps/launch/settings.js b/apps/launch/settings.js new file mode 100644 index 000000000..8be1adb36 --- /dev/null +++ b/apps/launch/settings.js @@ -0,0 +1,25 @@ +// make sure to enclose the function in parentheses +(function(back) { + let settings = require('Storage').readJSON('launch.json',1)||{}; + let fonts = g.getFonts(); + function save(key, value) { + settings[key] = value; + require('Storage').write('launch.json',settings); + } + const appMenu = { + '': {'title': 'Launcher Settings'}, + '< Back': back, + 'Font': { + value: fonts.includes(settings.font)? fonts.indexOf(settings.font) : fonts.indexOf("12x20"), + min:0, max:fonts.length-1, step:1,wrap:true, + onchange: (m) => {save('font', fonts[m])}, + format: v => fonts[v] + }, + 'Vector font size': { + value: settings.vectorsize || 10, + min:10, max: 20,step:1,wrap:true, + onchange: (m) => {save('vectorsize', m)} + } + }; + E.showMenu(appMenu); +}); diff --git a/apps/lazyclock/ChangeLog b/apps/lazyclock/ChangeLog index 984d29869..a3f125786 100644 --- a/apps/lazyclock/ChangeLog +++ b/apps/lazyclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: Launch app -0.02: Fix bug with the elusive one o'clock monster; Only change template when going over boundaries; Re-jig wording options \ No newline at end of file +0.02: Fix bug with the elusive one o'clock monster; Only change template when going over boundaries; Re-jig wording options +0.03: Use Bangle.setUI for launcher/buttons diff --git a/apps/lazyclock/bangle1-lazy-clock-screenshot.png b/apps/lazyclock/bangle1-lazy-clock-screenshot.png new file mode 100644 index 000000000..282adc289 Binary files /dev/null and b/apps/lazyclock/bangle1-lazy-clock-screenshot.png differ diff --git a/apps/lazyclock/lazyclock-app.js b/apps/lazyclock/lazyclock-app.js index 400e26ede..604448ce6 100644 --- a/apps/lazyclock/lazyclock-app.js +++ b/apps/lazyclock/lazyclock-app.js @@ -221,22 +221,13 @@ function addEvents() { } }); - setWatch(switchMode, BTN1, { - repeat: true, - edge: "falling" - }); - - setWatch(Bangle.showLauncher, BTN2, { - repeat: false, - edge: "falling" - }); - - setWatch(() => { - currentFormatter = null; - refreshTime(); - }, BTN3, { - repeat: true, - edge: "falling" + // Show launcher when button pressed + Bangle.setUI("clockupdown", btn=>{ + if (btn<0) switchMode(); + if (btn>0) { + currentFormatter = null; + refreshTime(); + } }); } @@ -245,9 +236,9 @@ function init() { startClock(); Bangle.loadWidgets(); - Bangle.drawWidgets(); + Bangle.drawWidgets(); addEvents(); } -init(); \ No newline at end of file +init(); diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog new file mode 100644 index 000000000..c5f8187b7 --- /dev/null +++ b/apps/lcars/ChangeLog @@ -0,0 +1,7 @@ +0.01: Launch app. +0.02: Swipe left/right to set an alarm. +0.03: New design with different icons if gps, hrm or compass is on. +0.04: Inluded LCARS Logo. +0.05: Additional icons for (1) charging and (2) bat < 30%. +0.06: Fix - Alarm disabled, if clock was closed. +0.07: Added settings to adjust data that is shown for each row. \ No newline at end of file diff --git a/apps/lcars/README.md b/apps/lcars/README.md new file mode 100644 index 000000000..15009e838 --- /dev/null +++ b/apps/lcars/README.md @@ -0,0 +1,18 @@ +# LCARS clock + +A simple LCARS inspired clock. +Note: To display the steps, its necessary to install +the [Pedometer widget](https://banglejs.com/apps/#pedometer%20widget). + +## Features + * LCARS Style watch face + * Shows satate (charging, out of battery etc.) + * SHows data that can be configured (steps, HRM, temperature etc.) + * Swipe left/right to activate an alarm + +## Icons +
Icons made by Smashicons, Freepik from www.flaticon.com
+ + +## Creator +Made by [David Peer](https://github.com/peerdavid) \ No newline at end of file diff --git a/apps/lcars/bg_large.png b/apps/lcars/bg_large.png new file mode 100644 index 000000000..56590e878 Binary files /dev/null and b/apps/lcars/bg_large.png differ diff --git a/apps/lcars/bg_small.png b/apps/lcars/bg_small.png new file mode 100644 index 000000000..ea3a75688 Binary files /dev/null and b/apps/lcars/bg_small.png differ diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js new file mode 100644 index 000000000..859e999a5 --- /dev/null +++ b/apps/lcars/lcars.app.js @@ -0,0 +1,317 @@ +const SETTINGS_FILE = "lcars.setting.json"; +const Storage = require("Storage"); + + +// ...and overwrite them with any saved values +// This way saved values are preserved if a new version adds more settings +const storage = require('Storage') +let settings = { + alarm: -1, + dataRow1: "Battery", + dataRow2: "Steps", + dataRow3: "Temp." +}; +let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; +for (const key in saved_settings) { + settings[key] = saved_settings[key] +} +let hrmValue = 0; + +/* + * Requirements and globals + */ +const locale = require('locale'); + +var backgroundImage = { + width : 176, height : 151, bpp : 3, + transparent : 2, + buffer : require("heatshrink").decompress(atob("AAdx48cATsAg4daIAX3799ATv2wEFDrUAgNHQDyDghaAeQcJKG86D4gRKGgAA4jxKFuBB5iaDF6BB5ZwyD6QAYCC4CD/Qf6Dzg/gQf8H/iD/n//wCD9gP///wQfpBKQf6D4h5BB/yD8jl/IIIABjiD5n4/DAAWAQe8B//8QYfHj//PAaDzHwICCAAP4gYCBQep6DIIYFBRgKD1j/+gB9BQYYKBn/gQen/+BBFQAUH/iDzGoZBHJoOAQeRBDj5BHj6PB0WKlACDJQIAofYZBFBAZBBAGMHPQZB8QYZAEIIcDIOiDI/hB3QZBBFjlx44CDuBBpg4DCIJEfIIPnz15AQeAQeH8gIDBGoJBCnnz54CDZ1UHPQMHIIUAIIKD3II6MBQYQCCQeI1B+BBC/BKCBASGCQeK5B/xBC4BKEn/gAoKDyj//45BFj/xZYSDzgF/IAP+JQrLCQecAgKDBF4cHQYKJDQecAn6EBAAiJEQeZBB/jICAAMcvwMDQevgQwR0CIIiDzgP/BA1/4CD3nAHGhyD3ABqD0ABiD/Qf4ADjiD/gEnQYuQQf6D7gaDFzxB5gFzQYnz4BB5hyDFATfkEoIdagEBQYoCcgEHDrReBhKDhwEBQbYABjiD/AH4A/AH4AGiFx48cATsAg4daIIWSpMkATuQEbkAgJfbQckJQDyDhZxQA1gRKFpBA4gEQQYtwIPMSQYtAIPKADQfqADAQRA5Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf4A/AH4A/AH4A/AFkcuPHAQdAIPOSpMkAQaD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf4A/AH4A/AH4A/AGUcuPHAQdwIPOSpMkAQaD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf4AciSDFoCD/QfcCQYtIIPMAQYoC6gEJQYgC6gEBQf7HCQf4ABiiD9")) +} + +var iconEarth = { + text: "EARTH", + width : 50, height : 50, bpp : 3, + buffer : require("heatshrink").decompress(atob("AFtx48ECBsDwU5k/yhARLjgjBjlzAQMQEZcIkOP/fn31IEZgCBnlz58cEpM4geugEgwU/8+WNZJHDuHHvgmBCQ8goEOnVgJoMnyV58mACItHI4X8uAFBuVHnnz4BuGxk4////Egz3IkmWvPgNw8f/prB//BghTC+AjE7848eMjNnzySBwUJkmf/BuGuPDAQIjBiPHhhTCSQnjMo0ITANJn44Dg8MuFBggCCiFBcAJ0Bv5xEh+ITo2OhHkyf/OIQdBWwVHhgjBNwUE+fP/5EEgePMoYLBhMgyVJk/+BQQdC688I4XxOIc8v//NAvr+QEBj/5NwKVBy1/QYUciPBhk1EAJrC+KeC489QYaMBgU/8BNB9+ChEjz1Jkn/QYMBDQIgCcYTCCiP/nlzJQmenMAgV4//uy/9wRaB/1J8iVCcAfHjt9TYYICnhKCgRKBw159/v//r927OIeeoASBDQccvv3791KYVDBYPLJQeCnPnz//AAP6ocEjEkXgMgJQtz79fLAP8KYkccAcJ8Gf/f/xu/cAMQ4eP5MlyQRCMolx40YsOGBAPfnnzU4KVDpKMBvz8Dh0/8me7IICgkxJQXPIgZTD58sEgcJk+eNoONnFBhk4/5uB/pcDg5KD+4mEv4CBXISVDhEn31/8/+mH7x//JQK5CAAMB4JBCnnxJQf/+fJEgkAa4L+CAQOOjMn/1bXIRxDJQXx58f//Hhlz/88EgsChMgz/Zs/+nfkyV/8huDOI6SD498NwoACi1Z8+S/Plz17/+QCI7jC+ZxBmfPnojIAAMDcYWSp//2wRJEwq2GABECjMgNYwAmA=")) +} + +var iconSaturn = { + text: "SATURN", + width : 50, height : 50, bpp : 3, + transparent : 1, + buffer : require("heatshrink").decompress(atob("AH4A/AEkQuPHCJ0ChEAwARNjAjBjgjOhs06Q2OEYVx4ARMhEggUMkANIDoIgBoEEgEBNxJEC6ZrBAAMwNxAjDNYcHNxIjB7dtEwIHBwRoKj158+cuPEjlwCRAjC23bpu0wRNDAAsHEYWeEwaSJ6YjCAQUNSRQjEzxQBWZMNEYlsmg2JWAIjCz95SoJuJggjDtuw6dMG5JKCz998wFBJRVNEYW0yaVBJRNhJQN9+4pCzhKJmBKC4YpB/fINxIgCzFxSoQ3J4ENm3CAQPb98wbpEcAQMYWwKYBNxMDXgc2/fv3g2IEAOAgAjBjy5CEhEMfYICBgfPnjdLjj+CgMHiC3JknDhhoINw4jCAB0IJQIANR4QjPAH4A/AFA")) +} + +var iconMoon = { + text: "MOON", + width : 50, height : 50, bpp : 3, + transparent : 1, + buffer : require("heatshrink").decompress(atob("AH4AQjlx44CCCZsg8eOkHDwAQKEYgmPhEgEQM48AOIgMHEYoCB4ATI8UAmH/x04JoRuJsImHuBKLn37EwZuIgEQOI8cEpXj/yYBhE8+YNGgkYoJxITBUPnAaC///nC+FjBuIOJZEB8YeCh/8AoYACoMEEAnEjhQDPQJKJ/DCDAoi5DoLdHAoMQgLjFWYPOnngh02IwXzwDjEgPGEYS8BI4MBYoSVG4fP/nghkAgZrDkngJQqSG4gvBg4sBQgkImHihEAWwP8ZBMBEYl5/+cSoVAGQIUFh04weJn///0gj/OEw5KEz45BzhuCTYQAEgePB4IACAoJuBnAQEa4XHjxKB//xFgWHJQsCRgMDEonipwjENwUBDQNx8+evvn/hTDLw3igE+EgZxB8UOXIvEJQUfEYOfv53DEQkgga5BJQvzx84cAj+CDoNh8/eEYJKDuCSEcocnEon+/7xEgFBIIcfB4Mf/IICXI2DgDdBAAn758gCIq5Dv4zBvJuIOIfjEgvP/ARHgwdCB4P3AoTdFAAk4EYk8SQgAFTALaDSQwAGh08//vnDmBABYmEEZYAzA==")) +} + +var iconMars = { + text: "MARS", + width : 50, height : 50, bpp : 3, + transparent : 1, + buffer : require("heatshrink").decompress(atob("AH4ATjlwCJ+Dh0wwAQMg0cuPHjFhCZkDps0yVJkmQCBMEjFx42atOmzQmLhMkEYQCCCREQoOGEYmmzB0IEY4CBkARGoJKBEYQCEzgSGkGSpAjDyYCCphuGiFhJQgCD8ASFgRHGAQKbB6BuHJRGeOIsINxEk6dNmARDgMEjQjHAQPnVQojIyZKB6YSDNwK5FAQt54BuDXJIjBEwK5EgxKKXgq5BJRdgXIojJAQJKMcAM0EwM2JUApDoCVFExa7FkGCgAmIkAREEwUEjAmHCIgABhEggQmFpACBCIojBEwRQCzVhwkQU4YADgQmBwQCCI4IFBCAojFAQojGJQQjDAQgRGEZICBEo4gFyUIkilFJQUYEAZrBAQMYNw5KDSQSbCNwwABgOGEwgCBsPACQ5xGwdNnARJcAVh48evvnCJK8Chs+/fv33gCRcB48cuPHCBYA/ADAA==")) +} + +var iconSatellite = { + text: "GPS ON", + width : 50, height : 50, bpp : 3, + transparent : 2, + buffer : require("heatshrink").decompress(atob("pMkyQC/ATGXhIRPyNl0gmPjlwCJ9ly1aCJ1c+fHJR1Hy1ZJR1I+fPnlx6QRLpe+/JKBr5KMuYjBJQMdCJce/fvJQW0CJUlEYQCBSpvvJQbXJjl0NwnzNxGQwEOnHhgF78+WqQyIrFx48cAQXz4ShJgAABh0+8cP//9LJEhg4jDuP3//0LhGQgYlBgeAn///5cIy8MuAmDCIP/9I4HkmCEYMOgHfCQWkCI0cuBuDgF/CIP+CI1Ny1IkeAgHANwIAB/QRFrj7BhkxEwQRC/4RFpbXDgSVBg4RCSorXDI4MJAQMfCIP8cwImDn37fwN58+kwHgLgSVFub7CI4NyBAJKDLgkuEYX78+evKtCLg0jEYRKC58JMoRcFkwjDJQTFDl65EkojEAQMdcwn/+gFC3YjEJQLXEpYRDWwQmEdI6SHAQO0CJUkx4jDF4gCIJQgRMXIjCEARIjCCJ2XEYPKCJqJBJQIROcAUpCJ0kybaDARtdCKAC2kAA=")) +} + +var iconAlarm = { + text: "TIMER", + width : 50, height : 50, bpp : 3, + transparent : 1, + buffer : require("heatshrink").decompress(atob("kmSpICEp//BAwCJn/+CJ8k//5CKAABCJs8uPH//x48EI5YjCAARNKEYUcv//jgFBExEnEYoAC+QmHIgIgC/gpCuPBCI2fIgU4AQXjA4P8CIuTEYZKBAolwHApXBEAWP//jxwpBAALaFDoYCIiQmDDIP4EAT+CEwnJEwYjLAQLaFEYomDKALmDNwoCIOIZuD8AkFgCYDHAQjMAQTdDNwOAEg0Dx0/cYeREZtxQYOTHgJuHOIvkXJy8DNwIACJQ8Ah4NDAAfxEZARHOIIkHg4jQAQb1CQ4KVJgEOnDIBSoIjNAQPBcAaVJcAKVBcDGOcD7OBMQM48BuH8f//JKCnhKNggRBkmfTQJxBEwhuD/gRCyVHJRlyCIVJXgYmB8ZQBAoIKBXIQmCOIt/NxAUCOIImCIgIpCBAJuDAQZEE/huIAQWTDgImBTYQGC8gRFcYpKFCI8kDwQAFCJBfBEAX/+IjBiQRIEw4jJAQc8v//NYwCIOgJrIJpA1OcwbaFAQWQA=")) +} + +var iconCharging = { + text: "CHARGE", + width : 50, height : 50, bpp : 3, + transparent : 5, + buffer : require("heatshrink").decompress(atob("23btugAwUBtoICARG0h048eODQYCJ6P/AAUCCJfbo4SDxYRLtEcuPHjlwgoRJ7RnIloUHoYjDAQfAExEAwUIkACEkSAIEYwCBhZKH6EIJI0CJRFHEY0BJRWBSgf//0AJRYSE4BKLj4SE8BKLv4RD/hK/JS2AXY0gXwRKG4cMmACCJQMAg8csEFJQsBAwfasEAm379u0gFbcBfHzgFBz1xMQZKBjY/D0E2+BOChu26yVEEYdww+cgAFCg+cgIfB6RKF4HbgEIkGChEAthfCJQ0eEAIjBBAMxk6GCJQtgtyVBwRKBAQMbHAJKGXIIFCgACBhl54qVG2E+EAJKBJoWAm0WJQ6SCXgdxFgMLJQvYjeAEAUwFIUitEtJQ14NwUHgEwKYZKGwOwNYX7XgWCg3CJQ5rB4MevPnAoPDJRJrCgEG/ECAoNsJRUwoEesIIBiJKI3CVDti/CJRKVDiJHBSo0YsOGjED8AjBcAcIgdhcAXAPIUAcAYIBcA4dBAQUG8BrBgBuCgOwcBEeXIK2BBAIFBgRqBGoYAChq8CcYUE4FbUYOACQsHzgjDgwFBCIImBAQsDtwYD7cAloRI22B86YBw5QBgoRJ7dAgYEDCJaeBJoMcsARMAQNoJIIRE6A")) +} + +var iconNoBattery = { + text: "NO BAT", + width : 50, height : 50, bpp : 3, + transparent : 1, + buffer : require("heatshrink").decompress(atob("kmSpIC/AWMyoQIFsmECJFJhMmA4QXByVICIwODAQ4RRFIQGD5JVLkIGDzJqMyAGDph8MiRKGyApEAoZKFyYIDQwMkSQNkQZABBhIIOOJRuEL5gRIAUKACVQMhmUSNYNDQYJTBBwYFByGTkOE5FJWYNMknCAQKYCiaSCpmGochDoSYBhMwTAZrChILBhmEzKPBF4ImBTAREBDoMmEwJVDoYjBycJFgWEJQRuLJQ1kmQCCjJlCBYbjCagaDBwyDBmBuBF4TjJAUQKINBChCDQxZBcZIIQF4NIgEAgKSDiQmEVQKMBoARBAAMCSQLLBVoxqKL4gaCChVCNwoRKOIo4CJIgABBoSMHpIRFgDdJOIJUBCAUJRgJuEAQb+DIIgRIAX4C/ASOQA")) +} + +// Font to use: +// +Graphics.prototype.setFontAntonioSmall = function(scale) { + // Actual height 18 (17 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAf4Mf/sYAMAAAAAAfgAfAAAAAfgAeAAAAAAiAAj8H/4fyEAv8f/gfiAAgAAAAD54H98eOPHn8Hz8AhwAAAP8Af+AYGAYCAf+AP8MAB8AHwA+AD4AfAAcf4A/8AwMAwMA/8Af4AAAAAwGD8f/8f8MY/cfz4PD8AHMAAAfAAeAAAAAAAAP/+f//YADAAAQABYADf//P/+AAAAAANAAPAAfwAfgAPAANAAAAAAEAAEAA/AA/AAEAAEAAAAAAZAAfAAYAAAAIAAIAAIAAIAAAAAAAAAMAAMAAAAAAAAEAB8Af4H+AfwAcAAAAAP/4f/8YAMf/8f/8H/wAAAAAAEAAMAAf/8f/8f/8AAAAAAAAAHgcfh8cH8YPMf8MPwEAAAAAAOB4eB8YYMY4Mf/8Pn4AAAAAgAHwA/wPwwf/8f/8AAwAAgAAAf54f58ZwMZwMY/8Qf4AAAAAAP/4f/8YYMYYMff8HP4AAAQAAYAAYD8Y/8f/AfgAcAAAAAAAAPv4f/8YYMY8Mf/8Pn4AAAAAAP94f98YGMcMMf/8H/wAAAAAABgwBgwAAAAAABgABg/Bg8AAAAEAAOAAbAA7gAxgBwwASAAbAAbAAbAAbAASAAAAAxwA5gAbAAPAAOAAAAPAAfHcYPcf8Af4AHgAAAAAAAB/gH/wOA4Y/MZ/sbAsbBkb/MZ/sOBsH/AAAAAAMAP8f/4fwwf4wH/8AH8AAMAAAf/8f/8YYMYYMf/8P/4ADgAAAP/4f/8YAMYAMfj8Pj4AAAAAAf/8f/8YAMYAMf/8P/4B/AAAAf/8f/8YMMYMMYIMAAAAAAf/8f/8YYAYYAYYAAAAAAAP/4f/8YAMYIMfP8Pv8AAAAAAf/8f/8AMAAMAf/8f/8f/8AAAAAAf/8f/8AAAAAAAD4AB8AAMf/8f/4f/gAAAAAAf/8f/8A+AD/gfj4eA8QAEAAAf/8f/8AAMAAMAAMAAAf/8f/8f8AB/wAB8AP8P/Af/8f/8AAAAAAf/8f/8HwAA+AAPwf/8f/8AAAAAAP/4f/8YAMYAMf/8P/4AAAAAAf/8f/8YGAYGAf8AP8ABAAAAAf/w//4wAYwAc//+f/yAAAAAAf/8f/8YMAYMAf/8f/8DA8CAAPj4fz8Y4MeeMfP8HD4YAAYAAf/8f/8YAAQAAAAAf/4f/8AAMAAMf/8f/4AAAYAAf4AP/4AP8AP8f/4fwAQAAYAAf8AP/8AD8D/8f8Af8AD/8AD8f/8f8AAAAQAEeB8P/4B/AP/4fA8QAEYAAfAAP4AB/8H/8fwAcAAAAMYD8Y/8f/MfwMcAMAAAf/+f//YADYADAAAAAAfAAf8AB/wAH8AAMQACYADf//f//AAAAA"), 32, atob("BAUHCAcTCAQFBQgGBAYFBggICAgICAgICAgEBQYGBggNCAgICAcHCAkECAgGCwkICAgIBwYICAwHBwYGBgY="), 18+(scale<<8)+(1<<16)); +} + +Graphics.prototype.setFontAntonioLarge = function(scale) { + // Actual height 34 (34 - 1) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAADwAAAAAeAAAAADwAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAD+AAAAH/wAAAP/+AAAf/+AAA//8AAB//4AAD//wAAD//gAAAf/AAAAD+AAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAB////gA/////AP////8D/////wfAAAA+DwAAADweAAAAeDwAAADwf////+D/////wP////8Af///+AAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAOAAAAADwAAAAAeAAAAAHgAAAAB/////wf////+D/////wf////+D/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AAPwH/4AP+B//AH/wf/4D/+D4AB/9weAAf4ODwAP8BweAP/AOD///gBwP//wAOA//4ABwB/4AAOAAAAAAAAAAAAAAAAAAAAB8AA/gA/gAH/AP8AA/8D/gAH/wfAHAA+DwA4ADweAHgAeDwB8ADwf7/+H+D/////gP/9//8A//H/+AA/AH/AAAAAAAAAAAAAAAAAABwAAAAD+AAAAD/wAAAH/+AAAH/5wAAH/wOAAP/gBwAP/gAOAD/////wf////+D/////wf////+AAAABwAAAAAOAAAAABwAAAAAAAAAAAAAAAAAAeAD//4D/Af//Af8D//4D/wf//Af+DwPAADweB4AAeDwPAADweB///+DwP///weA///8DwD//+AAAA/8AAAAAAAAAAAAAAAAAAAAAA////AA/////AP////8D/////wfgPAB+DwB4ADweAOAAeDwBwADwf+PAA+D/x///wP+H//8A/wf//AAAA//gAAAAAAAAAAAAADgAAAAAeAAAAADwAAAAAeAAAD+DwAAP/weAA//+DwA///weB///8Dx//8AAf//wAAD//gAAAf/AAAAD/AAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAD/wf/wB//v//AP////8D/////weAPwAeDwA8ADwcAHAAeDwB8ADwf////+D/////wP/9//8A//H//AA/AD/AAAAAAAAAAAAAAAAAAAAAD//gfAA///D/AP//8f8D///j/weAA8A+DwADgDweAAcAeDwAHgDwf////+B/////gP////8Af///+AAP//4AAAAAAAAAAAAAAAAAAAAAAD4AfAAAfAD4AAD4AfAAAfAD4AAD4AfAAAAAAAAAAAAAA=="), 46, atob("Cg4QEBAQEBAQEBAQCQ=="), 39+(scale<<8)+(1<<16)); +} + +/* + * Draw watch face + */ +var drawTimeout; +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +function printData(key, y){ + g.setFontAlign(-1,-1,0); + + if(key == "Battery"){ + var bat = E.getBattery(); + g.drawString("BAT:", 30, y); + g.drawString(bat+ "%", 68, y); + + } else if(key == "Steps"){ + var steps = getSteps(); + g.drawString("STEP:", 30, y); + g.drawString(steps, 68, y); + + } else if(key == "Temp."){ + var temperature = Math.floor(E.getTemperature()); + g.drawString("TEMP:", 30, y); + g.drawString(temperature + "C", 69, y); + + } else if(key == "HRM"){ + g.drawString("HRM:", 30, y); + g.drawString(hrmValue, 69, y); + + } else { + g.drawString("NOT FOUND", 30, y); + } +} + +function draw(){ + + // First handle alarm to show this correctly afterwards + handleAlarm(); + + // Next draw the watch face + g.reset(); + g.clearRect(0, 24, g.getWidth(), g.getHeight()); + + // Draw background image + g.drawImage(backgroundImage, 0, 24); + + // Draw symbol + var bat = E.getBattery(); + var timeInMinutes = getCurrentTimeInMinutes(); + + var iconImg = + isAlarmEnabled() ? iconAlarm : + Bangle.isCharging() ? iconCharging : + bat < 30 ? iconNoBattery : + Bangle.isGPSOn() ? iconSatellite : + timeInMinutes % 4 == 0 ? iconSaturn : + timeInMinutes % 4 == 1 ? iconMars : + timeInMinutes % 4 == 2 ? iconMoon : + iconEarth; + g.drawImage(iconImg, 115, 115); + + // Alarm within symbol + g.setFontAlign(0,0,0); + g.setFontAntonioSmall(); + g.drawString(iconImg.text, 115+25, 105); + if(isAlarmEnabled() > 0){ + g.drawString(getAlarmMinutes(), 115+25, 115+25); + } + + // Write time + var currentDate = new Date(); + var timeStr = locale.time(currentDate,1); + g.setFontAlign(0,0,0); + g.setFontAntonioLarge(); + g.drawString(timeStr, 60, 55); + + // Write date + g.setFontAlign(-1,-1, 0); + g.setFontAntonioSmall(); + + var dayName = locale.dow(currentDate, true).toUpperCase(); + var day = currentDate.getDate(); + g.drawString(day, 100, 35); + g.drawString(dayName, 100, 55); + + // Draw battery + printData(settings.dataRow1, 98); + printData(settings.dataRow2, 121); + printData(settings.dataRow3, 144); + + // Queue draw in one minute + queueDraw(); +} + +/* + * Step counter via widget + */ +function getSteps() { + if (stepsWidget() !== undefined) + return stepsWidget().getSteps(); + return "???"; +} + +function stepsWidget() { + if (WIDGETS.activepedom !== undefined) { + return WIDGETS.activepedom; + } else if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom; + } + return undefined; +} + +/* + * HRM Listener + */ +Bangle.on('HRM', function (hrm) { + hrmValue = hrm.bpm; +}); + +/* + * Handle alarm + */ +function getCurrentTimeInMinutes(){ + return Math.floor(Date.now() / (1000*60)); +} + +function isAlarmEnabled(){ + return settings.alarm > 0; +} + +function getAlarmMinutes(){ + var currentTime = getCurrentTimeInMinutes(); + return settings.alarm - currentTime; +} + +function handleAlarm(){ + if(!isAlarmEnabled()){ + return; + } + + if(getAlarmMinutes() > 0){ + return; + } + + // Alarm + var t = 300; + Bangle.buzz(t, 1) + .then(() => new Promise(resolve => setTimeout(resolve, t))) + .then(() => Bangle.buzz(t, 1)) + .then(() => new Promise(resolve => setTimeout(resolve, t))) + .then(() => Bangle.buzz(t, 1)) + .then(() => new Promise(resolve => setTimeout(resolve, t))) + .then(() => Bangle.buzz(t, 1)); + + // Update alarm state to disabled + settings.alarm = -1; + Storage.writeJSON(SETTINGS_FILE, settings); +} + + +/* + * Swipe to set an alarm + */ +Bangle.on('swipe',function(dir) { + // Increase alarm + if(dir == -1){ + if(isAlarmEnabled()){ + settings.alarm += 5; + } else { + settings.alarm = getCurrentTimeInMinutes() + 5; + } + } + + // Decrease alarm + if(dir == +1){ + if(isAlarmEnabled() && (settings.alarm-5 > getCurrentTimeInMinutes())){ + settings.alarm -= 5; + } else { + settings.alarm = -1; + } + } + + // Update UI + draw(); + + // Update alarm state + Storage.writeJSON(SETTINGS_FILE, settings); +}); + + +/* + * Stop updates when LCD is off, restart when on + */ +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); + +// Load widgets - needed by draw +Bangle.loadWidgets(); + +// Clear the screen once, at startup and draw clock +g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); +draw(); + +// After drawing the watch face, we can draw the widgets +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/lcars/lcars.icon.js b/apps/lcars/lcars.icon.js new file mode 100644 index 000000000..c404728e0 --- /dev/null +++ b/apps/lcars/lcars.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgeevPnAQsc+fPngCE+/fvoCEvAbIA4/AgFzEZwRBjwjNvBUBEZ3eCIMOEZtwCIMBEZuARYU5EZecTocHEZf0CIcBEbvgaggjKTwIAEbQpoHAAiSEeoYQHJQr1CCBJKEIgcBI4xKFaIdt3AOFgfuAYMeEYLRBj1pLQ4ICuYjBAgPbtoRHhu3AYN5VoMGzVpI49502AgPPVoM27dsK48N23cgE5CgOmzVoCI4LBzCSB8EP2wjJgILBAYMAhIjBsAjJzVwg47C7YRJEYhfBEZXmEZ53CI4q2BEAiVCkwjCNYaMGboQjDkBfDCAbdB04EBgyPDC4YAD/dt2wRCHIM5njXCCAcHboOmCIQ0B5/nfYT6DFIIjBeAcOvM8+EAjitFEYJEBAANzEYOeeowjCFgUDzwjB+YrDgAgBEYWcA4Mc+YjCvAQCgftEANuDIYOBEYXPNwIAIg4OCCgXkCBEOEZDvBEAhEB4AjF/inB8+OJQOOvILBoAjGU4IFDAQYjGbQIdCAQt4EY0DEZACDEYceEZACDC4bLBEZwCO")) diff --git a/apps/lcars/lcars.png b/apps/lcars/lcars.png new file mode 100644 index 000000000..167352ef4 Binary files /dev/null and b/apps/lcars/lcars.png differ diff --git a/apps/lcars/lcars.settings.js b/apps/lcars/lcars.settings.js new file mode 100644 index 000000000..2255caf9b --- /dev/null +++ b/apps/lcars/lcars.settings.js @@ -0,0 +1,54 @@ +(function(back) { + const SETTINGS_FILE = "lcars.setting.json"; + + // initialize with default settings... + const storage = require('Storage') + let settings = { + alarm: -1, + dataRow1: "Battery", + dataRow2: "Steps", + dataRow3: "Temp." + }; + let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; + for (const key in saved_settings) { + settings[key] = saved_settings[key] + } + + function save() { + storage.write(SETTINGS_FILE, settings) + } + + var data_options = ['Battery', 'Steps', 'Temp.', "HRM"]; + + E.showMenu({ + '': { 'title': 'LCARS Clock' }, + '< Back': back, + 'Row 1': { + value: 0 | data_options.indexOf(settings.dataRow1), + min: 0, max: 3, + format: v => data_options[v], + onchange: v => { + settings.dataRow1 = data_options[v]; + save(); + }, + }, + 'Row 2': { + value: 0 | data_options.indexOf(settings.dataRow2), + min: 0, max: 3, + format: v => data_options[v], + onchange: v => { + settings.dataRow2 = data_options[v]; + save(); + }, + }, + 'Row 3': { + value: 0 | data_options.indexOf(settings.dataRow3), + min: 0, max: 3, + format: v => data_options[v], + onchange: v => { + settings.dataRow3 = data_options[v]; + save(); + }, + } + }); +}) diff --git a/apps/lcars/screenshot.png b/apps/lcars/screenshot.png new file mode 100644 index 000000000..d74635f64 Binary files /dev/null and b/apps/lcars/screenshot.png differ diff --git a/apps/life/bangle1-game-of-life-screenshot.png b/apps/life/bangle1-game-of-life-screenshot.png new file mode 100644 index 000000000..f6e8c78a1 Binary files /dev/null and b/apps/life/bangle1-game-of-life-screenshot.png differ diff --git a/apps/lifeclk/ChangeLog b/apps/lifeclk/ChangeLog index dfd8b8775..cdde84463 100644 --- a/apps/lifeclk/ChangeLog +++ b/apps/lifeclk/ChangeLog @@ -2,4 +2,5 @@ 0.02: Faster algorithm, hours and minutes are now displayable whenever, using the upper button 2021-01-14 0.03: Ah yes. Some people prefer the 12 hour system 2021-01-14 0.04: Fixed a bug, doesn't run while display's on now 2021-01-18 -0.05: Fixed a bug, doesn't count the time it was asleep when calculating the update time 2021-01-19 \ No newline at end of file +0.05: Fixed a bug, doesn't count the time it was asleep when calculating the update time 2021-01-19 +0.06: Use Bangle.set UI, change to unminified upload to ensure this works ok on Bangle.js diff --git a/apps/lifeclk/app.js b/apps/lifeclk/app.min.js similarity index 98% rename from apps/lifeclk/app.js rename to apps/lifeclk/app.min.js index 6064aa162..51a8ff93c 100644 --- a/apps/lifeclk/app.js +++ b/apps/lifeclk/app.min.js @@ -1,4 +1,4 @@ -Bangle.setLCDTimeout(30); +// Name as .min.js so we don't try and pretokenise (which stops Bangle.js running this somehow) const is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; @@ -435,9 +435,11 @@ function showMinAgain(){ } function setButtons(){ - setWatch(showMinAgain, BTN1, {repeat:true,edge:"falling"}); - setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); - setWatch(regen, BTN3, {repeat:true,edge:"falling"}); + // Show launcher when button pressed + Bangle.setUI("clockupdown", btn=>{ + if (btn<0) showMinAgain(); + if (btn>0) regen(); + }); } let wentToSleepAt; diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index 3d64cf8d7..448f8119a 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -9,3 +9,8 @@ 0.07: Improve handling of non-ASCII characters (fix #469) 0.08: Added Mavigation units and en_NAV 0.09: Added New Zealand en_NZ +0.10: Apply 12hour setting to time +0.11: Added translations for nl_NL and changes one formatting +0.12: Fixed nl_NL formatting, because the full months won't fit on the Bangle.js2's screen +0.13: Now use shorter de_DE date format to more closely match other languages for size +0.14: Added some first translations for Messages in nl_NL diff --git a/apps/locale/locale.html b/apps/locale/locale.html index 3d806b44b..90a2e8d40 100644 --- a/apps/locale/locale.html +++ b/apps/locale/locale.html @@ -146,7 +146,7 @@ exports = { name : "en_GB", currencySym:"£", "%-m": "d.getMonth()+1", "%d": "('0'+d.getDate()).slice(-2)", "%-d": "d.getDate()", - "%HH": "('0'+d.getHours()).slice(-2)", + "%HH": "('0'+getHours(d)).slice(-2)", "%MM": "('0'+d.getMinutes()).slice(-2)", "%SS": "('0'+d.getSeconds()).slice(-2)", "%A": "day.split(',')[d.getDay()]", @@ -178,6 +178,13 @@ var month = ${js(locale.month + ',' + locale.abmonth)}; function round(n) { return n < 10 ? Math.round(n * 10) / 10 : Math.round(n); } +var is12; +function getHours(d) { + var h = d.getHours(); + if (is12===undefined) is12 = (require('Storage').readJSON('setting.json',1)||{})["12hour"]; + if (!is12) return h; + return (h%12==0) ? 12 : h%12; +} exports = { name: ${js(locale.lang)}, currencySym: ${js(locale.currency_symbol)}, diff --git a/apps/locale/locales.js b/apps/locale/locales.js index 34f259498..b607998a0 100644 --- a/apps/locale/locales.js +++ b/apps/locale/locales.js @@ -37,11 +37,30 @@ const codePages = { /* When it's not in the codepage, try and use these conversions */ const charFallbacks = { + "ą":"a", + "ā":"a", "č":"c", - "ř":"r", + "ć":"c", + "ě":"e", + "ę":"e", + "ē":"e", + "ģ":"g", + "i":"ī", + "ķ":"k", + "ļ":"l", + "ł":"l", + "ń":"n", + "ņ":"n", "ő":"o", - "ě":"e" -}; + "ó":"o", + "ř":"r", + "ś":"s", + "š":"s", + "ū":"u", + "ż":"z", + "ź":"z", + "ž":"z", + }; /* timePattern / datePattern: @@ -130,12 +149,13 @@ var locales = { temperature: "°C", ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, - datePattern: { 0: "%A, %d. %B %Y", "1": "%d.%m.%Y" }, // Sonntag, 1. März 2020 // 01.01.20 + datePattern: { 0: "%d. %b %Y", "1": "%d.%m.%Y" }, // 1. Mär 2020 // 01.03.20 abmonth: "Jan,Feb,Mär,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez", month: "Januar,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember", abday: "So,Mo,Di,Mi,Do,Fr,Sa", day: "Sonntag,Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag", - trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus", "< Back": "< Zurück" } + trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus", + "< Back": "< Zurück", "Delete": "Löschen", "Mark Unread": "Als ungelesen markieren" } }, "en_US": { lang: "en_US", @@ -184,12 +204,30 @@ var locales = { temperature: "°C", ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, - datePattern: { 0: "%A %B %d %Y", 1: "%d.%m.%y" }, // zondag 1 maart 2020 // 01.01.20 + datePattern: { 0: "%d %b %Y", 1: "%d-%m-%Y" }, // 28 feb 2020 // 28-02-2020 abday: "zo,ma,di,wo,do,vr,za", day: "zondag,maandag,dinsdag,woensdag,donderdag,vrijdag,zaterdag", abmonth: "jan,feb,mrt,apr,mei,jun,jul,aug,sep,okt,nov,dec", month: "januari,februari,maart,april,mei,juni,juli,augustus,september,oktober,november,december", - // No translation for english... + trans: { yes: "ja", Yes: "Ja", no: "nee", No: "Nee", ok: "ok", on: "aan", off: "uit", + "< Back": "< Terug", "Delete": "Verwijderen", "Mark Unread": "Markeer als ongelezen" } + }, + "en_NL": { // English date units with Dutch number, currency and navigation units. + lang: "en_NL", + decimal_point: ",", + thousands_sep: ".", + currency_symbol: "€", + int_curr_symbol: "EUR", + speed: "km/h", + distance: { 0: "m", 1: "km" }, + temperature: "°C", + ampm: { 0: "am", 1: "pm" }, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + datePattern: { 0: "%b %d %Y", 1: "%d/%m/%Y" }, // Feb 28 2020" // "01/03/2020"(short) + abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", + month: "January,February,March,April,May,June,July,August,September,October,November,December", + abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat", + day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", }, "en_CA": { lang: "en_CA", @@ -290,13 +328,15 @@ var locales = { speed: "kmh", distance: { 0: "m", 1: "km" }, temperature: "°C", + ampm: { 0: "", 1: "" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%A, %d. %B %Y", "1": "%d.%m.%y" }, // Sonntag, 1. März 2020 // 01.03.20 abmonth: "Jän,Feb,März,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez", month: "Jänner,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember", abday: "So,Mo,Di,Mi,Do,Fr,Sa", day: "Sonntag,Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag", - trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus" } + trans: { yes: "ja", Yes: "Ja", no: "nein", No: "Nein", ok: "ok", on: "an", off: "aus", + "< Back": "< Zurück", "Delete": "Löschen", "Mark Unread": "Als ungelesen markieren" } }, "en_IL": { lang: "en_IL", @@ -332,7 +372,8 @@ var locales = { month: "enero,febrero,marzo,abril,mayo,junio,julio,agosto,septiembre,octubre,noviembre,diciembre", abday: "dom,lun,mar,mié,jue,vie,sáb", day: "domingo,lunes,martes,miércoles,jueves,viernes,sábado", - trans: { yes: "sí", Yes: "Sí", no: "no", No: "No", ok: "ok", on: "on", off: "off" } + trans: { yes: "sí", Yes: "Sí", no: "no", No: "No", ok: "ok", on: "on", off: "off", + "< Back": "< Atrás", "Delete": "Borrar ", "Mark Unread": "Marcar como no leído" } }, "fr_BE": { lang: "fr_BE", @@ -496,6 +537,24 @@ var locales = { day: "Vasárnap,Hétfő,Kedd,Szerda,Csütörtök,Péntek,Szombat", trans: { yes: "igen", Yes: "Igen", no: "nem", No: "Nem", ok: "ok", on: "be", off: "ki" } }, + "oc_FR": { + lang: "oc_FR", + decimal_point: ",", + thousands_sep: " ", + currency_symbol: "€", + int_curr_symbol: "EUR", + speed: "km/h", + distance: { 0: "m", 1: "km" }, + temperature: "°C", + ampm: { 0: "", 1: "" }, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + datePattern: { 0: "%A %d %B de %Y", "1": "%d/%m/%Y" }, // dimenge 1 de març de 2020 // 01/03/2020 + abmonth: "gen.,febr.,març,abril,mai,junh,julh,ago.,set.,oct.,nov.,dec.", + month: "genièr,febrièr,març,abril,mai,junh,julhet,agost,setembre,octòbre,novembre,decembre", + abday: "dg,dl,dm,dc,dj,dv,ds", + day: "dimenge,diluns,dimars,dimècres,dijòus,divendres,dissabte", + trans: { yes: "òc", Yes: "Òc", no: "non", No: "Non", ok: "ok", on: "on", off: "off" } + }, "pt_BR": { lang: "pt_BR", decimal_point: ",", @@ -568,6 +627,42 @@ var locales = { day: "Domingo,Segunda-feira,Terça-feira,Quarta-feira,Quinta-feira,Sexta-feira,Sábado", trans: { yes: "sim", Yes: "Sim", no: "não", No: "Não", ok: "ok", on: "on", off: "off" } }, + "pl_PL": { + lang: "pl_PL", + decimal_point: ",", + thousands_sep: " ", + currency_symbol: "zł", + int_curr_symbol: "PLN", + speed: "kmh", + distance: { 0: "m", 1: "km" }, + temperature: "°C", + ampm: { 0: "", 1: "" }, + timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, + datePattern: { 0: "%d. %b %Y", "1": "%d.%m.%Y" }, // 1. Mar 2021 // 01.03.2021 + abmonth: "Sty,Lut,Mar,Kwi,Maj,Cze,Lip,Sie,Wrz,Paź,Lis,Gru", + month: "Styczeń,Luty,Marzec,Kwiecień,Maj,Czerwiec,Lipiec,Sierpień,Wrzesień,Październik,Listopad,Grudzień", + abday: "Ndz,Pon,Wt,Śr,Czw,Pt,Sob", + day: "Niedziela,Poniedziałek,Wtorek,Środa,Czwartek,Piątek,Sobota", + trans: { yes: "tak", Yes: "Tak", no: "nie", No: "Nie", ok: "ok", on: "on", off: "off", "< Back": "< Wstecz" } + }, + "lv_LV": { // Using charfallbacks + lang: "lv_LV", + decimal_point: ",", + thousands_sep: " ", + currency_symbol: "€", + int_curr_symbol: "EUR", + speed: "kmh", + distance: { 0: "m", 1: "km" }, + temperature: "°C", + ampm: { 0: "", 1: "" }, + timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" }, + datePattern: { 0: "%d. %b %Y", "1": "%d.%m.%Y" }, // 1. Mar 2020 // 01.03.20 + abmonth: "Jan,Feb,Mar,Apr,Mai,Jūn,Jūl,Aug,Sep,Okt,Nov,Dec", + month: "Janvāris,Februāris,Marts,Aprīlis,Maijs,Jūnijs,Jūlijs,Augusts,Septemberis,Oktobris,Novembris,Decembris", + abday: "Pr,Ot,Tr,Ce,Pk,Se,Sv", + day: "Pirmdiena,Otrdiena,Trešdiena,Ceturtdiena,Piektdiena,Sestdiena,Svētdiena", + trans: { yes: "jā", Yes: "Jā", no: "nē", No: "Nē", ok: "labi", on: "Ieslēgt", off: "Izslēgt", "< Back": "< Atpakaļ" } + }, /*, "he_IL": { // This won't work until we get a font - see https://github.com/espruino/BangleApps/issues/399 codePage : "ISO8859-8", diff --git a/apps/ltherm/README.md b/apps/ltherm/README.md new file mode 100644 index 000000000..b68cb1fc1 --- /dev/null +++ b/apps/ltherm/README.md @@ -0,0 +1,3 @@ +# Thermometer + +Localized Bangle.js 2 thermometer app. It also starts maintaining an average of the temperature to help lower the margin of error after 10 consecutive readings; due to the low quality die-thermometer. diff --git a/apps/ltherm/app.js b/apps/ltherm/app.js new file mode 100644 index 000000000..7accae2ed --- /dev/null +++ b/apps/ltherm/app.js @@ -0,0 +1,25 @@ +function drawTemperature() { + g.reset(1).clearRect(0,24,g.getWidth(),g.getHeight()); + g.setFont("6x8",2).setFontAlign(0,0); + var x = g.getWidth()/2; + var y = g.getHeight()/2 + 10; + g.drawString("Temp", x, y - 45); + g.setFontVector(70).setFontAlign(0,0); + var h = E.getTemperature(); + if (avg.length < 10) { + avg[avg.length] = h; + } else { + avg.shift(); + avg[avg.length] = h; + h = ((avg[0] + avg[1] + avg[2] + avg[3] + avg[4] + avg[5] + avg[6] + avg[7] + avg[8] + avg[9]) / 10); + } + var t = require('locale').temp(h); + g.drawString(t, x, y); +} +const avg = []; +setInterval(function() { + drawTemperature(); +}, 2000); +E.showMessage("Loading..."); +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/ltherm/icon.js b/apps/ltherm/icon.js new file mode 100644 index 000000000..4f3cc4b0a --- /dev/null +++ b/apps/ltherm/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4AChGIxGAC6eIAQgARFgUIC9ReCAYJgSC7BHDF6gUBC6ovWI/5Hga/6P/ABsCkABDC/4XxkQXDkQuSAQwXPDQkAC6BBCkQDDC6MCmczFoIXQCQQXBDgQXP2EA2YXBncAhYXR3YXB3YXRCQWznYcCC6ICBAYYXPhYrBApAwPFyQqCIoYuRLwZgDAH4A/")) diff --git a/apps/ltherm/thermf.png b/apps/ltherm/thermf.png new file mode 100644 index 000000000..bb33cb939 Binary files /dev/null and b/apps/ltherm/thermf.png differ diff --git a/apps/magnav/ChangeLog b/apps/magnav/ChangeLog index 35e8798c6..2b2782c7b 100644 --- a/apps/magnav/ChangeLog +++ b/apps/magnav/ChangeLog @@ -2,5 +2,6 @@ 0.02: Course marker 0.03: Tilt compensation and calibration 0.04: Fix Font size +0.05: Inital portable version diff --git a/apps/magnav/README.md b/apps/magnav/README.md index a036644fb..7ef506b2e 100644 --- a/apps/magnav/README.md +++ b/apps/magnav/README.md @@ -6,19 +6,20 @@ This is a tilt and roll compensated compass with a linear display. The compass w ## Calibration -Correct operation of this app depends critically on calibration. When first run on a Bangle, the app will request calibration. This lasts for 30 seconds during which you should move the watch slowly through figures of 8. It is important that during calibration the watch is fully rotated around each of it axes. If the app does give the correct direction heading or is not stable with respect to tilt and roll - redo the calibration by pressing *BTN3*. Calibration data is recorded in a storage file named `magnav.json`. +Correct operation of this app depends critically on calibration. When first run on a Bangle, the app will request calibration. This lasts for 20 seconds during which you should move the watch slowly through figures of 8. It is important that during calibration the watch is fully rotated around each of it axes. If the app does give the correct direction heading or is not stable with respect to tilt and roll - redo the calibration by pressing *BTN2*. Calibration data is recorded in a storage file named `magnav.json`. + +Note: Charging your Bangle due to the magnetic connector clamp seems to require recalibration afterwards for accurate readings. ## Controls -*BTN1* - switches to your selected clock app. +*BTN1* - marks the current heading with a blue circle - see screen shot. This can be used to take a bearing and then follow it.. +(Swipe UP on Bangle 2) -*BTN2* - switches to the app launcher. +*BTN2* - invokes calibration ( can be cancelled if pressed accidentally). +(*BTN1* on Bangle 2) -*BTN3* - invokes calibration ( can be cancelled if pressed accidentally) - -*Touch Left* - marks the current heading with a blue circle - see screen shot. This can be used to take a bearing and then follow it. - -*Touch Right* - cancels the marker (blue circle not displayed). +*BTN3* - cancels the marker (blue circle not displayed) +(swipe DOWN on Bangle 2) ## Support diff --git a/apps/magnav/magnav.min.js b/apps/magnav/magnav.min.js deleted file mode 100644 index 1d5439164..000000000 --- a/apps/magnav/magnav.min.js +++ /dev/null @@ -1,10 +0,0 @@ -var Yoff=80,pal2color=new Uint16Array([0,65535,2047,50712],0,2),buf=Graphics.createArrayBuffer(240,60,2,{msb:!0});Bangle.setLCDTimeout(30);function flip(b,c){g.drawImage({width:240,height:60,bpp:2,buffer:b.buffer,palette:pal2color},0,c);b.clear()}var labels="N NE E SE S SW W NW".split(" "),brg=null; -function drawCompass(b){buf.setColor(1);buf.setFont("Vector",24);var c=b-90;0>c&&(c+=360);buf.fillRect(28,45,212,49);var a=30,d=15-c%15;15>d?a+=d:d=0;for(var e=d;e<=180-d;e+=15){var f=c+e;0==f%90?(buf.drawString(labels[Math.floor(f/45)%8],a-8,0),buf.fillRect(a-2,25,a+2,45)):0==f%45?(buf.drawString(labels[Math.floor(f/45)%8],a-12,0),buf.fillRect(a-2,30,a+2,45)):0==f%15&&buf.fillRect(a,35,a+1,45);a+=15}brg&&(b=brg-b,180b&&(b+=360),b+=120,30>b&&(b=14),210c?1:-1;180<=a&&(a=360-a,d=-d);if(2>a)return c;a=c+d*(1+Math.round(a/5));0>a&&(a+=360);360a&&(a+=360);return a} -function reading(){var b=tiltfixread(CALIBDATA.offset,CALIBDATA.scale);heading=newHeading(b,heading);drawCompass(heading);buf.setColor(1);buf.setFont("6x8",2);buf.setFontAlign(-1,-1);buf.drawString("o",170,0);buf.setFont("Vector",54);b=Math.round(heading);var c=b.toString();buf.drawString(10>b?"00"+c:100>b?"0"+c:c,70,10);flip(buf,Yoff+80)} -function calibrate(){var b=-32E3,c=-32E3,a=-32E3,d=32E3,e=32E3,f=32E3,k=setInterval(function(){var h=Bangle.getCompass();b=h.x>b?h.x:b;c=h.y>c?h.y:c;a=h.z>a?h.z:a;d=h.x{ + CALIBDATA=r; require("Storage").write("magnav.json",r); - CALIBDATA = r; - startdraw(); - setButtons(); + restart() }); } else { - startdraw(); - setTimeout(setButtons,1000); - } + restart() + } } if (first===undefined) first=false; stopdraw(); - clearWatch(); if (first) E.showAlert(msg,title).then(action.bind(null,true)); else E.showPrompt(msg,{title:title,buttons:{"Start":true,"Cancel":false}}).then(action); } -Bangle.on('touch', function(b) { - if(!candraw) return; - if(b==1) brg=heading; - if(b==2) brg=null; - }); - var intervalRef; function startdraw(){ @@ -176,29 +174,17 @@ function stopdraw() { } function setButtons(){ - setWatch(()=>{load();}, BTN1, {repeat:false,edge:"falling"}); - setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); - setWatch(docalibrate, BTN3, {repeat:false,edge:"falling"}); + function actions(v){ + if (!v) docalibrate(false); + else if (v==1) brg=null; + else brg=heading; + } + Bangle.setUI("updown",actions); } -var SCREENACCESS = { - withApp:true, - request:function(){ - this.withApp=false; - stopdraw(); - clearWatch(); - }, - release:function(){ - this.withApp=true; - startdraw(); - setButtons(); - } -}; - Bangle.on('lcdPower',function(on) { - if (!SCREENACCESS.withApp) return; if (on) { - startdraw(); + if (!calibrating) startdraw(); } else { stopdraw(); } @@ -209,7 +195,7 @@ Bangle.on('kill',()=>{Bangle.setCompassPower(0);}); Bangle.loadWidgets(); Bangle.setCompassPower(1); if (!CALIBDATA) - docalibrate({},true); + docalibrate(true); else { startdraw(); setButtons(); @@ -217,4 +203,3 @@ else { - diff --git a/apps/magnav/magnav_b2.js b/apps/magnav/magnav_b2.js new file mode 100644 index 000000000..e54280796 --- /dev/null +++ b/apps/magnav/magnav_b2.js @@ -0,0 +1,192 @@ + +const Ypos = 40; + +const labels = ["N","NE","E","SE","S","SW","W","NW"]; +var brg=null; + +function drawCompass(course) { + "ram" + g.setColor(g.theme.fg); + g.setFont("Vector",18); + var start = course-90; + if (start<0) start+=360; + g.fillRect(16,Ypos+45,160,Ypos+49); + var xpos = 16; + var frag = 15 - start%15; + if (frag<15) xpos+=Math.floor((frag*4)/5); else frag = 0; + for (var i=frag;i<=180-frag;i+=15){ + var res = start + i; + if (res%90==0) { + g.drawString(labels[Math.floor(res/45)%8],xpos-6,Ypos+6); + g.fillRect(xpos-2,Ypos+25,xpos+2,Ypos+45); + } else if (res%45==0) { + g.drawString(labels[Math.floor(res/45)%8],xpos-9,Ypos+6); + g.fillRect(xpos-2,Ypos+30,xpos+2,Ypos+45); + } else if (res%15==0) { + g.fillRect(xpos,Ypos+35,xpos+1,Ypos+45); + } + xpos+=12; + } + if (brg) { + var bpos = brg - course; + if (bpos>180) bpos -=360; + if (bpos<-180) bpos +=360; + bpos= Math.floor((bpos*4)/5)+88; + if (bpos<16) bpos = 8; + if (bpos>160) bpos = 170; + g.setColor(g.theme.fg2); + g.fillCircle(bpos,Ypos+45,6); + } +} + +var heading = 0; +function newHeading(m,h){ + var s = Math.abs(m - h); + var delta = (m>h)?1:-1; + if (s>=180){s=360-s; delta = -delta;} + if (s<2) return h; + var hd = h + delta*(1 + Math.round(s/5)); + if (hd<0) hd+=360; + if (hd>360)hd-= 360; + return hd; +} + +var candraw = false; +var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; + +function tiltfixread(O,S){ + "ram" + var m = Bangle.getCompass(); + var g = Bangle.getAccel(); + m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z; + var d = Math.atan2(-m.dx,m.dy)*180/Math.PI; + if (d<0) d+=360; + var phi = Math.atan(-g.x/-g.z); + var cosphi = Math.cos(phi), sinphi = Math.sin(phi); + var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi)); + var costheta = Math.cos(theta), sintheta = Math.sin(theta); + var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta; + var yh = m.dz*sinphi - m.dx*cosphi; + var psi = Math.atan2(yh,xh)*180/Math.PI; + if (psi<0) psi+=360; + return psi; +} + +// Note actual mag is 360-m, error in firmware +function reading() { + "ram" + g.clearRect(0,24,175,175); + var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale); + heading = newHeading(d,heading); + drawCompass(heading); + g.setColor(g.theme.fg); + g.setFont("6x8",2); + g.setFontAlign(-1,-1); + g.drawString("o",120,Ypos+80); + g.setFont("Vector",40); + var course = Math.round(heading); + var cs = course.toString(); + cs = course<10?"00"+cs : course<100 ?"0"+cs : cs; + g.drawString(cs,50,Ypos+90); + g.setColor(g.theme.fg2); + g.fillPoly([88,Ypos+60,78,Ypos+80,98,Ypos+80]); + g.setColor(g.theme.fg); + g.flip(); +} + +function calibrate(){ + var max={x:-32000, y:-32000, z:-32000}, + min={x:32000, y:32000, z:32000}; + var ref = setInterval(()=>{ + var m = Bangle.getCompass(); + max.x = m.x>max.x?m.x:max.x; + max.y = m.y>max.y?m.y:max.y; + max.z = m.z>max.z?m.z:max.z; + min.x = m.x { + setTimeout(()=>{ + if(ref) clearInterval(ref); + var offset = {x:(max.x+min.x)/2,y:(max.y+min.y)/2,z:(max.z+min.z)/2}; + var delta = {x:(max.x-min.x)/2,y:(max.y-min.y)/2,z:(max.z-min.z)/2}; + var avg = (delta.x+delta.y+delta.z)/3; + var scale = {x:avg/delta.x, y:avg/delta.y, z:avg/delta.z}; + resolve({offset:offset,scale:scale}); + },20000); + }); +} + +var calibrating=false; +function docalibrate(first){ + calibrating=true; + const title = "Calibrate"; + const msg = "takes 20 seconds"; + function restart() { + calibrating=false; + setButtons(); + startdraw(); + } + function action(b){ + if (b) { + g.clearRect(0,24,175,175); + g.setColor(g.theme.fg); + g.setFont("Vector",18); + g.setFontAlign(0,-1); + g.drawString("Fig 8s to",88,Ypos); + g.drawString("Calibrate",88,Ypos+18); + g.flip(); + calibrate().then((r)=>{ + CALIBDATA=r; + require("Storage").write("magnav.json",r); + restart(); + }); + } else { + restart(); + } + } + if (first===undefined) first=false; + stopdraw(); + if (first) + E.showAlert(msg,title).then(action.bind(null,true)); + else + E.showPrompt(msg,{title:title,buttons:{"Start":true,"Cancel":false}}).then(action); +} + +var intervalRef; + +function startdraw(){ + g.clear(1); + Bangle.drawWidgets(); + candraw = true; + intervalRef = setInterval(reading,200); +} + +function stopdraw() { + candraw=false; + if(intervalRef) {clearInterval(intervalRef);} +} + +function setButtons(){ + function actions(v){ + if (!v) docalibrate(false); + else if (v==1) brg=null; + else brg=heading; + } + Bangle.setUI("updown",actions); +} + +Bangle.on('kill',()=>{Bangle.setCompassPower(0);}); + +Bangle.loadWidgets(); +Bangle.setCompassPower(1); +if (!CALIBDATA) + docalibrate(true); +else { + startdraw(); + setButtons(); +} + + + diff --git a/apps/magnav/screenshot-b2.png b/apps/magnav/screenshot-b2.png new file mode 100644 index 000000000..63f830bfc Binary files /dev/null and b/apps/magnav/screenshot-b2.png differ diff --git a/apps/magnav/screenshot-light-b2.png b/apps/magnav/screenshot-light-b2.png new file mode 100644 index 000000000..943dc392c Binary files /dev/null and b/apps/magnav/screenshot-light-b2.png differ diff --git a/apps/mandelbrotclock/ChangeLog b/apps/mandelbrotclock/ChangeLog new file mode 100644 index 000000000..d7bda0d78 --- /dev/null +++ b/apps/mandelbrotclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial Release + \ No newline at end of file diff --git a/apps/mandelbrotclock/README.md b/apps/mandelbrotclock/README.md new file mode 100644 index 000000000..387343a9e --- /dev/null +++ b/apps/mandelbrotclock/README.md @@ -0,0 +1,9 @@ +# Mandelbrot Clock + +A simple clock themed on the mandelbrot set. + +Written by [James Milner](https://www.github.com/jameslmilner) + +![](app.png) + + \ No newline at end of file diff --git a/apps/mandelbrotclock/app.png b/apps/mandelbrotclock/app.png new file mode 100644 index 000000000..95ab99a91 Binary files /dev/null and b/apps/mandelbrotclock/app.png differ diff --git a/apps/mandelbrotclock/mandelbrotclock-icon.js b/apps/mandelbrotclock/mandelbrotclock-icon.js new file mode 100644 index 000000000..a2898e734 --- /dev/null +++ b/apps/mandelbrotclock/mandelbrotclock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+vdvwMzq8CrGCwVewNRluCxAHBAAOsxAAB1gAD1oBB2fWAAPO1mBGZIvCrECq4bBglYmglBGgIxBFQItDFQQsC2es6/XF4OrwOsqwvIt4vBxFdgder0uwUoLQRXE1oqB1nQ1nW2RbCA4PW6HP52rF5d7KoKNBmcDIYIzBrBaB1vPFAOz2RVB1ml54qB1fQFwQvB5+kwSQJWIQoExAFBRYaBB0pVCQYRiB0wDC1erFoPO5ul02sF5QnBAAYFBwbkF0ul1eIAQOqOwLlBIwL7BGIOkAANvxErF49dF4dYGoJfBLoIwD6AfBEgJbBwIEBqwGBqw4BU4Osvd1uteSBDiEFIKLDdQey6ytBEQNWAQOBwMyKYcrAAILBWIRgIEofQ1mJAoS6B2ez665B5+rLQMrq1WWBAACHwJNBCA5WCbgPQ1pYBFYOl6CMB6vP0prB1l6kguLAAWBJgKPHRIOz03Q6+z2QsBVgOrdgOlvaKBLhhiG6AUGJoJOB6GmMgPPcQLHCdAgtRSYgHFrDKBXYWBLQOk0qlBcgNWBYJdSAAcCC4qOBAILzE62l0mCIYVWvQuVAAMsAokzR4WJ1us2fW6K/BMwMrgErAQIAcq+sGAOtF4Os1vXF4I5B1mlFzSQELwU0xGtAIOzF4LCBBgOrLbYwDwUuwVeYIiRB6ukwLBDF7QwCwVYKgJgBGAOt6PW54vB1i7cq2rVoNYFQJfCMAXW62rM4QWDGoPXMwNWAgIMDAw2B67XDlezwUAgYsCwWJLwK9B1YnBwLSEAwIeCBgXXBoQGDHgMr64vEDIOIwNXSAJfBF4RgB1elfQK+GqweCGIIvBCgJUCF4QHBF4rqBRIS/BxKOC1qPB54wBF4pSDE4IjCcAQ6BGYIPCNYYYCl1SKYI0BMwIvBDoIvBPgR1EDgdWKAINDFwIECFoIABbItRulYMYhfCF4Y8BCoYbBAANWEYJfCZALuCIgi/GveeRoIuBXgOt1uy6HV5+kF4olBAAIeBGIIDCAAILCRQYMCNgWs0uqEQOs2fQ6+y63R0vJ1d7q+IUwgAXNoOl5xeBGAOrdYPW6A5BHQWteAovXwWq569BVoWl0ur0g8BVAMrq2lU4gAVq2m1gvC1gwBSAOrLgSiECgIvZq+CKwPWL4IvBXoPQ0uBXQxiBLzCHCW4ItBxGt2fXMAN71iJGYK8r1jqBF4PXL4QvB62r1a+BF4yXBFytWxGB0us6/XdoWzF4TKBwKPGH4IwULgIoB55eB2YGCXoPQ5xeBq+BvUkOolXGAMBXaOCruCwXQ2es1ovC0vP0ulKoOmwWsSgI2BwV70rKBHQIuORgWkwWl2QvBAAXX1YJBwOrAQOAvYxBHoN65HOBQIIBqyeGFgZEBwJ2BKgIqC1ogC2XW0osB1fQ62k5+qMgJoBC4PQfgLYBEYIABNoNWljjCHgNeBgWr63W2QvBxOJBIWr54uCYgL0BLAIsCBIIKB1T+BVwN8WAJcBNQIABIgQGB1fX2RdBXoOJFQWzSIOz1uzAoIwBFgXX2ZHBOIRDCWAOBRgQtC53P1OB0wlBMgQuBdwQAF1oxBEwI7B1p0CBgIIBAAPP0mBcgNWBYOkBYbfB6wtCxCaFGYQKBAoQvBOQIACHoey2ey6D2D0uC0yIBLIILB0pJBEIU6wU0FQbEBF4hnFA4ZlBNoRhCGAJYBHYSKD1eyEYJfBrxfCAwNeAILVBwZZExIABGATNCGARvBCoIMBFwJzDAIderFYwWJsgyBCoI1BAYIABF4QeBL4IvDOIIvDL4PPBYIuCKQQRBEAWsrE0AocQAQJpBGgRNCIQIECCQQzD6Gr0qMBbwYADJ4ZUBl1YBAVelwpBNIQDBFIImCl2CagIVBAATkC5/WFwhLFFoMtwM0E4MtltevggBgcDwITCrEzxEulz5CDgNkMIer6GyLogsCwWmI4MzrFXGAMEA==")) \ No newline at end of file diff --git a/apps/mandelbrotclock/mandelbrotclock.js b/apps/mandelbrotclock/mandelbrotclock.js new file mode 100644 index 000000000..94636056e --- /dev/null +++ b/apps/mandelbrotclock/mandelbrotclock.js @@ -0,0 +1,34 @@ +// MIT License - James Milner 2021 + +const mandelbrotBmp = { + width: 176, + height: 176, + bpp: 8, + transparent: 254, + buffer: require("heatshrink").decompress( + atob( + "mdXkoAFmctgMBmcsq4EBAAkslsIgWCmcJrGCq8zrwCBrsICwwABhEsrwADrACBq8JgcDhMtDwIABhUKmlexGIsmIAgIKBxGsrwMCnQACBIIBBAQIGB1eIr4IBxIAC1mr1mt2et1mz2es63Q6/W63X6ACB0nO5vH0V5q+BxGCwN6q0rV8kyVwsySoKlCWAKWGq4OBlqLBmdYrtXgQFBroBBC40CEQOCQYKqCAAKVBZ4MthNe1mCNYM0hVeRQIGCmicBAYKuBV4VeVwKdCxFlsoJBBgSvFXAOkV4OPVwQAB1er6+yVgK1B1Wq52q0ejztWwOA1dWRQKvklksV4ssltXVQwAEXIKwCAIMCAAkIDYNemSxDBgdXP4NYVoaqBRIOCTgOIBwIFBiFYAwSnCnQSBmkQiE0YQLFBXQKuCCoOJ1gmBr4VB1gMBWAIEBWwSvD1mj54EB560B63W5/O53N0ecV4N6vUsVkYACq6vFlddmczV5pnBAQOCV4qxCq9YYAMIgMtboLDBli8BlqsBmizBV4qnCWASrBVwVkxClBB4VeToQHCU4KmBT4IIBsgHB1eyVYgAC1oHBWAeyAYOr1StB6qvBVwOjvNXvddq9WqxbBV8auFAAItBltXVhFXTgJvCrFYlqvHAAISBB4MswLVBrAKBruImavChSWBr06CYOCTwIABxE0WAM0rAyCCAKjBYoWIsqnCXwKjDB4OrBQKkDWQKvDW4XPWIavC5/P62q0avB5uivWBvVXwGlwOrqyvjq6vHmczrqvHgKVBMoKNBWgMIVIaoCmYGCroFBYgOIT4ILBq9elqvBRwKhBVIQFBAYU6BgYIBAoM6nWCVwKpCAQKnCBIKvCI4KnBAoSiB2WzVQWt1qlB6HW6wIBCgQGB0nX6CvB52j0d5wIABq4+BwKvkVw+BTwMzlkCliuEXYNeNoVeq6mDgUtr0JloIDBoLFBUAYnBhMDgcJwQICwSYDFIIFDAgSfDCYLmBxFkBwNkTYSeDUYgEBV4OtWAIAB2Wr6C1C6/X63X1nP0nO1YGBBIPV5/P0eizlzp5/Bq5FBwErV9IwBUgKvBUQKwFmY9Cr1YX4SnEgQZBBAqvCmctls0EoIeBAgKlCTAQEDWQSqCWYQKCAQeJBQIAC2QDCVQWJ1qjCAoILDZYPWAwOr6/QVASmBAoIDBVYOq53N0YABzl5p9WHoOBvSvtWAMt1mCV4q7CVoKlFmadBAwYJEr2CmcJhIlBBASVBT4WlQAIAEBgNkUQK0CsqwDBYIIB1mtSwOA56cBWQIhD2ezXQWr62yAAOz64MC2fPVwPW0YPBVoPW52p6nNAYPN0WczqvBwOrq0rV9adBlqRDmdXWIkIBwK/BAwKlCXQNdDAIADq9dTASuCEYMJmiWCV4WkSYKPBSISlB1ivCslfA4OJBYQMBAAXW0lWvWk1fQ2SzBWIaZBVYPWAYOz1ioBAgXP1QLBAQuk0fV0ej46uBAIKvCvVWgCvpTgKoBVYSlBRwIFBWAYACUQICClmBTIKvFrB6BV4demkKV4OsVwSXBwF5vSUBqyQDV4LLCWoQGBXoQCB2Ws6GqvVXq+j5/P66oD2ezW4YMB1er6+r1TEBXoIBB665B52r6oSB6qyB5vG4yvCq97WASxBV80rmddN4NYlqtBwUtV4IBBliwHAATEDWIgFBEQMzV4NdWAM0iACBr2CS4PPWAJiBqGqT4OzWQanCAQOr1oLBWgPW6HO0aBBQAPOUQQMBTwIpBUgOq0idBVIYMBAgWq6HWVAPO1SuB54oB5uj0SvBqGBAAOC1mAlavmkssFgKvBr2BgUIAIMIq9YmcyV4wCCmdYTYOsmawCZoIXBVwMJEoOIW4IABTYKfC5/NztXq965+yBIOJU4IPBVQSrB2a8D1V6ruCq9P0epWAKpB6GsAQIGB1N6vSgCXoKzC63OV4ILBVIIAB1OjzquBAAOi42cucsq0twWtwNPV88llqMBmdXliVBAAawBBIMsVwoUBTQICCr2CmYNBgYADV4KtCnU6V4SVB6yFCmd55vP1iwCVgQGBA4YWC63QV4LHBktzRgKSB1QCBUQOqT4Ojq1dq4OCBgILBCQKrDCoIECCwPNAQOd0WcvN5p8llmB1l6qyvhlctlklq4DBAAKaBSYQABmcIWAUzT4SyCBINe1esr00VoKxCr0thKuDlqnBxAQBWIOIr6ZCN4NXrt5PIXWVQPWAIqsCBYKKCvWClslp9zvKxB0d65qyCSYNdwOBqwNBXoYCBUYOq5oVB56rB5rsB0fMV4XGzlzudJV4NWV0UAEwKdBlstgMlgKuBq6fBlgMBWoajBCYNelkCXQMzxGIr2IrEJAANXUYNXq8DmcJmasBxE6AQIACS4V5q0zp6EBTwPP6GrUwPQ5+rA4LDB6wFCRQNWwMzliiBGINzq1XWYS1CwMtgQqCAAt60SqBA4fHWAIHB42jVwNzzmduclRAKwClavglqQBLQNXlkBFwOCVIStBloFChFdmifB1YXBAIINBXQM0lqvCAAKwBxC9BAgIOB1gDBV4mr0l7YYNzPQPO5yiBVYXP0lW5y5C5/WAgPO0edPgLgCxGrrsIWgNWzuczlQwNYBAVzvWd0Wizt6rt6zisEVQIABzgQB42ivOcp9zpLiBrus0tWV8EzrCVBSgKvCL4KpCq4DCAAUzmgSBY4QMClk0OwKrBBoNYAgOCEIOsUwKsBWQWCxFk1mswFWp9Qq9POAKwB5uq0eqUYN6qGd1POA4OqAQILBQANXwMshEzWoOCAQNdqF5udPwI4BBQNXrtWBQKXBwIFBW4Wi0YDCVgOc4wSBZ4IDBpMllkz1erV8QABrstq9XWAQCBgUISQKuDllXT4IJBAAOChC5BWwUJlqkCWgbDBBoIXEAwKvB2XP0iiBwR6BPAWdvNXvIEBuaOBQ4N6XIOq1PHCYN5p8lksCluB1eAvWATwNPBgKMBls0VAIABqFPq2INgNPvKiBGgOddYIABAgKuCuYABEQMmlgpBV0CvClqhClszVoKuBAIK8CBgVe1iQBq4KBTAIaBBQIEBVIUQhSvBhSmBxE0mk6nS1BDwIAB1gAB5+qq1dPQVzqFdmVXRAVPlmCdANQzujAASFDQAMCq+CxOswGkxAkBXYUIhNY1elq2BLwNX1esS4LCBD4IFBVIN4VYOc4C8Budyp9JEYKvBCQMsV76UBlihChCwBWIMCGANerGCq4NBSASlCAAIbBZAUzA4KvBmkKAANeNAIHBVgNenVk1lkxGJWAfOvSRBllJq9druBwQ5BliuBwNXW4Nzzud0WiQgLFBV4UImeIQIN71mHmZhBBYMtfQOrveAwJNBIYSYBq8lGYK0CuYnBAAIGCAYIuBkrNBwWrqyvfxFYU4MzSoL2BmeCAwKgBWANeBYKfBVwYOBB4MtgYABBINXC4SvCYoRrBVwM6xGrxCtC2ey62kq1zWAKuBk0lq8twKJBwOI0qHDq9WvN4zqACkrABmYPBvVWq2AqzGBrpNBZoVWlYlBmkzV4Wkq97hOrB4MsUoIABVglJAAMlk0CNAJRBV76eCVYMIAYJGCToKYBUoSkDUYK7DhK8CBYS+CUAKqBAAgICnQNBr1kV4IAC5/O0d6lldqxuBkqlBwOsWQN6ZAJKBryFBQAQRBCQNdCIKiBMIMyvWCwScBdoOrBYVWagOCHwIXCW4IQBZoMyll7q0lpKtBF4klgUICwLSBV70tlkBMQNXVwKDBmYABq9XTYK3CV4ddBANYVIJcBX4MJryGBDoJlBmkQiCvD1gMExOsAIOy6CwCzt5vOdT4MrluCmmC1mlwOrwVYwNdlklYgMsk0CCQOIUQakCbIOlZ4WkBgUr0i5B1eqSgMsW4OJ2a1BrtX1dXVIYAFgUCEoIkDADkBAANXP4NXSgKtBRoKbCrCvCrqoBW4NehS0BBYWsAQIFBUIM0nQEBXgIJBPQOIsqyBAAvW54BB53H0eizucudQwNXlrLB0lXvSWBwAKBgSOBRQQQBsmBV4ukvV6qwADlYKBAAYMBW4OAwGrwAHBWoK+BFoTgBAAMmgKuBhBwBDYQAdTIKbClszrEDVQM0rCRBAIKXDBAKhBxE0hTABBQIIBwUKBYQCBWASrBAYKuCxOJVYOz2Wz1ez1nQ6HOWAPG0WcvNPkpsBrydBlaLBwKJBJwMJrGIWwJDBxHQvUsMIIUBC4IABOZYMBli1CvUqleB2fWGANX0qyBkwBClivCmeBvQpMV6VYr0zq8zR4NerqqCAASSBmiZBTwQBCU4OrUISvBWoICBVwM6VwQNBAYNkV4QXB1qvBAAOr1fP5/O0fN0awBudJkz1B1l6TAMyRIKpBmhPBw9WruA0iTBq0yPzLKBWgOAEINW1mkvcsAAMCVoIABV4NeCAKvdgWCVgYAFS4KTBSga1CmgUCiCdCnQIBXwUQV4KqC1llAoVfsgHBxIBBAQOr2Ws5/W5+q53N5quBvFPktXwOrT4KcEq2BHwOC1mAlaJBlasZAAwkBFwOr1QxBwFXVgKwBloABmmBIgoAYmbSBTwRgBMYQFBVwOr1YFCVok6wVYUwIICXwYABYwmIxIiD1ioBWIWr2fW6+r63QV4OjV4OdudJV4V6UAKDFwIgBvYMBVcAAGGoIABdQNXQ4MJhMtAIM0wQ4er00QIWrRwSJDAAOdBQiaCV4NewSuCDoM6BAIZBDwymBAAIHB2ezGAS1B6HQ63W1fP6qvB0d5q8sVwOrwKuFAAOA0itpAAkyvV6H4OBWAMtVwM0rxGIACtXq+I1beBP4SDBAgXPvIECxKbCUwWCxCwBVQIABnQNBsjMFU4Os1oDBWgOz1apB2QEBVYPP53O0fH0eivNWwLhC1l6UgsrqyttGQujvVeq80mavCxFWkgqbrCTD1iwB1eqvN50mqGoOr2SUBTwWIP4QABr2CAgQKCCAIGBaYizE1nW2ey6/QWYPQ54AB0epVwOcvNPlkthM0wN6qxREVuCxGvVdr06V4eBIwoAWr0zrCaC0uj0d/52qvVzqGd56VExKkBVAS1CXYK8CVwTFB1VW0ioB2QBBAQOsVAKsBAIPP6Gq5w2BAAOiztzp8lgUIq4kBWAwA1WIeCrwABsuIeDlXlteEoOI1esAIKGC515vOjRAKvCPYI4Br+IWoKrDxLBE1eAq2jUwKxB1YlB5+j5ywDFoKuBvV5zoABVwSvCmZFB595OYMkWPeBwWJAAOtwKvbhFXmk0TYKXBSYey1fOztQ0eq0gLDVQSvCDIWsxKlB2anCvNXDIOqVYIAC0dWvSxB1StBVwOjqGBp6tBq0smUCq6uC1mlqxpbAETuBOYOJNgNWETVemkzrGCWAaxD2SLCq1z0nQBIQOCHYIDCCoQGB2QEBDINXUwOdWQXO5t6rtW0fN1WpVoOjztWwMmlkswIABq9ew4EBvRnbAEsrq2C2ez6+lezU0WAMthKwEsmsbIOqp9Xq9Q0fQ2QJBAAIDDVAQHD63W5/P52dqFdq95VAPHaQNXua4BAAPN0eizlPVIKtDwGrxGrq0rlit/WAmB6/X574aq+CUIMzVoM0wSwBxCYB0l5RgOd53P2SnFxOJVgeyVoOs6HQ5yeBZYOBqCwB0S3Bq9WWAN5WQIJBztPlksmeIwAPBWYOlV4Kr/AApLB63V0hLZlkthEzrEtmk0r06V4fQ595qCvC5/WWASqCVgQSB0gOBAAPO1WjTgNXxFXp6nBvNPkzjBkoJCzmcudPkslliqBwFPMoNWU/4AImWk53NJrMBmder0zVwOCnVeV4NkTwPQ1SwBvOq5/QBIKyBAAOs63W1fPvWjVgQACq1XwOBmclp4ABUQWBBYMsBIVPpMlk0tHANdlcAlYCBAH4AIlek42eJzEswVeq5yBAAU6WYOI1imB5+jWAKgB5yxB1YMB64CB5/P515q2dztXuedvNPFQOCmcBq8ykslVgOrwOClksq4JBXQMChM0xGrq0sUf6wNvWcqyvYVQUJhMtWoOCO4KvB2SgB0dzT4OjWgOqWIIBC5+qBQVdqFPmcsqErUAOCwMthFXq8shEJr2rwGIq9emcCk0Crq7BXgOALrCw3q1PlYaWOoMzWAMtWIK2CV4KwB1ejq1QTYNQmdW0fO1asB53OvN6qFWwSiBruBU4NdruI1eIawOBwVYFIOswF6xCoBq8CgVXwV6q96V34ARqsrV69dq8zPoMzgYEBWAes2Wrq16zudp6bBp+j4/OVwPNztWUwStBUgNW1gDB1YBBwGsVIOr1ml0qiBq2l0mAdYMJmleGIMALa4A6lcskoYVgUsgVXwUtgdYVoM0xCvB5+kvKmB1V6qFQllQzt60ej0SuBqC8Bp8rEIOBr0tEIWC0l6VISoBAYMrmQDBvQUBCIOIZQKu/ACkslqvWAAMshCyBr1YxFYPYOrvN51XQWgOqVINzq8sVQKrCvOdzoCBvIHBrrWBmczhFe1iuBfQNXAYIAEZgOssmJV34AYrpXVQgKwBhEzr0JhM0r1eV4OsAIOs2WyWIOjqysBq4ACvK5B44CBXoMlq8lAAMmawOswCrCJBFWwOzYAYA/ACsrwKvVlsClquCrCuCr2CxGIVwOsQYIAB1VWzqlBvStC5wAB1XN0VzXINPAAKwBq+B0tWq1VHhNW5+kV34AZqtPCqcthMtq+IV4KyBrwFBAYQABxKuC2WkvOq5/PVoXP1fP1SxBXINWud5ztzp8srrPBUAMrV5QZBSv4AalQUTq6kBmisBq8JAAKyCWYSwBsiUB1mr1es6ABB54BC63W1awCAAOizl5p9Xq4jB1dWHhUrqC8KAH4ARgITSgUyryoDAAMthKsBnU0mirBWIKwC2QCC6wBC2XP1fW5+q53N4+i0WduclgVXxGAV5kzSX4AwgUChEsrusrCIBlstWYOsr00xGCr2IxKxD1mzAYWr2fW6+r6HW53O0fN0edp8sq9YxGrV5dWlZ+/V+MBWIMzq8tgSuDmeCmgEBnU6xFkxC5BWIKvBxOs2SvB63QWAPV5/O5uivNXruBwOswCj/AH0sVwIACmctmatBhMJhU0wU6sleVQOCr2JVYIABxCuB1nQ62z2XP62q52q0edp8sr2lvVXq1WOX4A8mcCgKuBhFXryuBWIM0mirBAQIADxFlxAAC1iuB1mrAQKvB6HQ52j4+cucswLCBwOAvSwHlUqPn4Ahg6vQgIABlksUIM0hMtAYNewWIWgQABwSXBslfr6vB1mJxOt2Ws63X62q53N0edqFdwOCDQWqq0rlY5DldXRn4AzlivDQgKpBhMJmYEBr2IAAILBiAGBVQK5BAgWr1uJ1nQ2er5/P5yuBvNPwNdrsIhOBvV7wFWAAdQWogA/AD9PB5yuChEtVINerEzWAKxBmimCWgM0WoWrV4usV4IAB2avBVoNQp8sq8llgBBmeBEYOB1el1WjqyK/AElVqqvOgUsrteq8zrCxBWActW4M0hU0WIM6nQPBryrBAASsB2ey1nP1V6q+BAAKyBp8lksJZwLPC1nWwErRX4AlwIONVQKoBQIICBAASwDhNXmleVwSwDxGJAIKuC2WzTYPP0mjvNWqFQvNzudPlmBq8IboKxB1dWRH4AmmcyBxtXq8tAQKBBxGsxFehMDgcJXIIHBWQKxCxGIsqvB1ivBVwOrWAWq1Oj0ei0WdziwCgUChEJnWsvUrRH4AmgMtNJkshB/BAAUtwWrU4KuBgczwSnBV4QEBX4VlsgEBAAS0C6ywC5ywEzqwBkosBmgtBqyv/AFEslgNLVogABmaiBr0zV4UtU4U6miwGslfryvE6HX5/P6ywC46xBV4jUBwNXqyG/AFErldPV5ctmcsWAgABVwVXr1ewSpBrywBWIKpC1iwCW4Ws1fW2fQWIPP5ujV4OcV4LvBbgOBvUrQ34Apq1PNhUzrEzq6vDAAcDmeCsirBUIKwEAoSvBVwOs2Wz2eyAAOq63P5yvB0WiVwMllmBwLBBV/6vszl6BhNXlszmauGlteAAOCAQKiB1mIV4U0r06nSuE1ivB1nW1fQ6Gq52j0ecq1XhFdwN7vVWqyE/V9Z3BNxNYVpFXVQNYmkJAIWrV4UQVwKrBxAABXYOy1us5/XAAPW5/O1XHV4NPq9XwOBq4+BkiE/AFek5ukmILHUoNXVgUzlszwSuBmcJAAdYUoNeiCvBVIOIsuIxOJXgOJ1mr6/Q5/P1XO1Oj0Wduclk0JwOkq1WlaD/AFd653PwBwHliwBr1dluC1gFBr0thMtrADBhMKmk0VwNewWIV4SrBAAa5B2SyB5/O0ejzmcvNPkslGAOI1d6kiD/AFdW1fW6FWBY0Cq9XrFYmkzxCEBmaqBWgSvChUQmitBAAKmBsoDCAAfWVwPWV4N5q1zudQkssgUImmIwA9HAH4Aller6/X1ZyGgUClirBmczr00VISvBmk0Voa7BXAVeU4NkAYWz2er2SvB6HV53OztXqzbBbwIqBnSvBlaC/V9uBQoOzwNQV49XltXrFYT4MtV4SwCmmCVwU6AAWI1gOBAYOs2QAB1fW62q52j0VzlmBwKtBAYITB0ksQX4Atq2z1uJ1lWlavFAAMzmiZBr2CVwKwDVYOIAQIPBxCwCVoYAB63Q54ABVwOp0edV4OCwISBvVWq+AvI6EAH6vq1mJAAOBWAkBWAajDr0zlqvBltXBASvDAAQTBV4Wy63P0isBAAOj0d5VwOB1mA1mBV4MAlbqFAH6vqwOJR4KwFgSwCmeC1amCWISwCmanCwU0mmCVoTSBWAKuCq2dzqtB0edp9XEwOAvStBVwIABlav/V+U6T4NXq0kV4KwCq9ewQMCVANeV4IABXYawCr2s1lkVwOy0igBEoNXp95vNPruHwOlwOAwFWlh8/AGUrwKuBSQM6wSMBgCtBWAUsAAMIluC1lemauBlq2CVYNeDoOCVoIKB1nQ1WjvNXwVXrtXAANdmc0wOr1iwBH4aA/V99WwSQBAANert5q0BWASvBmctgSvBVAMJgcDWIIYCV4a1BVwQAC5+jq9WVoVzp9QktXwOCr2rcYMsqyzDAH6vsvSQBlsthMzr2BV4SwCrtXWgUtWgKuBV4NXWoMKmkQiCsCxGJWAer0mj0d6zudzmcvNPlmBq8zwN60uB0iv/V+FWrqvBhKwCq96VoMsAAMzXIMIWISuCBAIABxADCwSuCsmJxOs2es62r5/O53N4/H0ecudXlkmgVXDIWrV/6vwvWBmkJhEthCyBwUBgNXrterEzUYVXAAc01YJBV4eIAAVlTIOs1oCB63Q6vP1XO0fN0V5qFPkssq8JmmIwCv/V+FWwMzV4IAChOBlksrqiCABKnCmk0r06nVeVANkr4DB1mJV4PW1fQ5/PWAIABztzWAMChFewOBlaA/V996wFXhECAAcIrEzmahBr2CmcthIABmdYwSwCnSuBxFkWAKqBBYNkxOIAwOr2eyVwOq53O4+i0WcV4Msls0xGlqyA/V99WwMzV4SyDVwKWBAAStCrE0AYKqCXgIEBVwU6CoasB1mz2YCB1nP63Q1fO0ejV4VXq+BEIOkV/6vxvSvCk0lgMmgVerEtq4DCVwS0EUQSvBrCzBAINewSYBVwWyWAXW1nQ62q53NV4Odp8lruA1dXqyv/V+Gk0mAlkCkoADr0zmigBwUzVIU0mgEBmmI1avCAoKpCAYIEB2WsxIIB5/X63X6HP0nO0ejztzp8swN6Vv6vzq2BmcCgKvEgUCq9exFeq6vCU4MKAAILCVwK/BVwVlxGJVgOr1oDB6GzAwPW5/P1WjvNQkquBAAKwBHwKA/V+NXgSsCp9JpKvBluITgNYV4MzVIOCVIMQAAQFBVwOsVwKvD1gDD1mz1fP62q515GgI1Bq+CWIWAvUrQP4Atq2k1WBliuBpNPp8lV4NXV4UzV4VerwHBWYKsBAAVeCQNlsirCAAStCAAPWAAPO52jvNQGYUzq7WB1lWV/6vv1es0tWkqtBp9zuavBVAVelsDhMtVgQACAoOCBAerxFfVIIAB2Ws1aqB1nPAAWq1Wj0edvNzpMmgUJa4N6liB/AFssq2BlsmktJVoKvDgUtAAMDV4NX1imBmiwEUwIJBWQQHC2YAB1ez63XWYPP6vP53N0eizlzp8lk0zwOAqyv/AF1Wq1XwKuBp59BzqvDgStBgctV4M0wStCr06UwOIsqzBXAauBWAQCBVoKxC1XO0fN0SvBvKvBgSvBwMrlaB/AFkrq2r1dXk1JudzvPAzkzltXAANdr1dUIMQAQKsBUwOr1iuCxOsAAKvB1mt1nW2WsV4PP5+r53NAAOj0V4vFPFgSvBwNWQX6vtwOz1hzBp9PvOcued1ihBrE0rAEBVIU0mleUgNesgIB1mJV4OyVAK1B1qrBVgXO1XO53P0fHVwOizt5p8slkJrGI1d6laD/AFdWwCWBwNWVwWcvPGToMthM0hMzVwSyBAgKiBAgavC1es2YAB1mrWgOr0fO1IBB0ej5vHVwOcztzp8lk0ChI8CV/6wuwNdllPuatB0XG0VYrysBnU0mivFxFk1gABVwIABxOs1qwC1fWAAPOvV7vOj46tBWIIuBuYABV4MlhFXV4N6qyC/AFUrllWvWBqyvC0WizvGUwMJlteWgKtEAAKqCAAWt2awBWIKwB2SxB5+qztXqDXBAAWcqAyBAAKuBksCmbdB0qv/AFatC0qvDvKwBzmjVIUJq6uEVAWy1mk6wGDV4Or2Wr6wKC6HQV4NWrtPzudzmdvNQwKsClkCAALgCHwMrQv6vq0uIxOsq9Wued0XG0XNltdxEzhOCV4Wr1Wr1fP5151es62yAIOs64CB63X6HP5+q52jvNdq9PbgNzp8lq6sCq8zhEIV4WIWAKF/AFN6wNkwWlwWBvOcznG4/NgUzr0thIDBQIOs0l/vN5q155yjBWQOsWQPW2es54EB1SwB0educsWIMskslk0zwIGBFoVXmk0nTdBV/4Apq161dePQOBTQOi1PN0ejlsChKBBxCvCWAOs1V6WAOj5yhBVAIBB1fP6AFB1eq5whB0V5lddwMthEzhFdcgIABwIDCxFkxOywEsQ/6xqwFXk1PVwOiVwKcBVIIADwU0V4OI2Ws5+jq1zzt5vKlB56yC1YcB5yuB4+jzlzljdBEQICBwWrvVdAQOBwGB0us2XX1dWQv4AmlcrV4NXwKXBzivBRYPO6tehIACr2ImmCV4Os1nQ1V6mYABuejC4IADAwKtC0S/Bp8srul0qlBwOsVgMrq1WAQQAB0rMBBYKJ/AEtQNoOA0h8Buei0fNAAKUBVgSyBmk0WIOsryvB59/vIABqFXqFQVQQABvS4B0Wcp4ABpMlloeB1Y2BvSsCeY0yq2kztWRP4Akg1W52BxCuBqF5zmi4+jVwKvDVoKuBr06SQKyB1ek5+q0edq9WAIK2Budzq1PXokllksls0xGkTwNWkhHKldWli8GAH4AdM4OIwSbCq1zzmj0eqVwOjV4NXVgOIAQIECV4Os2XW5/PvIYBvN6UwNXruClkyp9PkoIBmddVwOsV4KePlkzRf4Ajq2AwU0mmC1ctwN6SwPP1SeBmawBVQNkr00VwOsWoKwBWIPQWIPO1SyCq8swOBlkllksruBxGrDYOAVyAABrsqRn4Ahll62WClsJQQNeV4XN53OV4NYV4MJwWsWATEBWQIAB2axGWAOdqFXxFYlszwWIvWkvQCBVyQABCaYA/AB1Wq2BrszgUsAgKDB0fOS4PP60zgavBryqB1iwBTIIAB1mz1qwB62s5+qWAVzlmI1eCwWHwGAq8rkiuUAH6xm0uBllPud5R4PN53Q63W1ctgawCmiyBmlYVgNkAQIAF1fW1XNztQq8zruBr1eWgNWGgKu/AG0rPQdXvWBp+dVwPO53P5/Q5/WVwMDltXSoKXCVgVeWIWy2ez63XDAPOvNWp9PmUChEtmmIq1WVzEHSP4AdPIIABvV6q2Bq2d4/N52k6vW5+q2SuBhOCxCsBr06AAKrBWoKzB2ey1iuC6HOaIOdvNPkslmeC1erGIJQXlcqSX4AbleA1eAq+s6GBwWBzuj53O1fQ1mr5+zmajBwWsxE6mixCBIK3B1my1oVBWYIkB1XO4+jziwClstYYOAV7EAwKT/ADdW0ut1eCr2BWYNQ0fH52k62r63X62zB4KwCr00V4KuCxAACVgOJWYOz5/Q5/O1Oj0SvCgUzZwN6lZSYmYaZAH6uBq2BVgMtlqaBwNz0ej53P62qV4XXmivCUoM6AwIEB1mIxIABVwICB1jKC1XOEYOdp8sVwM0VwNWKbMsliV/ADErwGAwMzhEIhMzr17uedV4XP1SVBWAMJhMKrCgBr00mmCWANlWAms6Gy63W6AdBV4VWq9XwOHwN7V7VWuYcaV3t61et1kzgUChGBxFWBYKvB62s2Wz2es1c0WANe1avDryoBslkAYOsCgWy1jMB52p4+izlzksmmdYxGrV7bVBWH6uWq2kSoNXlkBksswKiBq+BvXO5+r2eyAQOzmkKhM0xGIVwKvBAoOsr6wCCoKvBVwOqQ4Oj0WdudPksCmeCV7mq5+Alab/V6ixCwNXgUlAAKsBAAMyq+q6HWSwKvChQACiAACV4IABaAICB1mt1nQ6Gr0d5zudziuDmcImYTBvSRZqxCB1aw/VypbCvWBrslp9zud6BAOjwGr6GyV4Oy2eymlYVQIDBVAOIAAQFC1mIxKyB6HOvVXq9Qp9PlklkytBCYWkqyvZ1mJ1uBqyw/KyKvEwGIq8sued0Wj0ep53PVwOsAIIAB2aoEAAapCAYIABCgXW63P0d5qAqBq+BmcImeC1er0l5V7ErV4NksmsWH6uROIhcB0mBrtzVoPNVoXP63X6GsxIAB1leWAM6mk6nVeVwNlWAez2Wz1ez1nQ1QlBztPlirBDgOswFWRoNXR69WwOIr00dYIjBUP4AMleA596OgNWq9Xq2AwNW0eq52q5/P2aUBAAOJ1YCBNgOCmiyCr2rxFkBgISCAQOy2QeB1SwB0V5qEsq+CwCKBVa5YDvWBVwMtH4JVBErYAulVWKoOI1mkq+kwOrvWlV4XO1fW63Q2erWISzCDAOIOINer06nQGBV4ILBxLCBDgPWV4PO1Op0edp8lgVYG4NWTIpaUqxYBr0zhMJV4OB0l6E4oA/NIdW1eCnWJKQOCr2swGIrqvB1SOBSYSsBAAWs1esVYKwCxAAECQSyB1my1jOC53N4+ivKvBq4yClhFEq6wTq16vWChMIgUIq8zE4UkaSquwqwABwFemeIKYMJwWsvVdllX0nO6CYB2SoBTIYBBxE0miyBmgZBxOJCAQAC62y2bOB5/O52j0d5qElk0Jr2rqyGFIgKORLQOrwOBlsCAAMIhFYBAJnCWP5TE0ulq+AmcCq9XKoNXKoVQzujRwKYDUAOJxAGCsisBAAVexALD63WYAPW6Gr6CuC5vH0V5cIMzq+IHgKGBVwek0iNQUAWBmktwMCkzXBLwJZBwC8BvUsV39WwGARQJLBlklAAcChOCwNW0fO56XB6+r1uJWQivBwVeAAIiBV4Wr0mq1XP6AAB6yuB0YACzlzkszCoKEBCwOAWQWlFoOlAoMkLRl65+kwUzVoQAClldq9ewJCBwCv+llW0hnBr00wVXKgNJp9JV4MIruBvSvC1ey2QWB1urV4s0nU6VoWJNgOs0lQvOj1QdBAQPOztWvN5udPllXQAOIZYQFBVoWCaAV6ldWlZYFki7BV4OrZoOHLIlPktXA4MshM0xAUBCoIhGAGcrmVWNINemcJUoMsp9zP4JVCwVWzqvC1mzAIOyAYKvCAYNlr1knVe1iUCB4OrqywC52q1Wp4+dqAwBlktq6sCmcsH4NewIIBwQFBnWsvV60mAVAVPAYNWwF6AYOBmldrtXVwRaBLYQABhFe0oYDEIMkVmroEKgOBmcCgSuCvOducswKwBp6RB0fP5+yU4IAExNlAAOCRAKWBsiyBAAPP515lgeCAAWdp4pBVoNXlksq8zko+BluImkIq8IhEzxGCEYOywFQKgN61elBIOrvWBlpZBrsyLQWczl5WIUzwN7q+AwIYBva0BPQiuuq4HFq2lwRzBKYOdzpTBp5gBk1Jp9Wq+j1avCxOsxAABAYasBAAVkBQOs63Q5+jvNQa4N5AANzPgQ2BcoIsBAYNJktXw6zBlkmgQGCrE61mAq0qq16G4NecAOBrxYBkssrtPLQOivCwCBQOCwOBwGsJwOBZoN6WQKtuq15GIwJBwFdllzzmc0QABvVdwVXllXruBqyvDOQYABUwSvEAwOt2es2XP1SwBmdPlijBQoNeFQLlBXANWAYNzGQLnBktJWwMzq8IhFYw+rwFXvWlxEzlte1deCoNPuYABVoIADzjiDrpbBwUtrBQBEYMrPASspUYN61eqV41W0l6wKABKgOj0fH0awBwIABRgOAV4KcB1lkr1kWQM6r2swS5BxGJxOr1us62r5/O0edToNemaZBcQMsXAKJCzroBzqwCIAKXBkrABkoJBmYwBvWAG4MsgSdB1ZXBud4zipB0fGLYKvCq4lBlkmAYMChE0xGBPYaEBqyyjlcsE4NWwGsQAKvFGoKOBr1dq+dKQOp1SNB0d5q960YCB56tBxABBsms1eInSwBryuBBgQCBV4PQ5+qaQMswVVwKfEqFQFwPN47kBGYIJBua3BztPq1Pp8lk0trzyCr0Cli6CfgMruauB0fH5vNE4LZBqC9BD4NPqDTBaIOBvaoEwGkvSJBmUqVjjSCq2kfAJRBr2sFgI1CkgRBBYMJllzOoPOAAPP5/O0mkAoOkwGr2TPBUoesryvBnTOBry5B2YRBAIOraAVzliFBwFXkqIBzl5q2j1XN5wCBcYVPSwIABzt5vNzp8mmeC0uCVIMmTQVPWINPzquBVoQ2BAYIlBrrUCEISvBmeBPQKLCYAOz0uB0gJBlksla+EVaAABq16vQHBUAKJBrEJQgOBwDdBq1XCYOAxEzV4R5B1fW5/QWIPQ6Gs63WTQKtCAAauCry4EVoQeB5ySBVwNXwQABMQKICcQWq5+qcoKvCHwPH5vH0axCqCLBwSPBkokBTYOdvFPY4QYBEoImBAQIlCzogBAAOcudJk0tr2rqx2BAAOrr2CLIIJC0l6lgMBAQSfBllVU4QKBDgYABagIZB1ijBll6E4NXmcChMzxGrvWk1ekvd6wOswOBq1553P6ywBSoICBTIQACUoNeAgKwDr1kxGJCQfQTIedliMBp8skyHBvLfCGIIAB6o0B52pV4KKB1WpXwPHWAIcBV4MsktPp9QZ4LQBvNdq4nB0blB52r1TYBWgPNXYWjV4LMBLYOsvaLCq+BrC5B0t6q+ASgOB0qCB0ukvMsmUrV4S1BqBOB0urSwIWBJgOIEAN6wFXgQABhEzwOACAOICQIWB0mBmdXvRRBSYPWPoOzVgesxOIsixBfoJYBWoKwBBoLDD595PYN5qEzll5zoIBqCOB0Z/BVwQVBcYPVYwN6BoSVBXALPBp9Xw9ehEluecXgSbCp9dp+j1OqEAJXB5/WWQLfBBgIUBq1Xrp4B1aDB1eIwVXhECq+CvQOBNAKXCxOzCgKtDAAcrwBvBDwNeTYNYmeCwDQBwNXkoABFIOBFINdmczRwOk0uCmVzOIJ6BSoICB2etFIKvDsoEBmgACWINemisCAQPQNgKlBU4SABAAWdqGjVoTeBboOycQXP0mkHgIAB1azBRgIABmZUBqF50XH4+q1PNb4NXq5XBHIKtC5/VFAIyB53NYoN5p8sPQOlq2k1lehNXgUlWAKUBrsJQwRpB1mAliuGAAMswOInU0hNemcIFYK1Crslp9Pkq0Bk0slkCgUJrAPBCQRXB5/Q6/WU4OJTQKvBrylBWYICBVoMQAAKwCYYKZB1iuBvI7BvJ9B1WqAQOpXQOd5qgCGAOyV4PXDoKRB6C0B6C4B52jvV5uczwWBlgnBFAIABFIQQBa4XQ1Wr6wiBAwPVV4PO0mj0V5kstEQOlwOAwUskoABkzRBgUzgUIQwJnB1dXVxAABqyUBlsIDYMmaAdXllPuZXBCIK0BWoMsrqOBL4MsmSvCSYOyVgWJVINfUYSxCV4QFBrEKAYKtDVwNQUYNQQoKXCAIKXBq1Q5yuB2asB1es1aqBAwIBBBYKTBEgKPB0edp9dq9WqwoB5+qB4QQB0jKCDYPQbIJZBMAIKBD4KvCmWBRgOBawVXQwNPQoK0CAAKxBmmBvdWV5bOBlsCDIbQBq0rVoOdzmdWIIBBF4NXrurGQMmp9QCoJfB1mz2avCWANfQgOInU6U4KvBeYNehU01hmB1Wk0d5vOj1Wjv6kBTYPQWIOqvN60YJBb4LgB1oxBRwIDBWga8BDAPO5udlldp5cBDoKdBYASjB6AXC2RWBV4JbBIoI5BD4OjzlzlmBwUsqFPliuBvOcvKwBp4ACksIhJrBvSvLlbTBq6vBDQQIBllPVoOiAAguDX4KTBqGdzt60mrOoIADxGIstlAoNeVwKvEAYU0r3P0dQVgXPPYOrVoSlC6yXBvIRB56mBVIOt1uJAoLjCBQSRD62qaYL6BFgPO5yaBXoPX6AVCFwRZBcYIEBAYK0BdAIfBvNWq9WqFzOINWq150Wj0WdvK0Bua7BwNdwOlq8rV5VWwF6rqvBuYABp9XwNWzooB5vG0YsB4ywCbQOCCAIOB53PVglkUoIABA4SvCAYKqCAAM0rFe0dPvWjVgJyBPYQBBAQIABQQKwB1QNB1oKCRYKrBA4a0BSoQWB5/OV4IsB52qVwSrC66kBCwIdB2TJBAwI4BH4PP6weBvNXq6sCPgK3CAYPNQgWiziwBlkyWIN60lWV5erP4NXqDLBzjQBPoXHFIYCBFYVzktXwWCHIPO6ByCKoOsr6tBVAKvCWAStBVwK7CwUthN5DwOrVYaVDTgWIRIIGB0jfBBgIpCVwKvBF4QWBWgSRB6HP1WdmYuC5+r6/WEgSkBKYQgCGAYfB6wLBD4Ojq2BqytB5up1SAC0eq1Oj1PHQgSSBksCQwOrq8rV5MrwB4BktPzuiUYInC5pQB53O6oIC0QpCbIMrqx9BSAQADsipCwVeAAOCA4QICBwLmBmcJZoQdCPYKfDAQSgDAAYKBbYaOCFYIGBSIIHB6Gy1ZXBp9XzvN1XQ1YxB62rFASpBcQQFB1olBDwS+BDwKvBrtP0WpPoOq1QDCQoQQBQgOczqFBk0twWlqyvISIIABLgNWV4XNa4IpCV4IvBFgPNXYNzq0srtdvKQB55vBOARUBxFkA4Nemk0iEQmiKBAAQEBr0zlqvBVwYADDwIRCxCgDVoQHBxFfaYITDAwesToavC0d5q95LQOr2YABFwSkCGoY7FWoOz63Q0lWq9dqxuC5/W5/VAQS1C1KvCvNPllXV4OBUgSxFVwOk1erwGBFIOdZoLWC53Q6HV57tBBAOjzo9Bq9XubBBBoKRDOQOJsqJCVwIACWIi6Br1YhIAB0nQOAakCZwSwDslkZgeCbYgHBsozBCgKbCR4Wy6GqVwMszqOB6C5BWAWt1alCBIIeBA4KvE2QFBZ4NPZ4IfC5+qVoOrOgIEBRwPHcQNPksswJOBwGr0rOBWQStBq2ALAOH1dektPVwQpBbgQvBFQKwCFINdllPuecd4eyAAOtSYgCBVoSGBr0QhUKVoNewWCV4RuExFfBoKbBTQSqCQQTLCbQQNBVIIGCwQUCR4QmCR4NzR4Oj5+sfwS+CE4LGCHATpCZ4ZgBOgOqqyvDKIPQ67TBFoKyB1SEBzlWwNXwIACUgJYBWIUAgFW0hRBmdeCANWJIOp52kagWq1mrKQTbB0d6wVXp+dYgerNgZ9CnSUCVwQfBAoKvBAAKIBq6vCY4eIsihCC4IABPgS0CUATMCxE6F4LiDnTICDQQABKYOjvMzqF51auBb4IPDUwdfdAQIEEAmqvWj1XP62yVYPWZwOy63X56ECqFdQgMrq8zmeBRgJNB1avDwGImcIq8sCwN50T6BFgOr677C1gqBWYPOztWrtPYgKvBBQJhBfoKCBPQSvDNYOrr00U4KvBmiwBV4QOBVgKWES4IiBZoQEBEoVeiEQAgLeDwQjBDoVlSoIhBQoKvBq1QSAhNCGQa2BsllUoI3BVoQNCWAWk0mrEwInBQIOrA4XW6DZBV4NWwKDBvNzp8swMtgUtV4QABvWkwMzk0lkssmSbC5yuB6DWBF4IrBWQKvBvTRBmdWzqwC5xiCCgNkAIKBCV4QABVwUJmlYSQNXV4YACZQSlDwQcBAAKgBVwS2BAQTAFmk6RAWsV4Ws6GkvMsp6vC2Wz2ZOBr7CCY4NlCwOCEwIFBsjQDAAXQaoS9BD4OsV4Os64lB6DhBq6BC0WjvNXlahBruBwOr0l61eIwMsktPp9QwNXq965/PFQSzBFoLaBWQIbBvNWrtXp9zq15zoYBIwRQBP4U6RISLCU4QKDAwUtshoCSgJuBOwKuEiDPBDAIOCaYKIBB4YACBgWrAYRVBwGjztXvKvBF4JKCagTlCHYQGDWAINDVAKtCVgIOB2Wy1uJxKDC2er5+AzquB0fNAQN5vNzqFXgVdEgKvBmkzrtQvOczqcBCYPP62yEgIyBHAnW1nP52jEYNXDoIDBq2kYYJSCPQWCAwKXBAoKpCXAUzV4VXPIaUDAggkCiCJDEAM0hQDBbgjBBCgM6nSvD2XW5+qvJqB1SvCfgLCB1gUBXATJEAwRGCAATBEAQOtAIKDB2Wz2XX6HPGQPO53N46wBzl4p8lksCmeCwOsWwMlp+c0QRB0b5Cb4bcDAoQLC1ejp6rBvN6vNXueAV4uCSQesQgQKBVQQAElqVBBoQcBUwIICO4WChQiCsgIBmlYrwFBCAQtBV4IaCRggAB1VWvWk0iRCBoYUBnQhBdYavDBAWIsoVCZATKC2Wr1aCC2es6yDB56yCWIKcB0WcuawBhEJrzYCmavBvKsB5wZBD4ImBxImCTgLfBcQIAB6F6LoN6DAOjvNWZIKvExCvCUAMtq+CQINYVwszXgaoCrEKwRyDAQIIBhQGBAIOrKoNXGQITBV4gfBTAaLBBgOrqukMoKcEAgLoDVwZTCBYQMBWoQvBWYNfCoQhCV4SBC2XWSYKvB1fP5upV4h4BKYWlr1dmdWV4KuBJIIiBUwJiC1i1BKgTbB1mk1YTBb4Oqud6MYc6shYCwSiBgcDAYNdUoUzlstBAIWBXYM6mhnBO4IQBEYJ0BVoMJhQKCrAbClrVCCwU0iAHBmgiBEYQAB1nPvL5EAAKjDYoSuDiEQV4k6DwTwCAASvCQAKBB1oABAgPQ2eyVwPW1XO0eivKuBksIwOBmczr2rAoNQV4QWB6Gy2avCAAQtCHoQAG2WkqGqMgZfBKQWCmavCAAUtmaNDAYNYrxtCFYa9BmeIBgOCmjCBWQIGBEoYCBW4QOEHQQCDEwWr0hJCUwKnCiDABboLGCAgIKCdASlCXgWCDoOrWoaABPwiGBPoOs6/W5/PV4N5q9dlksOgUskszmUyrtXvSvBDIIAEr1fF4ItBsgFEWYurvV51auDI4VeVw0DgUJWAk0AYUKmgzBwVYlqZBWIVeAoISBYINXAwInDrouCCATkBc4TLBH4avCWwSdBBYQ6CT4IEBDQSwBG4M6A4IMBXAJiDDwJ5CawQBBVwOz2ey63W6CvDq1Xk1dwOBxFXp9zqwCCq+j5/Q62rD4LXB1j9DxFlFoQFBBQQFCWIRlDJYJPBM4SACq9erEJgQABq8zDYIQCV4QaBUASfCloMBDIKYDEgUzmYPBFALDBDwScC1grBAAS3BQwRTDc4SeCA4OCCIWrBIJXBWYK7DYAS5CAAIhBstlr9fGoWyAAOrV4IiB62q0ejvNPmeIp+Bllzzmd0ecWAOj53QZgOtV4QqB1mCFIQBCnRcBJwIHCAAS8CAAOsmkJhSYDPwQhBlqwCgVe1YHBmYQBSIQFCV4UDDYSpDVwS1BwSwBUgIHBnQcBFoIIBbYiNBVYiYCIQU0EQJaDAAU0KwIWBCoRmBEISuCNIJ0DslfboWtSQKtB2fQ5+q5ykCmbeBr1PVoPHBYN6qGj0nP2QaBFAVlEoSbCc4ZTCIYI9BAwKtDKQaOClqOBBIOrEIKvDhCTCRAKxBDAKlChIHBWAVXr1YBQSzCEgVegUtC4Q5CGYLMBhL6DUIIADSoQMDq9XrBpDVAUKAAK2DsiOBZIhxCDoKFDAYWsxOJ2WyVwIBBUYOjucswUsllXvPO5y8Cq1Xq2kV4KXDFQTwBAAIEBGwNkA4QQBsiOCXgJjCAoMJmkzPQKSBEQILBVwSwCgUzTAKgBrCjDCwIJBYgRBCV4aZCq4NBV4IvCAAjJBBYKZCfIb+BUAIeDDQISCCgQdChU0PQRxBHIMKB4S6DdgIoDWgeJ1nP63X1fW1Wq0edp8swMsued0fPXoXP0ekvOqV4VkagM6VIRYBiA2BUQI7CUgStBX4JMCmcDhKbBlpcCCQQHBlivEAAKlCSYrFDEYIQBloeBwQnBEYVXDoSQCVoktlqvBhLYCAYIsBDgL2CKANdYYZRBK4LrDhSwBCoOs1dYM4TUCAAZACDQOJAASxB5+y6HQ1XO0ejqEzq9drt6VwWs63WAQWr1ZvBAAKvCTwQACiFYHQSsCAwOCKYT3CKoKXBNYQAFVw8CrqnBSgIYBlszMAOs1QnBCAMIYIYADrEIlodBIYNXVYI1DEQMDRIYsBUgIvBXQTHDBQILBGwI8CryuBMoNYEwLeCPgopDRQNlWAIEB2ezAQKiB53N0ecp5rBp9QvWq56tCAAquDMALiDbYVeiAECXwT3CB4MJlwSBhNY1lXQAL3CAAKBBVw8Cls0BYNdCIIWBGYSFBC4cJb4KOCHIOClkzAAQZCrACBI4S8Bq9Yd4K8BFYMCH4SkBAAawCJgS9BHgIwBA4QACNgI8BQQSvDR4NksiUC2ey2XQVwOjzt5p9Xq9Q0ejq2k6AQB1mJVwyLCPIU0nRfB1bzBhTzBBoT9Bq4UBVYJ1BKwRzBNILFCSwMzroKBAAKABQgQiBA4Q3CrAEBljBEmeCwSmCxGrxEtlhhBDIQCBPYWrCYIYBrylCq+CwIDBf4IABloaCq6vBKoNXhIfBBQS+EdYKFDRQNeiEQV4WIsjoCAAKvB1WjvMzqFWqyuB1XO52rVoOz1urWALXCDgLYEAAVkSYJTBhQKCegJEBgaTBBgJqCMAMzVwNXluBwKHBBQKYCli3BDwNerssQ4RcBAYIABVwgABEgIYBrFXBwK+ChEzK4SLBZobrCAwJRCrEIcYQNBc4RvBLwIUDLIIrBVoSvDWAIhCdQOrV4M0W4q1C1nW5+jqFXq2d0YAB1XP6wABCIQABxLLBDgRABEwICCJAR+BH4UKJYZECgaPBmktMIOCD4NXRwcIlldToQJCOoMtQ4ISBB4KyBwTIBmldRIIAFFoKsCCYMCgMBFgOCwS6CrwZBTIIABq5XCUYRJBdwIACSwZFCJYSsEWgUtNIMtEQQdBAgMQAYOCnSHCTIOz2WyV4NPmd5VgPO5/P1myVwOz1irBDINlZoNenQrBVgNYcIS6CwTpCmiFBI4ZGBNIIQCDYQXBliCBQwQSBQQJ/Clh4BRYIGCAQSdBXIIcBCYSzCXgJsBBQYADFILTCmbIGIYMtDIOsCQJNBSoRRDXYLECaAJlDEoIRCNIIgBwU0PIM0iBrBRoIABE4IuB1nW5/O0dXq6vCVwPW62rB4IABDYLIDEISSBE4IGBlrqCJYIEBH4NXVoRGCSYUzq4XBBQMzQwauBAAMIrszrqRGV4gUBgWBDoKWCYIIsBAYUICYTZCAYTbBBoJMBXo53BAQIaCmb4BWoJnBAYITCA4LGCEYIFBxGrA4MDZQJ9BhQBBhQTBnTTCSYOJ1my5+k0d5mdQzvO1fQBYKlBCAOJxOIsg/BdoQGBTYTQBwUJgYABIoNYegQACCAQcBK4UtwSGCXASvFQ4KwBq6vHTATCDNQSvCloABBAIaDEYQpElhYBrDRDTQo0EYYL4BXAKsDAAUJwQgBMgJ5CQITwBOwaxCmgQBmgUBPQICC1mkvVQNYNz0eq5/P2Wr2ey1qvDsoVBmk0iAhBTgQDBGAKvCWA4SDwKIDNgMtmiyBQoaGFTwJwHYQ4QBRgQDBmdYryeEV5CcBFIcsdoJCBDoIaBIAjXBJgYAEmaUBVgZuBlqtBaISsBBQMKhSHCSAKPBDQNkTIPW5+jvNQq2j53P63WBgOzAQLECAAOCDwSyDcYOCmalBmdeLgQ3BLwIyBYgRvBLAjBCKISSEliHCCoKXBB4h/ChAQDhFdC4KXCDoLDHY40sIIQ1CAAMJq7JDYwtdCYYAENgLOBEIKBBrAMDBAMJBgKoCCAQCBnQHBTISfB6HO0dWll50fPVQWJCAQcCwQFBEgLTCbAMKrBIBGQQKBHYJdBC4KuCLIKVGOQJuDSAIGBEQKfDY4WCBwQdCUYQRDTAMtFIwAEmUsFwKvEDoNXEYlXro/DYopFBCYQUBCAUIq9YKIWs1dXlo9BNYKvBaoR2BVAOsTIUQV4Ws2Wy6HPvNXAAN5V4Oy1mtWQNfsicBnQYBTAIyBFgUKEYKvBH4OIfoQ3BXYLMBIwIKCAAQGCGYIIDCwLYClpyEE4NXlmCCoIA=" + ) + ), +}; + +function draw() { + g.drawImage(mandelbrotBmp); + // work out how to display the current time + const d = new Date(); + const h = d.getHours(), + m = d.getMinutes(); + const time = h + ":" + ("0" + m).substr(-2); + + // Reset the state of the graphics library + g.reset(); + g.setColor(1, 1, 1); + g.setFont("Vector", 30); + g.drawString(time, 70, 68, false); +} + +g.clear(); + +// draw immediately at first +draw(); +var secondInterval = setInterval(draw, 1000); diff --git a/apps/mandelbrotclock/mandelbrotclock.png b/apps/mandelbrotclock/mandelbrotclock.png new file mode 100644 index 000000000..19601fe2e Binary files /dev/null and b/apps/mandelbrotclock/mandelbrotclock.png differ diff --git a/apps/mandelbrotclock/screenshot_mandelbrotclock.png b/apps/mandelbrotclock/screenshot_mandelbrotclock.png new file mode 100644 index 000000000..542cff324 Binary files /dev/null and b/apps/mandelbrotclock/screenshot_mandelbrotclock.png differ diff --git a/apps/marioclock/bangle1-mario-clock-screenshot.png b/apps/marioclock/bangle1-mario-clock-screenshot.png new file mode 100644 index 000000000..ae2dc7800 Binary files /dev/null and b/apps/marioclock/bangle1-mario-clock-screenshot.png differ diff --git a/apps/marioclock/mario-clock-screen-shot.png b/apps/marioclock/mario-clock-screen-shot.png old mode 100755 new mode 100644 diff --git a/apps/matrixclock/ChangeLog b/apps/matrixclock/ChangeLog new file mode 100644 index 000000000..7cc9144b1 --- /dev/null +++ b/apps/matrixclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial Release +0.02: Support for Bangle 2 diff --git a/apps/matrixclock/README.md b/apps/matrixclock/README.md new file mode 100644 index 000000000..010524b60 --- /dev/null +++ b/apps/matrixclock/README.md @@ -0,0 +1,11 @@ +# Matrix Clock + +![](app.png) + +## Requests + +Please reach out to adrian@adriankirk.com if you have feature requests or notice bugs. + +## Creator + +Made by [Adrian Kirk](mailto:adrian@adriankirk.com) diff --git a/apps/matrixclock/app.png b/apps/matrixclock/app.png new file mode 100644 index 000000000..bc135c3ee Binary files /dev/null and b/apps/matrixclock/app.png differ diff --git a/apps/matrixclock/matrixclock-icon.js b/apps/matrixclock/matrixclock-icon.js new file mode 100644 index 000000000..5d5e9cd67 --- /dev/null +++ b/apps/matrixclock/matrixclock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lEowkBBpNgEKV3Bhd2CZEGFZAgGAwUHuEGuAMIs4JFCYIBBBItmFRA7BCY4+Gs5MDCbZ3CHwQTNJgwTPB4h3LHYQTQG44Tfd4zsCCZJ3GBwQTMCwYTEgwTSbBVnCYZPDdhZSICbo7EMZbbGRZivDT54AJHIITHdYoAGBgxjCHYYnEO5QyGJpgMDbZgTLHpITJT50GOQKfTCaoMRRdITRPQQAJBgZyRC4oAFA")) diff --git a/apps/matrixclock/matrixclock.js b/apps/matrixclock/matrixclock.js new file mode 100644 index 000000000..ab18c13b8 --- /dev/null +++ b/apps/matrixclock/matrixclock.js @@ -0,0 +1,258 @@ +/** + * Adrian Kirk 2021-10 + * + * Matrix Clock + * + * A simple clock inspired by the movie. + * Text shards move down the screen as a background to the + * time and date + **/ +const Locale = require('locale'); + +const SHARD_COLOR =[0,1.0,0]; +const SHARD_FONT_SIZE = 12; +const SHARD_Y_START = 30; +const w = g.getWidth(); + +/** +* The text shard object is responsible for creating the +* shards of text that move down the screen. As the +* shard moves down the screen the latest character added +* is brightest with characters being coloured darker and darker +* going back to the eldest +*/ +class TextShard { + + constructor(x,y,length){ + // The x and y coords of the first character of the shard + this.x = x; + this.y = y; + // The visible length of the shard. We don't make the + // whole chain visible just to save on cpu time + this.length = length; + // the list of characters making up this shard + this.txt = []; + } + /** + * The add method call adds another random character to + * the chain + */ + add(){ + this.txt.push(randomChar()); + } + /** + * The show method displays the latest shard image to the + * screen with the following rules: + * - latest addition is brightest, oldest is darker + * - display up to defined length of characters only + * of the shard to save cpu + */ + show(){ + g.setFontAlign(-1,-1,0); + for(var i=0; i this.length - 2){ + color_strength = 0; + } + g.setColor(color_strength*SHARD_COLOR[0], + color_strength*SHARD_COLOR[1], + color_strength*SHARD_COLOR[2]); + g.setFont("Vector",SHARD_FONT_SIZE); + g.drawString(this.txt[idx], this.x, this.y + idx*SHARD_FONT_SIZE); + } + } + /** + * Method tests to see if any part of the shard chain is still + * visible on the screen + */ + isVisible(){ + return (this.y + (this.txt.length - this.length - 2)*SHARD_FONT_SIZE < g.getHeight()); + } + /** + * resets the shard back to the top of the screen + */ + reset(){ + this.y = SHARD_Y_START; + this.txt = []; + } +} + +/** +* random character chooser to be called by the shard when adding characters +*/ +const CHAR_CODE_START = 33; +const CHAR_CODE_LAST = 126; +const CHAR_CODE_LENGTH = CHAR_CODE_LAST - CHAR_CODE_START; +function randomChar(){ + return String.fromCharCode(Math.floor(Math.random() * CHAR_CODE_LENGTH)+ CHAR_CODE_START); +} + +// Now set up the shards +// we are going to have a limited no of shards (to save cpu) +// but randomize the x value and length every reset to make it look as if there +// are more +var shards = []; +const NO_SHARDS = 3; +const channel_width = g.getWidth()/NO_SHARDS; + +function shard_x(i){ + return i*channel_width + Math.random() * channel_width; +} + +function shard_length(){ + return Math.floor(Math.random()*5) + 3; +} + +for(var i=0; i RESET_PROBABILITY){ + shards[i].reset(); + shards[i].length = shard_length(); + shards[i].x = shard_x(i); + if(shards[i].x > DATE_X_COORD - 20){ + shards[i].y = 50; + } + } + // If its still visble then add to the shard and show to screen + if(visible){ + shards[i].add(); + } + // we still have to show the shard even though it may be off the screen to keep the speed constant + shards[i].show(); + } + var now = new Date(); + // draw time. Have to draw time on every loop + + g.setFont("Vector", g.getWidth() / 5); + g.setFontAlign(0,-1); + if(last_draw_time == null || now.getMinutes() != last_draw_time.getMinutes()){ + g.setColor(g.theme.fg); + g.drawString(timeStr, w/2, TIME_Y_COORD); + timeStr = format_time(now); + } + g.setColor(SHARD_COLOR[0], SHARD_COLOR[1], SHARD_COLOR[2]); + g.drawString(timeStr, w/2, TIME_Y_COORD); + // + // draw date when it changes + g.setFont("Vector",15); + g.setFontAlign(0,-1,0); + if(last_draw_time == null || now.getDate() != last_draw_time.getDate()){ + g.setColor(g.theme.fg); + g.drawString(dateStr, w/2, DATE_Y_COORD); + dateStr = format_date(now); + g.setColor(SHARD_COLOR[0], SHARD_COLOR[1], SHARD_COLOR[2]); + g.drawString(dateStr, w/2, DATE_Y_COORD); + } + last_draw_time = now; +} + +function format_date(now){ + return Locale.dow(now,1) + " " + format00(now.getDate()); +} + + +function format_time(now){ + var time = new Date(now.getTime()); + var hours = time.getHours() % 12; + if(hours < 1){ + hours = 12; + } + var am_pm; + if(time.getHours() < 12){ + am_pm = "AM"; + } else { + am_pm = "PM"; + } + return format00(hours) + ":" + format00(time.getMinutes()) + " "+ am_pm; +} + +function format00(num){ + var value = (num | 0); + if(value > 99 || value < 0) + throw "must be between in range 0-99"; + if(value < 10) + return "0" + value.toString(); + else + return value.toString(); +} + +// The interval reference for updating the clock +let intervalRef = null; + +function clearTimers(){ + if(intervalRef != null) { + clearInterval(intervalRef); + intervalRef = null; + } +} + +function shouldRedraw(){ + return Bangle.isLCDOn(); +} + +function startTimers(){ + clearTimers(); + if (Bangle.isLCDOn()) { + intervalRef = setInterval(() => { + if (!shouldRedraw()) { + //console.log("draw clock callback - skipped redraw"); + } else { + draw_clock(); + } + }, 100 + ); + draw_clock(); + } else { + console.log("scheduleDrawClock - skipped not visible"); + } +} + + +Bangle.on('lcdPower', (on) => { + if (on) { + //console.log("lcdPower: on"); + startTimers(); + } else { + //console.log("lcdPower: off"); + clearTimers(); + } +}); + +Bangle.on('faceUp',function(up){ + //console.log("faceUp: " + up + " LCD: " + Bangle.isLCDOn()); + if (up && !Bangle.isLCDOn()) { + //console.log("faceUp and LCD off"); + clearTimers(); + Bangle.setLCDPower(true); + } +}); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +startTimers(); +Bangle.setUI("clock"); + + diff --git a/apps/matrixclock/matrixclock.png b/apps/matrixclock/matrixclock.png new file mode 100644 index 000000000..634253674 Binary files /dev/null and b/apps/matrixclock/matrixclock.png differ diff --git a/apps/matrixclock/screenshot_matrix.png b/apps/matrixclock/screenshot_matrix.png new file mode 100644 index 000000000..3d843848c Binary files /dev/null and b/apps/matrixclock/screenshot_matrix.png differ diff --git a/apps/mclock/ChangeLog b/apps/mclock/ChangeLog index cca1b6e6b..05b422406 100644 --- a/apps/mclock/ChangeLog +++ b/apps/mclock/ChangeLog @@ -4,3 +4,4 @@ 0.05: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast Fix issue where first digit could get stuck going from "2x:xx" to " x:xx" (fix #365) 0.06: Support 12 hour time +0.07: Use Bangle.setUI for button/launcher handling diff --git a/apps/mclock/bangle1-morphing-clock-screenshot.png b/apps/mclock/bangle1-morphing-clock-screenshot.png new file mode 100644 index 000000000..e8a6decaa Binary files /dev/null and b/apps/mclock/bangle1-morphing-clock-screenshot.png differ diff --git a/apps/mclock/clock-morphing.js b/apps/mclock/clock-morphing.js index 15ab206b9..f1254860b 100644 --- a/apps/mclock/clock-morphing.js +++ b/apps/mclock/clock-morphing.js @@ -216,5 +216,5 @@ Bangle.drawWidgets(); timeInterval = setInterval(showTime, 1000); showTime(); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); +// Show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/mclockplus/ChangeLog b/apps/mclockplus/ChangeLog index 835c33353..a1cecc698 100644 --- a/apps/mclockplus/ChangeLog +++ b/apps/mclockplus/ChangeLog @@ -1 +1,2 @@ -1.0: Created app +0.01: Created app +0.02: Use Bangle.setUI for button/launcher handling diff --git a/apps/mclockplus/mclockplus.app.js b/apps/mclockplus/mclockplus.app.js index 495e78f35..4c74ce1be 100644 --- a/apps/mclockplus/mclockplus.app.js +++ b/apps/mclockplus/mclockplus.app.js @@ -310,8 +310,8 @@ Bangle.drawWidgets(); timeInterval = setInterval(showTime, 1000); showTime(); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); +// Show launcher when button pressed +Bangle.setUI("clock"); // Start stopwatch when BTN3 is pressed setWatch(() => {swInterval=setInterval(stopWatch, 1000);stopWatch();}, BTN3, {repeat:false,edge:"falling"}); diff --git a/apps/menusmall/ChangeLog b/apps/menusmall/ChangeLog new file mode 100644 index 000000000..6de3d41f4 --- /dev/null +++ b/apps/menusmall/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: add `wrap` option, use Bangle.appRect \ No newline at end of file diff --git a/apps/menusmall/app.png b/apps/menusmall/app.png new file mode 100644 index 000000000..094ee447c Binary files /dev/null and b/apps/menusmall/app.png differ diff --git a/apps/menusmall/boot.js b/apps/menusmall/boot.js new file mode 100644 index 000000000..43c66089f --- /dev/null +++ b/apps/menusmall/boot.js @@ -0,0 +1,114 @@ +"";//not entirely sure why we need this - related to how bootupdate adds these to .boot0 +E.showMenu = function(items) { + g.clearRect(Bangle.appRect); // clear screen if no menu supplied + if (!items) { + Bangle.setUI(); + return; + } + + var menuItems = Object.keys(items); + var options = items[""]; + if (options) menuItems.splice(menuItems.indexOf(""),1); + if (!(options instanceof Object)) options = {}; + options.fontHeight = options.fontHeight|14; + if (options.selected === undefined) + options.selected = 0; + var ar = Bangle.appRect; + var x = ar.x; + var x2 = ar.x2; + var y = ar.y; + var y2 = ar.y2 - 11; // padding at end for arrow + if (options.title) + y += 15; + var loc = require("locale"); + var l = { + lastIdx : 0, + draw : function(rowmin,rowmax) { + var rows = 0|Math.min((y2-y) / options.fontHeight,menuItems.length); + var idx = E.clip(options.selected-(rows>>1),0,menuItems.length-rows); + if (idx!=l.lastIdx) rowmin=undefined; // redraw all if we scrolled + l.lastIdx = idx; + var iy = y; + g.reset().setFontAlign(0,-1,0); + g.setFontCustom(atob("AAAAAAAAAA/mAAAkAHAAAAEgA4AAAAAQATwDwDzwDwDyACAAAAOICIgREH/wRECIgI4AAAYGEhAkwDJgGSBCQwMAAAA8DoQiCEYQcyABgB6AAAkAHAAAAAfAMGCAIgAgAAgAiAIMGAfAAAAkADAB+ADAAkAAAAIABAAIAP4AIABAAIAAAABIAOAAABAAIABAAIABAAAAAGAAwAAAAQAMAGADABgAwAAAAP4CAghiEYQQEB/AAABAAQAEAA/+AAAQOEGQhCEQQcCAAAQEEAQhCEIQe8AAAAwAaAEQDCA/+ACAAAHwgiCEQQiCEPgAAD/giCEQQiCCPgAAEAAgeEMAmAHAAAAD3ghCEIQhCD3gAADwghCEIQhCD/gAABhgMMAAAMKBhgAAAIACgAiAIICAgAAAiAEQAiAEQAiAEQAAAQEBBAEQAUABAAAAQAEAAgmEIAiADgAAAD/ggCEcQkSEiQfwAAAH+DEAggDEAH+AAA/+EIQhCEIQe8AAAf8EAQgCEAQQEAAA/+EAQgCCAgP4AAA/+EIQhCEIQgCAAA/+EQAiAEQAgAAAAf8EAQgCEIQR8AAA/+AIABAAIA/+AAAgCH/wgCAAAgMEAQgCEAQ/8AAA/+AIACgBjAwGAAA/+AAQACAAQACAAA/+DAAGADAA/+AAA/+DAAGAAMA/+AAAf8EAQgCEAQf8AAA/+EIAhAEIAeAAAAf8EAQgKEAgf6AAA/+EIAhAEOAeOAAAcEEQQhCEEQQcAAAgAEAA/+EAAgAAAA/8AAQACAAQ/8AAA+AAPAAGAPA+AAAA/4AAwAYAMAAYAAw/4AAAwOBmADABmAwOAAA4AAwAB+AwA4AAAAgGEDQjiFgQwCAAA//EAIgBAAAwABgADAAGAAMAAQAAEAIgBH/4AAAgAYAEAAYAAgAAAAAQACAAQACAAQACAAAAAEAAQAAAACcAkQEiAkgD+AAA/+AQgECAgQD8AAAD8AgQECAgQCEAAAD8AgQECAQg/+AAAD8AkQEiAkQDkAAAEAD/wkAEgAkAAAADrAikEUgikHkggYAAH/wCAAgAEAAfwAAAAQECE/wACAAQAAAAIAAgAEEAk/4AAH/wAQAGADIAgwAAAAQgCH/wACAAQAAA/wEAA/wEAAfwAAA/wCAAgAEAAfwAAAfgECAgQECAfgAAA/8CEAgQECAfgAAAfgECAgQCEA/8AAA/wCAAgAEAAQAAAAYgEiAkQESARgAAAgA/8AgQECAgQAAA/gACAAQAEA/wAAA4AA4AAwA4A4AAAA/AAGAHAAGA/AAAAwwBIAGABIAwwAAA8GAbAAgAYA8AAAAgwEKAmQFCAwQAADk4jYkAEAAH/wAAEAEjYjk4AAAIACAAQABAAEAAgAIAAAA/wYgEEAYhg/yAAQAAAQH/4BBAQIABAAIAAC6AIgCCAQQCCAIgC6AAAH/4ABCAIgBAAIAADggiCEIQgiCDgAADYwkhESIhJDGwAADggiCEIQgiCDgAADggiCUIagiiDgAAEAAgAH/wgAEAAAAAgwEKCmQlCAwQAAAgwkKCmQlCAwQAAAgwEKCmQFCAwQAADAAkAEgAYAAAACcAkUEjQkiD+AAAAiEIQ/+AgQICAAAQAEAAAAAAIQBD/4QBEAIAAAYgUiEkQUSARgAAEAAQAEAAAAAYgkiCkQkSARgAAAYgEiQkaESgRgAAAQAf+AQICBFQIwAAAAEGAhQUyEoQGCAAAEGEhQUyEoQGCAAAEGAhQUyAoQGCAAA/+EIAhAEOAeOAAAH+DEAggDEAH+AAAH+DEAggDEAH+AAAH+DEAggDEAH+AAAH+DEAggDEAH+AAA/+AAQACAAQACAAAf8EAQgCEAQQEAAAf8EASgDUAUQEAAAf8EAQgCEAQQEAAA/+EIQhCEIQgCAAA/+EIUhDUISgCAAA/+EIQhCEIQgCAAA/+EIQhCEIQgCAAAgCH/wgCAAAgCH/wgCAAA/+EAQgCCAgP4AAA/+EIQhCCAgP4AAA/+DAAGAAMA/+AAA/+DAAGAAMA/+AAAf8EAQgCEAQf8AAAf8EAQgCEAQf8AAAf8EAQgCEAQf8AAAf8EAQgCEAQf8AAA/+EIAhAEOAeOAAAf+AAIgBAAIf+AAA/8AAQACAAQ/8AAA/8AAQACAAQ/8AAA/8AAQACAAQ/8AAA4AAwAB+AwA4AAAAgAEAC//kAAgAAAAf+EAAiCEQQdCAHgAAA/wCACgAkAAQAAAATgEiCkQkkAfwAAATgUiEkQUkAfwAAATgkiCkQkkAfwAAATgUiAkQUkAfwAAAAQgCH/wACAAQAAAfgECCgQkCAQgAAAfgECAgcECQQgAAAfgkCCgQkCAQgAAAfgEiCkQkiAcgAAAfgEiAkcEiQcgAAAfgUiAkQUiAcgAAAfgkiCkQkiAcgAAAAQECC/wgCAAQAAAAQUCE/wQCAAQAAAfwEBAgICCD/4gAAAAD8AgQECCQg/+CAAAAA/wCACgAkAAfwAAA/wiACgAkAAfwAAAfgECCgQkCAfgAAAfgUCEgQUCAfgAAAfgUCEgQUCEfgAAAfgUCAgQUCAfgAAA/wiACgAkAAQAAAAfwQBFAIQCAf4AAA/gACCAQgEA/wAAA/gQCEAQQEE/wAAA/gQCAAQQEA/wAAA8GAbCAggYA8AAAAgA/8AgSEDggQAAA"), 32, atob("AwIGCAgICAMFBQYIAwYDBwcFBgYHBgYGBgYDAwYHBgcHBgYGBgYGBgYEBgYGBgYGBgYGBgYGBggGBgYEBwQGBwQGBgYGBgYHBgYGBgYGBgYGBgYGBgYGBgYGBgQCBAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHCAYGBgAGBgYGAAYGBQYABgMGBgQABgYHBgAGBgYGBgYGBgYGBgYGBgYEBAYGBgYGBgYGAAYGBgYGBgYHBgYGBgYGBgYGBgYGBgYGBwcGBgYGBgYABgYGBgYGBg=="), 15); + + if (rowmin===undefined && options.title) + g.drawString(options.title,(x+x2)/2,y-14).drawLine(x,y-2,x2,y-2). + setColor(g.theme.fg).setBgColor(g.theme.bg); + iy += 12; + g.setColor((idx>0)?g.theme.fg:g.theme.bg).fillPoly([72,iy,104,iy,88,iy-12]); + if (rowmin!==undefined) { + if (idxrowmax) { + rows = 1+rowmax-rowmin; + } + } + while (rows--) { + var name = menuItems[idx]; + var item = items[name]; + var hl = (idx==options.selected && !l.selectEdit); + g.setColor(hl ? g.theme.bgH : g.theme.bg); + g.fillRect(x,iy,x2,iy+options.fontHeight-1); + g.setColor(hl ? g.theme.fgH : g.theme.fg); + g.setFontAlign(-1,-1); + g.drawString(loc.translate(name),x+1,iy+1); + if ("object" == typeof item) { + var xo = x2; + var v = item.value; + if (item.format) v=item.format(v); + v = loc.translate(""+v); + if (l.selectEdit && idx==options.selected) { + xo -= 24 + 1; + g.setColor(g.theme.bgH).fillRect(xo-(g.stringWidth(v)+4),iy,x2,iy+options.fontHeight-1); + g.setColor(g.theme.fgH).drawImage("\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@",xo,iy+(options.fontHeight-10)/2,{scale:2}); + } + g.setFontAlign(1,-1); + g.drawString(v,xo-2,iy+1); + } + g.setColor(g.theme.fg); + iy += options.fontHeight; + idx++; + } + g.setFontAlign(-1,-1); + g.setColor((idxitem.max) item.value = item.wrap ? item.min : item.max; + if (item.onchange) item.onchange(item.value); + l.draw(options.selected,options.selected); + } else { + var a=options.selected; + options.selected = (dir+options.selected+menuItems.length)%menuItems.length; + l.draw(Math.min(a,options.selected), Math.max(a,options.selected)); + } + } + }; + l.draw(); + Bangle.setUI("updown",dir => { + if (dir) l.move(dir); + else l.select(); + }); + return l; +}; diff --git a/apps/menuwheel/ChangeLog b/apps/menuwheel/ChangeLog new file mode 100644 index 000000000..defdb5049 --- /dev/null +++ b/apps/menuwheel/ChangeLog @@ -0,0 +1 @@ +0.01: New menu! diff --git a/apps/menuwheel/README.md b/apps/menuwheel/README.md new file mode 100644 index 000000000..22cb49466 --- /dev/null +++ b/apps/menuwheel/README.md @@ -0,0 +1,25 @@ +# Wheel Menu + +Replace Bangle.js 2's menus with a version that contains variable-size text and a back button. + +Bangle.js 1: +![Dark Mode Screenshot](screenshot_b1_dark.png) +![Light Mode Screenshot](screenshot_b1_light.png) + +Bangle.js 2: +![Dark Mode Screenshot](screenshot_b2_dark.png) +![Editing Screenshot](screenshot_b2_edit.png) +![Light Mode Screenshot](screenshot_b2_light.png) + + +## Features + +If the menu contains "Back" or "Exit", it is shown as a button instead. +The menu wraps around, with a divider between the last and first items. + +## Controls + +Bangle.js 1: Use BTN1/BTN3 to scroll through items, BTN2 to open/edit the selected item. +Bangle.js 2: Swipe up/down to scroll through items, tap/BTN to open/edit the selected item. + +Press the back button (if present) to go back. \ No newline at end of file diff --git a/apps/menuwheel/boot.js b/apps/menuwheel/boot.js new file mode 100644 index 000000000..3e708e9a8 --- /dev/null +++ b/apps/menuwheel/boot.js @@ -0,0 +1,213 @@ +E.showMenu = function(items) { + g.clearRect(Bangle.appRect); // clear screen if no menu supplied + // clean up back button listener + if (Bangle.backHandler) Bangle.removeListener('touch', Bangle.backHandler) + delete Bangle.backHandler; + if (!items) { + Bangle.setUI(); + return; + } + + var B2 = process.env.HWVERSION===2, + loc = require("locale"), + menuItems = Object.keys(items), + options = items[""]; + if (options) menuItems.splice(menuItems.indexOf(""),1); + if (!(options instanceof Object)) options = {}; + + // show "< Back" item (or similar) as button instead (i.e. remove from the menu) + var back,backLbl; + for (var b of ['Back', 'Exit', 'Cancel']) { + if (!items[b] && items['< '+b]) b = '< '+b; + back = items[b]; + if (typeof back === "function") { + backLbl = loc.translate(b); + menuItems.splice(menuItems.indexOf(b),1); + break; + } + else back = undefined; + } + // font sizes + var small = B2?15:22, + large = B2?30:45; + if (options.selected === undefined) options.selected = 0; + var ar = Bangle.appRect, + x = ar.x, + x2 = ar.x2, + w = ar.w, + y = ar.y, + y2 = ar.y2; + if (options.title) y += 22; + var wrap = menuItems.length>3; // don't wrap if all items are always in view anyway + + var vc=Math.round((y+y2)/2), // vertical center + hc = Math.round((x+x2)/2), // horizontal center + ih = large+small*2; // active item height + + var getItem = idx => { + // we wrap out-of-range indexes + while (idx<0) idx+=menuItems.length; + idx = idx%menuItems.length; + var name = menuItems[idx]; + var item = items[name]; + var v; + if ("object"== typeof item) { + v = item.value; + if (item.format) v = item.format(v); + v = loc.translate(""+v); + } + return {lbl: loc.translate(name), v: v}; + }; + var l = { + lastIdx : null, // we want a complete redraw on first run + draw : function() { + var idx = options.selected, + edit = l.selectEdit; + g.reset(); + + // don't highlight whole item when editing + g.setColor(edit?g.theme.fg:g.theme.fgH) + .setBgColor(edit?g.theme.bg:g.theme.bgH) + .setFont('Vector', large); + var item = getItem(idx), + lw = g.stringWidth(item.lbl)+2; + if (lw+2 >= w) { // label width doesn't fit at large size: scale it down + g.setFont('Vector', Math.floor(large*ar.w/lw)); + } + g.clearRect(x,vc-ih/2,x2,vc+ih/2) + .setFontAlign(0,0,0).drawString(item.lbl,hc,vc); + + if (item.v !== undefined) { + g.setColor(g.theme.fgH).setBgColor(g.theme.bgH) // always highlighted: either as part of item, or while editing + .setFontAlign(0,1,0) + .setFont('Vector', small) + .clearRect(x,vc+ih/2-small-2,x2,vc+ih/2) + .drawString(item.v,hc,vc+ih/2-1); + if (edit) { + g.drawImage("\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@",x2-23,vc+ih/2-small+(B2?1:5),{scale:2}); + } + } + if (l.lastIdx !== idx) { + // we scrolled: redraw all + l.lastIdx=idx; + g.reset(); + + if (options.title) { + if (B2) g.setFont('12x20'); + else g.setFont('6x8',2); + g.drawLine(x, y-2, x2, y-2) + .setFontAlign(0,1,0) + .drawString(options.title, (x+x2)/2, y-2); + } + + // clear prev/next items area + g.clearRect(x,y,x2,vc-ih/2-1) + .clearRect(x,vc+ih/2+1,x2,y2); + + // get display label by index + var lbl = idx => { + var item = getItem(idx); + if (item.v !== undefined) item.lbl+=': '+item.v; + return item.lbl; + } + // previous two items + g.setFontAlign(0, 1) + if (wrap||idx>0) g.setFont('Vector', small).drawString(lbl(idx-1), hc, vc-ih/2-5); + if (wrap||idx>1) g.setFont('Vector', small/2).drawString(lbl(idx-2), hc, vc-ih/2-small-10); + // next two items + g.setFontAlign(0, -1); + if (wrap||idx g.drawLine(x, y, x2, y); + if (idx===0) div(vc-ih/2-1); + if (idx===1) div(vc-ih/2-small-8); + // if (s === 2) div(vc-ih/2-small*1.5-13); + if (idx===menuItems.length-1) div(vc+ih/2+1); + if (idx===menuItems.length-2) div(vc+ih/2+small+6); + // if (s === 2) div(vc+ih/2+small*1.5+13); + } + + if (back) { + g.setBgColor(g.theme.bg2) + .setFont('Vector', small); + var bw=g.stringWidth(backLbl); + g.clearRect(x,y, x+bw+2, y+small+2); + var bx1=x, by1=y, bx2=x+bw+2, by2=y+small+2; + // g.drawRect(x,y, x+bw+2, y+small+2); + var poly = [ // button outline + bx1+2,by1, + bx2-2,by1, + bx2, by1+2, + bx2, by2-2, + bx2-2,by2, + bx1+2,by2, + bx1, by2-2, + bx1, by1+2, + ] + g.setColor(g.theme.bg2).fillPoly(poly, true) + .setColor(g.theme.fg2).drawPoly(poly, true) + .setFontAlign(-1,-1,0).drawString(backLbl, x+2,y+2); + } + } + g.flip(); + }, + select : function() { // same as default menu + var item = items[menuItems[options.selected]]; + if ("function" == typeof item) {l.lastIdx=null; item(l);} // force a redraw after callback + else if ("object" == typeof item) { + // if a number, go into 'edit mode' + if ("number" == typeof item.value) + l.selectEdit = l.selectEdit?undefined:item; + else { // else just toggle bools + if ("boolean" == typeof item.value) item.value=!item.value; + if (item.onchange) {l.lastIdx=null; item.onchange(item.value);} // force a redraw after callback + } + l.draw(); + } + }, + move : function(dir) { + if (l.selectEdit) { // same as default menu + var item = l.selectEdit; + item.value -= (dir||1)*(item.step||1); + if (item.min!==undefined && item.valueitem.max) item.value = item.wrap ? item.min : item.max; + if (item.onchange) {l.lastIdx=null; item.onchange(item.value);} // force a redraw after callback + } else { + if (B2) dir=-dir; // swipe vs button scrolling + if (!wrap && (options.selected+dir<0 || options.selected+dir>=menuItems.length)) { + return; + } + options.selected = (options.selected+dir+menuItems.length)%menuItems.length; + } + l.draw(); + } + }; + l.draw(); + Bangle.setUI("updown",dir => { + if (dir) l.move(dir); + else l.select(); + }); + if (back) { + // we have a back button: check touches before passing them to setUI's touchHandler + if (B2) { + Bangle.removeListener('touch', Bangle.touchHandler); + Bangle.backHandler = (b, xy) => { + // anywhere top-left (but above the active item) = back button + if (xy.x { + // left side = back button + if (b===1) back(); + } + } + // note: backHandler is cleaned up at the top of this file + Bangle.on('touch', Bangle.backHandler); + } + return l; +}; diff --git a/apps/menuwheel/icon.png b/apps/menuwheel/icon.png new file mode 100644 index 000000000..61f94a035 Binary files /dev/null and b/apps/menuwheel/icon.png differ diff --git a/apps/menuwheel/screenshot_b1_dark.png b/apps/menuwheel/screenshot_b1_dark.png new file mode 100644 index 000000000..c6dfb802b Binary files /dev/null and b/apps/menuwheel/screenshot_b1_dark.png differ diff --git a/apps/menuwheel/screenshot_b1_edit.png b/apps/menuwheel/screenshot_b1_edit.png new file mode 100644 index 000000000..a39b0a832 Binary files /dev/null and b/apps/menuwheel/screenshot_b1_edit.png differ diff --git a/apps/menuwheel/screenshot_b1_light.png b/apps/menuwheel/screenshot_b1_light.png new file mode 100644 index 000000000..35ac01fe9 Binary files /dev/null and b/apps/menuwheel/screenshot_b1_light.png differ diff --git a/apps/menuwheel/screenshot_b2_dark.png b/apps/menuwheel/screenshot_b2_dark.png new file mode 100644 index 000000000..1393838a3 Binary files /dev/null and b/apps/menuwheel/screenshot_b2_dark.png differ diff --git a/apps/menuwheel/screenshot_b2_edit.png b/apps/menuwheel/screenshot_b2_edit.png new file mode 100644 index 000000000..bca98a9a5 Binary files /dev/null and b/apps/menuwheel/screenshot_b2_edit.png differ diff --git a/apps/menuwheel/screenshot_b2_light.png b/apps/menuwheel/screenshot_b2_light.png new file mode 100644 index 000000000..4ffe08fe3 Binary files /dev/null and b/apps/menuwheel/screenshot_b2_light.png differ diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog new file mode 100644 index 000000000..94848a26c --- /dev/null +++ b/apps/messages/ChangeLog @@ -0,0 +1,23 @@ +0.01: New App! +0.02: Add 'messages' library +0.03: Fixes for Bangle.js 1 +0.04: Add require("messages").clearAll() +0.05: Handling of message actions (ok/clear) +0.06: New messages now go at the start (fix #898) + Answering true/false now exits the messages app if no new messages + Back now marks a message as read + Clicking top-left opens a menu which allows you to delete a message or mark unread +0.07: Added settings menu with option to choose vibrate pattern and frequency (fix #909) +0.08: Fix rendering of long messages (fix #969) + buzz on new message (fix #999) +0.09: Message now disappears after 60s if no action taken and clock loads (fix 922) + Fix phone icon (#1014) +0.10: Respect the 'new' attribute if it was set from iOS integrations +0.11: Open app when touching the widget (Bangle.js 2 only) +0.12: Extra app-specific notification icons + New animated notifcationicon (instead of large blinking 'MESSAGES') + Added screenshots +0.13: Add /*LANG*/ comments for internationalisation + Add 'Delete All' option to message options + Now update correctly when 'require("messages").clearAll()' is called +0.14: Hide widget when all unread notifications are dismissed from phone diff --git a/apps/messages/README.md b/apps/messages/README.md new file mode 100644 index 000000000..4952b1877 --- /dev/null +++ b/apps/messages/README.md @@ -0,0 +1,45 @@ +# Messages app + +This app handles the display of messages and message notifications. It stores +a list of currently received messages and allows them to be listed, viewed, +and responded to. + +It is a replacement for the old `notify`/`gadgetbridge` apps. + +## Settings + +You can change settings by going to the global `Settings` app, then `App Settings` +and `Messages`: + +* `Vibrate` - This is the pattern of buzzes that should be made when a new message is received +* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds +* `Unread Timer` - when a new message is received we go into the Messages app. +If there is no user input for this amount of time then the app will exit and return +to the clock where a ringing bell will be shown in the Widget bar. + +## Images +_1. Screenshot of a notification_ + +![](screenshot.png) + +_2. What the notify icon looks like (it's touchable on Bangle.js2!)_ + +![](screenshot-notify.gif) + + + +## Requests + +Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=messages%20app + +## Creator + +Gordon Williams + +## Contributors + +[Jeroen Peters](https://github.com/jeroenpeters1986) + +## Attributions + +Icons used in this app are from https://icons8.com diff --git a/apps/messages/app-icon.js b/apps/messages/app-icon.js new file mode 100644 index 000000000..6ed3c1141 --- /dev/null +++ b/apps/messages/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///rkcAYP9ohL/ABMBqoAEoALDioLFqgLDBQoABERIkEBZcFBY9QBed61QAC1oLF7wLD24LF24LD7wLF1vqBQOrvQLFA4IuC9QLFD4IuC1QLGGAQOBBYwgBEwQLHvQBBEZHVq4jI7wWBHY5TLNZaDLTZazLffMBBY9ABZsABY4KCgEVBQtUBYYkGEQYA/AAwA=")) diff --git a/apps/messages/app.js b/apps/messages/app.js new file mode 100644 index 000000000..79009e77e --- /dev/null +++ b/apps/messages/app.js @@ -0,0 +1,354 @@ +/* MESSAGES is a list of: + {id:int, + src, + title, + subject, + body, + sender, + tel:string, + new:true // not read yet + } +*/ + +/* For example for maps: + +// a message +{"t":"add","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"} +// maps +{"t":"add","id":1,"src":"Maps","title":"0 yd - High St","body":"Campton - 11:48 ETA","img":"GhqBAAAMAAAHgAAD8AAB/gAA/8AAf/gAP/8AH//gD/98B//Pg/4B8f8Afv+PP//n3/f5//j+f/wfn/4D5/8Aef+AD//AAf/gAD/wAAf4AAD8AAAeAAADAAA="} +// call +{"t":"add","id":"call","src":"Phone","name":"Bob","number":"12421312",positive:true,negative:true} +*/ + +var Layout = require("Layout"); +var fontSmall = "6x8"; +var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2"; +var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; +var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; +var colBg = g.theme.dark ? "#141":"#4f4"; +var colSBg1 = g.theme.dark ? "#121":"#cFc"; +var colSBg2 = g.theme.dark ? "#242":"#9F9"; +// hack for 2v10 firmware's lack of ':size' font handling +try { + g.setFont("6x8:2"); +} catch (e) { + g._setFont = g.setFont; + g.setFont = function(f,s) { + if (f.includes(":")) { + f = f.split(":"); + return g._setFont(f[0],f[1]); + } + return g._setFont(f,s); + }; +} + +/** this is a timeout if the app has started and is showing a single message +but the user hasn't seen it (eg no user input) - in which case +we should start a timeout for settings.unreadTimeout to return +to the clock. */ +var unreadTimeout; +/// List of all our messages +var MESSAGES = require("Storage").readJSON("messages.json",1)||[]; +if (!Array.isArray(MESSAGES)) MESSAGES=[]; +var onMessagesModified = function(msg) { + // TODO: if new, show this new one + if (msg && msg.new) { + if (WIDGETS["messages"]) WIDGETS["messages"].buzz(); + else Bangle.buzz(); + } + showMessage(msg&&msg.id); +}; +function saveMessages() { + require("Storage").writeJSON("messages.json",MESSAGES) +} + +function getBackImage() { + return atob("FhYBAAAAEAAAwAAHAAA//wH//wf//g///BwB+DAB4EAHwAAPAAA8AADwAAPAAB4AAHgAB+AH/wA/+AD/wAH8AA=="); +} +function getNotificationImage() { + return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A=="); +} +function getFBIcon() { + return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA=="); +} +function getPosImage() { + return atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="); +} +function getNegImage() { + return atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="); +} +function getMessageImage(msg) { + if (msg.img) return atob(msg.img); + var s = (msg.src||"").toLowerCase(); + if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); + if (s=="facebook") return getFBIcon(); + if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA="); + if (s=="instagram") return atob("GBiBAf////////////////wAP/n/n/P/z/f/b/eB7/c87/d+7/d+7/d+7/d+7/c87/eB7/f/7/P/z/n/n/wAP////////////////w=="); + if (s=="gmail") return getNotificationImage(); + if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); + if (s=="mail") return getNotificationImage(); + if (s=="messenger") return getFBIcon(); + if (s=="outlook mail") return getNotificationImage(); + if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA="); + if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA=="); + if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA=="); + if (s=="sms message") return getNotificationImage(); + if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA"); + if (s=="telegram") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA=="); + if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA=="); + if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql"); + if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A="); + if (msg.id=="back") return getBackImage(); + return getNotificationImage(); +} + +function showMapMessage(msg) { + var m; + var distance, street, target, eta; + m=msg.title.match(/(.*) - (.*)/); + if (m) { + distance = m[1]; + street = m[2]; + } else street=msg.title; + m=msg.body.match(/(.*) - (.*)/); + if (m) { + target = m[1]; + eta = m[2]; + } else target=msg.body; + layout = new Layout({ type:"v", c: [ + {type:"txt", font:fontMedium, label:target, bgCol:colBg, fillx:1, pad:2 }, + {type:"h", bgCol:colBg, fillx:1, c: [ + {type:"txt", font:"6x8", label:"Towards" }, + {type:"txt", font:fontLarge, label:street } + ]}, + {type:"h",fillx:1, filly:1, c: [ + msg.img?{type:"img",src:atob(msg.img), scale:2}:{}, + {type:"v", fillx:1, c: [ + {type:"txt", font:fontLarge, label:distance||"" } + ]}, + ]}, + {type:"txt", font:"6x8:2", label:eta } + ]}); + g.clearRect(Bangle.appRect); + layout.render(); + Bangle.setUI("updown",function() { + // any input to mark as not new and return to menu + msg.new = false; + saveMessages(); + layout = undefined; + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + }); +} + +function showMusicMessage(msg) { + function fmtTime(s) { + var m = Math.floor(s/60); + s = (parseInt(s%60)).toString().padStart(2,0); + return m+":"+s; + } + + function back() { + msg.new = false; + saveMessages(); + layout = undefined; + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + } + layout = new Layout({ type:"v", c: [ + {type:"h", fillx:1, bgCol:colBg, c: [ + { type:"btn", src:getBackImage, cb:back }, + { type:"v", fillx:1, c: [ + { type:"txt", font:fontMedium, label:msg.artist, pad:2 }, + { type:"txt", font:fontMedium, label:msg.album, pad:2 } + ]} + ]}, + {type:"txt", font:fontLarge, label:msg.track, fillx:1, filly:1, pad:2 }, + Bangle.musicControl?{type:"h",fillx:1, c: [ + {type:"btn", pad:8, label:"\0"+atob("FhgBwAADwAAPwAA/wAD/gAP/gA//gD//gP//g///j///P//////////P//4//+D//gP/4A/+AD/gAP8AA/AADwAAMAAA"), cb:()=>Bangle.musicControl("play")}, // play + {type:"btn", pad:8, label:"\0"+atob("EhaBAHgHvwP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP3gHg"), cb:()=>Bangle.musicControl("pause")}, // pause + {type:"btn", pad:8, label:"\0"+atob("EhKBAMAB+AB/gB/wB/8B/+B//B//x//5//5//x//B/+B/8B/wB/gB+AB8ABw"), cb:()=>Bangle.musicControl("next")}, // next + ]}:{}, + {type:"txt", font:"6x8:2", label:msg.dur?fmtTime(msg.dur):"--:--" } + ]}); + g.clearRect(Bangle.appRect); + layout.render(); +} + +function showMessageSettings(msg) { + E.showMenu({"":{"title":/*LANG*/"Message"}, + "< Back" : () => showMessage(msg.id), + /*LANG*/"Delete" : () => { + MESSAGES = MESSAGES.filter(m=>m.id!=msg.id); + saveMessages(); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); + }, + /*LANG*/"Mark Unread" : () => { + msg.new = true; + saveMessages(); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); + }, + /*LANG*/"Delete all messages" : () => { + E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => { + if (isYes) { + MESSAGES = []; + saveMessages(); + } + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); + }); + }, + }); +} + +function showMessage(msgid) { + var msg = MESSAGES.find(m=>m.id==msgid); + if (!msg) return checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); // go home if no message found + if (msg.src=="Maps") { + cancelReloadTimeout(); // don't auto-reload to clock now + return showMapMessage(msg); + } + if (msg.id=="music") { + cancelReloadTimeout(); // don't auto-reload to clock now + return showMusicMessage(msg); + } + // Normal text message display + var title=msg.title, titleFont = fontLarge, lines; + if (title) { + var w = g.getWidth()-48; + if (g.setFont(titleFont).stringWidth(title) > w) + titleFont = fontMedium; + if (g.setFont(titleFont).stringWidth(title) > w) { + lines = g.wrapString(title, w); + title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n"); + } + } + var buttons = [ + {type:"btn", src:getBackImage(), cb:()=>{ + msg.new = false; saveMessages(); // read mail + cancelReloadTimeout(); // don't auto-reload to clock now + checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:1}); + }} // back + ]; + if (msg.positive) { + buttons.push({type:"btn", src:getPosImage(), cb:()=>{ + msg.new = false; saveMessages(); + cancelReloadTimeout(); // don't auto-reload to clock now + Bangle.messageResponse(msg,true); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + }}); + } + if (msg.negative) { + buttons.push({type:"btn", src:getNegImage(), cb:()=>{ + msg.new = false; saveMessages(); + cancelReloadTimeout(); // don't auto-reload to clock now + Bangle.messageResponse(msg,false); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + }}); + } + lines = g.wrapString(msg.body, g.getWidth()-10); + var body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n"); + layout = new Layout({ type:"v", c: [ + {type:"h", fillx:1, bgCol:colBg, c: [ + { type:"btn", src:getMessageImage(msg), pad: 3, cb:()=>{ + cancelReloadTimeout(); // don't auto-reload to clock now + showMessageSettings(msg); + }}, + { type:"v", fillx:1, c: [ + {type:"txt", font:fontSmall, label:msg.src||"Message", bgCol:colBg, fillx:1, pad:2, halign:1 }, + title?{type:"txt", font:titleFont, label:title, bgCol:colBg, fillx:1, pad:2 }:{}, + ]}, + ]}, + {type:"txt", font:fontMedium, label:body, fillx:1, filly:1, pad:2 }, + {type:"h",fillx:1, c: buttons} + ]}); + g.clearRect(Bangle.appRect); + layout.render(); +} + + +/* options = { + clockIfNoMsg : bool + clockIfAllRead : bool + showMsgIfUnread : bool +} +*/ +function checkMessages(options) { + options=options||{}; + // If no messages, just show 'no messages' and return + if (!MESSAGES.length) { + if (!options.clockIfNoMsg) return E.showPrompt(/*LANG*/"No Messages",{ + title:/*LANG*/"Messages", + img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")), + buttons : {/*LANG*/"Ok":1} + }).then(() => { load() }); + return load(); + } + // we have >0 messages + var newMessages = MESSAGES.filter(m=>m.new); + // If we have a new message, show it + if (options.showMsgIfUnread && newMessages.length) + return showMessage(newMessages[0].id); + // no new messages - go to clock? + if (options.clockIfAllRead && newMessages.length==0) + return load(); + // we don't have to time out of this screen... + cancelReloadTimeout(); + // Otherwise show a menu + E.showScroller({ + h : 48, + c : Math.max(MESSAGES.length+1,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11) + draw : function(idx, r) {"ram" + var msg = MESSAGES[idx-1]; + if (msg && msg.new) g.setBgColor(colBg); + else g.setBgColor((idx&1) ? colSBg1 : colSBg2); + g.clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1).setColor(g.theme.fg); + if (idx==0) msg = {id:"back", title:"< Back"}; + if (!msg) return; + var x = r.x+2, title = msg.title, body = msg.body; + var img = getMessageImage(msg); + if (msg.id=="music") { + title = msg.artist || /*LANG*/"Music"; + body = msg.track; + } + if (img) { + g.drawImage(img, x+24, r.y+24, {rotate:0}); // force centering + x += 50; + } + var m = msg.title+"\n"+msg.body; + if (msg.src) g.setFontAlign(1,1).setFont("6x8").drawString(msg.src, r.x+r.w-2, r.y+r.h-2); + if (title) g.setFontAlign(-1,-1).setFont(fontBig).drawString(title, x,r.y+2); + if (body) { + g.setFontAlign(-1,-1).setFont("6x8"); + var l = g.wrapString(body, r.w-14); + if (l.length>3) { + l = l.slice(0,3); + l[l.length-1]+="..."; + } + g.drawString(l.join("\n"), x+10,r.y+20); + } + }, + select : idx => { + if (idx==0) load(); + else showMessage(MESSAGES[idx-1].id); + } + }); +} + +function cancelReloadTimeout() { + if (!unreadTimeout) return; + clearTimeout(unreadTimeout); + unreadTimeout = undefined; +} + + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +setTimeout(() => { + var unreadTimeoutSecs = (require('Storage').readJSON("messages.settings.json", true) || {}).unreadTimeout; + if (unreadTimeoutSecs===undefined) unreadTimeoutSecs=60; + if (unreadTimeoutSecs) + unreadTimeout = setTimeout(function() { + print("Message not seen - reloading"); + load(); + }, unreadTimeoutSecs*1000); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:1}); +},10); // if checkMessages wants to 'load', do that diff --git a/apps/messages/app.png b/apps/messages/app.png new file mode 100644 index 000000000..c9177692e Binary files /dev/null and b/apps/messages/app.png differ diff --git a/apps/messages/lib.js b/apps/messages/lib.js new file mode 100644 index 000000000..b3cb7d9d4 --- /dev/null +++ b/apps/messages/lib.js @@ -0,0 +1,73 @@ +/* Push a new message onto messages queue, event is: + {t:"add",id:int, src,title,subject,body,sender,tel, important:bool, new:bool} + {t:"add",id:int, id:"music", state, artist, track, etc} // add new + {t:"remove-",id:int} // remove + {t:"modify",id:int, title:string} // modified +*/ +exports.pushMessage = function(event) { + var messages, inApp = "undefined"!=typeof MESSAGES; + if (inApp) + messages = MESSAGES; // we're in an app that has already loaded messages + else // no app - load messages + messages = require("Storage").readJSON("messages.json",1)||[]; + // now modify/delete as appropriate + var mIdx = messages.findIndex(m=>m.id==event.id); + if (event.t=="remove") { + if (mIdx>=0) messages.splice(mIdx, 1); // remove item + mIdx=-1; + } else { // add/modify + if (event.t=="add"){ + if(event.new === undefined ) { // If 'new' has not been set yet, set it + event.new=true; // Assume it should be new + } + } + if (mIdx<0) { + mIdx=0; + messages.unshift(event); // add new messages to the beginning + } + else Object.assign(messages[mIdx], event); + } + require("Storage").writeJSON("messages.json",messages); + // if in app, process immediately + if (inApp) return onMessagesModified(mIdx<0 ? {id:event.id} : messages[mIdx]); + // if we've removed the last new message, hide the widget + if (event.t=="remove" && !messages.some(m=>m.new)) { + if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.hide(); + } + // ok, saved now - we only care if it's new + if (event.t!="add") { + return; + } else if(event.new == false) { + return; + } + // otherwise load messages/show widget + var loadMessages = Bangle.CLOCK || event.important; + // first, buzz + if (loadMessages && global.WIDGETS && WIDGETS.messages) + WIDGETS.messages.buzz(); + // after a delay load the app, to ensure we have all the messages + if (exports.messageTimeout) clearTimeout(exports.messageTimeout); + exports.messageTimeout = setTimeout(function() { + exports.messageTimeout = undefined; + // if we're in a clock or it's important, go straight to messages app + if (loadMessages) return load("messages.app.js"); + if (!global.WIDGETS || !WIDGETS.messages) return Bangle.buzz(); // no widgets - just buzz to let someone know + WIDGETS.messages.show(); + }, 500); +} +/// Remove all messages +exports.clearAll = function(event) { + var messages, inApp = "undefined"!=typeof MESSAGES; + if (inApp) { + MESSAGES = []; + messages = MESSAGES; // we're in an app that has already loaded messages + } else // no app - empty messages + messages = []; + // Save all messages + require("Storage").writeJSON("messages.json",messages); + // update app if in app + if (inApp) return onMessagesModified(); + // if we have a widget, update it + if (global.WIDGETS && WIDGETS.messages) + WIDGETS.messages.hide(); +} diff --git a/apps/messages/screenshot-notify.gif b/apps/messages/screenshot-notify.gif new file mode 100644 index 000000000..3d0ed0b32 Binary files /dev/null and b/apps/messages/screenshot-notify.gif differ diff --git a/apps/messages/screenshot.png b/apps/messages/screenshot.png new file mode 100644 index 000000000..a95045400 Binary files /dev/null and b/apps/messages/screenshot.png differ diff --git a/apps/messages/settings.js b/apps/messages/settings.js new file mode 100644 index 000000000..fd8ce8f39 --- /dev/null +++ b/apps/messages/settings.js @@ -0,0 +1,42 @@ +(function(back) { + function settings() { + let settings = require('Storage').readJSON("messages.settings.json", true) || {}; + if (settings.vibrate===undefined) settings.vibrate="."; + if (settings.repeat===undefined) settings.repeat=4; + if (settings.unreadTimeout===undefined) settings.unreadTimeout=60; + return settings; + } + function updateSetting(setting, value) { + let settings = require('Storage').readJSON("messages.settings.json", true) || {}; + settings[setting] = value; + require('Storage').writeJSON("messages.settings.json", settings); + } + + var vibPatterns = ["Off", ".", "-", "--", "-.-", "---"]; + var currentVib = settings().vibrate; + var mainmenu = { + "" : { "title" : "Messages" }, + "< Back" : back, + 'Vibrate': { + value: Math.max(0,vibPatterns.indexOf(settings().vibrate)), + min: 0, max: vibPatterns.length, + format: v => vibPatterns[v]||"Off", + onchange: v => { + updateSetting("vibrate", vibPatterns[v]); + } + }, + 'Repeat': { + value: settings().repeat, + min: 2, max: 10, + format: v => v+"s", + onchange: v => updateSetting("repeat", v) + }, + 'Unread timer': { + value: settings().unreadTimeout, + min: 0, max: 240, step : 10, + format: v => v?v+"s":"Off", + onchange: v => updateSetting("unreadTimeout", v) + }, + }; + E.showMenu(mainmenu); +}) diff --git a/apps/messages/widget.js b/apps/messages/widget.js new file mode 100644 index 000000000..f01d22ec7 --- /dev/null +++ b/apps/messages/widget.js @@ -0,0 +1,48 @@ +WIDGETS["messages"]={area:"tl",width:0,draw:function() { + Bangle.removeListener('touch', this.touch); + if (!this.width) return; + var c = (Date.now()-this.t)/1000; + g.reset().clearRect(this.x,this.y,this.x+this.width,this.y+23); + g.drawImage((c&1) ? atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA==") : atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+D///D///A//8CP/xDj/HD48DD+B8D/D+D/3vD/vvj/vvj/vvj/vvh/v/gfnvAAD+AAB8AAAAA=="), this.x, this.y); + //if (c<60) Bangle.setLCDPower(1); // keep LCD on for 1 minute + let settings = require('Storage').readJSON("messages.settings.json", true) || {}; + if (settings.repeat===undefined) settings.repeat = 4; + if (c<120 && (Date.now()-this.l)>settings.repeat*1000) { + this.l = Date.now(); + WIDGETS["messages"].buzz(); // buzz every 4 seconds + } + setTimeout(()=>WIDGETS["messages"].draw(), 1000); + if (process.env.HWVERSION>1) Bangle.on('touch', this.touch); +},show:function(quiet) { + WIDGETS["messages"].t=Date.now(); // first time + WIDGETS["messages"].l=Date.now()-10000; // last buzz + if (quiet) WIDGETS["messages"].t -= 500000; // if quiet, set last time in the past so there is no buzzing + WIDGETS["messages"].width=64; + Bangle.drawWidgets(); + Bangle.setLCDPower(1);// turns screen on +},hide:function() { + delete WIDGETS["messages"].t; + delete WIDGETS["messages"].l; + WIDGETS["messages"].width=0; + Bangle.drawWidgets(); +},buzz:function() { + let v = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrate || "."; + function b() { + var c = v[0]; + v = v.substr(1); + if (c==".") Bangle.buzz().then(()=>setTimeout(b,100)); + if (c=="-") Bangle.buzz(500).then(()=>setTimeout(b,100)); + } + b(); +},touch:function(b,c) { + var w=WIDGETS["messages"]; + if (!w||!w.width||c.xw.x+w.width||c.yw.y+23) return; + load("messages.app.js"); +}}; +/* We might have returned here if we were in the Messages app for a +message but then the watch was never viewed. In that case we don't +want to buzz but should still show that there are unread messages. */ +if (global.MESSAGES===undefined) (function() { + var messages = require("Storage").readJSON("messages.json",1)||[]; + if (messages.some(m=>m.new)) WIDGETS["messages"].show(true); +})(); diff --git a/apps/metronome/ChangeLog b/apps/metronome/ChangeLog index 894d62940..9bd33ca4e 100644 --- a/apps/metronome/ChangeLog +++ b/apps/metronome/ChangeLog @@ -4,3 +4,4 @@ 0.04: App shows instructions, Widgets remain visible, color changed 0.05: Buzz intensity and beats per bar can be changed via settings-app 0.06: Correct string position +0.07: Add support for Bangle.sjs2 \ No newline at end of file diff --git a/apps/metronome/README.md b/apps/metronome/README.md index f67b4adf1..05bd62a96 100644 --- a/apps/metronome/README.md +++ b/apps/metronome/README.md @@ -4,11 +4,12 @@ This metronome makes your watch blink and vibrate with a given rate. ## Usage -* Tap the screen at least three times. The app calculates the mean rate of your tapping. This rate is displayed in bmp while the text blinks and the watch softly vibrates with every beat. -* Use `BTN1` to increase the bmp value by one. -* Use `BTN3` to decrease the bmp value by one. +* Tap the screen at least three times. The app calculates the mean rate of your tapping. This rate is displayed in bpm while the text blinks and the watch softly vibrates with every beat. +* Use `BTN1` to increase the bpm value by one. +* Use `BTN3` to decrease the bpm value by one. * You can change the bpm value any time by tapping the screen or using `BTN1` and `BTN3`. * Intensity of buzzing and the beats per bar (default 4) can be changed with the settings-app. The first beat per bar will be marked in red. +* On Bangle.js 2 tapping the center of the screen initiates bpm. in- or decreasing bpm can by 1 can be done by tapping left or right site of the screen. ## Attributions diff --git a/apps/metronome/bangle1-metronome-screenshot.png b/apps/metronome/bangle1-metronome-screenshot.png new file mode 100644 index 000000000..1d684235d Binary files /dev/null and b/apps/metronome/bangle1-metronome-screenshot.png differ diff --git a/apps/metronome/metronome.js b/apps/metronome/metronome.js index e5e45559e..ffcaa1cfb 100644 --- a/apps/metronome/metronome.js +++ b/apps/metronome/metronome.js @@ -3,10 +3,9 @@ var cindex=0; // index to iterate through colous var bpm=60; // ininital bpm value var time_diffs = [1000, 1000, 1000]; //array to calculate mean bpm var tindex=0; //index to iterate through time_diffs - - -Bangle.setLCDTimeout(undefined); //do not deaktivate display while running this app - +// set background colour +g.setTheme({bg:"#000"}); +Bangle.setLCDTimeout(undefined); //do not deactivate display while running this app const storage = require("Storage"); const SETTINGS_FILE = 'metronome.settings.json'; @@ -15,7 +14,7 @@ function setting(key) { //define default settings const DEFAULTS = { 'beatsperbar': 4, - 'buzzintens': 0.75, + 'buzzintens': 1.0, }; if (!settings) { loadSettings(); } return (key in settings) ? settings[key] : DEFAULTS[key]; @@ -40,6 +39,10 @@ function changecolor() { 7: { value: 0xFFFF, name: "White" }, }; g.setColor(colors[cindex].value); + if ((process.env.HWVERSION==2 )) { + g.drawLine(39,0,39,g.getWidth()/3); + g.drawLine(136,0,136,g.getWidth()/3); + } if (cindex == setting('beatsperbar')-1) { cindex = 0; } @@ -50,43 +53,73 @@ function changecolor() { } function updateScreen() { - g.reset().clearRect(0, 50, 250, 150); + g.reset().clearRect(0, 50, 250, 120); changecolor(); try { Bangle.buzz(50, setting('buzzintens')); } catch(err) { } g.setFont("Vector",40).setFontAlign(0,0); - g.drawString(Math.floor(bpm)+"bpm", g.getWidth()/2, 100); + g.drawString(Math.floor(bpm)+"bpm", g.getWidth()/2, g.getWidth()/2); } -Bangle.on('touch', function(button) { -// setting bpm by tapping the screen. Uses the mean time difference between several tappings. - if (tindex < time_diffs.length) { - if (Date.now()-tStart < 5000) { - time_diffs[tindex] = Date.now()-tStart; - } - } else { - tindex=0; - time_diffs[tindex] = Date.now()-tStart; - } - tindex += 1; - mean_time = 0.0; - for(count = 0; count < time_diffs.length; count++) { - mean_time += time_diffs[count]; - } - time_diff = mean_time/count; +//Write user instructuins to screen +function printInstructions() { + g.clear(1).setFont("4x6"); + g.setColor(-1); //set color to white + g.drawString('Drum the beat on the center\nof the screen to set tempo.', 30, g.getWidth()/3*2+15); + if(process.env.HWVERSION==1) { + g.drawString('Use BTN1 to increase, and\nBTN3 to decrease bpm value by 1.', 30, g.getWidth()/3*2+30); + } + else { + g.drawString('Touch left part of the screen\nto decrease, or the right site\nto increase bpm value by 1.', 30, g.getWidth()/3*2+30); + } +} - tStart = Date.now(); - clearInterval(time_diff); - bpm = (60 * 1000/(time_diff)); - updateScreen(); - clearInterval(interval); - interval = setInterval(updateScreen, 60000 / bpm); - return bpm; +Bangle.on('touch', function(zone, e) { +// setting bpm by tapping the screen. Uses the mean time difference between several tappings. + if ((process.env.HWVERSION==2 && e.x > 39 && e.x < 136) || process.env.HWVERSION==1){ + if (tindex < time_diffs.length) { + if (Date.now()-tStart < 5000) { + time_diffs[tindex] = Date.now()-tStart; + } + } else { + tindex=0; + time_diffs[tindex] = Date.now()-tStart; + } + tindex += 1; + mean_time = 0.0; + for (count = 0; count < time_diffs.length; count++) { + mean_time += time_diffs[count]; + } + time_diff = mean_time/count; + + tStart = Date.now(); + clearInterval(time_diff); + bpm = (60 * 1000/(time_diff)); + updateScreen(); + clearInterval(interval); + interval = setInterval(updateScreen, 60000 / bpm); + return bpm; + } + else if (e.x < 39) { + if (bpm > 1) { + bpm -= 1; + clearInterval(interval); + interval = setInterval(updateScreen, 60000 / bpm); + } + } + else if (e.x > 136) { + if (bpm > 1) { + bpm += 1; + clearInterval(interval); + interval = setInterval(updateScreen, 60000 / bpm); + }} }); -// enable bpm finetuning via buttons. + +// enable bpm finetuning +if ((process.env.HWVERSION==1)) { setWatch(() => { bpm += 1; clearInterval(interval); @@ -101,10 +134,10 @@ setWatch(() => { } }, BTN3, {repeat:true}); +} interval = setInterval(updateScreen, 60000 / bpm); +printInstructions(); -g.clear(1).setFont("6x8"); -g.drawString('Touch the screen to set tempo.\nUse BTN1 to increase, and\nBTN3 to decrease bpm value by 1.', 25, 200); Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/miclock/ChangeLog b/apps/miclock/ChangeLog index f2e354bc1..e92bad2e3 100644 --- a/apps/miclock/ChangeLog +++ b/apps/miclock/ChangeLog @@ -1,3 +1,4 @@ 0.02: Modified for use with new bootloader and firmware 0.03: Localization 0.04: move jshint to the top +0.05: Use Bangle.setUI for button/launcher handling diff --git a/apps/miclock/bangle1-mixed-clock-screenshot.png b/apps/miclock/bangle1-mixed-clock-screenshot.png new file mode 100644 index 000000000..079aa17df Binary files /dev/null and b/apps/miclock/bangle1-mixed-clock-screenshot.png differ diff --git a/apps/miclock/clock-mixed.js b/apps/miclock/clock-mixed.js index 0bed137c6..b3d6bea8d 100644 --- a/apps/miclock/clock-mixed.js +++ b/apps/miclock/clock-mixed.js @@ -83,5 +83,5 @@ Bangle.drawWidgets(); setInterval(drawMixedClock, 5E3); drawMixedClock(); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); +// Show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/miclock/clock-mixed.png b/apps/miclock/clock-mixed.png old mode 100755 new mode 100644 diff --git a/apps/miclock2/bangle1-mixed-clock-2-screenshot.png b/apps/miclock2/bangle1-mixed-clock-2-screenshot.png new file mode 100644 index 000000000..29a9819c4 Binary files /dev/null and b/apps/miclock2/bangle1-mixed-clock-2-screenshot.png differ diff --git a/apps/miclock2/clock-mixed.png b/apps/miclock2/clock-mixed.png old mode 100755 new mode 100644 diff --git a/apps/minionclk/ChangeLog b/apps/minionclk/ChangeLog old mode 100755 new mode 100644 index 27dab7259..a8b6efc81 --- a/apps/minionclk/ChangeLog +++ b/apps/minionclk/ChangeLog @@ -2,3 +2,4 @@ 0.02: Improved date readability, fixed drawing of widgets 0.03: Fixed rendering for Espruino v2.06 0.04: Fixed overlapped rendering of dates +0.05: Use Bangle.setUI for button/launcher handling diff --git a/apps/minionclk/app-icon.js b/apps/minionclk/app-icon.js old mode 100755 new mode 100644 diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js old mode 100755 new mode 100644 index f0afbc45c..9648e3d89 --- a/apps/minionclk/app.js +++ b/apps/minionclk/app.js @@ -81,4 +81,5 @@ Bangle.on('lcdPower', (on) => { Bangle.loadWidgets(); startDrawing(); -setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: 'falling' }); +// Show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/minionclk/bangle1-minion-clock-screenshot.png b/apps/minionclk/bangle1-minion-clock-screenshot.png new file mode 100644 index 000000000..87038aa46 Binary files /dev/null and b/apps/minionclk/bangle1-minion-clock-screenshot.png differ diff --git a/apps/minionclk/minionclk.png b/apps/minionclk/minionclk.png old mode 100755 new mode 100644 diff --git a/apps/mmonday/manic-monday-icon.js b/apps/mmonday/manic-monday-icon.js index feba5fe86..2b1ee7f79 100644 --- a/apps/mmonday/manic-monday-icon.js +++ b/apps/mmonday/manic-monday-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("MDABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) +require("heatshrink").decompress(atob("mEwwIHEgPAAocP+AFDv4FDgf/Aoc/AocB/4FDh4FEv4FEAgIFIDgQFR+FwAoeAAof8gAFDLoIFC/wyBAoQ4CAoXgAoh0CAtybCAoJPBAoahDAoMHAoicBAoM54EfAoJqCAoQUBAoYUBAoYCBAoXgZAIFC4AFCCgOAYYI1CZIRHB/AFDcwmAAoj9Dj6mCdoQaBAAYWDgA")) diff --git a/apps/moonphase/bangle1-moon-phase-screenshot.png b/apps/moonphase/bangle1-moon-phase-screenshot.png new file mode 100644 index 000000000..1462cb1b3 Binary files /dev/null and b/apps/moonphase/bangle1-moon-phase-screenshot.png differ diff --git a/apps/multiclock/ChangeLog b/apps/multiclock/ChangeLog index c02e390b2..442a5277a 100644 --- a/apps/multiclock/ChangeLog +++ b/apps/multiclock/ChangeLog @@ -1,12 +1,12 @@ -0.01: New App! -0.02: Separate *.face.js files for faces -0.03: Renaming -0.04: Bug Fixes -0.05: Add README -0.06: Add txt clock -0.07: Add Time Date clock and fix font sizes -0.08: Add pinned clock face -0.09: Added Pedometer clock -0.10: Added GPS and Grid Ref clock faces -0.11: Updated Pedometer clock to retrieve steps from either wpedom or activepedom -0.12: Removed GPS and Grid Ref clock faces, superceded by GPS setup and Walkers Clock +0.01: Initial version +0.02: Add pinned clock facility +0.03: Lnng touch switch to night clock - ANCS off, dimmed +0.04: use theme, font heights etc +0.05: make Bangle compatible +0.06: add minute tick for efficiency and nifty A clock +0.07: compatible with Bang;e.js 2 +0.08: fix minute tick bug +0.09: use setUI clockupdown for controls + fix small display bug in nifty face + + + diff --git a/apps/multiclock/README.md b/apps/multiclock/README.md index b1773b8df..25c997329 100644 --- a/apps/multiclock/README.md +++ b/apps/multiclock/README.md @@ -1,30 +1,13 @@ # Multiclock -This is a clock app that supports multiple clock faces. The user can switch between faces while retaining widget state which makes the switch fast and preserves state such as bluetooth connections. Currently there are four clock faces as shown below. To my eye, these faces look better when widgets are hidden using **widviz**. - +This is a clock app that supports multiple clock faces. The user can switch between faces while retaining widget state which makes the switch fast. Currently there are four clock faces as shown below. There are currently an anlog, digital, text, big digit, time and date, and a clone of the Nifty-A-Clock faces. ### Analog Clock Face -![](anaface.jpg) - -### Digital Clock Face -![](digiface.jpg) - -### Big Digit Clock Face -![](bigface.jpg) - -### Text Clock Face -![](txtface.jpg) - -### Time and Date Clock Face ## Controls -Clock faces are kept in a circular list. - -*BTN1* - switches to the next clock face. - -*BTN2* - switches to the app launcher. - -*BTN3* - switches to the previous clock face. +Uses `setUI("clockupdown")` +BTN1 & BTH3 switch faces on the Bangle. +Touch upper right and lower right quadrant switch faces on the Bangle 2. ## Adding a new face Clock faces are described in javascript storage files named `name.face.js`. For example, the Analog Clock Face is described in `ana.face.js`. These files have the following structure: @@ -38,7 +21,7 @@ Clock faces are described in javascript storage files named `name.face.js`. For function drawAll(){ //draw background + initial state of digits, hands etc } - return {init:drawAll, tick:onSecond}; + return {init:drawAll, tick:onSecond, tickpersec:true}; } return getFace; })(); @@ -47,6 +30,5 @@ For those familiar with the structure of widgets, this is similar, however, ther The app at start up loads all files `*.face.js`. The simplest way of adding a face is thus to load it into `Storage` using the WebIDE. Similarly, to remove an unwanted face, simply delete it from `Storage` using the WebIDE. -## Support +If `tickpersec` is false then `tick` is only called each minute as this is more power effcient - especially on the BAngle 2. -Please report bugs etc. by raising an issue [here](https://github.com/jeffmer/JeffsBangleAppsDev). \ No newline at end of file diff --git a/apps/multiclock/ana.js b/apps/multiclock/ana.face.js similarity index 59% rename from apps/multiclock/ana.js rename to apps/multiclock/ana.face.js index 4fd5a7251..af1c84c9f 100644 --- a/apps/multiclock/ana.js +++ b/apps/multiclock/ana.face.js @@ -5,54 +5,60 @@ const p = Math.PI/2; const PRad = Math.PI/180; + var cx = g.getWidth()/2; + var cy = 12+g.getHeight()/2; + var scale = (g.getHeight()-24)/(240-24); + scale = scale>=1 ? 1 : scale; + function seconds(angle, r) { const a = angle*PRad; - const x = 120+Math.sin(a)*r; - const y = 134-Math.cos(a)*r; + const x = cx+Math.sin(a)*r; + const y = cy-Math.cos(a)*r; if (angle % 90 == 0) { - g.setColor(0,1,1); + g.setColor(g.theme.fg2); g.fillRect(x-6,y-6,x+6,y+6); } else if (angle % 30 == 0){ - g.setColor(0,1,1); + g.setColor(g.theme.fg); g.fillRect(x-4,y-4,x+4,y+4); } else { - g.setColor(1,1,1); + g.setColor(g.theme.fg); g.fillRect(x-1,y-1,x+1,y+1); } } function hand(angle, r1,r2, r3) { + r1 = scale*r1; r2=scale*r2; r3 = scale*r3; const a = angle*PRad; g.fillPoly([ - 120+Math.sin(a)*r1, - 134-Math.cos(a)*r1, - 120+Math.sin(a+p)*r3, - 134-Math.cos(a+p)*r3, - 120+Math.sin(a)*r2, - 134-Math.cos(a)*r2, - 120+Math.sin(a-p)*r3, - 134-Math.cos(a-p)*r3]); + cx+Math.sin(a)*r1, + cy-Math.cos(a)*r1, + cx+Math.sin(a+p)*r3, + cy-Math.cos(a+p)*r3, + cx+Math.sin(a)*r2, + cy-Math.cos(a)*r2, + cx+Math.sin(a-p)*r3, + cy-Math.cos(a-p)*r3]); } var minuteDate; var secondDate; function onSecond() { - g.setColor(0,0,0); + g.setColor(g.theme.bg); hand(360*secondDate.getSeconds()/60, -5, 90, 3); if (secondDate.getSeconds() === 0) { hand(360*(minuteDate.getHours() + (minuteDate.getMinutes()/60))/12, -16, 60, 7); hand(360*minuteDate.getMinutes()/60, -16, 86, 7); minuteDate = new Date(); } - g.setColor(1,1,1); + g.setColor(g.theme.fg); hand(360*(minuteDate.getHours() + (minuteDate.getMinutes()/60))/12, -16, 60, 7); hand(360*minuteDate.getMinutes()/60, -16, 86, 7); - g.setColor(0,1,1); + g.setColor(g.theme.fg2); secondDate = new Date(); hand(360*secondDate.getSeconds()/60, -5, 90, 3); - g.setColor(0,0,0); - g.fillCircle(120,134,2); + g.setColor(g.theme.bg); + g.fillCircle(cx,cy,2); } function drawAll() { @@ -60,11 +66,11 @@ // draw seconds g.setColor(1,1,1); for (let i=0;i<60;i++) - seconds(360*i/60, 100); + seconds(360*i/60, 100*scale); onSecond(); } - return {init:drawAll, tick:onSecond}; + return {init:drawAll, tick:onSecond, tickpersec:true}; } return getFace; diff --git a/apps/multiclock/anaface.jpg b/apps/multiclock/anaface.jpg deleted file mode 100644 index 86aaccd54..000000000 Binary files a/apps/multiclock/anaface.jpg and /dev/null differ diff --git a/apps/multiclock/apps_entry.json b/apps/multiclock/apps_entry.json deleted file mode 100644 index 6383609c1..000000000 --- a/apps/multiclock/apps_entry.json +++ /dev/null @@ -1,19 +0,0 @@ -{ "id": "multiclock", - "name": "Multi Clock", - "icon": "multiclock.png", - "version":"0.06", - "description": "Clock with multiple faces - Big, Analogue, Digital, Text.\n Switch between faces with BT1 & BTN3", - "readme": "README.md", - "tags": "clock", - "type":"clock", - "allow_emulator":false, - "storage": [ - {"name":"multiclock.app.js","url":"clock.min.js"}, - {"name":"big.face.js","url":"big.min.js"}, - {"name":"ana.face.js","url":"ana.min.js"}, - {"name":"digi.face.js","url":"digi.min.js"}, - {"name":"txt.face.js","url":"txt.min.js"}, - {"name":"ped.face.js","url":"ped.js"}, - {"name":"multiclock.img","url":"multiclock-icon.js","evaluate":true} - ] - }, diff --git a/apps/multiclock/big.face.js b/apps/multiclock/big.face.js new file mode 100644 index 000000000..2db4ee4d4 --- /dev/null +++ b/apps/multiclock/big.face.js @@ -0,0 +1,31 @@ +(() => { + + function getFace(){ + + const W = g.getWidth(); + const H = g.getHeight(); + const F = 132*H/240; // reasonable approximation + + function drawTime() { + d = new Date() + g.reset(); + var da = d.toString().split(" "); + var time = da[4].substr(0, 5).split(":"); + var hours = time[0], + minutes = time[1]; + g.clearRect(0,24,W-1,H-1); + g.setColor(g.theme.fg); + g.setFont("Vector",F); + g.setFontAlign(0,-1); + g.drawString(hours,W/2,24,true); + g.setColor(g.theme.fg2); + g.drawString(minutes,W/2,12+H/2,true); + } + + + return {init:drawTime, tick:drawTime, tickpersecond:false}; + } + + return getFace; + +})(); \ No newline at end of file diff --git a/apps/multiclock/big.js b/apps/multiclock/big.js deleted file mode 100644 index 2e83d8fb5..000000000 --- a/apps/multiclock/big.js +++ /dev/null @@ -1,32 +0,0 @@ -(() => { - - function getFace(){ - - function drawTime(d) { - g.reset(); - var da = d.toString().split(" "); - var time = da[4].substr(0, 5).split(":"); - var hours = time[0], - minutes = time[1]; - g.clearRect(0,24,239,239); - g.setColor(1,1,1); - g.setFont("Vector",132); - g.drawString(hours,50,24,true); - g.drawString(minutes,50,132,true); - } - - function onSecond(){ - var t = new Date(); - if (t.getSeconds() === 0) drawTime(t); - } - - function drawAll(){ - drawTime(new Date()); - } - - return {init:drawAll, tick:onSecond}; - } - - return getFace; - -})(); \ No newline at end of file diff --git a/apps/multiclock/bigface.jpg b/apps/multiclock/bigface.jpg deleted file mode 100644 index 685726864..000000000 Binary files a/apps/multiclock/bigface.jpg and /dev/null differ diff --git a/apps/multiclock/clock.info b/apps/multiclock/clock.info new file mode 100644 index 000000000..441de1463 --- /dev/null +++ b/apps/multiclock/clock.info @@ -0,0 +1 @@ +{"id":"clock","name":"Clock","type":"clock","src":"clock.app.js","icon":"clock.img","version":"0.06","files":"clock.info,clock.app.js,big.face.js,ana.face.js,digi.face.js,txt.face.js"} \ No newline at end of file diff --git a/apps/multiclock/clock.js b/apps/multiclock/clock.js deleted file mode 100644 index 50410f096..000000000 --- a/apps/multiclock/clock.js +++ /dev/null @@ -1,69 +0,0 @@ -var FACES = []; -var STOR = require("Storage"); -STOR.list(/\.face\.js$/).forEach(face=>FACES.push(eval(require("Storage").read(face)))); -var lastface = STOR.readJSON("multiclock.json")||{pinned:0}; -var iface = lastface.pinned; -var face = FACES[iface](); -var intervalRefSec; - -function stopdraw() { - if(intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} -} - -function startdraw() { - g.clear(); - g.reset(); - Bangle.drawWidgets(); - face.init(); - intervalRefSec = setInterval(face.tick,1000); -} - -function setButtons(){ - function newFace(inc){ - var n = FACES.length-1; - iface+=inc; - iface = iface>n?0:iface<0?n:iface; - stopdraw(); - face = FACES[iface](); - startdraw(); - } - function finish(){ - if (lastface.pinned!=iface){ - lastface.pinned=iface; - STOR.write("multiclock.json",lastface); - } - Bangle.showLauncher(); - } - setWatch(finish, BTN2, {repeat:false,edge:"falling"}); - setWatch(newFace.bind(null,1), BTN1, {repeat:true,edge:"rising"}); - setWatch(newFace.bind(null,-1), BTN3, {repeat:true,edge:"rising"}); -} - -var SCREENACCESS = { - withApp:true, - request:function(){ - this.withApp=false; - stopdraw(); - clearWatch(); - }, - release:function(){ - this.withApp=true; - startdraw(); - setButtons(); - } -}; - -Bangle.on('lcdPower',function(on) { - if (!SCREENACCESS.withApp) return; - if (on) { - startdraw(); - } else { - stopdraw(); - } -}); - -g.clear(); -Bangle.loadWidgets(); -startdraw(); -setButtons(); - diff --git a/apps/multiclock/digi.face.js b/apps/multiclock/digi.face.js new file mode 100644 index 000000000..21f339afc --- /dev/null +++ b/apps/multiclock/digi.face.js @@ -0,0 +1,38 @@ +(() => { + +function getFace(){ + + var W = g.getWidth(); + var H = g.getHeight(); + var scale = W/240; + + var buf = Graphics.createArrayBuffer(W,92,1,{msb:true}); + function flip() { + g.setColor(g.theme.fg); + g.drawImage({width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer},0,H/2-34); + } + + var W = g.getWidth(); + var H = g.getHeight(); + + function drawTime() { + buf.clear(); + buf.setColor(1); + var d = new Date(); + var da = d.toString().split(" "); + var time = da[4]; + buf.setFont("Vector",54*scale); + buf.setFontAlign(0,-1); + buf.drawString(time,W/2,0); + buf.setFont("6x8",scale<1?1:2); + buf.setFontAlign(0,-1); + var date = d.toString().substr(0,15); + buf.drawString(date, W/2, 70*scale); + flip(); + } + return {init:drawTime, tick:drawTime, tickpersec:true}; +} + +return getFace; + +})(); \ No newline at end of file diff --git a/apps/multiclock/digi.js b/apps/multiclock/digi.js deleted file mode 100644 index 4422e6b62..000000000 --- a/apps/multiclock/digi.js +++ /dev/null @@ -1,31 +0,0 @@ -(() => { - -function getFace(){ - - var buf = Graphics.createArrayBuffer(240,92,1,{msb:true}); - function flip() { - g.setColor(1,1,1); - g.drawImage({width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer},0,85); - } - - function drawTime() { - buf.clear(); - buf.setColor(1); - var d = new Date(); - var da = d.toString().split(" "); - var time = da[4]; - buf.setFont("Vector",54); - buf.setFontAlign(0,-1); - buf.drawString(time,buf.getWidth()/2,0); - buf.setFont("6x8",2); - buf.setFontAlign(0,-1); - var date = d.toString().substr(0,15); - buf.drawString(date, buf.getWidth()/2, 70); - flip(); - } - return {init:drawTime, tick:drawTime}; -} - -return getFace; - -})(); \ No newline at end of file diff --git a/apps/multiclock/digiface.jpg b/apps/multiclock/digiface.jpg deleted file mode 100644 index b0323bd55..000000000 Binary files a/apps/multiclock/digiface.jpg and /dev/null differ diff --git a/apps/multiclock/dk.face.js b/apps/multiclock/dk.face.js new file mode 100644 index 000000000..a89397a75 --- /dev/null +++ b/apps/multiclock/dk.face.js @@ -0,0 +1,40 @@ +(() => { + function getFace(){ + + const locale = require("locale"); + + var W = g.getWidth(); + var H = g.getHeight(); + var scale = W/240; + + function drawClock(){ + var now=Date(); + d=now.toString().split(' '); + var min=d[4].substr(3,2); + var sec=d[4].substr(-2); + var tm=d[4].substring(0,5); + var hr=d[4].substr(0,2); + lastmin=min; + g.reset(); + g.clearRect(0,24,W-1,H-1); + g.setColor(g.theme.fg); + g.setFontAlign(0,-1); + g.setFontVector(80*scale); + g.drawString(tm,4+W/2,H/2+24-80*scale); + g.setFontVector(36*scale); + g.setColor(g.theme.fg2); + d[1] = locale.month(now,3); + d[0] = locale.dow(now,3); + var dt=d[0]+" "+d[1]+" "+d[2];//+" "+d[3]; + g.drawString(dt,W/2,H/2+24); + g.flip(); + } + + + return {init:drawClock, tick:drawClock, tickpersec:false}; + } + + return getFace; + +})(); + diff --git a/apps/multiclock/multiclock-icon.img b/apps/multiclock/multiclock-icon.img new file mode 100644 index 000000000..57e0a935f Binary files /dev/null and b/apps/multiclock/multiclock-icon.img differ diff --git a/apps/multiclock/multiclock-icon.js b/apps/multiclock/multiclock-icon.js index 41a59f503..bad6313ba 100644 --- a/apps/multiclock/multiclock-icon.js +++ b/apps/multiclock/multiclock-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwwkEogA/AFGIAAQVVDKQWHDB1IC5OECx8z///mYYOBoWDCoIADnBJLFwQWGDAgwIEYU/CQXwh4EC+YwKBIOPFQYXE//4C5BGCIQgXF/5IILo4XGMIQXHLoYXIMIRGMC45IHC4KkGC45IBC4yNEC5KRBC7h2HC5B4GC5EggQXOBwvygEAl6QHC4sikRGEhGAJAgNBC75HIgZHNO48AgIJER54xCiYXKa5AxCGAjvPGA4XIwYXHbQs4C46QGGAbZDB4IXEPBQAEOwwXDJBJGEC4xILIxQwDSJCNDFwwXDMIh0ELoQXIJARhDC4hdCIw4wEDAQXDCwQuIGAgABmYXBmYHDFxIYGAAoWLJIgAGCxgYJCxwZGCqIA/AC4A=")) +require("heatshrink").decompress(atob("mEwwkEogA/AFGIAAQVVDKQWHDB1IC5OECx8z///mYYOBoWDCoIADnBJLFwQWGDAgwIEYU/CQXwh4EC+YwKBIOPFQYXE//4C5BGCIQgXF/5IILo4XGMIQXHLoYXIMIRGMC45IHC4KkGC45IBC4yNEC5KRBC7h2HC5B4GC5EggQXOBwvygEAl6QHC4sikRGEhGAJAgNBC75HIgZHNO48AgIJER54xCiYXKa5AxCGAjvPGA4XIwYXHbQs4C46QGGAbZDB4IXEPBQAEOwwXDJBJGEC4xILIxQwDSJCNDFwwXDMIh0ELoQXIJARhDC4hdCIw4wEDAQXDCwQuIGAgABmYXBmYHDFxIYGAAoWLJIgAGCxgYJCxwZGCqIA/AC4A=")) \ No newline at end of file diff --git a/apps/multiclock/multiclock.app.js b/apps/multiclock/multiclock.app.js new file mode 100644 index 000000000..0565a7040 --- /dev/null +++ b/apps/multiclock/multiclock.app.js @@ -0,0 +1,86 @@ +var FACES = []; +var STOR = require("Storage"); +STOR.list(/\.face\.js$/).forEach(face=>FACES.push(eval(require("Storage").read(face)))); +var lastface = STOR.readJSON("clock.json") || {pinned:0} +var iface = lastface.pinned; +var face = FACES[iface](); +var intervalRefSec; +var intervalRefSec; +var tickTimeout; + +function stopdraw() { + if(intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} + if(tickTimeout) {tickTimeout=clearTimeout(tickTimeout);} + g.clear(); +} + +function queueMinuteTick() { + if (tickTimeout) clearTimeout(tickTimeout); + tickTimeout = setTimeout(function() { + tickTimeout = undefined; + face.tick(); + queueMinuteTick(); + }, 60000 - (Date.now() % 60000)); +} + +function startdraw() { + g.reset(); + face.init(); + if (face.tickpersec) + intervalRefSec = setInterval(face.tick,1000); + else + queueMinuteTick(); + Bangle.drawWidgets(); +} + +var SCREENACCESS = { + withApp:true, + request:function(){ + this.withApp=false; + stopdraw(); + }, + release:function(){ + this.withapp=true; + startdraw(); + setButtons(); + } +}; + +Bangle.on('lcdPower',function(b) { + if (!SCREENACCESS.withApp) return; + if (b) { + startdraw(); + } else { + stopdraw(); + } +}); + +function setButtons(){ + function newFace(inc){ + if (!inc) Bangle.showLauncher(); + else { + var n = FACES.length-1; + iface+=inc; + iface = iface>n?0:iface<0?n:iface; + stopdraw(); + face = FACES[iface](); + startdraw(); + } + } + Bangle.setUI("clockupdown", newFace); +} + +E.on('kill',()=>{ + if (iface!=lastface.pinned){ + lastface.pinned=iface; + STOR.write("clock.json",lastface); + } +}); + +Bangle.loadWidgets(); +g.clear(); +startdraw(); +setButtons(); + + + diff --git a/apps/multiclock/nifty.face.js b/apps/multiclock/nifty.face.js new file mode 100644 index 000000000..54962da34 --- /dev/null +++ b/apps/multiclock/nifty.face.js @@ -0,0 +1,55 @@ +(() => { + function getFace(){ + + const locale = require("locale"); + const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; + + const scale = g.getWidth() / 176; + + const widget = 24; + + const viewport = { + width: g.getWidth(), + height: g.getHeight(), + } + + const center = { + x: viewport.width / 2, + y: Math.round(((viewport.height - widget) / 2) + widget), + } + + function d02(value) { + return ('0' + value).substr(-2); + } + + function drawClock() { + g.reset(); + g.clearRect(0, widget, viewport.width, viewport.height); + var now = new Date(); + const hour = d02(now.getHours() - (is12Hour && now.getHours() > 12 ? 12 : 0)); + const minutes = d02(now.getMinutes()); + const day = d02(now.getDate()); + const month = d02(now.getMonth() + 1); + const year = now.getFullYear(); + const month2 = locale.month(now, 3); + const day2 = locale.dow(now, 3); + g.setFontAlign(1, 0).setFont("Vector", 90 * scale); + g.drawString(hour, center.x + 32 * scale, center.y - 31 * scale); + g.drawString(minutes, center.x + 32 * scale, center.y + 46 * scale); + g.fillRect(center.x + 30 * scale, center.y - 72 * scale, center.x + 32 * scale, center.y + 74 * scale); + g.setFontAlign(-1, 0).setFont("Vector", 16 * scale); + g.drawString(year, center.x + 40 * scale, center.y - 62 * scale); + g.drawString(month, center.x + 40 * scale, center.y - 44 * scale); + g.drawString(day, center.x + 40 * scale, center.y - 26 * scale); + g.drawString(month2, center.x + 40 * scale, center.y + 48 * scale); + g.drawString(day2, center.x + 40 * scale, center.y + 66 * scale); + } + + + return {init:drawClock, tick:drawClock, tickpersec:false}; + } + + return getFace; + +})(); + diff --git a/apps/multiclock/ped.js b/apps/multiclock/ped.js deleted file mode 100644 index a0f81e2e5..000000000 --- a/apps/multiclock/ped.js +++ /dev/null @@ -1,41 +0,0 @@ -(() => { - - function getFace(){ - - function draw() { - let steps = "-"; - let show_steps = false; - - // only attempt to get steps if activepedom is loaded - if (WIDGETS.activepedom !== undefined) { - steps = WIDGETS.activepedom.getSteps(); - } else if (WIDGETS.wpedom !== undefined) { - steps = WIDGETS.wpedom.getSteps(); - } - - var d = new Date(); - var da = d.toString().split(" "); - var time = da[4].substr(0,5); - - g.reset(); - g.clearRect(0,24,239,239); - g.setFont("Vector", 80); - g.setColor(1,1,1); // white - g.setFontAlign(0, -1); - g.drawString(time, g.getWidth()/2, 60); - g.setColor(0,255,0); // green - g.setFont("Vector", 60); - g.drawString(steps, g.getWidth()/2, 160); - } - - function onSecond(){ - var t = new Date(); - if ((t.getSeconds() % 5) === 0) draw(); - } - - return {init:draw, tick:onSecond}; - } - - return getFace; - -})(); diff --git a/apps/multiclock/screen-ana.png b/apps/multiclock/screen-ana.png new file mode 100644 index 000000000..67d794aa3 Binary files /dev/null and b/apps/multiclock/screen-ana.png differ diff --git a/apps/multiclock/screen-big.png b/apps/multiclock/screen-big.png new file mode 100644 index 000000000..80544d552 Binary files /dev/null and b/apps/multiclock/screen-big.png differ diff --git a/apps/multiclock/screen-date.png b/apps/multiclock/screen-date.png new file mode 100644 index 000000000..21093f458 Binary files /dev/null and b/apps/multiclock/screen-date.png differ diff --git a/apps/multiclock/screen-nifty.png b/apps/multiclock/screen-nifty.png new file mode 100644 index 000000000..884456125 Binary files /dev/null and b/apps/multiclock/screen-nifty.png differ diff --git a/apps/multiclock/screen-sec.png b/apps/multiclock/screen-sec.png new file mode 100644 index 000000000..cc1149254 Binary files /dev/null and b/apps/multiclock/screen-sec.png differ diff --git a/apps/multiclock/screen-td.png b/apps/multiclock/screen-td.png new file mode 100644 index 000000000..edea06a2e Binary files /dev/null and b/apps/multiclock/screen-td.png differ diff --git a/apps/multiclock/screen-word.png b/apps/multiclock/screen-word.png new file mode 100644 index 000000000..ad029a60f Binary files /dev/null and b/apps/multiclock/screen-word.png differ diff --git a/apps/multiclock/timdat.js b/apps/multiclock/timdat.js deleted file mode 100644 index ff1bdf000..000000000 --- a/apps/multiclock/timdat.js +++ /dev/null @@ -1,38 +0,0 @@ -(() => { - function getFace(){ - - var lastmin=-1; - function drawClock(){ - var d=Date(); - if (d.getMinutes()==lastmin) return; - d=d.toString().split(' '); - var min=d[4].substr(3,2); - var sec=d[4].substr(-2); - var tm=d[4].substring(0,5); - var hr=d[4].substr(0,2); - lastmin=min; - g.reset(); - g.clearRect(0,24,239,239); - var w=g.getWidth(); - g.setColor(0xffff); - g.setFontVector(80); - g.drawString(tm,4+(w-g.stringWidth(tm))/2,64); - g.setFontVector(36); - g.setColor(0x07ff); - var dt=d[0]+" "+d[1]+" "+d[2];//+" "+d[3]; - g.drawString(dt,(w-g.stringWidth(dt))/2,160); - g.flip(); - } - - function drawFirst(){ - lastmin=-1; - drawClock(); - } - - return {init:drawFirst, tick:drawClock}; - } - - return getFace; - -})(); - diff --git a/apps/multiclock/txt.js b/apps/multiclock/txt.face.js similarity index 53% rename from apps/multiclock/txt.js rename to apps/multiclock/txt.face.js index 130455176..fddc07214 100644 --- a/apps/multiclock/txt.js +++ b/apps/multiclock/txt.face.js @@ -1,8 +1,14 @@ (() => { function getFace(){ - - function drawTime(d) { + + + var W = g.getWidth(); + var H = g.getHeight(); + var scale = W/240; + var F = 44 * scale; + + function drawTime() { function convert(n){ var t0 = [" ","one","two","three","four","five","six","seven","eight","nine"]; var t1 = ["ten","eleven","twelve","thirteen","fourteen","fifteen","sixteen","seventeen","eighteen","nineteen"]; @@ -13,28 +19,25 @@ return "error"; } g.reset(); - g.clearRect(0,40,239,210); - g.setColor(1,1,1); + g.clearRect(0,24,W-1,H-1); + var d = new Date(); + g.setColor(g.theme.fg); g.setFontAlign(0,0); - g.setFont("Vector",44); + g.setFont("Vector",F); var txt = convert(d.getHours()); - g.drawString(txt.top,120,60); - g.drawString(txt.bot,120,100); + g.setColor(g.theme.fg); + g.drawString(txt.top,W/2,H/2+24-2*F); + g.setColor(g.theme.fg2); + g.drawString(txt.bot,W/2,H/2+24-F); txt = convert(d.getMinutes()); - g.drawString(txt.top,120,140); - g.drawString(txt.bot,120,180); + g.setColor(g.theme.fg); + g.drawString(txt.top,W/2,H/2+24); + g.setColor(g.theme.fg2); + g.drawString(txt.bot,W/2,H/2+24+F); } - function onSecond(){ - var t = new Date(); - if (t.getSeconds() === 0) drawTime(t); - } - function drawAll(){ - drawTime(new Date()); - } - - return {init:drawAll, tick:onSecond}; + return {init:drawTime, tick:drawTime, tickpersec:false}; } return getFace; diff --git a/apps/multiclock/txtface.jpg b/apps/multiclock/txtface.jpg deleted file mode 100644 index e38341257..000000000 Binary files a/apps/multiclock/txtface.jpg and /dev/null differ diff --git a/apps/mylocation/ChangeLog b/apps/mylocation/ChangeLog new file mode 100644 index 000000000..7b83706bf --- /dev/null +++ b/apps/mylocation/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/mylocation/README.md b/apps/mylocation/README.md new file mode 100644 index 000000000..fd597397a --- /dev/null +++ b/apps/mylocation/README.md @@ -0,0 +1,41 @@ +# My Location + + *Sets and stores GPS lat and lon of your preferred city* + +* Select one of the preset Cities or setup through the GPS +* Other Apps can read this information to do calculations based on location +* When the City shows ??? it means the location has been set through the GPS + +## Example Code + + const LOCATION_FILE = "mylocation.json"; + let location; + + // requires the myLocation app + function loadLocation() { + location = require("Storage").readJSON(LOCATION_FILE,1)||{"lat":51.5072,"lon":0.1276,"location":"London"}; + } + +## Screenshots + +### Select one of the Preset Cities + +* The presets are London, Newcastle, Edinburgh, Paris, New York, Tokyo + +![](screenshot_1.png) + +### Or select 'Set By GPS' to start the GPS + +![](screenshot_2.png) + +### While the GPS is running you will see: + +![](screenshot_3.png) + +### When a GPS fix is received you will see: + +![](screenshot_4.png) + + + +Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/mylocation/mylocation.app.js b/apps/mylocation/mylocation.app.js new file mode 100644 index 000000000..fb2f73fa7 --- /dev/null +++ b/apps/mylocation/mylocation.app.js @@ -0,0 +1,75 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +const SETTINGS_FILE = "mylocation.json"; +let settings; + +// initialize with default settings... +let s = { + 'lat': 51.5072, + 'lon': 0.1276, + 'location': "London" +} + +function loadSettings() { + settings = require('Storage').readJSON(SETTINGS_FILE, 1) || s; +} + +function save() { + settings = s + require('Storage').write(SETTINGS_FILE, settings) +} + +const locations = ["London", "Newcastle", "Edinburgh", "Paris", "New York", "Tokyo","???"]; +const lats = [51.5072 ,54.9783 ,55.9533 ,48.8566 ,40.7128 ,35.6762, 0.0]; +const lons = [-0.1276 ,-1.6178 ,-3.1883 ,2.3522 , -74.0060 ,139.6503, 0.0]; + +function setFromGPS() { + Bangle.on('GPS', (gps) => { + //console.log("."); + if (gps.fix === 0) return; + //console.log("fix from GPS"); + s = {'lat': gps.lat, 'lon': gps.lon, 'location': '???' } + Bangle.buzz(1500); // buzz on first position + Bangle.setGPSPower(0); + save(); + + Bangle.setUI("updown", ()=>{ load() }); + E.showPrompt("Location has been saved from the GPS fix",{ + title:"Location Saved", + buttons : {"OK":1} + }).then(function(v) { + load(); // load default clock + }); + }); + + Bangle.setGPSPower(1); + E.showMessage("Waiting for GPS fix. Place watch in the open. Could take 10 minutes. Long press to abort", "GPS Running"); + Bangle.setUI("updown", undefined); +} + +function showMainMenu() { + console.log("showMainMenu"); + const mainmenu = { + '': { 'title': 'My Location' }, + '{ load(); }, + 'City': { + value: 0 | locations.indexOf(s.location), + min: 0, max: 6, + format: v => locations[v], + onchange: v => { + if (v != 6) { + s.location = locations[v]; + s.lat = lats[v]; + s.lon = lons[v]; + save(); + } + } + }, + 'Set From GPS': ()=>{ setFromGPS(); } + } + return E.showMenu(mainmenu); +} + +loadSettings(); +showMainMenu(); diff --git a/apps/mylocation/mylocation.icon.js b/apps/mylocation/mylocation.icon.js new file mode 100644 index 000000000..bfb38d5ac --- /dev/null +++ b/apps/mylocation/mylocation.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///t/7j/P3/vB4cBqtVoAbHBQIABBQ0FBYdQBYsVBYdUERIkGHIQADHoguEGAwuEGAwKFBZg8DHQw8EBYNf/1Vq3/8oLDIwNf/Wpv//0oLG9Wq3/qBYJUCBYuqBaBqBBYW+BepHEBbybCBYP+BYSnErYLDyoLFAANq/r8Ga5T7MBZZUBAAhSCfhA6DBZhIGBQg8FHQg8GHQgwGFwowFBQwwDFwwLMlS7Bqta1AKEn2q1K1C1WgBYf/1WqBYIDB1QKCgYLC0taBYoXB/QICBY0//7vBAAQ8EEgIABCwwME9QVEA")) diff --git a/apps/mylocation/mylocation.png b/apps/mylocation/mylocation.png new file mode 100644 index 000000000..7148990a4 Binary files /dev/null and b/apps/mylocation/mylocation.png differ diff --git a/apps/mylocation/screenshot_1.png b/apps/mylocation/screenshot_1.png new file mode 100644 index 000000000..a9c61b6b3 Binary files /dev/null and b/apps/mylocation/screenshot_1.png differ diff --git a/apps/mylocation/screenshot_2.png b/apps/mylocation/screenshot_2.png new file mode 100644 index 000000000..4c4404540 Binary files /dev/null and b/apps/mylocation/screenshot_2.png differ diff --git a/apps/mylocation/screenshot_3.png b/apps/mylocation/screenshot_3.png new file mode 100644 index 000000000..81570670b Binary files /dev/null and b/apps/mylocation/screenshot_3.png differ diff --git a/apps/mylocation/screenshot_4.png b/apps/mylocation/screenshot_4.png new file mode 100644 index 000000000..ffae679c9 Binary files /dev/null and b/apps/mylocation/screenshot_4.png differ diff --git a/apps/mysticclock/ChangeLog b/apps/mysticclock/ChangeLog new file mode 100644 index 000000000..b486a29a1 --- /dev/null +++ b/apps/mysticclock/ChangeLog @@ -0,0 +1,2 @@ +1.00: First published version. +1.01: Use Bangle.setUI for Launcher/buttons diff --git a/apps/mysticclock/README.md b/apps/mysticclock/README.md new file mode 100644 index 000000000..fd5bbb431 --- /dev/null +++ b/apps/mysticclock/README.md @@ -0,0 +1,40 @@ +# Mystic Clock for Bangle.js + +A retro-inspired watchface featuring time, date, and an interactive data display line. + +## Features + +- 24 or 12-hour time (adjustable via the Settings menu) +- Variable colors (also in the Settings) +- Interactive data display line (use upper and lower watch-buttons to rotate between values) +- Cover watch screen with your hand to put it to sleep (the watch, not your hand) +- International localization of date (which can be disabled via the Settings if memory becomes an issue) + +The interactive line rotates between the following items: + +- Current time zone +- Battery charge level +- Device ID (derived from the last 4 of the MAC) +- Memory usage +- Firmware version + + +## Inspirations + +- [CLI Clock](https://github.com/espruino/BangleApps/tree/master/apps/cliock) +- [Dev Clock](https://github.com/espruino/BangleApps/tree/master/apps/dclock) +- [Digital Clock](https://github.com/espruino/BangleApps/tree/master/apps/digiclock) +- [Simple Clock](https://github.com/espruino/BangleApps/tree/master/apps/sclock) +- [Simplest Clock](https://github.com/espruino/BangleApps/tree/master/apps/simplest) + +Icon adapted from [Public Domain Vectors](https://publicdomainvectors.org/en/free-clipart/Digital-clock-display-vector-image/10845.html). + + +## Changelog + +- 1.00: First published version. (June 2021) + + +## Author + +Eric Wooodward https://itsericwoodward.com/ diff --git a/apps/mysticclock/bangle1-mystic-clock-screenshot.png b/apps/mysticclock/bangle1-mystic-clock-screenshot.png new file mode 100644 index 000000000..2aff6d69a Binary files /dev/null and b/apps/mysticclock/bangle1-mystic-clock-screenshot.png differ diff --git a/apps/mysticclock/mystic-clock-app.js b/apps/mysticclock/mystic-clock-app.js new file mode 100644 index 000000000..2d95633fe --- /dev/null +++ b/apps/mysticclock/mystic-clock-app.js @@ -0,0 +1,208 @@ +/** + * Mystic Clock for Bangle.js + * + * + Original Author: Eric Wooodward https://itsericwoodward.com/ + * + see README.md for details + */ + +/* jshint esversion: 6 */ + +const timeFontSize = 6; +const dataFontSize = 2; +const font = "6x8"; + +const xyCenter = g.getWidth() / 2; + +const yposTime = 75; +const yposDate = 125; +const yposSymbol = 160; +const yposInfo = 220; + +const settings = require('Storage').readJSON('mysticclock.json', 1) || {}; +const colors = ['white', 'blue', 'green', 'purple', 'red', 'teal', 'other']; +const color = settings.color ? colors[settings.color] : 0; + +const infoData = { + '*GMT_MODE': { + calc: () => (new Date()).toString().split(" ")[5], + }, + BATT_MODE: { + calc: () => `BATT: ${E.getBattery()}%`, + }, + ID_MODE: { + calc: () => { + const val = NRF.getAddress().split(":"); + return `ID: ${val[4]}${val[5]}`; + }, + }, + MEM_MODE: { + calc: () => { + const val = process.memory(); + return `MEM: ${Math.round(val.usage * 100 / val.total)}%`; + }, + }, + VER_MODE: { + calc: () => `FW: ${process.env.VERSION}`, + }, +}; +const infoList = Object.keys(infoData).sort(); +let infoMode = infoList[0]; + +function setColor() { + const colorCommands = { + white: () => g.setColor(1, 1, 1), + blue: () => g.setColor(0, 0, 1), + green: () => g.setColor(0, 1, 0), + purple: () => g.setColor(1, 0, 1), + red: () => g.setColor(1, 0, 0), + teal: () => g.setColor(0, 1, 1), + other: () => g.setColor(1, 1, 0) + }; + + // default if value unknown + if (!color || !colorCommands[color]) return colorCommands.white(); + return colorCommands[color](); +} + +function getLocale() { + return require('locale'); +} + +function drawClock() { + + // default draw styles + g.reset(); + + // drawSting centered + g.setFontAlign(0, 0); + + // setup color + setColor(); + + // get date + const d = new Date(); + const dLocal = d.toString().split(" "); + + const useLocale = !settings.useLocale; + + const minutes = (`0${d.getMinutes()}`).substr(-2); + const seconds = (`0${d.getSeconds()}`).substr(-2); + + let hours = (`0${d.getHours()}`).substr(-2); + let meridian = ""; + + if (settings.use12Hour) { + hours = parseInt(hours, 10); + meridian = 'AM'; + if (hours === 0) { + hours = 12; + } + else if (hours >= 12) { + meridian = 'PM'; + if (hours > 12) hours -= 12; + } + hours = (' ' + hours).substr(-2); + } + + g.setFont(font, timeFontSize); + g.drawString(`${hours}${(d.getSeconds() % 2) ? ' ' : ':'}${minutes}`, xyCenter - 15, yposTime, true); + g.setFont(font, dataFontSize); + + if (settings.use12Hour) { + g.drawString(seconds, xyCenter + 97, yposTime - 10, true); + g.drawString(meridian, xyCenter + 97, yposTime + 10, true); + } + else { + g.drawString(seconds, xyCenter + 97, yposTime + 10, true); + } + + // draw DoW, name of month, date, year + g.setFont(font, dataFontSize); + g.drawString([ + useLocale ? getLocale().dow(d, 1) : dLocal[0], + useLocale ? getLocale().month(d, 1) : dLocal[1], + d.getDate(), + d.getFullYear() + ].join(" "), xyCenter, yposDate, true); + +} + +function drawInfo() { + if (infoData[infoMode] && infoData[infoMode].calc) { + // clear info + g.setColor(0, 0, 0); + g.fillRect(0, yposInfo - 8, 239, yposInfo + 25); + + // draw info + g.setFont(font, dataFontSize); + setColor(); + g.drawString((infoData[infoMode].calc()), xyCenter, yposInfo, true); + } +} + +function drawImage() { + setColor(); + g.drawPoly([xyCenter - 100, yposSymbol, xyCenter + 100, yposSymbol, xyCenter, yposSymbol + 30], true); +} + +function drawAll() { + drawClock(); + drawInfo(); + drawImage(); +} + +function nextInfo() { + let idx = infoList.indexOf(infoMode); + if (idx > -1) { + if (idx === infoList.length - 1) infoMode = infoList[0]; + else infoMode = infoList[idx + 1]; + } +} + +function prevInfo() { + let idx = infoList.indexOf(infoMode); + if (idx > -1) { + if (idx === 0) infoMode = infoList[infoList.length - 1]; + else infoMode = infoList[idx - 1]; + } +} + + +let secondInterval; + +// handle LCD power state change +Bangle.on('lcdPower', on => { + + // stop running when screen turns off + if (secondInterval) clearInterval(secondInterval); + secondInterval = undefined; + + // start running + if (on) { + secondInterval = setInterval(drawAll, 1000); + drawAll(); // draw immediately + } +}); + +// cover screen to put it to sleep +Bangle.on('touch', (button) => { + if (button === 3 && Bangle.isLCDOn()) Bangle.setLCDPower(false); +}); + +// clean app screen +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// if screen already active, draw now and start interval +if (Bangle.isLCDOn()) { + secondInterval = setInterval(drawAll, 1000); + drawAll(); // draw immediately +} + +// Show launcher when button pressed +Bangle.setUI("clockupdown", btn=>{ + if (btn<0) prevInfo(); + if (btn>0) nextInfo(); + drawAll(); +}); diff --git a/apps/mysticclock/mystic-clock-icon.js b/apps/mysticclock/mystic-clock-icon.js new file mode 100644 index 000000000..7415fccd5 --- /dev/null +++ b/apps/mysticclock/mystic-clock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBIf4A/AH4A/AH4A/AH4ALs1msADCA4MGAgQDBBYIAGg93u92s4DBuEAAYN3swDCC5AhBuwMBg4XBuwEBs4dCC49nHgNwCQREBCYNnEYYXHHQQvBAAJZBAgRPEC5IOCu0GM4YLCuCGDAAREBHwtnJ41gDQIXEOAQvBDoZ7CuwjCWwimTJgLCFZojWEbwbWIAH4A/AH4A/AH4A/AH4AFA")) diff --git a/apps/mysticclock/mystic-clock-settings.js b/apps/mysticclock/mystic-clock-settings.js new file mode 100644 index 000000000..2fa0c49c5 --- /dev/null +++ b/apps/mysticclock/mystic-clock-settings.js @@ -0,0 +1,41 @@ +// make sure to enclose the function in parentheses +(function (back) { + + const settings = require('Storage').readJSON('mysticclock.json',1)||{}; + const colors = ['White', 'Blue', 'Green', 'Purple', 'Red', 'Teal', 'Yellow']; + const offon = ['Off','On']; + const onoff = ['On','Off']; + + function save(key, value) { + settings[key] = value; + require('Storage').writeJSON('mysticclock.json',settings); + } + + const appMenu = { + '': {'title': 'Clock Settings'}, + '< Back': back, + 'Color': { + value: 0|settings['color'], + min:0, + max:6, + format: m => colors[m], + onchange: m => {save('color', m)} + }, + '12 Hour Clock': { + value: 0|settings['use12Hour'], + min:0, + max:1, + format: m => offon[m], + onchange: m => {save('use12Hour', m)} + }, + 'Use Locale': { + value: 0|settings['useLocale'], + min:0, + max:1, + format: m => onoff[m], + onchange: m => {save('useLocale', m)} + } + }; + E.showMenu(appMenu) + +}) diff --git a/apps/mysticclock/mystic-clock.png b/apps/mysticclock/mystic-clock.png new file mode 100644 index 000000000..915e2ee32 Binary files /dev/null and b/apps/mysticclock/mystic-clock.png differ diff --git a/apps/mysticdock/ChangeLog b/apps/mysticdock/ChangeLog new file mode 100644 index 000000000..34fe53627 --- /dev/null +++ b/apps/mysticdock/ChangeLog @@ -0,0 +1 @@ +1.00: First published version. diff --git a/apps/mysticdock/README.md b/apps/mysticdock/README.md new file mode 100644 index 000000000..09e81ba09 --- /dev/null +++ b/apps/mysticdock/README.md @@ -0,0 +1,43 @@ +# Mystic Dock for Bangle.js + +A retro-inspired dockface that displays the current time and battery charge while plugged in, and which features an interactive mode that shows the time, date, and a rotating data display line. + +## Features + +- Screensaver-like dock mode while charging (displays the current time for 8 seconds and a blank screen for 2, changing text placement with each draw) +- 24 or 12-hour time (adjustable via the Settings menu) +- Variable colors (also in the Settings) +- Interactive watchface display (use upper and lower watch-buttons to activate it and rotate between values at the bottom) +- International localization of watchface date (which can be disabled via the Settings if memory becomes an issue) +- Automatic watchface reload when unplugged (toggleable via the Settings menu) +- Rotates display 90 degrees if it detects it is sideways (for use in a charging dock) + +When in interactive display mode, the bottom line rotates between the following items: + +- Current time zone +- Battery charge level +- Device ID (derived from the last 4 of the MAC) +- Memory usage +- Firmware version + + +## Inspirations + +- [Bluetooth Dock](https://github.com/espruino/BangleApps/tree/master/apps/bluetoothdock) +- [CLI Clock](https://github.com/espruino/BangleApps/tree/master/apps/cliock) +- [Dev Clock](https://github.com/espruino/BangleApps/tree/master/apps/dclock) +- [Digital Clock](https://github.com/espruino/BangleApps/tree/master/apps/digiclock) +- [Simple Clock](https://github.com/espruino/BangleApps/tree/master/apps/sclock) +- [Simplest Clock](https://github.com/espruino/BangleApps/tree/master/apps/simplest) + +Icon adapted from [this one](https://publicdomainvectors.org/en/free-clipart/Digital-clock-display-vector-image/10845.html) and [this one](https://publicdomainvectors.org/en/free-clipart/Vector-image-of-power-manager-icon/20141.html) from [Public Domain Vectors](https://publicdomainvectors.org). + + +## Changelog + +- 1.00: First published version. (June 2021) + + +## Author + +Eric Wooodward https://itsericwoodward.com/ diff --git a/apps/mysticdock/mystic-dock-app.js b/apps/mysticdock/mystic-dock-app.js new file mode 100644 index 000000000..2e6fdafc5 --- /dev/null +++ b/apps/mysticdock/mystic-dock-app.js @@ -0,0 +1,247 @@ +/** + * Mystic Dock for Bangle.js + * + * + Original Author: Eric Wooodward https://itsericwoodward.com/ + * + see README.md for details + */ + +/* jshint esversion: 6 */ + +const timeFontSize = 6; +const dataFontSize = 2; +const font = "6x8"; + +const xyCenter = g.getWidth() / 2; + +const ypos = [ + 45, // Time + 105, // Date + 145, // Symbol + 210 // Info +]; + +const settings = require('Storage').readJSON('mysticdock.json', 1) || + require('Storage').readJSON('mysticclock.json', 1) || {}; +const colors = ['white', 'blue', 'green', 'purple', 'red', 'teal', 'other']; +const color = settings.color ? colors[settings.color] : 0; + +const yposMax = 190; +const yposMin = 60; +let y = yposMax; + +let lastButtonPressTime; +let wasInActiveMode = false; + + +const infoData = { + '*GMT_MODE': { + calc: () => (new Date()).toString().split(" ")[5], + }, + BATT_MODE: { + calc: () => `BATT: ${E.getBattery()}%`, + }, + ID_MODE: { + calc: () => { + const val = NRF.getAddress().split(":"); + return `ID: ${val[4]}${val[5]}`; + }, + }, + MEM_MODE: { + calc: () => { + const val = process.memory(); + return `MEM: ${Math.round(val.usage * 100 / val.total)}%`; + }, + }, + VER_MODE: { + calc: () => `FW: ${process.env.VERSION}`, + }, +}; +const infoList = Object.keys(infoData).sort(); +let infoMode = infoList[0]; + + +function setColor() { + const colorCommands = { + white: () => g.setColor(1, 1, 1), + blue: () => g.setColor(0, 0, 1), + green: () => g.setColor(0, 1, 0), + purple: () => g.setColor(1, 0, 1), + red: () => g.setColor(1, 0, 0), + teal: () => g.setColor(0, 1, 1), + other: () => g.setColor(1, 1, 0) + }; + + // default if value unknown + if (!color || !colorCommands[color]) return colorCommands.white(); + return colorCommands[color](); +} + + +function drawInfo() { + if (infoData[infoMode] && infoData[infoMode].calc) { + // clear info + g.setColor(0, 0, 0); + g.fillRect(0, ypos[3] - 8, 239, ypos[3] + 25); + + // draw info + g.setFont(font, dataFontSize); + setColor(); + g.drawString((infoData[infoMode].calc()), xyCenter, ypos[3], true); + } +} + +function drawImage() { + setColor(); + g.drawPoly([xyCenter - 100, ypos[2], xyCenter + 100, ypos[2], xyCenter, ypos[2] + 30], true); +} + +function drawClock() { + + // default draw styles + g.reset(); + + // get date + const d = new Date(); + const dLocal = d.toString().split(" "); + + const minutes = (`0${d.getMinutes()}`).substr(-2); + const seconds = (`0${d.getSeconds()}`).substr(-2); + + const useLocale = !settings.useLocale; + + let hours = (`0${d.getHours()}`).substr(-2); + let meridian = ""; + + if (d.getSeconds() % 10 === 0) { + y = Math.floor(Math.random() * (yposMax - yposMin)) + yposMin; + } + + // drawSting centered + g.setFontAlign(0, 0); + + // setup color + setColor(); + + if (settings.use12Hour) { + hours = parseInt(hours, 10); + meridian = 'AM'; + if (hours === 0) { + hours = 12; + } + else if (hours >= 12) { + meridian = 'PM'; + if (hours > 12) hours -= 12; + } + hours = (' ' + hours).substr(-2); + } + + g.setFont(font, timeFontSize); + + if (lastButtonPressTime && ((d.getTime() - lastButtonPressTime) / 1000) < 5) { + + // clear screen when switching modes + if (!wasInActiveMode) { + g.clear(); + wasInActiveMode = true; + } + + // draw clock in center w/ seconds + // show date (locale'd, based on settings) + // show info line below it + g.drawString(`${hours}${(d.getSeconds() % 2) ? ' ' : ':'}${minutes}`, xyCenter - 15, ypos[0], true); + g.setFont(font, dataFontSize); + + if (settings.use12Hour) { + g.drawString(seconds, xyCenter + 97, ypos[0] - 10, true); + g.drawString(meridian, xyCenter + 97, ypos[0] + 10, true); + } + else { + g.drawString(seconds, xyCenter + 97, ypos[0] + 10, true); + } + + // draw DoW, name of month, date, year + g.setFont(font, dataFontSize); + g.drawString([ + useLocale ? require('locale').dow(d, 1) : dLocal[0], + useLocale ? require('locale').month(d, 1) : dLocal[1], + d.getDate(), + d.getFullYear() + ].join(' '), xyCenter, ypos[1], true); + + drawInfo(); + drawImage(); + } + else if (d.getSeconds() % 10 === 8) { + g.clear(); + wasInActiveMode = false; + } + else if (d.getSeconds() % 10 !== 9) { + // clear screen when switching modes + if (wasInActiveMode) { + g.clear(); + wasInActiveMode = false; + } + g.drawString(`${hours}${(d.getSeconds() % 2) ? ' ' : ':'}${minutes}`, xyCenter - (settings.use12Hour ? 15 : 0), y, true); + g.setFont(font, dataFontSize); + if (settings.use12Hour) { + g.drawString(meridian, xyCenter + 97, y + 10, true); + } + g.drawString(`BATT: ${E.getBattery() === 100 ? '100' : ('0' + E.getBattery()).substr(-2)}%`, xyCenter, y + 35, true); + } + + g.flip(); +} + + +function nextInfo() { + lastButtonPressTime = Date.now(); + let idx = infoList.indexOf(infoMode); + + if (idx > -1) { + if (idx === infoList.length - 1) infoMode = infoList[0]; + else infoMode = infoList[idx + 1]; + } +} + + +function prevInfo() { + lastButtonPressTime = Date.now(); + let idx = infoList.indexOf(infoMode); + + if (idx > -1) { + if (idx === 0) infoMode = infoList[infoList.length - 1]; + else infoMode = infoList[idx - 1]; + } +} + + +if (Bangle.getAccel().x < -0.7) { + g.setRotation(3); // assume watch in charge cradle +} + +g.clear(); + +setInterval(drawClock, 1000); +drawClock(); + +if (Bangle.isCharging()) { + Bangle.on("charging", isCharging => { + const reloadOnUplug = !settings.reloadOnUplug; + + if (!isCharging && reloadOnUplug) load(); + }); +} + +// show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); + +// change to "active mode" and rotate through info when the buttons are pressed +setWatch(() => { + nextInfo(); + drawClock(); +}, BTN3, { repeat: true }); + +setWatch(() => { + prevInfo(); + drawClock(); +}, BTN1, { repeat: true }); diff --git a/apps/mysticdock/mystic-dock-boot.js b/apps/mysticdock/mystic-dock-boot.js new file mode 100644 index 000000000..7cb7fa8a4 --- /dev/null +++ b/apps/mysticdock/mystic-dock-boot.js @@ -0,0 +1 @@ +Bangle.on("charging", isCharging => { if (isCharging) load("mysticdock.app.js"); }); diff --git a/apps/mysticdock/mystic-dock-icon.js b/apps/mysticdock/mystic-dock-icon.js new file mode 100644 index 000000000..527825dd7 --- /dev/null +++ b/apps/mysticdock/mystic-dock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBIf4A6g93u9gs4DCBBIAFu9ms9wAYYIJAAt2FAN2BYMHEwIIIAAkGBQV3AYNns1mBAwXGg4KCIgYTEBAZ2JCYQABBBIXJQoRcCBA0GDQpPCBAUGuwTBBAwfCUwgTDMoVmBA8GQIIXGWoJ9DBA4vHAAIOBcoYIHC4xqCCQR2BBBEGJAKSGAH4Adb4SIBDCYXCUwQwVDCjJCXYS/CDh4SCAAoxPDA72CPaQdCTB57CLgQYCGCIdFJJ4QFTIQXUGwpHQJAapQI4qQPCIqtDVCQECMCR5BJgN2bSArCuACCbSIRCIobZQOgZMCgx4OJIjvCCyAYCCYJMBYB4zHC6oA/AE4=")) diff --git a/apps/mysticdock/mystic-dock-settings.js b/apps/mysticdock/mystic-dock-settings.js new file mode 100644 index 000000000..7bfda1c0f --- /dev/null +++ b/apps/mysticdock/mystic-dock-settings.js @@ -0,0 +1,48 @@ +// make sure to enclose the function in parentheses +(function (back) { + + const settings = require('Storage').readJSON('mysticdock.json',1)||{}; + const colors = ['White', 'Blue', 'Green', 'Purple', 'Red', 'Teal', 'Yellow']; + const offon = ['Off','On']; + const onoff = ['On','Off']; + + function save(key, value) { + settings[key] = value; + require('Storage').writeJSON('mysticdock.json',settings); + } + + const appMenu = { + '': {'title': 'Dock Settings'}, + '< Back': back, + 'Color': { + value: 0|settings['color'], + min:0, + max:6, + format: m => colors[m], + onchange: m => {save('color', m)} + }, + '12 Hour Clock': { + value: 0|settings['use12Hour'], + min:0, + max:1, + format: m => offon[m], + onchange: m => {save('use12Hour', m)} + }, + 'Reload on Unplug': { + value: 0|settings['reloadOnUplug'], + min:0, + max:1, + format: m => onoff[m], + onchange: m => {save('reloadOnUplug', m)} + }, + 'Use Locale': { + value: 0|settings['useLocale'], + min:0, + max:1, + format: m => onoff[m], + onchange: m => {save('useLocale', m)} + }, + }; + E.showMenu(appMenu) + +}) diff --git a/apps/mysticdock/mystic-dock.png b/apps/mysticdock/mystic-dock.png new file mode 100644 index 000000000..4c0dce770 Binary files /dev/null and b/apps/mysticdock/mystic-dock.png differ diff --git a/apps/mywelcome/ChangeLog b/apps/mywelcome/ChangeLog index bca4ff2dd..f2b54e42c 100644 --- a/apps/mywelcome/ChangeLog +++ b/apps/mywelcome/ChangeLog @@ -13,3 +13,5 @@ BTN2 now goes to menu on release 0.10: Add birthday style 0.11: Skip double buffering, use 240x240 size +0.12: Fix swipe direction (#800) +0.13: Bangle.js 2 support diff --git a/apps/mywelcome/app.js b/apps/mywelcome/app-bangle1.js similarity index 90% rename from apps/mywelcome/app.js rename to apps/mywelcome/app-bangle1.js index 23cdd0d49..772f3986b 100644 --- a/apps/mywelcome/app.js +++ b/apps/mywelcome/app-bangle1.js @@ -11,11 +11,9 @@ function animate(seq,period) { // Fade in to FG color with angled lines function fade(col, callback) { var n = 0; - function f() { + function f() {"ram" g.setColor(col); - for (var i=n;i<240;i+=10) { - g.drawLine(i,0,0,i).drawLine(i,240,240,i); - } + for (var i=n;i<240;i+=10) g.drawLine(i,0,0,i).drawLine(i,240,240,i); g.flip(); n++; if (n<10) setTimeout(f,0); @@ -25,19 +23,22 @@ function fade(col, callback) { } -var scenes = [ - function() { +var SCENE_COUNT=11; +function getScene(n) { + if (n==0) return function() { console.log("Start app"); g.clear(1); eval(require("Storage").read("mywelcome.custom.js")); - },function() { + } + if (n==1) return function() { g.clear(1); g.setFont("4x6",2); var n=0; + var l = Bangle.getLogo(); var i = setInterval(function() { n+=0.04; g.setColor(n,n,n); - g.drawImage(Bangle.getLogo(),(240-222)/2,(240-100)/2); + g.drawImage(l,(240-222)/2,(240-100)/2); if (n>=1) { clearInterval(i); setTimeout(()=>g.drawString("Open",34,144), 500); @@ -45,7 +46,8 @@ var scenes = [ setTimeout(()=>g.drawString("Smart Watch",34,168), 1500); } },50); - },function() { + }; + if (n==2) return function() { var img = require("heatshrink").decompress(atob("ptRxH+qYAfvl70mj5gAC0ekvd8FkAAdz3HJAYAH4+eJXWkJJYAF0hK2vfNJaIAB5t7S3fN5/V6wAD6vOTg9SumXy2W3QAB3eXul2JdnO63XAApPEVYvAJQIACJoRQDzBLoJQ3W5/NIwr4GJohMFAAROgJYvVJQiPGABZNN3bsdvYyESwnWJSIAC3RNM3V1JjZAES4nVJSYAB4xMNJrbkE56WD5xLVdB5NbFofNJbgABJh26qREPrFXrlbAAWjFgfWJgRLaTQhMLy5KNJINhsJLDrYrD5xLC6pLa5nGTR7oLq9bJQJMKTAXWJbbnR3RLJSoRMHv4pC5rkec6SaIrBLGw2r2XW1epcoqYeJiOXJYziEsOH2RBBw7lF56Yg5nGc6FScZOGJQPX2TmDFIfVTEBMSc4hLEw5KB6+rsJMH63X6pMf5hMQzBLCq5LD1ZLEJhTlfJiWXTA2GJYpMIcwPNc2O6TAuGRIPX1igDJg/PJmyYDcgXWwxMH1ApC53XcsHAJiVYcg2HJYZME0YpC5vWJkhLNJgLlDTAeFJhF/FQfVJkG6JiGXcomyJgOrJYhMErYqD53NJj7lRzBMDcoeGJhzoBJb3GJiN1qZBCJgWyJYpNF1LigAAXAJiNSJgzlGJgt/JkZLRy9TJgeHJhznFcuSZGw5MHJomjcuhLBqdcJiSaiTChMV1CYxy5LCqdXIAWy6+rJhCalTCN2JgdYH4WHJiGpTF7kDc43W2RMJTUZLQzBLFc4mr6+GJh2jTFmXJYyaEwuyc5Sag4xLZTQmG2WFJhxNaJYZMLJZSaEJoOHTR9/Ja+6JbdTqRNETRRNF1JLV4BLcAANYI5ToK1BLYJhWYJZwABq5NoJZ91JaAABdAZNS0ZLey9SJaRNYv5KM426JZmXuxKUJrKcL0lTzBLKzBKYJrVXvfGSol7EYWXJI27zF1JLQADq5NUrgYB4wAEEIV0comXI7wAFrCcPJgYWBTIIAETIN2JYmWuhMkdSdYCgOeJgueqRLFyzhfTi9bq4TC45MF49TuuXJlpONcogAC0hKB0gHDvZMEqRMpAANSq9crlbJAYADqwRDxGk0mIA4eCTQOeveXJdYAHqxNFdAeIAAQGCrOI0oHEAGVXTRJMGvgGCwRM7TAZMHwQGCvhM1rBMERIhMGAwdZJmtSqVTwNcwJEDJg19cvIADa4d9JhANDJnSLHJgrl6AAhFFAwpZDegjn7vhMGcvwABrJAFJgjl/TQpBBI4jl/AAN8TQhHDcv4ADcJBMDvpM+IYaeDAAhL+qd9SgycEJn7iEAA18Jf7nEcv4AIrJLIcv6aMcv4ADvhMHrJJ/AAbl/c6ZM/AAt9cv7nSIv7nLcv4AHrLl/TRpJBvgnjA==")); g.reset(); g.setBgColor("#6633ff"); @@ -76,7 +78,8 @@ var scenes = [ },20); },3500); - },function() { + }; + if (n==3) return function() { g.reset(); g.setBgColor("#ffa800");g.clear(); g.setFont("6x8",2); @@ -91,8 +94,8 @@ var scenes = [ ()=>g.drawString("2",200,120), ()=>g.drawString("3",200,200) ],200); - }, - function() { + }; + if (n==4) return function() { g.reset(); g.setBgColor("#00a8ff");g.clear(); g.setFontAlign(0,0); @@ -101,8 +104,8 @@ var scenes = [ g.setFontAlign(-1,-1); g.setFont("6x8",2); g.drawString("Move up\nin menus\n\nTurn Bangle.js on\nif it was off", 20,40); - }, - function() { + }; + if (n==5) return function() { g.reset(); g.setBgColor("#00a8ff");g.clear(); g.setFontAlign(0,0); @@ -111,8 +114,8 @@ var scenes = [ g.setFontAlign(-1,-1); g.setFont("6x8",2); g.drawString("Select menu\nitem\n\nLaunch app\nwhen watch\nis showing", 20,70); - }, - function() { + }; + if (n==6) return function() { g.reset(); g.setBgColor("#00a8ff");g.clear(); g.setFontAlign(0,0); @@ -121,8 +124,8 @@ var scenes = [ g.setFontAlign(-1,-1); g.setFont("6x8",2); g.drawString("Move down\nin menus\n\nLong press\nto exit app\nand go back\nto clock", 20,100); - }, - function() { + }; + if (n==7) return function() { g.reset(); g.setBgColor("#ff3300");g.clear(); g.setFontAlign(0,0); @@ -132,8 +135,8 @@ var scenes = [ g.setFontAlign(-1,-1); g.setFont("6x8",2); g.drawString("If Bangle.js\never stops,\nhold buttons\n1 and 2 for\naround six\nseconds.\n\n\n\nBangle.js will\nthen reboot.", 20,20); - }, - function() { + }; + if (n==8) return function() { g.reset(); g.setBgColor("#00a8ff");g.clear(); g.setFont("6x8",2); @@ -150,8 +153,8 @@ var scenes = [ g.drawString("work too. Try now",x,y+=h); g.drawString("to change page.",x,y+=h);} ],300); - }, - function() { + }; + if (n==9) return function() { g.reset(); g.setBgColor("#339900");g.clear(); g.setFont("6x8",2); @@ -168,8 +171,8 @@ var scenes = [ g.drawString("with a Bluetooth",x,y+=h); g.drawString("capable device",x,y+=h);}, ],400); - }, - function() { + }; + if (n==10) return function() { g.reset(); g.setBgColor("#990066");g.clear(); g.setFont("6x8",2); @@ -182,6 +185,7 @@ var scenes = [ g.drawString("banglejs.com",x,y+=h); var rx = 0, ry = 0; + E.defrag(); // rearrange memory to ensure we have space var h = Graphics.createArrayBuffer(96,96,1,{msb:true}); // draw a cube function draw() { @@ -230,8 +234,8 @@ var scenes = [ } setInterval(draw,50); - }, - function() { + }; + if (n==11) return function() { g.reset(); g.setBgColor("#660099");g.clear(); g.setFontAlign(0,0); @@ -248,20 +252,18 @@ var scenes = [ g.drawString("Bangle.js",x,y+=h);} ],400); } -]; +} var sceneNumber = 0; function move(dir) { - if (dir>0 && sceneNumber+1 == scenes.length) return; // at the end - sceneNumber = (sceneNumber+dir)%scenes.length; + if (dir>0 && sceneNumber+1 == SCENE_COUNT) return; // at the end + sceneNumber = (sceneNumber+dir)%SCENE_COUNT; if (sceneNumber<0) sceneNumber=0; clearInterval(); - Bangle.setLCDMode(); - g.clear(); - scenes[sceneNumber](); + getScene(sceneNumber)(); if (sceneNumber>2) { - var l = scenes.length; + var l = SCENE_COUNT; for (var i=0;i move(-dir)); setWatch(()=>move(1), BTN3, {repeat:true}); setWatch(()=>{ // If we're on the last page diff --git a/apps/mywelcome/app-bangle2.js b/apps/mywelcome/app-bangle2.js new file mode 100644 index 000000000..aeee6918d --- /dev/null +++ b/apps/mywelcome/app-bangle2.js @@ -0,0 +1,254 @@ +// exec each function from seq one after the other +function animate(seq,period) { + var c = g.getColor(); + var i = setInterval(function() { + if (seq.length) { + var f = seq.shift(); + g.setColor(c); + if (f) f(); + } else clearInterval(i); + },period); +} + +// Fade in to FG color with angled lines +function fade(col, callback) { + var n = 0; + function f() {"ram" + g.setColor(col); + for (var i=n;i<240;i+=10) g.drawLine(i,0,0,i).drawLine(i,240,240,i); + g.flip(); + n++; + if (n<10) setTimeout(f,0); + else callback(); + } + f(); +} + + +var SCENE_COUNT=11; +function getScene(n) { + if (n==0) return function() { + console.log("Start app"); + g.clear(1); + eval(require("Storage").read("mywelcome.custom.js")); + } + if (n==1) return function() { + g.reset().setBgColor(0).clearRect(0,0,176,176); + g.setFont("6x15"); + var n=0; + var l = Bangle.getLogo(); + var im = g.imageMetrics(l); + var i = setInterval(function() { + n+=0.1; + g.setColor(n,n,n); + g.drawImage(l,(176-im.width)/2,(176-im.height)/2); + if (n>=1) { + clearInterval(i); + setTimeout(()=>g.drawString("Open",44,104), 500); + setTimeout(()=>g.drawString("Hackable",44,116), 1000); + setTimeout(()=>g.drawString("Smart Watch",44,128), 1500); + } + },50); + }; + if (n==2) return function() { + var img = require("heatshrink").decompress(atob("ptR4n/j/4gH+8H5wl+jOukVVoHZ8dt/n//n37OtgH9sHhwHp4H5xmkGiH72MRje/LL/7iIAEE7sPEgoAC+AlagIlIiMQErPxDwUYxAABwIHCj8N7nOl3uEqa6BEggnFjfM5nCkUil3gEq5KDAAQmC6QmBE4JxSEhIABiQmB8QmSXoQlCYRMdEwIlCAAIlNhYlOiO85nNEyMPEoZwIAAcsYIYmPXoYlMiKaFExX/u9VEqLBBOYrCH+czmtVqJyDEpiaCOYsgSYszmc3qtTEqMR7hzG8AlGmd1OQglOOY6aEgYlCmmZoJMCTBrnD6SaIEoU/zOUuolSjbnBJgqaCEoU5zOXX4RyQYBBzCS4X5zNDqqZCJiERJg5zBEoVJEoM1JgYlQjhMHc4JLEmZMEEp6ZIJgPzS4WTmZMVTILmFYAK+BmglCmd1JgUYJiPNEorABEIOZygDBm5MCiJMQlhMH8ByBXwIlBJgUxJiMd5nOTIzlBTAK+BAANVq4jPAAS/HJgJyCTATAEACC/B4S/IJgIlCYAgAPiS/Kn5yEYANTEyPc5niOQxMB/LlCOapyJJgbpBYAZzROQK/Gl0ATIWfEoZzBc6IlB6SYGgBJBJgpzSlhyH8EAh5MBTIjnCuIlOjjlHTAJzC/LmDTSSYIEoTABOYIlETSKYHXwIABOYM0yYmETSCYHEobnDOYqaBExu8TAwlEc4U5EoiaCmK+NTAolFEwX0TQzBMXwXiEpTBCAAomNEoS+EEo4mIYIImKEoS+EEpDoBEyUbEo3gEo4mJdAImIJY4lJEycdEoPOOBYmPuIlE+HcJYhKKTZ1fhYkB2EAhnNcYMuEhomMr8A3YABEoJyB5gjOAAYmHm9VgELEoJMBEoXAEyXzE45YBJgXwEqx1I+ByDOYJyVJw5yCgEB3cQGgJMWJwQnCu6/CgFBigDB13S/glVAAf1qomCglEoADB1QDBADEPEoNVqEAolEgEKolKErJMDYAJMD0lE0AmaEoNaAgJMCFIYAahV/IgIiDOTgABNYJMEOToiCIoJMCOTzfCN4RMBOTxsDJIRyfIwZMBKQZzfJgRyfOYZMBOUBzCJgNKOT5zDJgLoCADxKBOAIABOT6aCAARyfOYRyjOYRyjOYlKEsBzEEsBzEOUJzDOUIABOUiaDOURzCOUZzCEscKCiY")); + var im = g.imageMetrics(img); + g.reset(); + g.setBgColor("#ff00ff"); + var y = 176, speed = 5; + function balloon(callback) { + y-=speed; + var x = (176-im.width)/2; + g.drawImage(img,x,y); + g.clearRect(x,y+81,x+77,y+81+speed); + if (y>30) setTimeout(balloon,0,callback); + else callback(); + } + fade("#ff00ff", function() { + balloon(function() { + g.setColor(-1).setFont("6x15:2").setFontAlign(0,0); + g.drawString("Welcome.",88,130); + }); + }); + setTimeout(function() { + var n=0; + var i = setInterval(function() { + n+=4; + g.scroll(0,-4); + if (n>150) + clearInterval(i); + },20); + },3500); + + }; + if (n==3) return function() { + g.reset(); + g.setBgColor("#ffff00").setColor(0).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 70, y = 25, h=25; + animate([ + ()=>g.drawString("Your",x,y+=h), + ()=>g.drawString("Bangle.js",x,y+=h), + ()=>g.drawString("has one",x,y+=h), + ()=>g.drawString("button",x,y+=h), + ()=>{g.setFont("12x20:2").setFontAlign(0,0,1).drawString("HERE!",150,88);} + ],200); + }; + if (n==4) return function() { + g.reset(); + g.setBgColor("#00ffff").setColor(0).clear(); + g.setFontAlign(0,0).setFont("6x15:2"); + g.drawString("Press",88,40).setFontAlign(0,-1); + g.setFont("12x20"); + g.drawString("To wake the\nscreen up, or to\nselect", 88,60); + }; + if (n==5) return function() { + g.reset(); + g.setBgColor("#00ffff").setColor(0).clear(); + g.setFontAlign(0,0).setFont("6x15:2"); + g.drawString("Long Press",88,40).setFontAlign(0,-1); + g.setFont("12x20"); + g.drawString("To go back to\nthe clock", 88,60); + }; + if (n==6) return function() { + g.reset(); + g.setBgColor("#ff0000").setColor(0).clear(); + g.setFontAlign(0,0).setFont("12x20"); + g.drawString("If Bangle.js ever\nstops, hold the\nbutton for\nten seconds.\n\nBangle.js will\nthen reboot.", 88,78); + }; + if (n==7) return function() { + g.reset(); + g.setBgColor("#0000ff").setColor(-1).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 88, y = -20, h=60; + animate([ + ()=>{g.drawString("Bangle.js has a\nfull touchscreen",x,y+=h);}, + 0,0, + ()=>{g.drawString("Drag up and down\nto scroll and\ntap to select",x,y+=h);}, + ],300); + }; + if (n==8) return function() { + g.reset(); + g.setBgColor("#00ff00").setColor(0).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 88, y = -35, h=80; + animate([ + ()=>{g.drawString("Bangle.js comes\nwith a few\napps installed",x,y+=h);}, + 0,0, + ()=>{g.drawString("To add more, visit\nbanglejs.com/apps",x,y+=h);}, + ],400); + }; + if (n==9) return function() { + g.reset(); + g.setBgColor("#ff0000").setColor(0).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 88; + g.drawString("You can also make\nyour own apps!",x,30); + g.drawString("Check out\nbanglejs.com",x,130); + + var rx = 0, ry = 0; + // draw a cube + function draw() { + // rotate + rx += 0.1; + ry += 0.11; + var rcx=Math.cos(rx), + rsx=Math.sin(rx), + rcy=Math.cos(ry), + rsy=Math.sin(ry); + // Project 3D coordinates into 2D + function p(x,y,z) { + var t; + t = x*rcy + z*rsy; + z = z*rcy - x*rsy; + x=t; + t = y*rcx + z*rsx; + z = z*rcx - y*rsx; + y=t; + z += 4; + return [88 + 60*x/z, 78+ 60*y/z]; + } + + var a; + // draw a series of lines to make up our cube + var s = 30; + g.clearRect(88-s,78-s,88+s,78+s); + a = p(-1,-1,-1); g.moveTo(a[0],a[1]); + a = p(1,-1,-1); g.lineTo(a[0],a[1]); + a = p(1,1,-1); g.lineTo(a[0],a[1]); + a = p(-1,1,-1); g.lineTo(a[0],a[1]); + a = p(-1,-1,-1); g.lineTo(a[0],a[1]); + a = p(-1,-1,1); g.moveTo(a[0],a[1]); + a = p(1,-1,1); g.lineTo(a[0],a[1]); + a = p(1,1,1); g.lineTo(a[0],a[1]); + a = p(-1,1,1); g.lineTo(a[0],a[1]); + a = p(-1,-1,1); g.lineTo(a[0],a[1]); + a = p(-1,-1,-1); g.moveTo(a[0],a[1]); + a = p(-1,-1,1); g.lineTo(a[0],a[1]); + a = p(1,-1,-1); g.moveTo(a[0],a[1]); + a = p(1,-1,1); g.lineTo(a[0],a[1]); + a = p(1,1,-1); g.moveTo(a[0],a[1]); + a = p(1,1,1); g.lineTo(a[0],a[1]); + a = p(-1,1,-1); g.moveTo(a[0],a[1]); + a = p(-1,1,1); g.lineTo(a[0],a[1]); + } + + setInterval(draw,50); + }; + if (n==10) return function() { + g.reset(); + g.setBgColor("#ffffff");g.clear(); + g.setFontAlign(0,0); + g.setFont("12x20"); + + var x = 88, y = 10, h=21; + animate([ + ()=>g.drawString("That's it!",x,y+=h), + ()=>{g.drawString("Press",x,y+=h*2); + g.drawString("the button",x,y+=h); + g.drawString("to start",x,y+=h); + g.drawString("Bangle.js",x,y+=h);} + ],400); + } +} + +var sceneNumber = 0; + +function move(dir) { + if (dir>0 && sceneNumber+1 == SCENE_COUNT) return; // at the end + sceneNumber = (sceneNumber+dir)%SCENE_COUNT; + if (sceneNumber<0) sceneNumber=0; + clearInterval(); + getScene(sceneNumber)(); + if (sceneNumber>1) { + var l = SCENE_COUNT; + for (var i=0;i move(dir)); +setWatch(()=>{ + if (sceneNumber == SCENE_COUNT-1) + load(); + else + move(1); +}, BTN1, {repeat:true}); + +Bangle.setLCDTimeout(0); +Bangle.setLocked(0); +Bangle.setLCDPower(1); +move(0); diff --git a/apps/mywelcome/bangle1-customized-welcome-screenshot.png b/apps/mywelcome/bangle1-customized-welcome-screenshot.png new file mode 100644 index 000000000..5d5520c41 Binary files /dev/null and b/apps/mywelcome/bangle1-customized-welcome-screenshot.png differ diff --git a/apps/mywelcome/custom.html b/apps/mywelcome/custom.html index b021b7b1a..340f178e8 100644 --- a/apps/mywelcome/custom.html +++ b/apps/mywelcome/custom.html @@ -28,20 +28,20 @@ function getApp() { var line3 = document.getElementById("line3").value; var line4 = document.getElementById("line4").value; var style = document.getElementById("style").value; + // build the app's text using a templated String if (style=="Birthday") return `(function() { var ib = require("heatshrink").decompress(atob("jk0ggGDhOZAAWQCYwMEBxAMFAAIaHyc/+c5DgwMC/84Dg4aCBgwcDBoOf+Y4GBoQEBn4zCI44DBDQ4NEyf4BpgoIBoefxINMBhApEBrQAKBrrrGWpANZHBT7FBpYqIFAYcJBggNOFQwoFDgwMHBwoMIBwYMKBrkykANLmcwBu0zBrMDBv4AFN5gA/ADY")); var ir = require("heatshrink").decompress(atob("jk0ggGDhvdAAXQCYwMEBxAMFAAIaH6c/+c9DgwMC/8zDg4aC/4YCHIwNB7/zHAwNCAgM/DQwqDAYIaHBonT/oNMFBAND74NNBhApEBrQAKBrrrGWpANZHBT7FBpYqIFAYcJBgkA5oMF7gNFFQwoFDgwMHHIoMIAAPM5gMKBrk0oANLmcwBu0zBrMDBv4AFN5gA/ADYA=")); var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/+c3DgwMC/8yDg4aC/4YCHIwNBv/zHAwNCAgM/DQwqDAYIaHBolz+4NMFBANDv8nBpgMIFIgNaABQNddYy1IBrI4KfYoNLFRAoDDhIMEgHnBgt+BooqGFAoqGBg4OFBhAODBhQNcmUgBpczmAN2mYNZgYN/AApvMAH4Ab")); var igift = require("heatshrink").decompress(atob("q1QxH+ADOi0QbZ5nMHDQAbKgIACKa4ACKnJWVKghW0KgxWTKgxWyKhBWRKhBWwKhRWPKhRWuKhhWNKhhWtKpxWKKhys8KxBU8Ky5U+KypU/KyhU/KyhU/KynGKn5WTKn5WUKmHCADpJJE7uYABZUfKuuYKv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/AAv+Kv5VT/wADyIAaKpIlbABZSEKv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/Kv5V/ADNtKv6rdKzZVwKhAABy5V/Khw")); - - var W=240,H=240; + var W=g.getWidth(),H=g.getHeight(); + var titleFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; var blns = []; function updateFlake(f) { f.im = [ir,ig,ib][Math.round(Math.random()*100)%3]; f.s = 0.4+Math.random()*0.5; } - for (var i=0;i<6;i++) { var f = { y:Math.random()*H,x:(0.5+(i<3?i:i+5))*W/11, @@ -51,7 +51,6 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ updateFlake(f); blns.push(f); } - function draw() { blns.forEach(f=>{ f.y-=f.v;f.r+=f.t; @@ -63,7 +62,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ }); var x = W/2, y = H/2; g.drawImage(igift,x-43,y-80); - g.setFont("6x8",2).setFontAlign(0,0); + g.setFont(titleFont).setFontAlign(0,0); g.drawString(${JSON.stringify(line1)},x,y+=20); g.drawString(${JSON.stringify(line2)},x,y+=20); g.setFont("6x8"); @@ -71,16 +70,15 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ g.drawString(${JSON.stringify(line4)},x,y+=10); g.flip(); } - - g.clear(); + g.clear(1).setBgColor(0).setColor(-1).clearRect(0,0,W,H); setInterval(draw,50); })()`; // if (style=="Christmas") return `(function() { var isnow = require("heatshrink").decompress(atob("jEagQWTgfAAocf+gFDh4FDiARBggVB3AFBl3Agf8jfkn/AgX/v/9/+Agfv/2//YrBgfwh4wCgfghYFJCIYdFFIw1EIIpNFL44FFOIoAP")); var itree = require("heatshrink").decompress(atob("mtWxH+ADHHDTI0aGuXH5vNGmhqvTYIzBGtoxF6fTG4g4oGgQyBAAZssGoI0Ga1g1FGdo01ZgIAEGmHHNoLSuAAN/rdb0YFBGlgCBGYIABA4YArGYY1CGn4znAAM6GeVd5PQ5Iyurc/vQ0oGZFAn+d4XC3d5GddiGYIEBy+7zoEBGlFhoEcsQ9GT08+oFk1mkGdaVBMgNArnJ6/KzswGs/J6GlrlbqtbvPC5PCy8wGohniMIPJvIpCqmX3e7vI0BqhqlMIY0DqhtBqoEBa0xgBMIIoEqoABGQwzfsIhBv4qHABM50vQGjg1CGaN66DoBGt1ioGd5LoBGjo1PGYNhvLoCa7wnBqgvGA4YzCAgN5GUAsCqoDBmAHCAYU/wPQ0oSDGcBiDqkwAYcxoFd5PX6GdGjrIIqtUAAc3jk5vPC4fCy5pef5I2BTQMcnAHBy+7y95T0oADnFk1ekBpI2aGRUin7NGAA9hsIzVsIgHTAKZBZoPJ5LNDGhBpXGolcwOsrtcA4TNB3bNDGb/+sVin9AoGe6HX5InEvN/TkP+5XQwM/sRsBzqWB4QuKGjvC6HQ4QdDvKWBZYMwmAuHmFUCYNbqibX3fD5O7qolEZQQ0FBwgKDqgJBGiphEDwNUEgJbBFIQqCAgYOCB4IzCnE6GyhYFGoQnDABYzGAAQ1UAAo2NBoQSBnOB0t/Gjo2EABIPCoGe6HX4QzTGRIAEqtVF4QEBBQc4oE4y/J5PCvIxeABk/oADBvO73eXTyAyZMwM/Awd5vIOFGslAr2Av4PLNcU/jmA6HX5I1KasFcn8dTIOd5PJ4SZGGiNhAAIyNn0ckU+ZYe7AAJpJEYJnNGZk+n9kw9cBAcwGoN5aZg1JJJQABm8/oEjoDKC5ALCrUwqh/NrvQ6HDGp04n9doEdoE/sQJBZQZhCqgABGZk6zw0K/1dnVAoNAFwOlCYL1FubJBy4GCGh1AnOX4XC3YzHFYOeCgdV5PQ5OdD4rKBqqYNGYlbv+X3edGY3CGgKMDAAO7JAJgDAClcr2BEYgADaIZ0DL4uXGbDuB6HX5I1GsP+sNhOgWXIhBmWd4Od5PK4TwFGIJoBAYI2BAD0/jlcQoO7AAJaEGQQADGr0/sjNEvOdAoZmDGgw2ZsVAkeAZpQACGZI2VsU/kVGn1bZoPJZogpGGhA4GfRYwBoGC1mlBQbNFFoo0JNxAGCEod/wM6oFAn9iv/J6/Kzo1Ey9/MZQAKCg4GCFgTDEvPCSwI0BC5I0RN4ocEYYPQ5OdHgeXSwTFKGaJyKFYPC3f+MIdbpzFLAD4zB/1OqtbqtOGgYArGAIADGl9UAAI0wGQN5GoQ0vvIABGoI0uGYQABqo0zNOg0uaQY0/GllOGn40//w=")); - var W=g.getWidth(),H=g.getHeight(); + var titleFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; var flakes = []; for (var i=0;i<10;i++) { var f = { @@ -94,7 +92,6 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ f.v = f.s * (1+Math.random()); flakes.push(f); } - function draw() { flakes.forEach(f=>{ f.y+=f.v;f.r+=f.t; @@ -103,7 +100,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ }); var x = W/2, y = H/2; g.drawImage(itree,x-27,y-80); - g.setFont("6x8",2).setFontAlign(0,0); + g.setFont(titleFont).setFontAlign(0,0); g.drawString(${JSON.stringify(line1)},x,y+=20); g.drawString(${JSON.stringify(line2)},x,y+=20); g.setFont("6x8"); @@ -111,8 +108,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/ g.drawString(${JSON.stringify(line4)},x,y+=10); g.flip(); } - - g.clear(); + g.clear(1).setBgColor(0).setColor(-1).clearRect(0,0,W,H); setInterval(draw,50); })(); `; diff --git a/apps/nato/bangle1-NATO-alphabet-screenshot.png b/apps/nato/bangle1-NATO-alphabet-screenshot.png new file mode 100644 index 000000000..87c864d3a Binary files /dev/null and b/apps/nato/bangle1-NATO-alphabet-screenshot.png differ diff --git a/apps/nato/bangle1-NATO-alphabet-screenshot2.png b/apps/nato/bangle1-NATO-alphabet-screenshot2.png new file mode 100644 index 000000000..0f4e3861e Binary files /dev/null and b/apps/nato/bangle1-NATO-alphabet-screenshot2.png differ diff --git a/apps/ncrclk/ChangeLog b/apps/ncrclk/ChangeLog index 68209352b..31e5d42c8 100644 --- a/apps/ncrclk/ChangeLog +++ b/apps/ncrclk/ChangeLog @@ -1 +1,2 @@ 0.01: A copy of the analogimgclk to work for NodeConf Remote +0.02: Use Bangle.setUI for button/launcher handling diff --git a/apps/ncrclk/app.js b/apps/ncrclk/app.js index acf611b1d..16724fa5e 100644 --- a/apps/ncrclk/app.js +++ b/apps/ncrclk/app.js @@ -125,5 +125,5 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); drawHands(true); -// Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); +// Show launcher when button pressed +Bangle.setUI("clock"); diff --git a/apps/nixie/ChangeLog b/apps/nixie/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/nixie/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/nixie/README.md b/apps/nixie/README.md new file mode 100644 index 000000000..17a49ebad --- /dev/null +++ b/apps/nixie/README.md @@ -0,0 +1,17 @@ +## Nixie clock + +This clock displays the time in nixie-inspired numerals and works on both Bangle versions (1 and 2). It uses a generic +coordinate system (0 <= width < 1) and has helper functions to use inline. + +The app makes use of a module called "m_vatch" which manages all the timers, and makes calls to functions in the 'main' file +to manage drawing the background, time, and any data like sensor info, step counters, battery, etc. The idea is that it is +reusable if you write many watch apps... you just need to implement functions to draw the background (called on start, and every +time the 'mode' changes (regular and night mode), the time (which gets a call every second), and the data (also every second, +except not in night mode)). + +Night mode is a mode that can be set manually or automatically, allowing the watch code to adjust colors and detail. Mainly, +used as a night clock, you can draw no background, and use dim colors for your digits. If set to auto, the accelerometer is used so +when the watch is placed on its side, it switches to night mode (your watch may need a tweak... and Bangle 2 is a different story!) + +It also handles step counting so that it's stored on a daily +basis - survives a system reset, zeroes when the date changes and keeps a record in a history file by day. diff --git a/apps/nixie/app-icon.js b/apps/nixie/app-icon.js new file mode 100644 index 000000000..99de52534 --- /dev/null +++ b/apps/nixie/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBIf4A/AH4A/AH4AtgtVAANQAwIFCAwYTGBIQDBqsF6AXCqFQroXDBQQXFAxNUBRAQKAAXVAYUEFBY7EBgtVC5UEDotdERwQBC4pGDTgQXJgoRDMoQGCqisFR5pICDQQwDCAxVJZAYXKQo7nEC6AtBCYgXGCYYDCJQYXBF5ThDKwoNCB4UMC4yyBToIGDAYNUDoRiBO5CyBLwi5Ea4RyGAH4A/AH4A/AH4A/ACQ=")) diff --git a/apps/nixie/app.js b/apps/nixie/app.js new file mode 100644 index 000000000..a62590f75 --- /dev/null +++ b/apps/nixie/app.js @@ -0,0 +1,429 @@ +const EMULATOR = false; +// which Bangle? +const isB2 = g.getWidth() < 200; + +// global coordinate system +const wX = g.getWidth(); +const wY = g.getHeight(); +const midX = wX/2, midY = wY/2; +// relative positioning: send 0 <= coord < 1 +function relX(x) { return Math.floor(x*wX); } +function relY(y) { return Math.floor(y*wY); } + +// colors +const col_bg = 0; +const col_nm =isB2 ? 1 :"#206040"; +const col_sep = isB2 ? 6 :"#202020"; +const col_off = isB2 ? 1 : "#202020"; +const col_shad1 = isB2 ? 4 :"#FF0000"; +const col_shad2 = isB2 ? 6 :"#FF6000"; +const col_hi =isB2 ? 7 : "#FFC000"; +const col_data = isB2 ? 6 :"#C06000"; + +g.setBgColor(col_bg); +g.clear(); + +var imgTube = { + width : 64, height : 128, bpp : 2, + buffer : require("heatshrink").decompress(atob("AE9AB7sQD54AOiFQB5tVsgPN0uoBxkByEFB5kGyIPNhVVB5tpLwKAMoJuOgNQggMJgtVDhsVqtEZ5cVrWlEBcFtWq1WlJxUaBwOq1IgJgIdCqoABEBEC1WVBwTkGKgUGFYIOCgIRDC4kaFoVUOQQKCQ4IgCB4YKDCYIgCq2QgEqHwJLIEoOkgFqB4KaIEoNkB4Z7JHQVqquqD5EVDYQPCVRIPE1IPKgsAtJTCAA8GyEBD4TrKqAPOgNRB5sRB5wfPgAPOiA/RP4IPaiD6BB5oCBB5kAdQIPNH5wPCvIPMBgIPMR4QPcL4QPNgIPQvS/MqtAB59+B9cVB91VL91BF91RF79RB4OVD5wPsH59BB51FB5sQB/0AD7xvPV4elD5wPLqIPOgJPeX/6//X8YPMH5wPPL74PfN55PQB6TfPB5afDB51/D57P/Z/7P/B97vOB5kAB58VoAA=")) +}; +var imgTubeBW = { + width : 46, height : 92, bpp : 1, + buffer : require("heatshrink").decompress(atob("AD0EAomAAgcCBQkQEykwAgcP/gFD/wKECok4AgcB4A7DgwQEjAFEsYWExg2DhkgAoVAE4kA8AEDgZqEhw+JgA+DCwIKEhhrJCyJELFqBbQIiByLIk6gWZyC3WOSItWOVq3nCywA=")) +}; + +require("Font8x12").add(Graphics); +g.setFont("8x12", 1); +let interval = null; + +let alarming = false; +let nightMode = false; + +// our scale factor +let xs = 0.5 * wX/240; +let ys = 0.75 * wY/240; + +let prevH1 = -1; +let prevH2 = -1; +let prevM1 = -1; +let prevM2 = -1; + + +let points0 = new Uint8Array([ + 0, 40, + 1, 35, + 7, 20, + 16, 8, + 28, 2, + 40, 0, + + 51, 2, + 63, 10, + 72, 20, + 77, 35, + 78, 40, + + 78, 59, + 77, 64, + 72, 79, + 63, 89, + 51, 97, + + 40, 99, + 28, 97, + 16, 91, + 7, 79, + 1, 64, + 0, 59, + 0, 40 +]); + +let points1 = new Uint8Array([ 40, 99, 40, 0]); + +let points2 = new Uint8Array([ 0, 25, + 2, 22, + 6, 13, + 17, 5, + 28, 2, + 40, 0, + 52, 2, + 63, 5, + 74, 13, + 79, 23, + 79, 28, + 74, 38, + 63, 46, + 51, 54, + 40, 58, + 29, 62, + 17, 68, + 8, 80, + 0, 99, + 79, 99 + ]); + +let points4 = new Uint8Array([ 60, 99, 60, 0, 0, 75, 79, 75 ]); + +let points8 = new Uint8Array([ + 40, 40, + 26, 42, + 15, 46, + 4, 56, + 1, 66, + 1, 77, + 6, 87, + 17, 94, + 28, 97, + 38, 99, + 42, 99, + 52, 97, + 63, 94, + 74, 87, + 79, 77, + 79, 66, + 75, 56, + 64, 46, + 54, 42, + 40, 40, + + 52, 39, + 62, 34, + 69, 29, + 72, 23, + 72, 19, + 69, 12, + 62, 6, + 52, 2, + 40, 0, + + 28, 2, + 18, 6, + 11, 12, + 8, 19, + 8, 23, + 11, 29, + 18, 34, + 28, 39, + 40, 40, + ]); + +let points6 = new Uint8Array([ + 50, 0, + 4, 56, + 1, 66, + 1, 77, + 6, 87, + 17, 94, + 28, 97, + 40, 99, + 52, 97, + 63, 94, + 74, 87, + 79, 77, + 79, 66, + 75, 56, + 64, 46, + 52, 42, + 40, 40, + 26, 42, + 15, 46, + 4, 56, + ]); + +let points3 = new Uint8Array([ + 1, 77, + 6, 87, + 17, 94, + 28, 97, + 40, 99, + 52, 97, + 63, 94, + 74, 87, + 79, 77, + 79, 66, + 75, 56, + 64, 46, + 52, 42, + 39, 40, + 79, 0, + 1, 0 + ]); + +let points7 = new Uint8Array([ 0, 0, 79, 0, 30, 99 ]); + +let points9 = new Uint8Array(points6.length); +let points5 = new Uint8Array([ + 1, 77, + 6, 87, + 17, 94, + 28, 97, + 38, 99, + 42, 99, + 52, 97, + 63, 94, + 74, 87, + 79, 77, + 79, 66, + 75, 56, + 64, 46, + 54, 42, + 40, 40, + 26, 42, + 15, 46, + 27, 0, + 79, 0, +]); + +function drawPoints(points, x0, y0) { + let x = points[0]*xs+x0, y = points[1]*ys+y0; + //g.drawEllipse(x-2, y-2, x+2, y+2); + g.moveTo(x, y); + for(let idx=1; idx*2 < points.length; idx ++) { + let x = points[idx*2]*xs+x0; + let y = points[idx*2+1]*ys+y0; + //g.drawEllipse(x-2, y-2, x+2, y+2); + g.lineTo(x, y); + } +} + +/* create 5 from 2 */ +/* uncomment if you want the 5 to look more authentic (but uglier) +for (let idx=0; idx*2 < points2.length; idx++) { + points5[idx*2] = points2[idx*2]; + points5[idx*2+1] = 99-points2[idx*2+1]; +} +*/ +/* create 9 from 6 */ +for (let idx=0; idx*2 < points6.length; idx++) { + points9[idx*2] = 79-points6[idx*2]; + points9[idx*2+1] = 99-points6[idx*2+1]; +} + +pointsArray = [points0, points1, points2, points3, points4, points5, points6, points7, points8, points9]; + +function eraseDigit(d, x, y) { + if(d < 0 || d > 9) return; + g.setColor(col_bg); + if(nightMode) { + drawPoints(pointsArray[d], x, y); + return; + } + drawPoints(pointsArray[d], x-2, y-2); + drawPoints(pointsArray[d], x+2, y-2); + drawPoints(pointsArray[d], x-2, y+2); + drawPoints(pointsArray[d], x+2, y+2); + drawPoints(pointsArray[d], x-1, y-1); + drawPoints(pointsArray[d], x+1, y-1); + drawPoints(pointsArray[d], x-1, y+1); + drawPoints(pointsArray[d], x+1, y+1); +} + +function drawDigit(d, x, y) { + if(nightMode) { + g.setColor(col_nm); + drawPoints(pointsArray[d], x, y); + return; + } + g.setColor(col_off); + for (let idx = pointsArray.length - 1; idx >= 0 ; idx--) { + if(idx == d) { + g.setColor(col_shad1); + drawPoints(pointsArray[d], x-2, y-2); + drawPoints(pointsArray[d], x+2, y-2); + drawPoints(pointsArray[d], x-2, y+2); + drawPoints(pointsArray[d], x+2, y+2); + g.setColor(col_shad2); + drawPoints(pointsArray[d], x-1, y-1); + drawPoints(pointsArray[d], x+1, y-1); + drawPoints(pointsArray[d], x-1, y+1); + drawPoints(pointsArray[d], x+1, y+1); + + g.setColor(col_hi); + drawPoints(pointsArray[d], x, y); + + g.setColor(col_off); + } else { + drawPoints(pointsArray[idx], x, y); + } + } +} + +function drawBkgd(nm) { + g.clear(); + prevH1=-1;prevH2=-1;prevM1=-1;prevM2=-1; + if(nm) return; + + if(!isB2) { + // tube images + g.setColor(col_shad2); + + [relX(0),relX(0.25),relX(0.5),relX(0.75)].forEach((v,i,a) => { + g.drawImage(imgTube,v,relY(0.225)); + }); + // something to sit on + g.setColor(col_shad2); + g.fillRect(0, relY(0.76),wX,relY(0.76)); + } else { + // simple tubes + [1,45,89,133].forEach((v,i,a) => { + g.setColor(col_shad1); + g.drawEllipse(v, 52, v+41, 90); + g.drawRect(v,66,v+41,125); + g.clearRect(v+1,66,v+40,124); + }); + } + g.setColor(col_shad2); + g.moveTo(relX(0.125), 0); + g.lineTo(relX(0.25), relY(0.125)); + g.lineTo(relX(0.75), relY(0.125)); + g.lineTo(relX(0.875),0); + + g.moveTo(relX(0.125), wY); + g.lineTo(relX(0.25), relY(0.875)); + g.lineTo(relX(0.75), relY(0.875)); + g.lineTo(relX(0.875), wY); + +} + +function drawTime(d,nm) { + const dx = [relX(0.042), relX(0.29), relX(0.55), relX(0.791)]; //[ 10, 65, 135, 190]; + const dy = [relY(0.38),relY(0.38),relY(0.38),relY(0.38)]; + + let h1 = Math.floor(d.hour / 10); + let h2 = d.hour % 10; + let m1 = Math.floor(d.min / 10); + let m2 = d.min % 10; + + if(h1 == prevH1 && h2 == prevH2 && m1 == prevM1 && m2 == prevM2) { + return; + } + nightMode = nm; + + if(h1 != prevH1) { + eraseDigit(prevH1, dx[0], dy[0]); + drawDigit(h1, dx[0], dy[0]); + } + if(h2 != prevH2) { + eraseDigit(prevH2, dx[1], dy[1]); + drawDigit(h2, dx[1], dy[1]); + } + if(m1 != prevM1) { + eraseDigit(prevM1, dx[2], dy[2]); + drawDigit(m1, dx[2], dy[2]); + } + if(m2 != prevM2) { + eraseDigit(prevM2, dx[3], dy[3]); + drawDigit(m2, dx[3], dy[3]); + } + prevH1 = h1; + prevH2 = h2; + prevM1 = m1; + prevM2 = m2; + +} + +function drawData(d) { + if(!nightMode) { + g.setColor(col_data); + g.setFontAlign(0, -1); + g.drawString(` ${d.dow}, ${d.mon3} ${d.date} `, wX/2, relX(0.042), true); + g.setFontAlign(-1,-1); + g.drawString("STEP ", 0, relY(0.82), true); + g.drawString(`${d.steps} `,0, relY(0.875), true); + g.setFontAlign(1,-1); + g.drawString(" BTY", relX(0.999), relY(0.82), true); + g.drawString(` ${d.batt}`, relX(0.999), relY(0.875), true); + g.setFontAlign(0,-1); + g.setColor(col_shad2); + g.drawString('BANGLE.JS', wX/2, relY(0.925)); + } +} + +//setWatch(E.showLauncher, BTN1, {repeat:true,edge:"falling"}); +if(EMULATOR) { + let d = new Date(); + + let hour = d.getHours(); + let minute = d.getMinutes(); + + let h1 = Math.floor(hour / 10); + let h2 = hour % 10; + let m1 = Math.floor(minute / 10); + let m2 = minute % 10; + + let data = { + h1: h1, + h2: h2, + m1: m1, + m2: m2, + hour: hour, + min: minute, + }; + + drawBkgd(nightMode); + + drawTime(data, nightMode); + const mstr="JanFebMarAprMayJunJulAugSepOctNovDec"; + const dowstr = "SunMonTueWedThuFriSat"; + + let month = d.getMonth(); + let dow = d.getDay(); + data.month = month; + data.date = d.getDate(); + + data.mon3 = mstr.slice(month*3,month*3+3); + data.dow = dowstr.substr(dow*3,3); + data.dateStr = data.dow + " " + data.mon3 + " " + data.date; + data.steps = 12345; + data.batt = E.getBattery() + (Bangle.isCharging() ? "+" : ""); + data.charging = Bangle.isCharging(); + + drawData(data); +} else { + Bangle.setUI("clock"); + let v = require("m_vatch.js"); + v.setDrawTime(drawTime); + v.setDrawBackground(drawBkgd); + v.setDrawData(drawData); + v.begin(); +} diff --git a/apps/nixie/m_vatch.js b/apps/nixie/m_vatch.js new file mode 100644 index 000000000..430892424 --- /dev/null +++ b/apps/nixie/m_vatch.js @@ -0,0 +1,317 @@ +const _Storage = require('Storage'); + +let interval = null; + +let nightMode = false; + +let stepFile = 'v.steps.json'; +let stepArchiveFile = 'v.stephist.json'; + +let _Options = {}; +let optsFile = 'm_vatch.opts.json'; + +let _Alarm = { + inAlarm: false, + reload: () => {}, + scheduleAlarms: () => {}, + showMsg: (title, msg) => {}, + showNotes: () => {}, +}; + +let _StepData = {}; + +const pad0 = (n) => (n > 9) ? n : ("0"+n); + +const getToday = () => { + let d = new Date(); + return d.getFullYear()+'-'+ pad0(d.getMonth()+1) + '-' + pad0(d.getDate()); +}; + +function reload() { + _StepData = _Storage.readJSON(stepFile); + if(!_StepData) { + _StepData = { + lastDate: '2020-01-01', + stepCache: 0, + lastStepCount: 0, + updated: true, + }; + } + if(getToday() === _StepData.lastDate) { + _StepData.stepCache += _StepData.lastStepCount; + _StepData.lastStepCount = 0; + } +} + +function stringFromArray(data) +{ + var count = data.length; + var str = ""; + + for(var index = 0; index < count; index += 1) + str += String.fromCharCode(data[index]); + + return str; +} + +function logD(str) { + if(_Options.debug) console.log(str); +} + + +let lastH1 = -1; +let lastH2 = -1; +let lastM1 = -1; +let lastM2 = -1; + + +let drawBackground = () => {}; +let drawTime = () => {}; +let drawData = () => {}; + + +function timeCheck() { + + if(_Alarm.inAlarm) return; + + logD('Again, ' + JSON.stringify(_Options)); + logD('opt.nm = '+_Options.autoNightMode); + if(_Options.autoNightMode) { + // this may vary by Bangle.. adjust to taste + let a = Bangle.getAccel(); + a.x = Math.floor(a.x * 100); + logD('a.x = ' + a.x); + if(a.x <= 101 && a.x >= 99) { + if(!nightMode) { + nightMode = ! nightMode; + redrawScreen(); + } + } else { + if(nightMode) { + nightMode = ! nightMode; + redrawScreen(); + } + } + } + + let d = new Date(); + + let hour = d.getHours(); + let minute = d.getMinutes(); + + let h1 = Math.floor(hour / 10); + let h2 = hour % 10; + let m1 = Math.floor(minute / 10); + let m2 = minute % 10; + + logD("lastH1 = "+lastH1+": lastM2 = "+lastM2); + if(h1 == lastH1 && h2 == lastH2 && m1 == lastM1 && m2 == lastM2) { + return; + } + + logD("drawing time"); + let data = { + h1: h1, + h2: h2, + m1: m1, + m2: m2, + hour: hour, + min: minute, + }; + drawTime(data, nightMode); + + lastH1 = h1; + lastH2 = h2; + lastM1 = m1; + lastM2 = m2; + + if(!nightMode && !_Alarm.inAlarm) { + logD("drawing data..."); + const mstr="JanFebMarAprMayJunJulAugSepOctNovDec"; + const dowstr = "SunMonTueWedThuFriSat"; + + let month = d.getMonth(); + let dow = d.getDay(); + data.month = month; + data.date = d.getDate(); + + data.mon3 = mstr.slice(month*3,month*3+3); + data.dow = dowstr.substr(dow*3,3); + data.dateStr = data.dow + " " + data.mon3 + " " + data.date; + data.steps = _StepData.stepCache + _StepData.lastStepCount; + data.batt = E.getBattery() + (Bangle.isCharging() ? "+" : ""); + data.charging = Bangle.isCharging(); + + drawData(data); + } + + if(_StepData.updated) { + _Storage.writeJSON(stepFile, _StepData); + logD(JSON.stringify(_StepData)); + _StepData.updated = false; + } +} + +function stop () { + if (interval) { + clearInterval(interval); + } +} + +function start () { + if (interval) { + clearInterval(interval); + } + // first time init + interval = setInterval(timeCheck, 1000); + timeCheck(); +} + + +function btn1Func() { + logD("btn1Func"); + + if(_Alarm.inAlarm ) { + _Alarm.inAlarm = false; + } else { + if( ! _Options.autoNightMode) { + nightMode = ! nightMode; + logD('nm is '+nightMode); + } + } + redrawScreen(); +} + +function redrawScreen() { + logD("redrawScreen"); + + if(nightMode) { + g.setRotation(1,0); + } else { + g.setRotation(0,0); + } + lastM1 = -1; + lastM2 = -1; + lastH1 = -1; + lastH2 = -1; + drawBackground(nightMode); + timeCheck(); +} + +function btn2Func() { + _Alarm.reload(); + _Alarm.scheduleAlarms(); + _Alarm.showNotes(); +} + +Bangle.on('step', function(cnt) { + if(!_StepData.lastDate) return; + if(_StepData.lastDate !== getToday()) { + // save previous day's step count + try { + let sf = _Storage.readJSON(stepArchiveFile); + if(!sf) sf = []; + logD('sf is '+ (typeof sf) +':'+sf); + // trim to 30 + if(sf.length >= 30 ) sf.shift(); + let steps = _StepData.stepCache +_StepData.lastStepCount; + let sd = `${_StepData.lastDate},${steps}`; + sf.push(sd); + _Storage.writeJSON(stepArchiveFile, sf); + } catch (err) { + _Storage.write('err.txt',err); + } + /* individual step files by date + _Storage.write(_StepData.lastDate +'.steps', JSON.stringify( + _StepData.stepCache +_StepData.lastStepCount + )); + */ + _StepData.stepCache = 0 - cnt; + _StepData.lastDate = getToday(); + } + _StepData.lastStepCount = cnt; + _StepData.updated = true; +}); + +/* +** Advertise a writeable characteristic. Accepts text (in 20 char +** chunks) terminated with __EOM__ by itself. If there's text, show +** it (as an alarm), otherwise reload the alarm & msg files (empty +** string signals another BLE process updated those files) +*/ +/* +var BLEMessage = ""; +NRF.setServices({ + "feb10001-f00d-ea75-7192-abbadabadebb": { + "feb10002-f00d-ea75-7192-abbadabadebb": { + value : [0], + maxLen : 20, + writable : true, + onWrite : function(evt) { + let str = stringFromArray(evt.data); + if(str === "__EOM__") { + if(BLEMessage) { + showMsg('Message',BLEMessage); + } else { + reload(); + scheduleAlarms(); + showMsg('', 'Reloading...'); + } + BLEMessage = ''; + } else { + BLEMessage += str; + } + } + } + } +}, { }); +*/ + +exports.setDrawBackground = function(dBkgd) { + drawBackground = dBkgd; +}; +exports.setDrawTime = function(dTime) { + drawTime = dTime; +}; +exports.setDrawData = function( dData) { + drawData = dData; +}; +exports.begin = function() { + _Options = _Storage.readJSON(optsFile); + if(!_Options) _Options = { + autoNightMode: true, + useAlarms: false, + stepManager: true, + debug: true, + }; + + console.log(JSON.stringify(_Options)); + + if(_Options.useAlarms) { + _Alarm = require('m_alarms'); + _Alarm.reload(); + _Alarm.scheduleAlarms(); + } + // separate the Bangles now + const isB2 = g.getWidth() < 200; + + if(!isB2) { + Bangle.on('lcdPower', function (on) { + if (on) { + start(); + } else { + stop(); + } + }); + setWatch(btn1Func, BTN1, {repeat:true,edge:"falling"}); + + if(_Options.useAlarms) { + setWatch(btn2Func, BTN2, {repeat:true,edge:"falling"}); + } + setWatch(Bangle.showLauncher, BTN3, {repeat:false,edge:"falling"}); + } + reload(); + drawBackground(nightMode); + start(); +}; + diff --git a/apps/nixie/nixie.info b/apps/nixie/nixie.info new file mode 100644 index 000000000..66f5ff2a5 --- /dev/null +++ b/apps/nixie/nixie.info @@ -0,0 +1,10 @@ +{ +"id":"jvNixie", +"name":"Nixie Clock", +"type":"clock", +"src":"nixie.app.js", +"icon": "nixie.img", +"sortorder":1, +"version":"1.1", +"files":"nixie.info,nixie.app.js,nixie.img, m_vatch.js" +} diff --git a/apps/nixie/nixie.png b/apps/nixie/nixie.png new file mode 100644 index 000000000..d21714191 Binary files /dev/null and b/apps/nixie/nixie.png differ diff --git a/apps/notify/ChangeLog b/apps/notify/ChangeLog index 2b7a4f990..8803b82b6 100644 --- a/apps/notify/ChangeLog +++ b/apps/notify/ChangeLog @@ -5,3 +5,6 @@ 0.06: Support background color 0.07: Auto-calculate height, and pad text down even when there's no title (so it stays on-screen) 0.08: Don't turn on screen during Quiet Mode +0.09: Add onHide callback +0.10: Improvements to help notifications work with themes +0.11: Fix regression that caused no notifications and corrupted background diff --git a/apps/notify/README.md b/apps/notify/README.md index 11c493102..7b2473015 100644 --- a/apps/notify/README.md +++ b/apps/notify/README.md @@ -1,9 +1,12 @@ # Notifications (default) -A handler for displaying notifications that displays them in a bar at the top of the screen +The default version of the `notify` module for displaying notifications in a bar at the top of the screen -This is not an app, but instead it is a library that can be used by -other applications or widgets to display messages. +This module is installed by default by client applications such as Gadgetbridge. + +**Note:** There are other implementations of this library available such +as `notifyfs` (Fullscreen Notifications). These can be used in the exact +same way from code, but they look different to the user. ## Usage diff --git a/apps/notify/notify.js b/apps/notify/notify.js index b5ef32d8b..332c301d5 100644 --- a/apps/notify/notify.js +++ b/apps/notify/notify.js @@ -1,5 +1,6 @@ let pos = 0; let id = null; +let hideCallback = undefined; /** * Fit text into area, trying to insert newlines between words @@ -44,6 +45,7 @@ function fitWords(text,rows,width) { render : function(y) // function callback to render bgColor : int/string // optional background color (default black) titleBgColor : int/string // optional background color for title (default black) + onHide : function() // callback when notification is hidden } */ /* @@ -92,17 +94,18 @@ exports.show = function(options) { y = 320-size, h = size, b = y+h-1, r = x+w-1; // bottom,right - g.setClipRect(x,y, r,b); // clear area - g.setColor(options.bgColor||0).fillRect(x,y, r,b); + g.reset().setClipRect(x,y, r,b); + if (options.bgColor!==undefined) g.setColor(options.bgColor); + g.clearRect(x,y, r,b); // bottom border - g.setColor(0x39C7).fillRect(0,b-1, r,b); + g.setColor("#333").fillRect(0,b-1, r,b); b -= 2;h -= 2; // title bar if (options.title || options.src) { g.setColor(options.titleBgColor||0x39C7).fillRect(x,y, r,y+20); const title = options.title||options.src; - g.setColor(-1).setFontAlign(-1, -1, 0).setFont("6x8", 2); + g.setColor(g.theme.fg).setFontAlign(-1, -1, 0).setFont("6x8", 2); g.drawString(title.trim().substring(0, 13), x+25,y+3); if (options.title && options.src) { g.setFont("6x8", 1).setFontAlign(1, 1, 0); @@ -120,7 +123,7 @@ exports.show = function(options) { } // body text if (options.body) { - g.setColor(-1).setFont("6x8", 1).setFontAlign(-1, -1, 0).drawString(text, x+6,y+4); + g.setColor(g.theme.fg).setFont("6x8", 1).setFontAlign(-1, -1, 0).drawString(text, x+6,y+4); } if (options.render) { @@ -142,6 +145,8 @@ exports.show = function(options) { } anim(); Bangle.on("touch", exports.hide); + if (options.onHide) + hideCallback = options.onHide; }; /** @@ -152,6 +157,8 @@ exports.show = function(options) { exports.hide = function(options) { options = options||{}; if ("id" in options && options.id!==id) return; + if (hideCallback) hideCallback({id:id}); + hideCallback = undefined; id = null; Bangle.removeListener("touch", exports.hide); function anim() { diff --git a/apps/notifyfs/ChangeLog b/apps/notifyfs/ChangeLog index 974e138f7..cf0a13866 100644 --- a/apps/notifyfs/ChangeLog +++ b/apps/notifyfs/ChangeLog @@ -6,3 +6,7 @@ 0.06: Adjust position of notification src text and notifications without title 0.07: Support background color 0.08: Don't turn on screen during Quiet Mode +0.09: Add onHide callback +0.10: Ensure dismissing a notification dismissal doesn't enter launcher if in clock mode +0.11: Improvements to help notifications work with themes, Bangle.js 2 support +0.12: More use of themes, title now uses theme highlight colors, font adjusts diff --git a/apps/notifyfs/notify.js b/apps/notifyfs/notify.js index 07801cedb..9cadbb124 100644 --- a/apps/notifyfs/notify.js +++ b/apps/notifyfs/notify.js @@ -1,6 +1,7 @@ let oldg; let id = null; - +let hideCallback = null; +const titleFont = g.getWidth() / 8; /** * See notify/notify.js */ @@ -40,6 +41,7 @@ function fitWords(text,rows,width) { render : function(y) // function callback to render bgColor : int/string // optional background color (default black) titleBgColor : int/string // optional background color for title (default black) + onHide : function() // callback when notification is hidden } */ exports.show = function(options) { @@ -48,32 +50,33 @@ exports.show = function(options) { if (options.on===undefined) options.on=true; id = ("id" in options)?options.id:null; let size = options.size||120; - if (size>120) {size=120} - Bangle.setLCDMode("direct"); + if (size>120) size=120; + try { Bangle.setLCDMode("direct"); } catch(e) {} // not supported/needed on Bangle.js 2 let x = 0, y = 40, - w = 240, + w = g.getWidth(), h = size; // clear screen - g.setColor(options.bgColor||0).fillRect(0,0,g.getWidth(),g.getHeight()); + g.reset(); + if (options.bgColor!==undefined) g.setColor(options.bgColor); + g.clearRect(0,0,g.getWidth(),g.getHeight()); // top bar if (options.title||options.src) { - const title = options.title || options.src - g.setColor(options.titleBgColor||0x39C7).fillRect(x, y, x+w-1, y+30); - g.setColor(-1).setFontAlign(-1, -1, 0).setFont("6x8", 3); + const title = options.title || options.src; + g.setColor(options.titleBgColor||g.theme.bgH).fillRect(x, y, x+w-1, y+30); + g.setColor(g.theme.fgH).setFontAlign(-1, -1, 0).setFont("Vector", titleFont); g.drawString(title.trim().substring(0, 13), x+5, y+3); if (options.title && options.src) { - g.setColor(-1).setFontAlign(1, 1, 0).setFont("6x8", 2); + g.setColor(g.theme.fg).setFontAlign(1, 1, 0).setFont("6x8", 2); // above drawing area, but we are fullscreen - print(options.src.substring(0, 10), w-23, y-4); g.drawString(options.src.substring(0, 10), w-16, y-4); } y += 30;h -= 30; } if (options.icon) { let i = options.icon, iw,ih; - if ("string"==typeof i) {iw=i.charCodeAt(0); ih=i.charCodeAt(1)} - else {iw=i[0]; ih=i[1]} + if ("string"==typeof i) {iw=i.charCodeAt(0); ih=i.charCodeAt(1);} + else {iw=i[0]; ih=i[1];} const iy=y ? (y+4) : (h-ih)/2; // show below title bar if present, otherwise center vertically g.drawImage(i, x+4,iy); x += iw+4;w -= iw+4; @@ -83,17 +86,16 @@ exports.show = function(options) { const maxRows = Math.floor((h-4)/16), // font=2*(6x8) maxChars = Math.floor((w-4)/12), text=fitWords(options.body, maxRows, maxChars); - g.setColor(-1).setFont("6x8", 2).setFontAlign(-1, -1, 0).drawString(text, x+4, y+4); + g.setColor(g.theme.fg).setFont("6x8", 2).setFontAlign(-1, -1, 0).drawString(text, x+4, y+4); } - if (options.render) { - const area={x:x, y:y, w:w, h:h} - options.render(area); - } - if (options.on && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { + if (options.render) + options.render({x:x, y:y, w:w, h:h}); + if (options.on && !(require('Storage').readJSON('setting.json',1)||{}).quiet) Bangle.setLCDPower(1); // light up - } Bangle.on("touch", exports.hide); + if (options.onHide) + hideCallback = options.onHide; // Create a fake graphics to hide draw attempts oldg = g; g = Graphics.createArrayBuffer(8,8,1); @@ -108,6 +110,8 @@ exports.show = function(options) { exports.hide = function(options) { options = options||{}; if ("id" in options && options.id!==id) return; + if (hideCallback) hideCallback({id:id}); + hideCallback = undefined; id = null; if (oldg) { g=oldg; @@ -122,7 +126,7 @@ exports.hide = function(options) { Bangle.setLCDPower(1); } // hack for E.showMenu/showAlert/showPrompt - can force a redraw by faking next/back - if (Bangle.btnWatches) { + if (!Bangle.CLOCK && Bangle.btnWatches && Bangle.btnWatches.length==3) { global["\xff"].watches[Bangle.btnWatches[0]].callback(); global["\xff"].watches[Bangle.btnWatches[1]].callback(); } diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog index 2869074a6..57818c180 100644 --- a/apps/numerals/ChangeLog +++ b/apps/numerals/ChangeLog @@ -6,3 +6,5 @@ 0.06: Improve rendering of Numeral 1, fix issue with alarms not showing up 0.07: Add date on touch and some improvements (see settings and readme) 0.08: Add new draw styles, tidy up draw functionality +0.09: Tweak for faster rendering +0.10: Enhance for use with Bangle2, insert new draw mode 'thickfill' \ No newline at end of file diff --git a/apps/numerals/README.md b/apps/numerals/README.md index ebf4c10fe..7a8c25212 100644 --- a/apps/numerals/README.md +++ b/apps/numerals/README.md @@ -7,14 +7,20 @@ Settings can be accessed through the app/widget settings menu of the Bangle.js ### Color: * rnd - shows numerals in different color combinations every time the watches wakes -* r/g - red/green -* y/w - yellow/white -* o/c - orange/cyan -* b/y - blue/yellow'ish +* r/g - red/green (Bangle1/Bangle2) +* y/w - yellow/white (Bangle1 only) +* o/c - orange/cyan (Bangle1 only) +* b/y - blue/yellow'ish (Bangle1 only) +* r/g - red/green (Bangle2 only) +* g/b - green/blue (Bangle2 only) +* r/c - red/cyan (Bangle2 only) +* m/g - magenta/green (Bangle2 only) ### Draw mode * fill - fill numerals * frame - only shows outline of numerals +* framefill - frame with lighter color fill +* thickfill - thick frame in theme foreground color ### Menu button * choose button to start launcher menu with diff --git a/apps/numerals/bangle1-numerals-screenshot.png b/apps/numerals/bangle1-numerals-screenshot.png new file mode 100644 index 000000000..b663f5935 Binary files /dev/null and b/apps/numerals/bangle1-numerals-screenshot.png differ diff --git a/apps/numerals/numerals.app.js b/apps/numerals/numerals.app.js index 49a41732e..baf859915 100644 --- a/apps/numerals/numerals.app.js +++ b/apps/numerals/numerals.app.js @@ -6,7 +6,7 @@ * + see README.md for details */ -var numerals = { + var numerals = { 0:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,9],[30,25,61,25,69,33,69,67,61,75,30,75,22,67,22,33]], 1:[[50,1,82,1,90,9,90,92,82,100,73,100,65,92,65,27,50,27,42,19,42,9]], 2:[[9,1,82,1,90,9,90,53,82,61,21,61,21,74,82,74,90,82,90,92,82,100,9,100,1,92,1,48,9,40,70,40,70,27,9,27,1,19,1,9]], @@ -19,19 +19,20 @@ var numerals = { 9:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,82,9,74,69,74,69,61,9,61,1,53,1,9],[22,27,69,27,69,41,22,41]], }; var _12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false; -var _hCol = ["#ff5555","#ffff00","#FF9901","#2F00FF"]; -var _mCol = ["#55ff55","#ffffff","#00EFEF","#FFBF00"]; +var _hCol = []; +var _mCol = []; var _rCol = 0; +var scale = g.getWidth()/240; var interval = 0; const REFRESH_RATE = 10E3; var drawFuncs = { fill : function(poly,isHole){ - if (isHole) g.setColor(0); + if (isHole) g.setColor(g.theme.bg); g.fillPoly(poly,true); }, framefill : function(poly,isHole){ var c = g.getColor(); - g.setColor(isHole ? 0 : ((c&0b1111011111011110)>>1)); // 16 bit half bright + g.setColor(isHole ? g.theme.bg : ((c&0b1111011111011110)>>1)); // 16 bit half bright g.fillPoly(poly,true); g.setColor(c); g.drawPoly(poly,true); @@ -41,14 +42,23 @@ var drawFuncs = { }, thickframe : function(poly,isHole){ g.drawPoly(poly,true); - g.drawPoly(translate(1,0,poly),true); - g.drawPoly(translate(1,1,poly),true); - g.drawPoly(translate(0,1,poly),true); + g.drawPoly(translate(1,0,poly,1),true); + g.drawPoly(translate(1,1,poly,1),true); + g.drawPoly(translate(0,1,poly,1),true); + }, + thickfill : function(poly,isHole){ + if (isHole) g.setColor(g.theme.bg); + g.fillPoly(poly,true); + g.setColor(g.theme.fg); + g.drawPoly(translate(1,0,poly,1),true); + g.drawPoly(translate(1,1,poly,1),true); + g.drawPoly(translate(0,1,poly,1),true); } }; -function translate(tx, ty, p){ - return p.map((x, i)=> x+((i&1)?ty:tx)); +function translate(tx, ty, p, ascale){ + //return p.map((x, i)=> x+((i&1)?ty:tx)); + return g.transformVertices(p, {x:tx,y:ty,scale:ascale==undefined?scale:ascale}); } @@ -57,15 +67,14 @@ if (!settings) { settings = { color:0, drawMode:"fill", - menuButton:24, showDate:0 }; } function drawNum(num,col,x,y,func,funcName){ g.setColor(col); - let tx = x*100+25; - let ty = y*104+32; + let tx = (x*100+25) * scale; + let ty = (y*104+32) * scale; for (let i=0;i0); @@ -98,9 +107,21 @@ function setUpdateInt(set){ if (set) interval=setInterval(draw, REFRESH_RATE); } -Bangle.setLCDMode(); -g.reset().clear(); -setWatch(Bangle.showLauncher, settings.menuButton, {repeat:false,edge:"falling"}); +function setUp(){ + if (process.env.HWVERSION==1){ + _hCol = ["#ff5555","#ffff00","#FF9901","#2F00FF"]; + _mCol = ["#55ff55","#ffffff","#00EFEF","#FFBF00"]; + } else { + _hCol = ["#ff0000","#00ff00","#ff0000","#ff00ff"]; + _mCol = ["#00ff00","#0000ff","#00ffff","#00ff00"]; + } + if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length); +} + +setUp(); +g.clear(1); +// Show launcher when button pressed +Bangle.setUI("clock"); if (settings.color>0) _rCol=settings.color-1; setUpdateInt(1); draw(); @@ -110,11 +131,12 @@ if (settings.showDate) { } Bangle.on('lcdPower', function(on){ if (on){ - if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length); + setUp(); draw(); setUpdateInt(1); } else setUpdateInt(0); }); +Bangle.on('lock', () => setUp()); Bangle.loadWidgets(); -Bangle.drawWidgets(); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/numerals/numerals.settings.js b/apps/numerals/numerals.settings.js index 991abc888..ae321322a 100644 --- a/apps/numerals/numerals.settings.js +++ b/apps/numerals/numerals.settings.js @@ -6,16 +6,14 @@ numeralsSettings = { color:0, drawMode:"fill", - menuButton:22, showDate:0 }; updateSettings(); } let numeralsSettings = storage.readJSON('numerals.json',1); if (!numeralsSettings) resetSettings(); - if (numeralsSettings.menuButton===undefined) numeralsSettings.menuButton=22; - let dm = ["fill","frame","framefill","thickframe"]; - let col = ["rnd","r/g","y/w","o/c","b/y"]; + let dm = ["fill","frame","framefill","thickframe","thickfill"]; + let col = process.env.HWVERSION==1?["rnd","r/g","y/w","o/c","b/y"]:["rnd","r/g","g/b","r/c","m/g"]; let btn = [[24,"BTN1"],[22,"BTN2"],[23,"BTN3"],[11,"BTN4"],[16,"BTN5"]]; var menu={ "" : { "title":"Numerals"}, @@ -31,12 +29,6 @@ format: v=>dm[v], onchange: v=> { numeralsSettings.drawMode=dm[v]; updateSettings();} }, - "Menu button": { - value: btn.findIndex(e=>e[0]==numeralsSettings.menuButton), - min:0,max:btn.length-1, - format: v=>btn[v][1], - onchange: v=> { numeralsSettings.menuButton=btn[v][0]; updateSettings();} - }, "Date on touch": { value: 0|numeralsSettings.showDate, min:0,max:1, diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 64b39b509..6cb9d061e 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -3,3 +3,10 @@ 0.03: Show widgets (mainly so we can use the GPS recorder widget) 0.04: Move map rendering to a module (fix #396) 0.05: Show currently active gpsrec GPS trace (fix #395) +0.06: Add support for scrolling, option for 3 bit maps +0.07: Move to 96px tiles - less files (64 -> 25) and speed up rendering +0.08: Update for drag event refactor +0.09: Use current theme cols when drawing GPS info +0.10: Improve scale factor calculation to fix scaling issues (#984) +0.11: Add slight offset to OSM data to align it properly (fix #984) + Fix alignment of satellite info text diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index 940557361..62597ca20 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -8,6 +8,7 @@ function redraw() { m.draw(); drawMarker(); if (WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) { + g.flip(); // force immediate draw on double-buffered screens - track will update later g.setColor(0.75,0.2,0); WIDGETS["gpsrec"].plotTrack(m); } @@ -24,17 +25,14 @@ function drawMarker() { var fix; Bangle.on('GPS',function(f) { fix=f; - g.clearRect(0,y1,240,y1+8); - g.setColor(1,1,1); - g.setFont("6x8"); - g.setFontAlign(0,0); + g.reset().clearRect(0,y1,g.getWidth()-1,y1+8).setFont("6x8").setFontAlign(0,0); var txt = fix.satellites+" satellites"; if (!fix.fix) txt += " - NO FIX"; - g.drawString(txt,120,y1 + 4); + g.drawString(txt,g.getWidth()/2,y1 + 4); drawMarker(); }); -Bangle.setGPSPower(1); +Bangle.setGPSPower(1, "app"); if (HASWIDGETS) { Bangle.loadWidgets(); @@ -49,9 +47,25 @@ if (HASWIDGETS) { redraw(); -setWatch(function() { +function recenter() { if (!fix.fix) return; m.lat = fix.lat; m.lon = fix.lon; redraw(); -}, BTN2, {repeat:true}); +} + +setWatch(recenter, global.BTN2?BTN2:BTN1, {repeat:true}); + +var hasScrolled = false; +Bangle.on('drag',e=>{ + if (e.b) { + g.setClipRect(0,y1,g.getWidth()-1,y2); + g.scroll(e.dx,e.dy); + m.scroll(e.dx,e.dy); + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); + hasScrolled = true; + } else if (hasScrolled) { + hasScrolled = false; + redraw(); + } +}); diff --git a/apps/openstmap/custom.html b/apps/openstmap/custom.html index 81a55a4f8..56dea1188 100644 --- a/apps/openstmap/custom.html +++ b/apps/openstmap/custom.html @@ -32,6 +32,7 @@
+

3 bit