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 a454fec09..47233d1f5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ package-lock.json appdates.csv .vscode .idea/ +_config.yml +tests/Layout/bin/tmp.* +tests/Layout/testresult.bmp diff --git a/CHANGELOG.md b/CHANGELOG.md index c243093c6..86fe0c10c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,3 +29,4 @@ Changed for individual apps are listed in `apps/appname/ChangeLog` * Added progress bar on Bangle.js for uploads * Provide a proper error message in case JSON decode fails * Check you're connecting with a Bangle.js of the correct version +* Allow 'data' style app files to be uploaded with the app (and switch over settings files for various apps) diff --git a/README.md b/README.md index 240163f6c..20ae8afb2 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 @@ -249,14 +261,23 @@ and which gives information about the app for the Launcher. {"name":"appid.js", // filename to use in storage. // If name=='RAM', the code is sent directly to Bangle.js and is not saved to a file "url":"", // URL of file to load (currently relative to apps/) - "content":"..." // if supplied, this content is loaded directly - "evaluate":true // if supplied, data isn't quoted into a String before upload + "content":"...", // if supplied, this content is loaded directly + "evaluate":true, // if supplied, data isn't quoted into a String before upload // (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 {"name":"appid.data.json", // filename used in storage "storageFile":true // if supplied, file is treated as storageFile + "url":"", // if supplied URL of file to load (currently relative to apps/) + "content":"...", // if supplied, this content is loaded directly + "evaluate":true, // if supplied, data isn't quoted into a String before upload + // (eg it's evaluated as JS) }, {"wildcard":"appid.data.*" // wildcard of filenames used in storage }, // this is mutually exclusive with using "name" @@ -295,10 +316,10 @@ version of what's in `apps.json`: + + + diff --git a/apps/accelrec/README.md b/apps/accelrec/README.md index 40d981b6a..c9d91df38 100644 --- a/apps/accelrec/README.md +++ b/apps/accelrec/README.md @@ -1,7 +1,8 @@ # Acceleration Recorder -This app records a short period of acceleration data from the accelerometer -and +This app records a short period of acceleration data from the accelerometer at +100Hz (starting when acceleration happens) and graphs it, working out max acceleration +and max velocity. Data can also be downloaded to your PC. ## Usage diff --git a/apps/aclock/ChangeLog b/apps/aclock/ChangeLog index 9687bc58f..aa910b6f6 100644 --- a/apps/aclock/ChangeLog +++ b/apps/aclock/ChangeLog @@ -8,3 +8,5 @@ 0.11: shift face down for widget area, maximize face size, 0 pad single digit date, use locale for date 0.12: Fix regression after 0.11 0.13: Fix broken date padding (fix #376) +0.14: Remove hardcoded hour buzz (you can install widchime if you miss it) +0.15: Add color scheme support diff --git a/apps/aclock/README.md b/apps/aclock/README.md new file mode 100644 index 000000000..7d31cdae2 --- /dev/null +++ b/apps/aclock/README.md @@ -0,0 +1,4 @@ +# Analogue Clock + +![](screenshot_analog.png) + diff --git a/apps/aclock/clock-analog.js b/apps/aclock/clock-analog.js index 951145c4e..146f022fc 100644 --- a/apps/aclock/clock-analog.js +++ b/apps/aclock/clock-analog.js @@ -2,13 +2,16 @@ const locale = require('locale'); const p = Math.PI / 2; const pRad = Math.PI / 180; -const faceWidth = 100; // watch face radius (240/2 - 24px for widget area) +const faceWidth = g.getWidth()/2 - 20; // watch face radius (240/2 - 24px for widget area) const widgetHeight=24+1; let timer = null; let currentDate = new Date(); const centerX = g.getWidth() / 2; const centerY = (g.getWidth() / 2) + widgetHeight/2; - +g.theme.dark=false; +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; @@ -46,40 +49,35 @@ 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 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, 0, 0); + g.setColor("#f00"); g.setFont('6x8', 2); const dayString = locale.dow(currentDate, true); @@ -89,7 +87,7 @@ const drawDate = () => { // console.log(`${dayString}|${dateString}`); // center date const l = (g.getWidth() - g.stringWidth(dateDisplay)) / 2; - const t = centerY + 37; + const t = centerY + faceWidth*0.37; g.drawString(dateDisplay, l, t, true); // console.log(l, t); }; @@ -99,7 +97,7 @@ const onMinute = () => { resetSeconds(); } // clear existing hands - g.setColor(0, 0, 0); + g.setColor(g.theme.bg); // Hour hand((360 * (currentDate.getHours() + currentDate.getMinutes() / 60)) / 12, -8, faceWidth - 35); // Minute @@ -107,15 +105,12 @@ const onMinute = () => { // get new date, then draw new hands currentDate = new Date(); - g.setColor(1, 0.9, 0.9); + g.setColor(g.theme.fg); // Hour hand((360 * (currentDate.getHours() + currentDate.getMinutes() / 60)) / 12, -8, faceWidth - 35); - g.setColor(1, 1, 0.9); + g.setColor(g.theme.fg); // Minute hand((360 * currentDate.getMinutes()) / 60, -8, faceWidth - 10); - if (currentDate.getHours() >= 0 && currentDate.getMinutes() === 0) { - Bangle.buzz(); - } drawDate(); }; @@ -140,8 +135,9 @@ 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/aclock/screenshot_analog.png b/apps/aclock/screenshot_analog.png new file mode 100644 index 000000000..f0e84b428 Binary files /dev/null and b/apps/aclock/screenshot_analog.png differ diff --git a/apps/activepedom/ChangeLog b/apps/activepedom/ChangeLog index ca26a648a..66a1e8df9 100644 --- a/apps/activepedom/ChangeLog +++ b/apps/activepedom/ChangeLog @@ -1,4 +1,9 @@ 0.01: New Widget! 0.02: Distance calculation and display 0.03: Data logging and display -0.04: Steps are set to 0 in log on new day \ No newline at end of file +0.04: Steps are set to 0 in log on new day +0.05: Fix default step/distance display if it hasn't been set up first +0.06: Added WIDGETS.activepedom.getSteps() +0.07: Added settings to be able to hide line1 and line2 so there is no visible widget +0.08: Fixed zero steps issue caused by 0.07 +0.09: Prettied up user interface, decluttered graphs diff --git a/apps/activepedom/README.md b/apps/activepedom/README.md index a2a351a12..ac32a1dd6 100644 --- a/apps/activepedom/README.md +++ b/apps/activepedom/README.md @@ -31,6 +31,7 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a * Step detection sensitivity from firmware can be configured * Steps are saved to a file and read-in at start (to not lose step progress) * Settings can be changed in Settings - App/widget settings - Active Pedometer +* Can hide the widget display if required using settings ## Features App diff --git a/apps/activepedom/app.js b/apps/activepedom/app.js index ec9b1237f..12969a0a0 100644 --- a/apps/activepedom/app.js +++ b/apps/activepedom/app.js @@ -88,28 +88,40 @@ times = undefined; //steps - var csvFile = storage.open(filename, "r"); + csvFile = storage.open(filename, "r"); steps = getArrayFromCSV(csvFile, 1); first = first + " " + steps[0] + "/" + setting('stepGoal'); last = last + " " + steps[steps.length-1] + "/" + setting('stepGoal'); //define y-axis grid labels stepsLastEntry = steps[steps.length-1]; - if (stepsLastEntry < 1000) gridyValue = 100; - if (stepsLastEntry >= 1000 && stepsLastEntry < 10000) gridyValue = 1000; - if (stepsLastEntry > 10000) gridyValue = 5000; + // the labels on the y axis are fairly unreadable so minimise them + if (stepsLastEntry < 1000) gridyValue = 500; + if (stepsLastEntry >= 1000 && stepsLastEntry < 2000) gridyValue = 1000; + if (stepsLastEntry >= 2000 && stepsLastEntry < 5000) gridyValue = 2000; + if (stepsLastEntry >= 5000 && stepsLastEntry < 10000) gridyValue = 5000; + if (stepsLastEntry >= 10000 && stepsLastEntry < 20000) gridyValue = 10000; + if (stepsLastEntry > 20000) gridyValue = 20000; - //draw - drawMenu(); - g.drawString("First: " + first, 10, 30); - g.drawString(" Last: " + last, 10, 40); + // draw the chart + g.clear(); + g.setFont("6x8", 2); + g.setColor(1,1,1); require("graph").drawLine(g, steps, { - //title: "Steps Counted", + //title: "Steps", axes : true, gridy : gridyValue, y : 60, //offset on screen x : 5, //offset on screen }); + + // show steps and duration of the chart + g.setFont("6x8", 2); + g.setColor(0,1,0); + g.drawString("steps", 30, 24); + g.drawString(stepsLastEntry, 30, 44); + g.drawString((history/3600000) + " hrs", 30, 64); + //free memory from big variables allData = undefined; allDataFile = undefined; @@ -117,13 +129,48 @@ times = undefined; } - function drawMenu () { - g.clear(); - g.setFont("6x8", 1); - g.drawString("BTN1:Timespan | BTN2:Draw", 20, 10); - g.drawString("Timespan: " + history/1000/60/60 + " hours", 20, 20); + function getImage() { + return require("heatshrink").decompress(atob("mEwwIGDvAEDgP+ApMD/4FVEZY1FABcP8AFDn/wAod/AocB//4AoUHAokPAokf5/8AocfAoc+j5HDvgFEvEf7+AAoP4AoJCC+E/54qCsE/wYkDn+AAos8AohZDj/AAohrEp4FEs5xEuJfDgF5Aon4GgYFBGgZOBnyJD+EeYgfgj4FEh6VD4AFDh+AAIJMCBoIFFLQQtBgYFCHIIFDjA3BC4I=")); } + function drawMenu() { + var x = 100; + var y = 24; + var stps ="-"; + var y_inc = 25; + + g.clear(); + g.setColor(1,1,1); + g.drawImage(getImage(),0 ,60 , {scale:2} ); + g.setFont("6x8",2); + + // timespan + g.setColor('#7f8c8d'); + g.setFontAlign(-1,0); + g.drawString("Timespan", x, y, true); + y += y_inc; + g.setColor('#bdc3c7'); + g.drawString(history/1000/60/60 + " hrs" , x, y, true); + + // BTN1 info + y += 2*y_inc; + g.setColor('#7f8c8d'); + g.setFontAlign(-1,0); + g.drawString("BTN1", x, y, true); + y += y_inc; + g.setColor('#bdc3c7'); + g.drawString("Timespan", x, y, true); + + // BTN2 info + y += 2*y_inc; + g.setColor('#7f8c8d'); + g.setFontAlign(-1,0); + g.drawString("BTN2", x, y, true); + y += y_inc; + g.setColor('#bdc3c7'); + g.drawString("Draw", x, y, true); + } + setWatch(function() { //BTN1 switch(history) { case 3600000 : //1h @@ -140,7 +187,9 @@ }, BTN1, {edge:"rising", debounce:50, repeat:true}); setWatch(function() { //BTN2 - g.setFont("6x8", 2); + g.clear(); + g.setColor(1,1,1); + g.setFont("6x8", 3); g.drawString ("Drawing...",30,60); drawGraph(); }, BTN2, {edge:"rising", debounce:50, repeat:true}); @@ -161,5 +210,4 @@ } drawMenu(); - -})(); \ No newline at end of file +})(); diff --git a/apps/activepedom/settings.js b/apps/activepedom/settings.js index 94ae435d2..3b64d8735 100644 --- a/apps/activepedom/settings.js +++ b/apps/activepedom/settings.js @@ -4,7 +4,7 @@ */ (function(back) { const SETTINGS_FILE = 'activepedom.settings.json'; - const LINES = ['Steps', 'Distance']; + const LINES = ['Steps', 'Distance', 'Hide']; // initialize with default settings... let s = { @@ -109,4 +109,4 @@ }, }; E.showMenu(menu); -}); \ No newline at end of file +}); diff --git a/apps/activepedom/widget.js b/apps/activepedom/widget.js index ed91a4cfd..62c2d857a 100644 --- a/apps/activepedom/widget.js +++ b/apps/activepedom/widget.js @@ -68,6 +68,8 @@ 'stepSensitivity' : 80, 'stepGoal' : 10000, 'stepLength' : 75, + 'lineOne' : "Distance", + 'lineTwo' : "Steps", }; if (!settings) { loadSettings(); } return (key in settings) ? settings[key] : DEFAULTS[key]; @@ -139,6 +141,7 @@ function draw() { var height = 23; //width is deined globally + distance = (stepsCounted * setting('stepLength')) / 100 /1000; //distance in km //Check if same day @@ -152,6 +155,12 @@ stepsOutsideTime = 0; } lastUpdate = date; + + // not everyone likes a widget, having refreshed lastUpdate we can exit + if (setting('lineOne') == 'Hide' && setting('lineTwo') == 'Hide') { + settings = 0; //reset settings to save memory + return; + } g.reset(); g.clearRect(this.x, this.y, this.x+width, this.y+height); @@ -160,7 +169,6 @@ if (active == 1) g.setColor(0x07E0); //green else g.setColor(0xFFFF); //white g.setFont("6x8", 2); - if (setting('lineOne') == 'Steps') { g.drawString(kFormatterSteps(stepsCounted),this.x+1,this.y); //first line, big number, steps } @@ -227,6 +235,6 @@ setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive) timerStoreData = setInterval(storeData, storeDataInterval); //store data regularly - //Add widget - WIDGETS["activepedom"]={area:"tl",width:width,draw:draw}; -})(); \ No newline at end of file + //Add widget, use: WIDGETS.activepedom.getSteps() inside another App to return todays step count + WIDGETS["activepedom"]={area:"tl",width:width,draw:draw, getSteps:()=>stepsCounted}; +})(); diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index 23b8ee562..d129e9f9f 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -8,3 +8,8 @@ 0.08: Make alarm scheduling more reliable 0.09: Add per alarm auto-snooze option 0.10: Fix auto-snooze option (this stopped new alarms being added) (fix #506) +0.11: Respect Quiet Mode +0.12: Fix widget for bangle 2, now uses theme + Widgets now shown on Alarm screen +0.13: Alarm widget state now updates when setting/resetting an alarm +0.14: Order of 'back' menu item diff --git a/apps/alarm/alarm.js b/apps/alarm/alarm.js index 28261110a..bb5722106 100644 --- a/apps/alarm/alarm.js +++ b/apps/alarm/alarm.js @@ -18,8 +18,10 @@ function showAlarm(alarm) { var buzzCount = 10; if (alarm.msg) msg += "\n"+alarm.msg; + Bangle.loadWidgets(); + Bangle.drawWidgets(); E.showPrompt(msg,{ - title:"ALARM!", + title:alarm.timer ? "TIMER!" : "ALARM!", buttons : {"Sleep":true,"Ok":false} // default is sleep so it'll come back in 10 mins }).then(function(sleep) { buzzCount = 0; @@ -38,6 +40,7 @@ function showAlarm(alarm) { load(); }); function buzz() { + if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence Bangle.buzz(100).then(()=>{ setTimeout(()=>{ Bangle.buzz(100).then(function() { diff --git a/apps/alarm/app.js b/apps/alarm/app.js index b6019ca08..53c7154bc 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -9,6 +9,7 @@ var alarms = require("Storage").readJSON("alarm.json",1)||[]; last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day! rp : true, // repeat as : false, // auto snooze + timer : 5, // OPTIONAL - if set, this is a timer and it's the time in minutes } ];*/ @@ -18,6 +19,12 @@ function formatTime(t) { return hrs+":"+("0"+mins).substr(-2); } +function formatMins(t) { + mins = (0|t)%60; + hrs = 0|(t/60); + return hrs+":"+("0"+mins).substr(-2); +} + function getCurrentHr() { var time = new Date(); return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600); @@ -25,17 +32,25 @@ function getCurrentHr() { function showMainMenu() { const menu = { - '': { 'title': 'Alarms' }, - 'New Alarm': ()=>editAlarm(-1) + '': { 'title': 'Alarm/Timer' }, + '< Back' : ()=>{load();}, + 'New Alarm': ()=>editAlarm(-1), + 'New Timer': ()=>editTimer(-1) }; alarms.forEach((alarm,idx)=>{ - txt = (alarm.on?"on ":"off ")+formatTime(alarm.hr); - if (alarm.rp) txt += " (repeat)"; + if (alarm.timer) { + txt = "TIMER "+(alarm.on?"on ":"off ")+formatMins(alarm.timer); + } else { + txt = "ALARM "+(alarm.on?"on ":"off ")+formatTime(alarm.hr); + if (alarm.rp) txt += " (repeat)"; + } menu[txt] = function() { - editAlarm(idx); + if (alarm.timer) editTimer(idx); + else editAlarm(idx); }; }); - menu['< Back'] = ()=>{load();}; + + if (WIDGETS["alarm"]) WIDGETS["alarm"].reload(); return E.showMenu(menu); } @@ -55,7 +70,8 @@ function editAlarm(alarmIndex) { as = a.as; } const menu = { - '': { 'title': 'Alarms' }, + '': { 'title': 'Alarm' }, + '< Back' : showMainMenu, 'Hours': { value: hrs, onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this' @@ -105,7 +121,60 @@ function editAlarm(alarmIndex) { showMainMenu(); }; } - menu['< Back'] = showMainMenu; + return E.showMenu(menu); +} + +function editTimer(alarmIndex) { + var newAlarm = alarmIndex<0; + var hrs = 0; + var mins = 5; + var en = true; + if (!newAlarm) { + var a = alarms[alarmIndex]; + mins = (0|a.timer)%60; + hrs = 0|(a.timer/60); + en = a.on; + } + const menu = { + '': { 'title': 'Timer' }, + 'Hours': { + value: hrs, + onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this' + }, + 'Minutes': { + value: mins, + onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this' + }, + 'Enabled': { + value: en, + format: v=>v?"On":"Off", + onchange: v=>en=v + } + }; + function getTimer() { + var d = new Date(Date.now() + ((hrs*60)+mins)*60000); + var hr = d.getHours() + (d.getMinutes()/60) + (d.getSeconds()/3600); + // Save alarm + return { + on : en, + timer : (hrs*60)+mins, + hr : hr, + rp : false, as: false + }; + } + menu["> Save"] = function() { + if (newAlarm) alarms.push(getTimer()); + else alarms[alarmIndex] = getTimer(); + require("Storage").write("alarm.json",JSON.stringify(alarms)); + showMainMenu(); + }; + if (!newAlarm) { + menu["> Delete"] = function() { + alarms.splice(alarmIndex,1); + require("Storage").write("alarm.json",JSON.stringify(alarms)); + showMainMenu(); + }; + } return E.showMenu(menu); } diff --git a/apps/alarm/widget.js b/apps/alarm/widget.js index dbe91b3dd..e8bb79fc7 100644 --- a/apps/alarm/widget.js +++ b/apps/alarm/widget.js @@ -1,11 +1,7 @@ -(() => { - var alarms = require('Storage').readJSON('alarm.json',1)||[]; - alarms = alarms.filter(alarm=>alarm.on); - if (!alarms.length) return; // no alarms, no widget! - delete alarms; - // add the widget - WIDGETS["alarm"]={area:"tl",width:24,draw:function() { - g.setColor(-1); - g.drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y); - }}; -})() +WIDGETS["alarm"]={area:"tl",width:0,draw:function() { + if (this.width) g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y); + },reload:function() { + WIDGETS["alarm"].width = (require('Storage').readJSON('alarm.json',1)||[]).some(alarm=>alarm.on) ? 24 : 0; + } +}; +WIDGETS["alarm"].reload(); diff --git a/apps/alpinenav/README.md b/apps/alpinenav/README.md new file mode 100644 index 000000000..d18cdfd6d --- /dev/null +++ b/apps/alpinenav/README.md @@ -0,0 +1,30 @@ +Alpine Navigator +================ +App that performs GPS monitoring to track and display position relative to a given origin in realtime. + +![screenshot](./sample.png) + +Functions +--------- +Note if you've not used GPS yet I suggest using one of the GPS apps to get your first fix and confirm as I've found that helps initially. + +The GPS and magnetometer will be turned on and after a few moments, when the watch buzzes and the dot turns from red to pink, that means the GPS is fixed. all your movements now will be displayed with a line drawn back to show your position relative to the start. New waypoints will be added based on checking every 10 seconds for at least 5 meters of movement. The map will scale to your distance travelled so the route will always remain within the window, the accelerometer/pedometer is not used - this is a purely GPS and compass solution so can be used for driving/cycling etc. A log file will be recorded that tracks upto 1000 waypoints, this isn't a big file and you could remove the limit but I've kept it fairly conservative here as it's not intended as a main feature, there's already good GPS recorders for the Bangle. The following other items are displayed: + +1. altitude at origin, this is displayed left of the centre. +2. current altitude, displayed centre right +3. distance from origin, bottom left (meters) +4. distance travelled, bottom right (meters) +5. compass heading, at the top + +For the display, the route is kept at a set resolution, so there's no risk of running into memory problems if you run this for long periods or any length of time because the waypoints will be reduced when it reaches a set threshold so you may see the path smooth out slightly at intervals. + +If you get strange values or dashes for the compass, it just needs calibration so you need to move the watch around briefly for this each time, ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or Dr Strange making a Sling Ring but often just moving your wrist a bit is enough. + +The buttons do the following: +BTN1: this will display an 'X' in the bottom of the screen and lock all the buttons, this is to prevent you accidentally pressing either of the below. Remember to press this again to unlock it! soft and hard reset will both still work. +BTN2: this removes all waypoints aside from the origin and your current location; sometimes during smaller journeys and walks, the GPS can give sporadic differences in locations because of the error margins of GPS and this can add noise to your route. +BTN3: this will pause the GPS and magnetometer, useful for saving power for situations where you don't necessarily need to track parts of your route e.g. you're going indoors/shelter for some time. You'll know it's paused because the compass won't update it's reading and all the metrics will be blacked out on the screen. + +Things to do next +----------------- +There's a GPS widget that's been developed to leverage low-power mode capability on the sensor, will look to incorporate that. diff --git a/apps/alpinenav/app-icon.js b/apps/alpinenav/app-icon.js new file mode 100644 index 000000000..dba084202 --- /dev/null +++ b/apps/alpinenav/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mUywkEIf4A/AHUBiAYWgcwDC0v+IYW///C6sC+c/kAYUj/xj/wDCgvBgfyVihhBAQQASh6TCMikvYoRkU/73CMicD+ZnFViJFBj5MBMiU/+IuBJoJkRCoUvfIPy/5kQVgM//7gBC4KCDFxSsDgTHCl8QWgaRKmBJBFIzmDSJXzYBECWobbJAAKNIMhYlBOoK/IMhZXCmYMLABAkCS4RkSXZoNJRBo/CgK6UBwTWBBIs/SJBAGl7UFegIXMaogHEehAAHj/yIYsfehAAGMQISFMRxbCiEDU4ZiQZY5iQZYpiSbQ8/cwzLOCiQA/AH4A1A")) \ No newline at end of file diff --git a/apps/alpinenav/app-icon.png b/apps/alpinenav/app-icon.png new file mode 100644 index 000000000..f28a733d8 Binary files /dev/null and b/apps/alpinenav/app-icon.png differ diff --git a/apps/alpinenav/app.js b/apps/alpinenav/app.js new file mode 100644 index 000000000..29eeab0c9 --- /dev/null +++ b/apps/alpinenav/app.js @@ -0,0 +1,237 @@ +var background_colour = "#000000"; +var foregound_colour = "#ccff99"; +var position_colour = "#ff3329"; +var log_limit = 1000; +var max_array_size = 50; +var pause_tracker = false; +var temp; +var file; +var d; +var origin_lat; +var origin_lon; +var current_lat; +var current_lon; +var current_speed; +var distance_from_origin = 0; +var distane_travelled = 0; +var log_size; +var waypoints = []; +var start_alt = 0; +var current_alt = 0; +var button_lock = false; +var display_waypoints = []; +var waypoint = { + lat: "", + lon: "" +}; +var compass_heading = "---"; + +function calcCrow(lat1, lon1, lat2, lon2) { + var R = 6371e3; + var dLat = toRad(lat2 - lat1); + var dLon = toRad(lon2 - lon1); + lat1 = toRad(lat1); + lat2 = toRad(lat2); + + var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + var d = R * c; + return d; +} + +function toRad(Value) { + return Value * Math.PI / 180; +} + +function draw() { + if (pause_tracker) + g.setColor(background_colour); + else + g.setColor(foregound_colour); + g.setFont("6x8", 2); + g.setFontAlign(0, 1); + g.drawString(distance_from_origin.toFixed(0), 40, 220, true); + g.drawString(distane_travelled.toFixed(0), 200, 220, true); + g.setFont("6x8", 1); + g.drawString(start_alt.toFixed(0), 40, 120, true); + g.drawString(current_alt.toFixed(0), 200, 120, true); + if (button_lock) { + g.setFont("6x8", 2); + g.setFontAlign(0, 0); + g.drawString("X", 120, 220, true); + g.setFont("6x8", 1); + } +} + +function cull_array() { + for (var i = 2; i <= waypoints.length; i += 2) + waypoints.splice(i, 1); +} + +function process_and_display() { + g.setColor(background_colour); + g.fillRect(10, 65, 230, 230); + g.setColor(foregound_colour); + if (waypoints.length > max_array_size) + cull_array(); + rescale(); + if (display_waypoints.length > 0) { + for (let x = 0; x < display_waypoints.length - 1; x++) { + g.reset(); + g.setColor(foregound_colour); + g.drawLine(display_waypoints[x].lon, display_waypoints[x].lat, display_waypoints[x + 1].lon, display_waypoints[x + 1].lat); + } + g.reset(); + g.setColor(position_colour); + g.fillCircle(display_waypoints[display_waypoints.length - 1].lon, display_waypoints[display_waypoints.length - 1].lat, 3); + } +} + +function process_GPS() { + if (waypoints.length > 0) { + //check distance + temp_distance = calcCrow(current_lat, current_lon, waypoints[waypoints.length - 1].lat, waypoints[waypoints.length - 1].lon); + if (temp_distance > 5) { + var temp = Object.create(waypoint); + temp.lat = current_lat; + temp.lon = current_lon; + waypoints.push(temp); + distane_travelled += temp_distance; + distance_from_origin = calcCrow(current_lat, current_lon, waypoints[0].lat, waypoints[0].lon); + process_and_display(); + if (log_size < log_limit) { + var csv = [ + d, + origin_lat - current_lat, + current_lon - origin_lon, + current_speed, + current_alt + ]; + file.write(csv.join(",") + "\n"); + log_size += 1; + } + } + } + else { + g.setColor(position_colour); + g.fillCircle(120, 120, 3); + } + draw(); +} + +function rescale() { + var max_val = 0; + display_waypoints = []; + + for (let x = 0; x < waypoints.length; x++) { + if (Math.abs(waypoints[x].lat) > max_val) + max_val = Math.abs(waypoints[x].lat); + if (Math.abs(waypoints[x].lon) > max_val) + max_val = Math.abs(waypoints[x].lon); + } + + scaler = 60 / max_val; + + for (let x = 0; x < waypoints.length; x++) { + temp = Object.create(waypoint); + temp.lat = 140 - Math.round(waypoints[x].lat * scaler); + temp.lon = 120 - Math.round(waypoints[x].lon * scaler); + display_waypoints.push(temp); + } +} + +Bangle.setCompassPower(1); +Bangle.setGPSPower(1); +g.clear(); +process_GPS(); +var poll_GPS = setInterval(process_GPS, 9000); + +setWatch(function () { + if (!button_lock) { + waypoints.splice(1); + process_GPS(); + } +}, BTN2, { repeat: true, edge: "falling" }); + +setWatch(function () { + if (!button_lock) { + if (!pause_tracker) { + Bangle.setCompassPower(0); + Bangle.setGPSPower(0); + pause_tracker = true; + } + else { + Bangle.setCompassPower(1); + Bangle.setGPSPower(1); + pause_tracker = false; + } + } +}, BTN3, { repeat: true, edge: "falling" }); + +setWatch(function () { + if (button_lock) { + button_lock = false; + g.setFontAlign(0, 0); + g.drawString(" ", 120, 220, true); + } + else { + button_lock = true; + g.setFontAlign(0, 0); + g.drawString("X", 120, 220, true); + } +}, BTN1, { repeat: true, edge: "falling" }); + +Bangle.on('GPS', function (g) { + if (g.fix) { + if (waypoints.length == 0) { + file = require("Storage").open("alpine_log.csv", "w"); + file.write(""); + file = require("Storage").open("alpine_log.csv", "a"); + Bangle.buzz(); + position_colour = 0xF81F; + origin_lat = g.lat; + origin_lon = g.lon; + start_alt = g.alt; + current_speed = g.speed; + sat_count = g.satellites; + var csv = [ + d, + origin_lat, + origin_lon, + current_speed, + current_alt + ]; + file.write(csv.join(",") + "\n"); + var temp = Object.create(waypoint); + temp.lat = 0; + temp.lon = 0; + process_GPS(); + waypoints.push(temp); + } + else { + current_lat = g.lat - origin_lat; + current_lon = origin_lon - g.lon; + current_speed = g.speed; + sat_count = g.satellites; + current_alt = g.alt; + gps_time = g.time; + } + } +}); + +Bangle.on('mag', function (m) { + if (isNaN(m.heading)) + compass_heading = "---"; + else + compass_heading = 360 - Math.round(m.heading); + current_colour = g.getColor(); + g.reset(); + g.setColor(background_colour); + g.fillRect(140, 30, 190, 55); + g.setColor(foregound_colour); + g.setFont("6x8", 2); + if(compass_heading<100) + compass_heading = " " + compass_heading.toString(); + g.drawString(compass_heading, 150, 15, true); +}); diff --git a/apps/alpinenav/sample.png b/apps/alpinenav/sample.png new file mode 100644 index 000000000..07c7d3c6b Binary files /dev/null and b/apps/alpinenav/sample.png differ diff --git a/apps/analogimgclk/ChangeLog b/apps/analogimgclk/ChangeLog index 864afc91e..877ecc04d 100644 --- a/apps/analogimgclk/ChangeLog +++ b/apps/analogimgclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Add BTN2 -> launcher +0.03: Update to use setUI diff --git a/apps/analogimgclk/app.js b/apps/analogimgclk/app.js index 99dace78e..1aea97961 100644 --- a/apps/analogimgclk/app.js +++ b/apps/analogimgclk/app.js @@ -114,5 +114,5 @@ if (g.drawImages) { E.showMessage("Please update\nBangle.js firmware\nto use this clock","analogimgclk"); } -// 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/android/ChangeLog b/apps/android/ChangeLog new file mode 100644 index 000000000..e881c9ec2 --- /dev/null +++ b/apps/android/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Remove messages on disconnect + Fix music control diff --git a/apps/android/app-icon.js b/apps/android/app-icon.js new file mode 100644 index 000000000..9253ec839 --- /dev/null +++ b/apps/android/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4MA///xF9FstggwFDuEOAoc//gFJv/+AoZHBAgUB8/nwAFCBYIFCgYFB4AFHABdjCIPGAoPzAoPPAvpHFMpYFFPosAnk8NgYFdjEYfMo=")) diff --git a/apps/android/app.js b/apps/android/app.js new file mode 100644 index 000000000..b210886fd --- /dev/null +++ b/apps/android/app.js @@ -0,0 +1,2 @@ +// Config app not implemented yet +setTimeout(()=>load("messages.app.js"),10); diff --git a/apps/android/app.png b/apps/android/app.png new file mode 100644 index 000000000..65150f08d Binary files /dev/null and b/apps/android/app.png differ diff --git a/apps/android/boot.js b/apps/android/boot.js new file mode 100644 index 000000000..4b6c2c6ff --- /dev/null +++ b/apps/android/boot.js @@ -0,0 +1,62 @@ +(function() { + function gbSend(message) { + Bluetooth.println(""); + Bluetooth.println(JSON.stringify(message)); + } + + var _GB = global.GB; + global.GB = (event) => { + // feed a copy to other handlers if there were any + if (_GB) setTimeout(_GB,0,Object.assign({},event)); + + /* TODO: Call handling, fitness */ + var HANDLERS = { + // {t:"notify",id:int, src,title,subject,body,sender,tel:string} add + "notify" : function() { event.t="add";require("messages").pushMessage(event); }, + // {t:"notify~",id:int, title:string} // modified + "notify~" : function() { event.t="modify";require("messages").pushMessage(event); }, + // {t:"notify-",id:int} // remove + "notify-" : function() { event.t="remove";require("messages").pushMessage(event); }, + // {t:"find", n:bool} // find my phone + "find" : function() { + if (Bangle.findDeviceInterval) { + clearInterval(Bangle.findDeviceInterval); + delete Bangle.findDeviceInterval; + } + if (event.n) // Ignore quiet mode: we always want to find our watch + Bangle.findDeviceInterval = setInterval(_=>Bangle.buzz(),1000); + }, + // {t:"musicstate", state:"play/pause",position,shuffle,repeat} + "musicstate" : function() { + require("messages").pushMessage({t:"modify",id:"music",title:"Music",state:event.state}); + }, + // {t:"musicinfo", artist,album,track,dur,c(track count),n(track num} + "musicinfo" : function() { + require("messages").pushMessage(Object.assign(event, {t:"modify",id:"music",title:"Music"})); + }, + // {"t":"call","cmd":"incoming/end","name":"Bob","number":"12421312"}) + "call" : function() { + event.t=t.cmd=="incoming"?"add":"remove"; + event.id="call"; + require("messages").pushMessage(event); + }, + }; + var h = HANDLERS[event.t]; + if (h) h(); else console.log("GB Unknown",event); + }; + + // Battery monitor + function sendBattery() { gbSend({ t: "status", bat: E.getBattery() }); } + NRF.on("connect", () => setTimeout(sendBattery, 2000)); + NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect + setInterval(sendBattery, 10*60*1000); + // Health tracking + Bangle.on('health', health=>{ + gbSend({ t: "act", stp: health.steps, hrm: health.bpm }); + }); + // Music control + Bangle.musicControl = cmd => { + // play/pause/next/previous/volumeup/volumedown + gbSend({ t: "music", n:cmd }); + } +})(); diff --git a/apps/animclk/ChangeLog b/apps/animclk/ChangeLog index 7852105a0..348448c34 100644 --- a/apps/animclk/ChangeLog +++ b/apps/animclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Fix bug if image clock wasn't installed +0.03: Update to use setUI diff --git a/apps/animclk/app.js b/apps/animclk/app.js index ced5372a0..4bf63daf6 100644 --- a/apps/animclk/app.js +++ b/apps/animclk/app.js @@ -102,5 +102,5 @@ if (g.drawImages) { } else { E.showMessage("Please update\nBangle.js firmware\nto use this clock","animclk"); } -// 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/antonclk/ChangeLog b/apps/antonclk/ChangeLog new file mode 100644 index 000000000..f88276a90 --- /dev/null +++ b/apps/antonclk/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Load widgets after setUI so widclk knows when to hide +0.03: Clock now shows day of week under date. diff --git a/apps/antonclk/app-icon.js b/apps/antonclk/app-icon.js new file mode 100644 index 000000000..fad03d50f --- /dev/null +++ b/apps/antonclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgX/AH4A/AAf+14BBAoPq/Wq1QFB+EP+kLAoNA+CdB3//yEP6ALB/sABYf8gEMgALB6ALJqoLB+tVgH//kQhkQhUABYImCIwPwh3whcA94UCgJHD+AXB/4LCcQQLCKYQLB+EDBZQFCBY/Qh4LJ4ALLl4LFioLCgE/KZMAv4XFBYimBC4/+BYw7DRwQLIV4ILWTQQLDOIUA/ALDAC+t1uv/263/6/Pq/YLC3vv//vM4Oq33rBYP6/u//2/C4Pr1YXC12vBwO+BYO69XmJDQA/ACIA==")) diff --git a/apps/antonclk/app.js b/apps/antonclk/app.js new file mode 100644 index 000000000..7912dfc0f --- /dev/null +++ b/apps/antonclk/app.js @@ -0,0 +1,61 @@ +Graphics.prototype.setFontAnton = function(scale) { +// Actual height 69 (68 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78+(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 = g.getHeight()/2; + g.reset(); + var date = new Date(); + var timeStr = require("locale").time(date,1); + var dateStr = require("locale").date(date).toUpperCase(); + var dowStr = require("locale").dow(date).toUpperCase(); + // draw time + g.setFontAlign(0,0).setFont("Anton"); + g.clearRect(0,y-40,g.getWidth(),y+35); // clear the background + g.drawString(timeStr,x,y); + // draw date + y += 40; + g.setFontAlign(0,0).setFont("6x8",2); + g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background + g.drawString(dateStr,x,y); + //draw day of week + y += 16; + g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background + g.drawString(dowStr,x,y); + // queue draw in one minute + queueDraw(); +} + +// Clear the screen once, at startup +g.clear(); +// draw immediately at first, queue update +draw(); +// 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 +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/antonclk/app.png b/apps/antonclk/app.png new file mode 100644 index 000000000..d96f17758 Binary files /dev/null and b/apps/antonclk/app.png differ diff --git a/apps/antonclk/screenshot.png b/apps/antonclk/screenshot.png new file mode 100644 index 000000000..c66f8bdd8 Binary files /dev/null and b/apps/antonclk/screenshot.png differ diff --git a/apps/arrow/ChangeLog b/apps/arrow/ChangeLog new file mode 100644 index 000000000..edd5ccb3d --- /dev/null +++ b/apps/arrow/ChangeLog @@ -0,0 +1,6 @@ +0.01: First version +0.02: Moved arrow image load to global scope +0.03: faster drawCompass() function, does not cause buttons to become unresponsive +0.04: removed LED1.write() as it was keeping LCD on +0.05: Turn compass off when screen off + Calibrate at start if no info diff --git a/apps/arrow/README.md b/apps/arrow/README.md new file mode 100644 index 000000000..4b77dbc42 --- /dev/null +++ b/apps/arrow/README.md @@ -0,0 +1,45 @@ +# Arrow Compass + +A variation of jeffmer's Navigation Compass. The compass points +North and shows the current heading. + +This is a tilt and roll compensated compass with a linear +display. The compass will display the same direction that it shows +when flat as when it is tilted (rotation around the W-S axis) or +rolled (rotation around the N-S) axis. *Even with compensation, it +would be beyond foolish to rely solely on this app for any serious +navigational purpose.* + +![](arrow_screenshot.jpg) + +## 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`. + +It is also worth noting that the presence of the magnetic charging +clamps will require the compass to be recalibrated after every +charge. + +## Controls + +*BTN1* - switches to your selected clock app. + +*BTN2* - switches to the app launcher. + +*BTN3* - invokes calibration ( can be cancelled if pressed accidentally) + + +## Issues +* detect when calibration data is missing + +## Acknowledgement + +This app is based in the work done by [jeffmer](https://github.com/jeffmer/JeffsBangleAppsDev) + diff --git a/apps/arrow/app.js b/apps/arrow/app.js new file mode 100644 index 000000000..f1f85e880 --- /dev/null +++ b/apps/arrow/app.js @@ -0,0 +1,202 @@ +var pal1color = new Uint16Array([g.theme.bg,0xFFC0],0,1); +var pal2color = new Uint16Array([g.theme.bg,g.theme.fg],0,1); +var buf1 = Graphics.createArrayBuffer(128,128,1,{msb:true}); +var buf2 = Graphics.createArrayBuffer(80,40,1,{msb:true}); +var intervalRef; +var bearing=0; // always point north +var heading = 0; +var oldHeading = 0; +var candraw = false; +var isCalibrating = false; +var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; + +function flip1(x,y) { + g.drawImage({width:128,height:128,bpp:1,buffer:buf1.buffer, palette:pal1color},x,y); + buf1.clear(); +} + +function flip2(x,y) { + g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal2color},x,y); + buf2.clear(); +} + +function radians(d) { + return (d*Math.PI) / 180; +} + +// takes 32ms +function drawCompass(hd) { + if(!candraw) return; + if (Math.abs(hd - oldHeading) < 2) return 0; + hd=hd*Math.PI/180; + var p = [0, 1.1071, Math.PI/4, 2.8198, 3.4633, 7*Math.PI/4 , 5.1760]; + + // using polar cordinates, 64,64 is the offset from the 0,0 origin + var poly = [ + 64+60*Math.sin(hd+p[0]), 64-60*Math.cos(hd+p[0]), + 64+44.7214*Math.sin(hd+p[1]), 64-44.7214*Math.cos(hd+p[1]), + 64+28.2843*Math.sin(hd+p[2]), 64-28.2843*Math.cos(hd+p[2]), + 64+63.2455*Math.sin(hd+p[3]), 64-63.2455*Math.cos(hd+p[3]), + 64+63.2455*Math.sin(hd+p[4]), 64-63.2455*Math.cos(hd+p[4]), + 64+28.2843*Math.sin(hd+p[5]), 64-28.2843*Math.cos(hd+p[5]), + 64+44.7214*Math.sin(hd+p[6]), 64-44.7214*Math.cos(hd+p[6]) + ]; + + buf1.fillPoly(poly); + flip1(56, 56); +} + +// stops violent compass swings and wobbles, takes 3ms +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; +} + +// takes approx 7ms +function tiltfixread(O,S){ + var start = Date.now(); + 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; +} + +function reading(m) { + var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale); + heading = newHeading(d,heading); + var dir = bearing - heading; + if (dir < 0) dir += 360; + if (dir > 360) dir -= 360; + drawCompass(dir); // we want compass to show us where to go + oldHeading = dir; + buf2.setColor(1); + buf2.setFontAlign(-1,-1); + buf2.setFont("Vector",38); + var course = Math.round(heading); + var cs = course.toString(); + cs = course<10?"00"+cs : course<100 ?"0"+cs : cs; + buf2.drawString(cs,0,0); + flip2(90, 200); +} + +function calibrate(){ + var max={x:-32000, y:-32000, z:-32000}, + min={x:32000, y:32000, z:32000}; + function onMag(m) { + 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(()=>{ + Bangle.removeListener('mag', onMag); + 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}); + },30000); + }); +} + +function docalibrate(e,first){ + const title = "Calibrate"; + const msg = "takes 30 seconds"; + function action(b){ + if (b) { + buf1.setColor(1); + buf1.setFont("Vector", 20); + buf1.setFontAlign(0,-1); + buf1.drawString("Figure 8s",64, 0); + buf1.drawString("to",64, 40); + buf1.drawString("Calibrate",64, 80); + flip1(56,56); + + calibrate().then((r)=>{ + isCalibrating = false; + require("Storage").write("magnav.json",r); + Bangle.buzz(); + CALIBDATA = r; + startdraw(); + setButtons(); + }); + } else { + startdraw(); + setTimeout(setButtons,1000); + } + } + + if (first === undefined) first = false; + + stopdraw(); + clearWatch(); + isCalibrating = true; + + if (first) + E.showAlert(msg,title).then(action.bind(null,true)); + else + E.showPrompt(msg,{title:title,buttons:{"Start":true,"Cancel":false}}).then(action); +} + +function startdraw(){ + Bangle.setCompassPower(1, "app"); + + g.clear(); + g.setColor(1,1,1); + Bangle.drawWidgets(); + candraw = true; + if (intervalRef) clearInterval(intervalRef); + intervalRef = setInterval(reading,200); +} + +function stopdraw() { + candraw=false; + + Bangle.setCompassPower(0, "app"); + if (intervalRef) { + clearInterval(intervalRef); + intervalRef = undefined; + } +} + +function setButtons(){ + setWatch(()=>{load();}, BTN1, {repeat:false,edge:"falling"}); + setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); + setWatch(docalibrate, BTN3, {repeat:false,edge:"falling"}); +} + +Bangle.on('lcdPower',function(on) { + if (isCalibrating) return; + if (on) { + startdraw(); + } else { + stopdraw(); + } +}); + +Bangle.loadWidgets(); +setButtons(); + +Bangle.setLCDPower(1); +if (CALIBDATA) startdraw(); else docalibrate({},true); diff --git a/apps/arrow/arrow.png b/apps/arrow/arrow.png new file mode 100644 index 000000000..9f20f5dde Binary files /dev/null and b/apps/arrow/arrow.png differ diff --git a/apps/arrow/arrow_screenshot.jpg b/apps/arrow/arrow_screenshot.jpg new file mode 100644 index 000000000..ecb45a942 Binary files /dev/null and b/apps/arrow/arrow_screenshot.jpg differ diff --git a/apps/arrow/icon.js b/apps/arrow/icon.js new file mode 100644 index 000000000..380728484 --- /dev/null +++ b/apps/arrow/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mUywIebg/4AocP//AAoUf//+BYgMDh/+j/8Dol/wEAgYFBg/wgEBFIV+AQIVCh4fBnwFBgISBj8AhgJCh+Ag4BB4ED8ED+ASCAYJDBnkAvkAIYIWBjw8B/EB8AcBn//gF4DwJdBAQMA/EP738FYM8g/nz+A+EPgHx8YKBgfAjF4sAKBHIItBBQJMBFoJEBHII1BIQIDCvAUCAYYUBHIIDBMIXACgQpBRAIUBMIIrBDAIWCVYaiBTYQJCn4FBQgIIBEYKrDQ4MBVYUf8CQCCoP/w6DBAAKIBAocHAoIwBBgb5DDoYAZA=")) diff --git a/apps/assistedgps/custom.html b/apps/assistedgps/custom.html index e86c660b9..139c232af 100644 --- a/apps/assistedgps/custom.html +++ b/apps/assistedgps/custom.html @@ -12,10 +12,10 @@