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 307323846..e71d7eee0 100644 --- a/README.md +++ b/README.md @@ -249,14 +249,20 @@ 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 }, ] "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" @@ -439,13 +445,16 @@ The screen is parted in a widget and app area for lcd mode `direct`(default). | areas | as rectangle or point | | :-:| :-: | | Widget | (0,0,239,23) | -| Apps | (0,24,239,239) | +| Widget bottom bar (optional) | (0,216,239,239) | +| Apps | (0,24,239,239) (see below) | | BTN1 | (230, 55) | | BTN2 | (230, 140) | | BTN3 | (230, 210) | | BTN4 | (0,0,119, 239)| | BTN5 | (120,0,239,239) | +- If there are widgets at the bottom of the screen, apps should actually keep the bottom 24px free, so should keep to the area (0,24,239,215) + - Use `g.setFontAlign(0, 0, 3)` to draw rotated string to BTN1-BTN3 with `g.drawString()`. - For BTN4-5 the touch area is named diff --git a/apps.json b/apps.json index 5a19eabf1..89be44fcf 100644 --- a/apps.json +++ b/apps.json @@ -4,11 +4,12 @@ "tags": "tool,system", "type":"bootloader", "icon": "bootloader.png", - "version":"0.22", + "version":"0.25", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "storage": [ {"name":".boot0","url":"boot0.js"}, - {"name":".bootcde","url":"bootloader.js"} + {"name":".bootcde","url":"bootloader.js"}, + {"name":"bootupdate.js","url":"bootupdate.js"} ], "sortorder" : -10 }, @@ -41,7 +42,7 @@ "name": "Launcher (Default)", "shortName":"Launcher", "icon": "app.png", - "version":"0.04", + "version":"0.06", "description": "This is needed by Bangle.js to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.", "tags": "tool,system,launcher", "type":"launch", @@ -53,7 +54,7 @@ { "id": "about", "name": "About", "icon": "app.png", - "version":"0.07", + "version":"0.08", "description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers", "tags": "tool,system", "allow_emulator":true, @@ -65,7 +66,7 @@ { "id": "locale", "name": "Languages", "icon": "locale.png", - "version":"0.08", + "version":"0.09", "description": "Translations for different countries", "tags": "tool,system,locale,translate", "type": "locale", @@ -80,7 +81,7 @@ "name": "Notifications (default)", "shortName":"Notifications", "icon": "notify.png", - "version":"0.05", + "version":"0.08", "description": "A handler for displaying notifications that displays them in a bar at the top of the screen", "tags": "widget", "type": "notify", @@ -93,7 +94,7 @@ "name": "Fullscreen Notifications", "shortName":"Notifications", "icon": "notify.png", - "version":"0.06", + "version":"0.08", "description": "A handler for displaying notifications that displays them fullscreen. This may not fully restore the screen after on some apps. See `Notifications (default)` for more information about the notifications library.", "tags": "widget", "type": "notify", @@ -104,7 +105,7 @@ { "id": "welcome", "name": "Welcome", "icon": "app.png", - "version":"0.09", + "version":"0.10", "description": "Appears at first boot and explains how to use Bangle.js", "tags": "start,welcome", "allow_emulator":true, @@ -139,7 +140,7 @@ { "id": "gbridge", "name": "Gadgetbridge", "icon": "app.png", - "version":"0.18", + "version":"0.22", "description": "The default notification handler for Gadgetbridge notifications from Android", "tags": "tool,system,android,widget", "readme": "README.md", @@ -171,22 +172,24 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.23", + "version":"0.26", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "readme": "README.md", "storage": [ {"name":"setting.app.js","url":"settings.js"}, - {"name":"setting.boot.js","url":"boot.js"}, {"name":"setting.img","url":"settings-icon.js","evaluate":true} ], + "data": [ + {"name":"setting.json", "url":"settings.min.json","evaluate":true} + ], "sortorder" : -2 }, { "id": "alarm", "name": "Default Alarm", "shortName":"Alarms", "icon": "app.png", - "version":"0.10", + "version":"0.11", "description": "Set and respond to alarms", "tags": "tool,alarm,widget", "storage": [ @@ -213,6 +216,69 @@ {"name":"wclock.img","url":"clock-word-icon.js","evaluate":true} ] }, + { "id": "fontclock", + "name": "Font Clock", + "icon": "fontclock.png", + "version":"0.01", + "description": "Choose the font and design of clock face from a library of available designs", + "tags": "clock", + "type":"clock", + "allow_emulator":false, + "readme": "README.md", + "custom":"custom.html", + "storage": [ + {"name":"fontclock.app.js","url":"fontclock.js"}, + {"name":"fontclock.img","url":"fontclock-icon.js","evaluate":true}, + {"name":"fontclock.hand.js","url":"fontclock.hand.js"}, + {"name":"fontclock.thinhand.js","url":"fontclock.thinhand.js"}, + {"name":"fontclock.thickhand.js","url":"fontclock.thickhand.js"}, + {"name":"fontclock.hourscriber.js","url":"fontclock.hourscriber.js"}, + {"name":"fontclock.font.js","url":"fontclock.font.js"}, + {"name":"fontclock.font.abril_ff50.js","url":"fontclock.font.abril_ff50.js"}, + {"name":"fontclock.font.cpstc58.js","url":"fontclock.font.cpstc58.js"}, + {"name":"fontclock.font.mntn25.js","url":"fontclock.font.mntn25.js"}, + {"name":"fontclock.font.mntn50.js","url":"fontclock.font.mntn50.js"}, + {"name":"fontclock.font.vector25.js","url":"fontclock.font.vector25.js"}, + {"name":"fontclock.font.vector50.js","url":"fontclock.font.vector50.js"} + ] + }, + { "id": "slidingtext", + "name": "Sliding Clock", + "icon": "slidingtext.png", + "version":"0.05", + "description": "Inspired by the Pebble sliding clock, old times are scrolled off the screen and new times on. You are also able to change language on the fly so you can see the time written in other languages using button 1. Currently English, French, Japanese, Spanish and German are supported", + "tags": "clock", + "type":"clock", + "allow_emulator":false, + "readme": "README.md", + "custom":"custom.html", + "storage": [ + {"name":"slidingtext.app.js","url":"slidingtext.js"}, + {"name":"slidingtext.img","url":"slidingtext-icon.js","evaluate":true}, + {"name":"slidingtext.locale.en.js","url":"slidingtext.locale.en.js"}, + {"name":"slidingtext.locale.en2.js","url":"slidingtext.locale.en2.js"}, + {"name":"slidingtext.utils.en.js","url":"slidingtext.utils.en.js"}, + {"name":"slidingtext.locale.es.js","url":"slidingtext.locale.es.js"}, + {"name":"slidingtext.locale.fr.js","url":"slidingtext.locale.fr.js"}, + {"name":"slidingtext.locale.jp.js","url":"slidingtext.locale.jp.js"}, + {"name":"slidingtext.locale.de.js","url":"slidingtext.locale.de.js"}, + {"name":"slidingtext.dtfmt.js","url":"slidingtext.dtfmt.js"} + ] + }, + { "id": "sweepclock", + "name": "Sweep Clock", + "icon": "sweepclock.png", + "version":"0.04", + "description": "Smooth sweep secondhand with single hour numeral. Use button 1 to toggle the numeral font, button 3 to change the colour theme and button 4 to change the date placement", + "tags": "clock", + "type":"clock", + "allow_emulator":true, + "readme": "README.md", + "storage": [ + {"name":"sweepclock.app.js","url":"sweepclock.js"}, + {"name":"sweepclock.img","url":"sweepclock-icon.js","evaluate":true} + ] + }, { "id": "imgclock", "name": "Image background clock", "shortName":"Image Clock", @@ -246,7 +312,7 @@ { "id": "aclock", "name": "Analog Clock", "icon": "clock-analog.png", - "version": "0.13", + "version": "0.14", "description": "An Analog Clock", "tags": "clock", "type":"clock", @@ -269,6 +335,30 @@ {"name":"clock2x3.img","url":"clock2x3-icon.js","evaluate":true} ] }, + { "id": "geissclk", + "name": "Geiss Clock", + "icon": "clock.png", + "version":"0.02", + "description": "7 segment clock with animated background in the style of Ryan Geiss' music visualisation. NOTE: The first run will take ~1 minute to do some precalculation", + "tags": "clock", + "type":"clock", + "storage": [ + {"name":"geissclk.app.js","url":"clock.js"}, + {"name":"geissclk.precompute.js","url":"precompute.js"}, + {"name":"geissclk.img","url":"clock-icon.js","evaluate":true} + ], + "data": [ + {"name":"geissclk.0.map"}, + {"name":"geissclk.1.map"}, + {"name":"geissclk.2.map"}, + {"name":"geissclk.3.map"}, + {"name":"geissclk.4.map"}, + {"name":"geissclk.5.map"}, + {"name":"geissclk.0.pal"}, + {"name":"geissclk.1.pal"}, + {"name":"geissclk.2.pal"} + ] + }, { "id": "trex", "name": "T-Rex", "icon": "trex.png", @@ -363,7 +453,7 @@ { "id": "gpsrec", "name": "GPS Recorder", "icon": "app.png", - "version":"0.17", + "version":"0.19", "interface": "interface.html", "description": "Application that allows you to record a GPS track. Can run in background", "tags": "tool,outdoors,gps,widget", @@ -388,14 +478,16 @@ "interface":"waypoints.html", "storage": [ {"name":"gpsnav.app.js","url":"app.min.js"}, - {"name":"waypoints.json","url":"waypoints.json","evaluate":false}, {"name":"gpsnav.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"waypoints.json","url":"waypoints.json"} ] }, { "id": "heart", "name": "Heart Rate Recorder", "icon": "app.png", - "version":"0.02", + "version":"0.05", "interface": "interface.html", "description": "Application that allows you to record your heart rate. Can run in background", "tags": "tool,health,widget", @@ -479,7 +571,7 @@ { "id": "widbat", "name": "Battery Level Widget", "icon": "widget.png", - "version":"0.05", + "version":"0.06", "description": "Show the current battery level and charging status in the top right of the clock", "tags": "widget,battery", "type":"widget", @@ -487,6 +579,17 @@ {"name":"widbat.wid.js","url":"widget.js"} ] }, + { "id": "widlock", + "name": "Lock Widget", + "icon": "widget.png", + "version":"0.01", + "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked", + "tags": "widget,lock", + "type":"widget", + "storage": [ + {"name":"widlock.wid.js","url":"widget.js"} + ] + }, { "id": "widbatpc", "name": "Battery Level Widget (with percentage)", "shortName": "Battery Widget", @@ -508,7 +611,7 @@ "shortName": "Battery Warning", "icon": "widget.png", "readme": "README.md", - "version":"0.01", + "version":"0.02", "description": "Show a warning when the battery runs low.", "tags": "tool,battery", "type":"widget", @@ -532,6 +635,21 @@ {"name":"widbt.wid.js","url":"widget.js"} ] }, + { "id": "widchime", + "name": "Hour Chime", + "icon": "widget.png", + "version":"0.02", + "description": "Buzz or beep on every whole hour.", + "tags": "widget", + "type": "widget", + "storage": [ + {"name":"widchime.wid.js","url":"widget.js"}, + {"name":"widchime.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"widchime.json"} + ] + }, { "id": "widram", "name": "RAM Widget", "shortName":"RAM Widget", @@ -547,7 +665,7 @@ { "id": "hrm", "name": "Heart Rate Monitor", "icon": "heartrate.png", - "version":"0.02", + "version":"0.04", "description": "Measure your heart rate and see live sensor data", "tags": "health", "storage": [ @@ -673,7 +791,7 @@ { "id": "route", "name": "Route Viewer", "icon": "app.png", - "version":"0.01", + "version":"0.02", "description": "Upload a KML file of a route, and have your watch display a map with how far around it you are", "tags": "", "custom": "custom.html", @@ -739,10 +857,23 @@ {"name":"sclock.img","url":"clock-simple-icon.js","evaluate":true} ] }, + { "id": "s7clk", + "name": "Simple 7 segment Clock", + "icon": "icon.png", + "version":"0.02", + "description": "A simple 7 segment Clock with date", + "tags": "clock", + "type":"clock", + "allow_emulator":true, + "storage": [ + {"name":"s7clk.app.js","url":"app.js"}, + {"name":"s7clk.img","url":"icon.js","evaluate":true} + ] + }, { "id": "vibrclock", "name": "Vibrate Clock", "icon": "app.png", - "version":"0.01", + "version":"0.02", "description": "When BTN1 is pressed, vibrate out the time as a series of buzzes, one digit at a time. Hours, then Minutes. Zero is signified by one long buzz. Otherwise a simple digital clock.", "tags": "clock", "type":"clock", @@ -970,7 +1101,7 @@ "name": "Large Digit Blob Clock", "shortName" : "Blob Clock", "icon": "clock-blob.png", - "version":"0.04", + "version":"0.05", "description": "A clock with big digits", "tags": "clock", "type":"clock", @@ -1007,7 +1138,7 @@ { "id": "widpedom", "name": "Pedometer widget", "icon": "widget.png", - "version":"0.11", + "version":"0.13", "description": "Daily pedometer widget", "tags": "widget", "type":"widget", @@ -1138,7 +1269,7 @@ { "id": "marioclock", "name": "Mario Clock", "icon": "marioclock.png", - "version":"0.14", + "version":"0.15", "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.", "tags": "clock,mario,retro", "type": "clock", @@ -1153,7 +1284,7 @@ "name": "Commandline-Clock", "shortName":"CLI-Clock", "icon": "app.png", - "version":"0.11", + "version":"0.12", "description": "Simple CLI-Styled Clock", "tags": "clock,cli,command,bash,shell", "type":"clock", @@ -1190,7 +1321,7 @@ { "id": "dotclock", "name": "Dot Clock", "icon": "clock-dot.png", - "version":"0.01", + "version":"0.02", "description": "A Minimal Dot Analog Clock", "tags": "clock", "type":"clock", @@ -1492,7 +1623,7 @@ "name": "Digital Assistant, not EDITH", "shortName": "DANE", "icon": "app.png", - "version": "0.15", + "version": "0.16", "description": "A Watchface inspired by Tony Stark's EDITH and based on https://arwes.dev/", "tags": "clock", "type": "clock", @@ -1553,7 +1684,7 @@ "name": "BangleRun", "shortName": "BangleRun", "icon": "banglerun.png", - "version": "0.09", + "version": "0.10", "interface": "interface.html", "description": "An app for running sessions. Displays info and logs your run for later viewing.", "tags": "run,running,fitness,outdoors", @@ -1636,9 +1767,9 @@ { "id": "rclock", "name": "Round clock with seconds, minutes and date", - "shortName":"Round Clock", + "shortName": "Round Clock", "icon": "app.png", - "version":"0.03", + "version": "0.05", "description": "Designed round clock with ticks for minutes and seconds and heart rate indication", "tags": "clock", "type": "clock", @@ -1647,6 +1778,20 @@ {"name":"rclock.img","url":"app-icon.js","evaluate":true} ] }, + { + "id": "fclock", + "name": "fclock", + "shortName": "F Clock", + "icon": "app.png", + "version": "0.01", + "description": "Simple design of a digital clock", + "tags": "clock", + "type": "clock", + "storage": [ + {"name":"fclock.app.js","url":"fclock.app.js"}, + {"name":"fclock.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "hamloc", "name": "QTH Locator / Maidenhead Locator System", "shortName": "QTH Locator", @@ -1835,7 +1980,7 @@ "id": "beebclock", "name": "Beeb Clock", "icon": "beebclock.png", - "version":"0.02", + "version":"0.03", "description": "Clock face that may be coincidentally familiar to BBC viewers", "tags": "clock", "type": "clock", @@ -2035,7 +2180,7 @@ "name": "SleepPhaseAlarm", "shortName":"SleepPhaseAlarm", "icon": "app.png", - "version":"0.01", + "version":"0.02", "description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.", "tags": "alarm", "storage": [ @@ -2139,7 +2284,7 @@ { "id": "multiclock", "name": "Multi Clock", "icon": "multiclock.png", - "version":"0.11", + "version":"0.13", "description": "Clock with multiple faces - Big, Analogue, Digital, Text, Time-Date.\n Switch between faces with BTN1 & BTN3", "readme": "README.md", "tags": "clock", @@ -2153,8 +2298,6 @@ {"name":"txt.face.js","url":"txt.js"}, {"name":"timdat.face.js","url":"timdat.js"}, {"name":"ped.face.js","url":"ped.js"}, - {"name":"gps.face.js","url":"gps.js"}, - {"name":"osref.face.js","url":"osref.js"}, {"name":"multiclock.img","url":"multiclock-icon.js","evaluate":true} ] }, @@ -2162,7 +2305,7 @@ "name": "Apple Notification Widget", "shortName":"ANCS Widget", "icon": "widget.png", - "version":"0.06", + "version":"0.07", "description": "Displays call, message etc notifications from a paired iPhone. Read README before installation as it only works with compatible apps", "readme": "README.md", "tags": "widget", @@ -2259,7 +2402,7 @@ {"id": "counter", "name": "Counter", "icon": "counter_icon.png", - "version": "0.02", + "version": "0.03", "description": "Simple counter", "tags": "tool", "allow_emulator": true, @@ -2336,9 +2479,11 @@ "readme": "README.md", "storage": [ {"name":"worldclock.app.js","url":"app.js"}, - {"name":"worldclock.settings.json"}, {"name":"worldclock.img","url":"worldclock-icon.js","evaluate":true} - ] + ], + "data": [ + {"name":"worldclock.settings.json"} + ] }, { "id": "digiclock", "name": "Digital Clock Face", @@ -2530,7 +2675,7 @@ "name": "Hard Alarm", "shortName":"HardAlarm", "icon": "app.png", - "version":"0.01", + "version":"0.02", "description": "Make sure you wake up! Count to the right number to turn off the alarm", "tags": "tool,alarm,widget", "storage": [ @@ -2581,14 +2726,16 @@ "readme": "README.md", "storage": [ {"name":"breath.app.js","url":"app.js"}, - {"name":"breath.settings.json","url":"settings.json"}, {"name":"breath.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"breath.settings.json","url":"settings.json"} ] }, { "id": "lazyclock", "name": "Lazy Clock", "icon": "lazyclock.png", - "version":"0.01", + "version":"0.02", "readme": "README.md", "description": "Tells the time, roughly", "tags": "clock", @@ -2639,11 +2786,11 @@ ] }, { "id": "speedalt", - "name": "GPS Speedo and Altimeter", - "shortName":"GPS Speed Alt", + "name": "GPS Adventure Sports", + "shortName":"GPS Adv Sport", "icon": "app.png", - "version":"0.07", - "description": "GPS speed and altitude display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.", + "version":"1.02", + "description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.", "tags": "tool,outdoors", "type":"app", "allow_emulator":true, @@ -2679,9 +2826,11 @@ "storage": [ {"name":"gpsservice.app.js","url":"app.js"}, {"name":"gpsservice.settings.js","url":"settings.js"}, - {"name":"gpsservice.settings.json","url":"settings.json"}, {"name":"gpsservice.wid.js","url":"widget.js"}, {"name":"gpsservice.img","url":"gpsservice-icon.js","evaluate":true} + ], + "data": [ + {"name":"gpsservice.settings.json","url":"settings.json"} ] }, { "id": "mclockplus", @@ -2714,12 +2863,15 @@ "name": "Planetarium", "shortName":"Planetarium", "icon": "planetarium.png", - "version":"0.02", + "readme": "README.md", + "version":"0.03", "description": "Planetarium showing up to 500 stars using the watch location and time", "tags": "", "storage": [ {"name":"planetarium.app.js","url":"planetarium.app.js"}, {"name":"planetarium.data.csv","url":"planetarium.data.csv"}, + {"name":"planetarium.const.csv","url":"planetarium.const.csv"}, + {"name":"planetarium.extra.csv","url":"planetarium.extra.csv"}, {"name":"planetarium.settings.js","url":"settings.js"}, {"name":"planetarium.img","url":"planetarium-icon.js","evaluate":true} ], @@ -2730,14 +2882,14 @@ { "id": "tapelauncher", "name": "Tape Launcher", "icon": "icon.png", - "version":"0.01", + "version":"0.02", "description": "An App launcher, icons displayed in a horizontal tape, swipe or use buttons", "readme": "README.md", "tags": "tool,system,launcher", "type":"launch", "storage": [ - {"name":"tapelaunch.app.js","url":"app.js"}, - {"name":"tapelaunch.img","url":"icon.js","evaluate":true} + {"name":"tapelauncher.app.js","url":"app.js"}, + {"name":"tapelauncher.img","url":"icon.js","evaluate":true} ] }, { "id": "oblique", @@ -2750,6 +2902,361 @@ {"name":"oblique.app.js","url":"app.js"}, {"name":"oblique.img","url":"app-icon.js","evaluate":true} ] +}, +{ "id": "testuserinput", + "name": "Test User Input", + "shortName":"Test User Input", + "icon": "app.png", + "version":"0.06", + "description": "App to test the bangle.js input interface. It displays the user action in text, circle buttons or on/off switch UI elements.", + "readme": "README.md", + "tags": "input,interface,buttons,touch,UI", + "storage": [ + {"name":"testuserinput.app.js","url":"app.js"}, + {"name":"testuserinput.img","url":"app-icon.js","evaluate":true} + ] +}, +{ "id": "gpssetup", + "name": "GPS Setup", + "shortName":"GPS Setup", + "icon": "gpssetup.png", + "version":"0.02", + "description": "Configure the GPS power options and store them in the GPS nvram", + "tags": "gps, tools, outdoors", + "readme": "README.md", + "storage": [ + {"name":"gpssetup","url":"gpssetup.js"}, + {"name":"gpssetup.settings.js","url":"settings.js"}, + {"name":"gpssetup.app.js","url":"app.js"}, + {"name":"gpssetup.img","url":"icon.js","evaluate":true} + ], + "data": [ + {"name":"gpssetup.settings.json","url":"settings.json"} + ] +}, +{ "id": "walkersclock", + "name": "Walkers Clock", + "shortName":"Walkers Clock", + "icon": "walkersclock48.png", + "version":"0.04", + "description": "A large font watch, displays steps, can switch GPS on/off, displays grid reference", + "type":"clock", + "tags": "clock, gps, tools, outdoors", + "readme": "README.md", + "storage": [ + {"name":"walkersclock.app.js","url":"app.js"}, + {"name":"walkersclock.img","url":"icon.js","evaluate":true} + ] +}, +{ "id": "widgps", + "name": "GPS Widget", + "icon": "widget.png", + "version":"0.02", + "description": "Tiny widget to show the power on/off status of the GPS. Require firmware v2.08.167 or later", + "tags": "widget,gps", + "type":"widget", + "readme": "README.md", + "storage": [ + {"name":"widgps.wid.js","url":"widget.js"} + ] +}, +{ "id": "widhrt", + "name": "HRM Widget", + "icon": "widget.png", + "version":"0.02", + "description": "Tiny widget to show the power on/off status of the Heart Rate Monitor. Requires firmware v2.08.167 or later", + "tags": "widget, hrm", + "type":"widget", + "readme": "README.md", + "storage": [ + {"name":"widhrt.wid.js","url":"widget.js"} + ] +}, +{ "id": "countdowntimer", + "name" : "Countdown Timer", + "icon": "countdowntimer.png", + "version": "0.01", + "description": "A simple countdown timer with a focus on usability", + "tags": "timer, tool", + "readme": "README.md", + "storage": [ + {"name": "countdowntimer.app.js", "url": "countdowntimer.js"}, + {"name": "countdowntimer.img", "url": "countdowntimer-icon.js", "evaluate": true} + ] +}, +{ "id": "helloworld", + "name": "hello, world!", + "shortName":"hello world", + "icon": "app.png", + "version":"0.02", + "description": "A cross cultural hello world!/hola mundo! app with colors and languages", + "readme": "README.md", + "tags": "input,interface,buttons,touch", + "storage": [ + {"name":"helloworld.app.js","url":"app.js"}, + {"name":"helloworld.img","url":"app-icon.js","evaluate":true} + ] +}, +{ "id": "widcom", + "name": "Compass Widget", + "icon": "widget.png", + "version":"0.01", + "description": "Tiny widget to show the power on/off status of the Compass. Requires firmware v2.08.167 or later", + "tags": "widget, compass", + "type":"widget", + "readme": "README.md", + "storage": [ + {"name":"widcom.wid.js","url":"widget.js"} + ] +}, +{ "id": "arrow", + "name": "Arrow Compass", + "icon": "arrow.png", + "type":"app", + "version":"0.04", + "description": "Moving arrow compass that points North, shows heading, with tilt correction. Based on jeffmer's Navigation Compass", + "tags": "tool,outdoors", + "readme": "README.md", + "storage": [ + {"name":"arrow.app.js","url":"app.js"}, + {"name":"arrow.img","url":"icon.js","evaluate":true} + ] +}, +{ "id": "waypointer", + "name": "Way Pointer", + "icon": "waypointer.png", + "version":"0.01", + "description": "Navigate to a waypoint using the GPS for bearing and compass to point way, uses the same waypoint interface as GPS Navigation", + "tags": "tool,outdoors,gps", + "readme": "README.md", + "interface":"waypoints.html", + "storage": [ + {"name":"waypointer.app.js","url":"app.js"}, + {"name":"waypointer.img","url":"icon.js","evaluate":true} + ], + "data": [ + {"name":"waypoints.json","url":"waypoints.json"} + ] +}, +{ "id": "color_catalog", + "name": "Colors Catalog", + "shortName":"Colors Catalog", + "icon": "app.png", + "version":"0.01", + "description": "Displays RGB565 and RGB888 colors, its name and code in screen.", + "readme": "README.md", + "tags": "Color, input,buttons,touch,UI", + "storage": [ + {"name":"color_catalog.app.js","url":"app.js"}, + {"name":"color_catalog.img","url":"app-icon.js","evaluate":true} + ] +}, +{ "id": "UI4swatch", + "name": "UI 4 swatch", + "shortName":"UI 4 swatch", + "icon": "app.png", + "version":"0.01", + "description": "A UI/UX for espruino smartwatches, displays dinamically calc. x,y coordinates.", + "readme": "README.md", + "tags": "Color, input,buttons,touch,UI", + "storage": [ + {"name":"UI4swatch.app.js","url":"app.js"}, + {"name":"UI4swatch.img","url":"app-icon.js","evaluate":true} + ] +}, +{ "id": "simplest", + "name": "Simplest Clock", + "icon": "simplest.png", + "version":"0.01", + "description": "The simplest working clock, acts as a tutorial piece", + "tags": "clock", + "type":"clock", + "readme": "README.md", + "storage": [ + {"name":"simplest.app.js","url":"app.js"}, + {"name":"simplest.img","url":"icon.js","evaluate":true} + ] +}, +{ "id": "stepo", + "name": "Stepometer Clock", + "icon": "stepo.png", + "version":"0.03", + "description": "A large font watch, displays step count in a doughnut guage and warns of low battery, requires one of the steps widgets to be installed", + "tags": "clock", + "type":"clock", + "readme": "README.md", + "storage": [ + {"name":"stepo.app.js","url":"app.js"}, + {"name":"stepo.img","url":"icon.js","evaluate":true} + ] +}, +{ "id": "gbmusic", + "name": "Gadgetbridge Music Controls", + "shortName":"Music Controls", + "icon": "icon.png", + "version":"0.05", + "description": "Control the music on your Gadgetbridge-connected phone", + "tags": "tools,bluetooth,gadgetbridge,music", + "type":"app", + "allow_emulator": false, + "readme": "README.md", + "storage": [ + {"name":"gbmusic.app.js","url":"app.js"}, + {"name":"gbmusic.settings.js","url":"settings.js"}, + {"name":"gbmusic.wid.js","url":"widget.js"}, + {"name":"gbmusic.img","url":"icon.js","evaluate":true} + ], + "data": [ + {"name":"gbmusic.json"}, + {"name":"gbmusic.load.json"} + ] +}, +{ + "id": "battleship", + "name":"Battleship", + "icon":"battleship-icon.png", + "version": "0.01", + "readme": "README.md", + "description": "The classic game of battleship", + "tags": "game", + "allow_emulator": true, + "storage": [ + { + "name": "battleship.app.js", + "url": "battleship.js" + }, + { + "name": "battleship.img", + "url": "battleship-icon.js", + "evaluate": true + } + ] +}, +{ "id": "kitchen", + "name": "Kitchen Combo", + "icon": "kitchen.png", + "version":"0.10", + "description": "Combination of the Stepo, Walkersclock, Arrow and Waypointer apps into a multiclock format. 'Everything but the kitchen sink'. Requires firmware v2.08.167 or later", + "tags": "tool,outdoors,gps", + "type":"clock", + "readme": "README.md", + "interface":"waypoints.html", + "storage": [ + {"name":"kitchen.app.js","url":"kitchen.app.js"}, + {"name":"stepo.kit.js","url":"stepo.kit.js"}, + {"name":"gps.kit.js","url":"gps.kit.js"}, + {"name":"digi.kit.js","url":"digi.kit.js"}, + {"name":"heart.kit.js","url":"heart.kit.js"}, + {"name":"swatch.kit.js","url":"swatch.kit.js"}, + {"name":"compass.kit.js","url":"compass.kit.js"}, + {"name":"kitchen.img","url":"kitchen.icon.js","evaluate":true} + ], + "data": [ + {"name":"waypoints.json","url":"waypoints.json"} + ] +}, +{ "id": "qmsched", + "name": "Quiet Mode Schedule", + "shortName":"Quiet Mode", + "icon": "app.png", + "version":"0.01", + "description": "Automatically turn Quiet Mode on or off at set times", + "readme": "README.md", + "tags": "tool", + "storage": [ + {"name":"qmsched","url":"lib.js"}, + {"name":"qmsched.app.js","url":"app.js"}, + {"name":"qmsched.boot.js","url":"boot.js"}, + {"name":"qmsched.img","url":"icon.js","evaluate":true} + ], + "data": [ + {"name":"qmsched.json"} + ] +}, +{ + "id": "hourstrike", + "name": "Hour Strike", + "shortName": "Hour Strike", + "icon": "app-icon.png", + "version": "0.07", + "description": "Strike the clock on the hour. A great tool to remind you an hour has passed!", + "tags": "tool,alarm", + "readme": "README.md", + "storage": [ + {"name":"hourstrike.app.js","url":"app.js"}, + {"name":"hourstrike.boot.js","url":"boot.js"}, + {"name":"hourstrike.img","url":"app-icon.js","evaluate":true} + ] +}, +{ "id": "whereworld", + "name": "Where in the World?", + "shortName" : "Where World", + "icon": "app.png", + "version": "0.01", + "description": "Shows your current location on the world map", + "tags": "gps", + "storage": [ + {"name":"whereworld.app.js","url":"app.js"}, + {"name":"whereworld.img","url":"app-icon.js","evaluate":true}, + {"name":"whereworld.worldmap","url":"worldmap"} + ] +}, +{ + "id": "omnitrix", + "name":"Omnitrix", + "icon":"omnitrix.png", + "version": "0.01", + "readme": "README.md", + "description": "An Omnitrix Showpiece", + "tags": "game", + "readme": "README.md", + "storage": [ + {"name":"omnitrix.app.js","url":"omnitrix.app.js"}, + {"name":"omnitrix.img","url":"omnitrix.icon.js","evaluate":true} + ] +}, +{ "id": "batclock", + "name": "Bat Clock", + "shortName":"Bat Clock", + "icon": "bat-clock.png", + "version":"0.01", + "description": "Morphing Clock, with an awesome \"The Dark Knight\" themed logo.", + "tags": "clock", + "type": "clock", + "readme": "README.md", + "storage": [ + {"name":"batclock.app.js","url":"bat-clock.app.js"}, + {"name":"batclock.img","url":"bat-clock.icon.js","evaluate":true} + ] +}, +{ "id":"doztime", + "name":"Dozenal Time", + "shortName":"Dozenal Time", + "icon":"app.png", + "version":"0.01", + "description":"A dozenal Holocene calendar and dozenal diurnal clock", + "tags":"clock", + "type":"clock", + "allow_emulator":true, + "readme": "README.md", + "storage": [ + {"name":"doztime.app.js","url":"app.js"}, + {"name":"doztime.img","url":"app-icon.js","evaluate":true} + ] +}, +{ "id":"gbtwist", + "name":"Gadgetbridge Twist Control", + "shortName":"Twist Control", + "icon":"app.png", + "version":"0.01", + "description":"Shake your wrist to control your music app via Gadgetbridge", + "tags":"tools,bluetooth,gadgetbridge,music", + "type":"app", + "allow_emulator":false, + "readme": "README.md", + "storage": [ + {"name":"gbtwist.app.js","url":"app.js"}, + {"name":"gbtwist.img","url":"app-icon.js","evaluate":true} + ] } - ] diff --git a/apps/UI4swatch/Changelog b/apps/UI4swatch/Changelog new file mode 100644 index 000000000..d81132fb6 --- /dev/null +++ b/apps/UI4swatch/Changelog @@ -0,0 +1 @@ +0.01: 1st ver, defining a common UI/UX diff --git a/apps/UI4swatch/README.md b/apps/UI4swatch/README.md new file mode 100644 index 000000000..11d61cd40 --- /dev/null +++ b/apps/UI4swatch/README.md @@ -0,0 +1,38 @@ +# UI/UX for Espruino Smartwatches + +This is a very basic app that defines a common UI/UX for espruino smartwatchesm and specifically for the *bangle.js*, also it displays dinamically calculated x,y position coordinates and screen areas based in detected smartwatch models. + + +Launcher icon + +![](UI4swatch_icon.png) + +1st screen - Main page + +![](UI4swatch_s1.png) + +## Usage + +Open and see x,y coordinates for areas +Interact with buttons or touch screen to print the event or leave the app + +## Features + +Colours, font, user input,, load widgets + + +## Controls +Press left area - Prints Touch1 +Press righ area - Prints Touch2 +Press center area - Prints Touch3 +Swipe Left - Prints <-- +Swipe Right - Prints --> +BTN1 - Prints Button1 +BTN2 - Prints Button2 +BTN3 - Quit to Launcher + + +## Support + +This app is so basic that probably the easiest is to just edit the code +Otherwise you can contact me [here](https://github.com/dapgo) \ No newline at end of file diff --git a/apps/UI4swatch/UI4swatch_icon.png b/apps/UI4swatch/UI4swatch_icon.png new file mode 100644 index 000000000..11dc192b6 Binary files /dev/null and b/apps/UI4swatch/UI4swatch_icon.png differ diff --git a/apps/UI4swatch/UI4swatch_s1.png b/apps/UI4swatch/UI4swatch_s1.png new file mode 100644 index 000000000..bc667e950 Binary files /dev/null and b/apps/UI4swatch/UI4swatch_s1.png differ diff --git a/apps/UI4swatch/app-icon.js b/apps/UI4swatch/app-icon.js new file mode 100644 index 000000000..11a709efe --- /dev/null +++ b/apps/UI4swatch/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AEMimYASkQXBkYXTmQXzn//mYBBn/zC6nzC6wvTR/4XG/4ASC/4XlQgwOCkQAEdAIXEdI4OBBIwvGC5JHNC/J3XL/53/C/4JGO/533L4wAJC65HNC9rKFAB4XBAH4AeA=")) \ No newline at end of file diff --git a/apps/UI4swatch/app.js b/apps/UI4swatch/app.js new file mode 100644 index 000000000..8cf3891b4 --- /dev/null +++ b/apps/UI4swatch/app.js @@ -0,0 +1,196 @@ +/* UI/UX for Espruino and Bangle.js +Testing USer interface and User usability +v20200307 +identify device and dimensions +max printable position max_x-1 i.e 239 +*/ + + var colbackg='#111111';//black + var colorange='#e56e06'; //RGB format rrggbb + var v_color_lines=0xFFFF; //White hex format + var v_color_b_area='#111111'; + var v_color_text=0x07E0;//'#FB0E01';//red + var v_font1size=10; //out of quotes + var v_font2size=12; + var v_font_banner_size=30; + var v_clicks=0; + //pend identify widget area dinamically + var v_model=process.env.BOARD; + console.log("device="+v_model); + + var x_max_screen=g.getWidth();//240; + var y_max_screen=g.getHeight(); //240; + var y_wg_bottom=g.getHeight()-25; + var y_wg_top=25; + if (v_model=='BANGLEJS') { + var x_btn_area=215; + var x_max_usable_area=x_btn_area;//Pend! only for bangle.js + var y_btn2=124; //harcoded for bangle.js cuz it is not the half of display height + } else x_max_usable_area=240; + + + var x_mid_screen=x_max_screen/2; + + //PEND comment + console.log("*** UI dimensions***"); + console.log("x="+x_max_screen); + console.log("y_wg_bottom="+y_wg_bottom); + + + + //the biggest usable area, button area not included +function ClearActiveArea(){ + g.setColor(v_color_b_area); + g.fillRect(0,y_wg_top,x_max_usable_area,y_wg_bottom); //fill all screen except widget areas + g.flip(); +} + + +//Clean fill top area with a color +function ClearBannerArea(){ + g.setColor(v_color_b_area); + g.fillRect(55,28,185,60); + g.flip(); +} + + +function PrintUserInput(boton){ + //var v_font_banner_size=30;//now global size= almost px height + console.log("Pressed touch/BTN",boton); + ClearBannerArea(); + g.setColor(colorange); + //print in banner area + g.setFontVector(v_font_banner_size).drawString(boton, 63, 29); + g.flip(); +} + +function PrintBtn1(boton){ + console.log("Pressed BTN1"); + PrintUserInput("Button1"); + +} + +function PrintBtn2(boton){ + console.log("Pressed BTN2"); + PrintUserInput("Button2"); +} + +function DrawBangleButtons(){ + g.setFontVector(v_font1size); + g.setColor(v_color_lines);//White + //line-separation with buttons area + g.drawLine(x_btn_area, 35, x_btn_area, 180);//vline right sep buttons + + //x for Border position in 2 lines + g.drawString("x=",x_max_screen-g.stringWidth("x= "),68); + g.drawString(x_max_screen,x_max_screen-g.stringWidth("3ch"),78); + + g.drawString("Dwn", x_max_screen-g.stringWidth("Dwn"),y_wg_top+v_font1size+1); + //above Btn2 + //g.setFontVector(v_font1size).drawString("Off", x_max_screen-g.stringWidth("Off"),y_btn2-(2*v_font1size)); + g.drawString("Set", x_max_screen-g.stringWidth("Set"),y_btn2-v_font1size); + //above Btn3 + g.drawString("Quit", x_max_screen-g.stringWidth("Quit"),y_wg_bottom-(2*v_font1size)); + g.flip(); + g.setColor(v_color_text); //green + g.setFontVector(v_font1size); + g.drawString("B1", x_max_screen-g.stringWidth("B1"),y_wg_top); + g.drawString("B2", x_max_screen-g.stringWidth("B2"),y_btn2); + g.drawString("B3",x_max_screen-g.stringWidth("B3"),y_wg_bottom-v_font1size); + g.flip(); +} +function DrawBottomInfoBanner(){ +/* External Vars:v_color_text,v_font2size,x_max_usable_area,y_wg_bottom +*/ + g.setColor(v_color_text); + var info_text1="Swipe: Next/Back screen"; + var info_text2="Touch: Left=Up Right=Down"; + //aligned left of max usable area + g.setFontVector(v_font2size); + g.drawString(info_text2, x_max_usable_area-g.stringWidth(info_text2)-2 ,y_wg_bottom-(2*v_font2size)); + g.drawString(info_text1, x_max_usable_area-g.stringWidth(info_text1)-2 ,y_wg_bottom-v_font2size); + g.flip(); +} + +function PrintAreas(){ + console.log("********************************"); + console.log("Log: *** Print Areas in screen"); + ClearActiveArea(); + g.setColor(v_color_lines); + + // **** Borders and Separation Lines for areas + g.drawLine(1, 35, 1, 180);//line for left border + // + g.drawLine(x_max_screen-1, 50, x_max_screen-1, 75);//vlide right border + g.drawLine(x_mid_screen, 80, x_mid_screen, 105);//vline middle separation part1 up + g.setFontVector(v_font2size).drawString("Output area for "+v_model,(x_max_usable_area-g.stringWidth("Output area for "+v_model))/2,67); + g.setFontVector(v_font2size).drawString("x="+x_mid_screen,x_mid_screen-g.stringWidth("x=xxx"),85); + g.drawLine(x_mid_screen, 140, x_mid_screen, 180);//vline middle separation part2 down + + + //y=26 after widget line y=215 below widget line + + if (v_model=='BANGLEJS') DrawBangleButtons(); + + g.setFontVector(v_font2size); + g.setColor(v_color_text); + g.drawString("Touch", 80,110); + g.drawString("Middle area", 80,125); + g.drawString("Left area", 15, 145); + g.drawString("Right area", 140,145); + g.flip(); + DrawBottomInfoBanner(); +} + +function UserInput(){ + Bangle.on('touch', function(button){ + switch(button){ + case 1: + PrintUserInput("Touch 1");//left + break; + case 2: + PrintUserInput("Touch 2");//right + break; + case 3: + PrintUserInput("Touch 3");//center 1+2 + break; + } + }); + + if (v_model=='BANGLEJS') { + //only the name of the function + setWatch(PrintBtn1, BTN1, { repeat: true }); + setWatch(PrintBtn2, BTN2, { repeat: true }); + setWatch(Bangle.showLauncher, BTN3, { repeat: true }); + } + Bangle.on('swipe', dir => { + if(dir == 1) PrintUserInput(" --->"); + else PrintUserInput(" <---"); + }); + console.log("Log: Input conditions loaded"); +} //end of UserInput + +//Main code +ClearActiveArea(); +PrintAreas(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + + + //optional lines below and above both widget areas + g.setColor(v_color_lines); + //line-separation with top widget area + g.drawLine(60, y_wg_top, 180, y_wg_top); + g.setFontVector(v_font2size).drawString("y="+y_wg_top,10,y_wg_top+1); + //line-separation with bottom widget area + g.drawLine(60, y_wg_bottom, 180, y_wg_bottom); + g.setFontVector(v_font2size).drawString("y="+y_wg_bottom,10,y_wg_bottom-v_font2size); + + + + + + g.flip(); + //end optional + UserInput(); \ No newline at end of file diff --git a/apps/UI4swatch/app.png b/apps/UI4swatch/app.png new file mode 100644 index 000000000..f26037a9b Binary files /dev/null and b/apps/UI4swatch/app.png differ diff --git a/apps/about/ChangeLog b/apps/about/ChangeLog index 22409a4ec..62e8d0126 100644 --- a/apps/about/ChangeLog +++ b/apps/about/ChangeLog @@ -5,3 +5,4 @@ 0.05: Actual pixels as of 27 Apr 2020 0.06: Actual pixels as of 12 Jun 2020 0.07: Pressing a button now exits immediately (fix #618) +0.08: Make about (mostly) work on non-240px screens diff --git a/apps/about/app.js b/apps/about/app.js index a9c6854d9..9edd0c94f 100644 --- a/apps/about/app.js +++ b/apps/about/app.js @@ -5,8 +5,12 @@ var s = require("Storage"); g.clear(1); g.setFont("6x8"); var y = 24, h=8; +if (g.getWidth()>=240) { g.drawImage(require("heatshrink").decompress(atob("vE4gQZWg//AAI3Zh4dCoAd6wAd64Ad2j4d6l4dcn4dC6Adc+AdYv4dUggHG//kgN//AGB1WkDpkOAwsH/gDBgJ4CTRwdGl6RDl/0gHQgJeMDo2/AgcDIAIkBnAdRgJyCAAQdDlgdRgZPDgbWBDoUcDqMPRYcJgEfoA7Uh9AAgQ1BEgIdBngdRKQIACmBbB6AdB2gdRnoEDyB+C8tbbQVpgNAqOkAwMGyEQDoMB1AIBvgdDPYMC+H//7zBg//+fAA4OAgH//twDoMv/4WB3iyEAAPwHINvTYMAv/A/sC6BmBh/wDoP4gIuBdwayBAAP/DoMH4F4ToQSB+EPJQUOgKmDBgIABhAdFB4L7BgfAAYNwjpKChwJBTIQdDiAdFgHgAYIdDmDaCO4MD9Wq14dM+CdCDoU0nDjChyhBAAIdFsgdTZgaVDmPYLJk0LIodDaIcxcILRDSo80jiVECgUAvgDCmG0YQTRHDoTRBgLRCMwJDBnodDeAMDKoUvAIU/DocD6ELDoKRCAIM/LIcGG4PQUIKCBU4PzDoaEB/p3BFQKKCh9ADoXsKIVVqonCtVBoFQcAUKyFwghdB3IPBCwJZCAQMfEgQAL2AGFgZJBDoZgDABEMWYQJFgLwCkACB/gdLWYMCfoQAE35BEDpkH8EfdgYADl4mDl68BABazBFBA2CgK8CABcBUZP/8kBv58CAC1//4ABUQwASn4dgOxoALl4dC4AdYj4d6h4d+wAd6oAd2g4dCAwQA=")),120,y); g.drawString("BANGLEJS.COM",120,y-4); +} else { + y=-(4+h); // small screen, start right at top +} g.drawString("Powered by Espruino",0,y+=4+h); g.drawString("Version "+ENV.VERSION,0,y+=h); g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h); @@ -25,11 +29,11 @@ g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h); if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h); if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h); g.setFontAlign(0,-1); -g.drawString(NRF.getAddress(),120,232); g.flip(); // Pixel chooser image -g.drawImage(require("heatshrink").decompress(atob("+FQgl+xnu8AIBwGQgHuAoN3gF/hcLgEHu943G3hwUCDwIBCAAV3uEAhoBBhsO90OgHgoACBh0IhP5AAQXBg8H8Hw+GwEAXn4AECxGAh0MEAOeJAMP3+/huIG4cMg1mMog8BhnsAQIBC///J4MN6HcBIOIAAPs8Hl9nM5gcB0Hg852BAIMAI4YACIIIACh8AKAcAvA6D7vd7wTBTYJ3B9e+hEAhA4CyHuy8HXw29NgIABx+ASQKsBYgR3DgHQCIXMsEAAIOZyGZzx3Dh/A57IDPoXN4HNHwQoB9wAByDvBO4LhDOwR4Fd4cP/4oB0DWCd45VCgFFAYPuO4QACgEed4PweAILBN4NpwEMXILvBO4bvD/f/d4cPCYJ1BAAKSCzp3E/hNBJwPziEP+H8hrvD9DtC5MJd4RTBGoLvBhe7BQJSBAAeAI4IoCO4T2Ch8N6DvDeAPgqFQd48MiB3BE4cI/AvC5ns4AKCdgQAD//wUwMMhhgBO4Nmd4xED57vD+EwFgKTCYoON/+v////OZwGXgF55vQI4TaBEQRxB6Hw7DRCAAPgO44ACKYlFoB3CHIcAiEAi93I4JpCdARmBd4IAFd4QAE4HA5//hh1BAIIPByA5BEQUM/n8O4TzCAAQtBhvd/X8d4YYBvwOBO4LvBYIoKBh/YewfA6B3DLoP/d4JXGABMBiKkEAAwKH9LyFO4fwOoR3Dd4TDD5/AJQcwDgcO9zvC1vd7ocBxuAvh3CuEHh5jCEoOPgHf/53CGgMAoGgbgX/CgJZEAIYAB5HIbxRCBAYULhZfBAAMA/GA/47Bd44ABh4CBg1mg8A3YAB3vtO4cMWxvG5vdZYWIw8AvPQd4NwRwUwAYIlBhsNGoR3CqB3BIAR4BFAXHAIg/CRAIDBIgtHHIR3D3ZhCZYXwwBrCOAXP5n855kNO4OABIyxCHYcDmdutOZA4VAAYUNqB0DAAQfDKIVms3AAgJ3BhBMBJwgAHhi7DDIQABgl9CIrvCeAJ3JABPM4AoBhqbDIgI0CMQfdOgR3E5nG5MzIAIBBAQIABwA5BgUgkEiEAe7hwECtgCB2B3BbwMJ9OeyBLIh3gFATvCPITuDhoCEgFVqq0B//w///MQWIbYJkFAAIjBEoR3DCoOA8A3CYAOvh/wE4LvEKoLvCoEE/7xDAAy/C2G+gw2DNQ2e9I0DBgxIBxGAWgS1DAAfd7pYE6BrBWwUIh2OAwLcGNQOA5jbCd4gACO4OgAgMHu4aBDokKgGIZ4LtBogABBgXw4HwhnL5lwEQXgd4V3BAIdBb4jvBO4/uIAfQKAJ3Gh7sC6/X7ogBUIL0BCwJ3ChHoO4QeCO4YHBXAQCBO4xQBJoYVBNwIBBhWq0HDwEOCIPuoDtIH4LuCAAOwMIR3BUATnIfgZ9BFYKHBd5nQKwICBBYWAPoJ3B///d5HM5jvD4DxBd4PQGwIBCHIMAeAQAEhQIC4GIboQABB4ifBW4ZeCAAO+EwJyBNQV2sDvCAAw6DAAaLFDgPwB4kNGIUJ5I3CcooAHO4OZzILH+AABFgcKeAa+Dd4p3Jd4+Ld4juChnMuz0DNQQABBAMOM4RqDuFwY4IUEGpLwB8DjB+ACBC4kJyAEC93uyAABDoxLB8HwFYTlBAIMMFIJlEQQJ3BCoIYBDgULCIpZCQ4YGBu5pBhn/u4UExB2BNoMO9wBB9xqDO4JeEEQKTFxABBwHJh3ex2P9+JxncZAJcBhMJO4mZO4dgXYRPCWQQzF4AABRIhHB5gACBYPeSAcAxOAAYICCdwK0CQYfc/I6BNYeAOwIAKBgMMQIIHC8EP///AoLkBgH4+AMCd4uoxWI1B3EAAOQzIDBswCBcIwAGBosOh7dChuNAYXvL4IPChGYgEP+AnFFox3B9vtO4LvBG47/CcofOPoYABWIIzCd4bYCB4NwgwFBd4IBBhI0Bh64CdwIHBdwJIBdAq7BEgTwDAgaxBAQMJhvdBALuBBAIQDeAMPh/ADQOH2+IhpeDfgbvDZAMP54ACMoJcCsAYC5nOV4OXcgQADd4QADs8HsF2g1QSwQAE+AcGRILhD/5cDE4ySDAgcGwGdxqvDd4j3BCIMP5iSCvfQcA6SB9wLBxBmBAAX/H4LkDSAcOFoOXgG72AgEd4IADqEFAQkL93rhzHCLgRIBCwbwCBgSFBOoLvBwEMg6XBBgIXDO4WJhuNHQyOF+DvCu+w2/QHoQACBYPt7qsCAAPgOQLvJAAeXhYdCZYIBBKYOAAII/I3yMB6CoBd4UDgbvDO44gBPIQ+BW4YADD4TvBOoI2FKA0A0AABAwfu9oOFOwPgAQLgBDoqwBAQIJFO5QACJIP/JQIDC+AVCO4LrBdgjuE24uB/7uFd4nwQob0DxEN7uIVxJ3E1R3Bh0ONoZ+E93gAIIPCVQ7fDgENAwRhC8AWBE4LvNAAXdaQsAmAHEO4QABhOZyB6BxB3BIg3QH4PQ/GIEIIAGQIMPTQMAhTuB1DaE9xNCAQTvCLgQACyDcDAAWIFARbD3ew9ycEKILvCABkMAAMAgZKCAAYlBHog8BAArqDO4mPx5bBuCTDCYWfh/P6AeFNgVwg7FEaITvC4BIB4B3HMgXdEwP/VwyCBO4QpB8A4GABiUCACB2COoIBCxH4wEM28A5hYCgEGszvC6F3NojKBuF3O4g+DPQPAAAWQ/7GB5nMH48D+AsCAAZDBF4YFCP4OAwD4GJgQCBhkJBYg8BBQJeBCgoABBAQCBNgIABd4UL5dwBASZQxGAKQcNAgPuQgJuBhnAz8A/kM553GFwMwO4PPhYfFTYjvBhAwBfAQABuA/GVAKKCTgxdR/GI+EM3gXCSIZeBg8Au7vEO4vQJgIAB+BTB8DvI//8FQLzBFYPL5YDBKQvQd5Z3FYoUPO4ZUBCQOf/5YDVoIFDIwNw+CUHBgQADEAOIUQnHg9wg+8714zUQCYbvBO4pDFXwRPBd4UOfwIzB5e7U4gAMO4R4BA4S4HhgiBO452DRQcP54ECyEJzJ3DkYXDGIIABRQTvCVoI0EhvcZghFCu4QBaQhKEdYIIFO4m7hewGIIRFEJAAFMYRQCRQZ3FXYUOCYXgd4cJhJ5BBIMOgE9mAYCxGAd4kAdwJ3DzIYBhu9OwbvDPwqTCcI8LAYU83gEC2B4BCoP85ns4Z6BO5UP/5lCAAz+DF4kPOoIBBC4eggGpdoJeBh3ggEDkLvGHROeDAMI7rFETYLVB3ew6AMDJwxKEgcAQgZ3D5//53Onk8O4a+BAIO62DvIKQMJKIMIZofQh3uOQIABR4X/BgLtBd4h3B4+QiF2gzjCeggAB5vmwGrd4YADSYMGy2Wd4jODd4j5EAA52BMwLvB53uO4MNTIUBgIRB1TOBAAJlBABkHJAXgHYI9CXAK6Cbwvghx3BAoNgAQI1BiMAw53ExJ3BAAUMhWQhptCd4T3DNwzGBhh5BhnMPoQEDBAnM5jvB4YIBFQUQ+EQd4plBFYZLCGgvQuDvCO4/gdoWZzIWDO4TvDGYIBBxGLw+HO4OKO4nA1WQ4GwFYMGBIML3a6I/53CgEOZxoAFO4MPgPxSwIAE93gSIQACqsFqEMF4MLeAbjFW4UA0ABCAAmOSwp3Dxe7hAiGha3BhOQhANCd4W/l7EDyGQzILBG4L4GP4Z3ODgKVBLgYhBL4MM/kA/LcBoHwoCAF6HueALdBh3+eAQABuEHcgKdFbgQBB4JtD3YAGgGwUoIiDAYTdB2Xy2DiCOgJ4BO4vQPYfMGQJdB5nM55rELYg9CA4fvO4cIxEAzJoBh4uBO4sLH4QOBC4X/PAMHAAQSCg/ud4UMAAYMCzOIwB2GO4oABJQbvFAAg3BHAPgFIKpDO4TgB//5zML1cAjUAhUQeAYABxAeC7qWDAALvCAAfAK4bbB92QAAJCFg93d4gGBAgSVBO4sJxbvI2EIBwPYAQOqVoYOBXAICDbI5YDO4cJzOZznMhQiCKYXQO4PMCQLCBLYorIABGQhp3CewTvDKIbvB54TBd453Hd4sNPQWZGITnDbQMPX4jLFABEONQMK3QGBFAR3Cg8Gd4JwRDYRwDUQJHC8HgCg2wd4XA+B3DeYO/BgMJxDvHhYMBd4l3agRCI7sNAAJEEFgLtCJ4nM5gbGhqRBg9gMgUPdoYBDfwIaExAABwDvEAIUOhIBBQAMJAYJ3D93Ah7RDAAO7+ARBEQgADBAbvBAoPuO48OW4R2FAAZ2GCoPOEAMLX4gDCNYTvB+Hw/8AuAIBAQScBDQQBBG4SoBF4OQAALvDO4ZQCd4eZOwbDCd4WZwEPGwQAL7p3BhOQDALMBQQPgNY/bO4R4DCAXx/DOGAAZnBAAMPd4JCBg4ABTgo4BAIPuEwXteAhlDJgOQd4UL3YMC/PwAgW52EJ/grDh//O4IpDeQ0A5iLBGIOwc4ZBB5nAG4OZm71BIoR3DhyrC/8QEgYiBu50BRIdwUwLvBAAp3DdwYlBEwS3CACLvGO4fM5h3CBQIpDgEIxAFDqoeCD4PdhvQRYOA//w8CsBMIML7zaCMoYACiMfF4PwX4OQuFwdgZ3B6BgBeAMAd4oRB3cLVgLFFhoEBha7Ch8PhAABAgJ4G+ycCd4vHvjBBVIZ5Ed4gABSoQxChsIdYWQ8HphOnVw4iCT4hQBO4TvDMYR3DdQVwBIR3ChcLPALvDHwXAFQQSCABXwPoP/sBCHO4SMCwBxEhAFB5ncDYIsMEoKFCa4YDC8DCBAQOZ5nMBILvIAoPdH4UPdgIBDSAQACJgMIHYzvDdoQADBweZzMAsx3CKgZIBIofAMAoMBwBKB6AMELAQCBIIIAKXRGZ/6YDIQNwg7vBO4buBABewAAK+DGh4AEz3pegZtBGwLyC4C1DOwj/DO5BYBhOQ3JCBh7LBgHuAAMA5vgvI9HVAKpCABDkBO4ztDgEEdwYAJd4TqDgwFEO4sP95ABO4TiBbYp4EKoncgEKAIPdRoMJCoJCDbYQjBDQPA8Fw0BQLAYyYBAAuIwAABg75DCAISE+DVBAQTvHsFgZQ2Zd45TCGwgIC8HuAQINDd4Wg0HQ5j4ByAaEHoTvFO4OwMouYmcwh//AIIKDYgYADh4IBPIMHg7dBgxoFCAMAwACBEIgACdwMGAwYWDhvLD4sOeoMHAwWJwDvIO4JxBeALvB5jdKABf4RAOImCNBKoVQAQOOG4YAC/5UBd4Y7BBYQ4Sd4sPj6OCLQIAHO4cIH4R2BPAwAChcOXYMMgYNHhpODAA7XBO4rvBMwMI9HoeYZBC5kM4AGBd4TPC4D5Cu+Zh5iB3ew2HP5nAdAbwBAocP+J3ChItCOIYtCAoYOBgHgOwUMdYIADBIOw8Fw6GQLwIAG6GZzLvKFYJ6Bd4arC7qRCO4cM5gABAwIyB8DvDCARKC+C8BAgP//4GBABEBiJ3BqAcCuF3O4l3AwgAF4AABIQJ3Ch7wDyYIB1MK7gOCYwOQDgcMNYP/NwQMCyDtBBAQHBhv9/p3FOwTZBXQcJx3ugF3uEHvKnDO4LvDdQYADL4kP81wdA14KQmwcoq3CAQP8BYfweATvCyGQ6EMI4J3Bd5UAhQEDxEIdoOgO4MPDQJ3GMIPILQhEB8BXCJQR3EGpIAFh/g8AtCLwQlBHoIgCAQbwFPQcAggLEd4SUB6ARBuF96EAhML3YABDYMJCwQwCNYWAAQJVB7vw/oaBO4Y0B5iuD4+Qhx3Kh4DCWoIGBh7tCAgIUE+HuAYJ3D/8A7iTDhgeCegQAEBIdEoBoB9IIDO4PcDQNwuDvD2CaC4HACALuEd4iRB7vzO4JTBg5JCeAXohEMvLvGAgMD//yOALVBBgIDCAA8OBYLvDAAVQ+ABBcooBBeQ54CggABEgKZCQYgABO4QXDO4wAJdQMN7vddwOIg93XIXMh3gwDuBLgQ3CNoJdB+Hw/7iChnsFIkNhsMHoUOCAJ3BegQABgtVNQwnBAYMLWYIADNgVAOwNAd4UN5pfFKwR3GgEJgBkBLIX/VoKoCXQgAHB4QAFOAPwLYIBBO4QDBAIIjBSIPMDYxyDhaCBb4zvJ9wAE2C4BeAKlFO40AtvM5wdBO4O7fgg+BH4JJCM5ByEhjjEAA4KBBg4XCh//UoRsBNoXdPIWw2HQ2G9BAIYBhcJYYIFBD4TRCAAiWDO4sAyALCUgZ3DAA94vEO70AzOQK4JmH6BfEhvdFAUDmEzmDkCAAe72BTBKosHu93VYIAENwKOBd4R6CVYXA2GQgyLCfhTvHLYJ3Bd5IAD997SoNwhCJDEgPuCIn/MwItBAQR3BhoWCOgIBBAA2q0BaBKRLvCGggABCZTqEAwsIDojvGaYTvGAA0Ph33uELg94BYjKECIP/boMNAwPe6HMd4Q8BxGAAIKFBeAgIBh2OMoXgcYIAJ5jvCfQvdeIQANh7vLGRbvEvOQW4JeBwGA5jLG/+IMgXtOwImHmDvFyB5ExAkCIQIbCNYNwg93hGIgHA4CIBg4gETYdAA4SHBEAIXBAIIRCC4h3EgyOKhi6CBIsIaIICCO4cIQYP/d4S8B9x3HmZ4BIIcM/IMDd4sNDIsHg6uBO4QJCeAl3AoJiBRIUO9wLBYoJOBAAOwPAoAD8C2EAAY8BVIJEC7oPHwBBEbwQmEaYXnSgwAGHAojFHwbuBd4QHB5iBEGwzaCN4MMCQTvF34qFhyDCO4MJ/kAx2wBAP8hvQ5h2CPoLXD9ns8GIwEMKYcLeAR2EJooAHXAR3CDQMMAATvFh1w87vCLobuDAIJ3EXwaJBxBIBdwKSCh5CCu4ZBAAMIzOAO4h/CgxxBPAJ2BL4XQhoGBYxI/F9x4BDIPgEwUA3YABNwToDyB4B2CvCACihGg8GKwLvCxjvGVgVwTYIYDBgIYBd4Z3Cd4JxBOALwD7tOMYQ3EUAMJeAQKE9ylCqA4CNQIACIQcM/IaBAAIZCgjADJANgAIQAIuEDmEwmZPBDIsM5iPKO4tAgGQMIbvEAAMOAATuCBATvCg93uB3BNAQAEhzvDmDdEAgLuEAALuBd5JABwFng53JdwsIWINwCYuIMAQACQAV3AAJBCHoZ3EBQTvB7vQc4UOhqlDd4R1BO4X/O44FEfgLvEO4JuHQIQoBd4Z3Du5jBh8PdwwDCmDBB8BKEDwYfCA4bNBSQ+IhMJhSWBACp3CAoSfBIoXuCpLvH5n5eASQBSIuIaIMPvIGBh/wE5J3Bd4RlCLoeIBQOIO5sIO4WoFQ7xBdgICBhrdFuAhC/4ABA4IABDotm5nMgBXBhe7gG7dwSrH8AABaAgBBg6gBABGgAwruEdYQDCAoX8HgJ3CAAnwd4qLD1orGAAbDFAAUP/4rBP4J3E5/8s3uO4IAIwB7CFQgrFO4QoBGw6aB1QoJbIKiBNwR3C4HAhhABJYkP94UB6GQD4vbTgXuAATJC8BABYgwAHeoI1Bhh3DQwIABoBNDhbwINAZ3EGgpUBh8LmfuYhRxBhg7BhgIC/gDCg8HgGIFIRGBA4IAGd4hxCgF3uB3GhB3IhOZFALvC5h3DoFPgjkB7sA2AcCHYkPSYVwYokOKIbvF126AoNEgigB9RHCUAJ1BdARsCewVwwF4WAYvBMoI/Cu4zBxwGB3cL2BxBFAJNBO4v3+/wVAOQJYJNChP5c4sDgEwgGEwB3B93QJoUHNoICCXYb7BeAIADYYvA53u93qeAVAAAJWB1wRDd4wAEsEIHIMGs1mu4ABHQQCBhHIAoOwAALvDAoI3B9x3Cv9/CwPPyGN6ABBd4h3HppOBhzvCMoR2BAQKxBO4TvGIwQAD5nA8Hg92u1QuCAILwEd4Z3Hg0GgGIgB2BO4d2sw+Bd4mwAIJ3FEQqRCd48P/+QO4kAkQFCojGCRQLdDGwJwCDYJTBdxZlBgB2BA==")),0,135); +g.drawImage(require("heatshrink").decompress(atob("+FQgl+xnu8AIBwGQgHuAoN3gF/hcLgEHu943G3hwUCDwIBCAAV3uEAhoBBhsO90OgHgoACBh0IhP5AAQXBg8H8Hw+GwEAXn4AECxGAh0MEAOeJAMP3+/huIG4cMg1mMog8BhnsAQIBC///J4MN6HcBIOIAAPs8Hl9nM5gcB0Hg852BAIMAI4YACIIIACh8AKAcAvA6D7vd7wTBTYJ3B9e+hEAhA4CyHuy8HXw29NgIABx+ASQKsBYgR3DgHQCIXMsEAAIOZyGZzx3Dh/A57IDPoXN4HNHwQoB9wAByDvBO4LhDOwR4Fd4cP/4oB0DWCd45VCgFFAYPuO4QACgEed4PweAILBN4NpwEMXILvBO4bvD/f/d4cPCYJ1BAAKSCzp3E/hNBJwPziEP+H8hrvD9DtC5MJd4RTBGoLvBhe7BQJSBAAeAI4IoCO4T2Ch8N6DvDeAPgqFQd48MiB3BE4cI/AvC5ns4AKCdgQAD//wUwMMhhgBO4Nmd4xED57vD+EwFgKTCYoON/+v////OZwGXgF55vQI4TaBEQRxB6Hw7DRCAAPgO44ACKYlFoB3CHIcAiEAi93I4JpCdARmBd4IAFd4QAE4HA5//hh1BAIIPByA5BEQUM/n8O4TzCAAQtBhvd/X8d4YYBvwOBO4LvBYIoKBh/YewfA6B3DLoP/d4JXGABMBiKkEAAwKH9LyFO4fwOoR3Dd4TDD5/AJQcwDgcO9zvC1vd7ocBxuAvh3CuEHh5jCEoOPgHf/53CGgMAoGgbgX/CgJZEAIYAB5HIbxRCBAYULhZfBAAMA/GA/47Bd44ABh4CBg1mg8A3YAB3vtO4cMWxvG5vdZYWIw8AvPQd4NwRwUwAYIlBhsNGoR3CqB3BIAR4BFAXHAIg/CRAIDBIgtHHIR3D3ZhCZYXwwBrCOAXP5n855kNO4OABIyxCHYcDmdutOZA4VAAYUNqB0DAAQfDKIVms3AAgJ3BhBMBJwgAHhi7DDIQABgl9CIrvCeAJ3JABPM4AoBhqbDIgI0CMQfdOgR3E5nG5MzIAIBBAQIABwA5BgUgkEiEAe7hwECtgCB2B3BbwMJ9OeyBLIh3gFATvCPITuDhoCEgFVqq0B//w///MQWIbYJkFAAIjBEoR3DCoOA8A3CYAOvh/wE4LvEKoLvCoEE/7xDAAy/C2G+gw2DNQ2e9I0DBgxIBxGAWgS1DAAfd7pYE6BrBWwUIh2OAwLcGNQOA5jbCd4gACO4OgAgMHu4aBDokKgGIZ4LtBogABBgXw4HwhnL5lwEQXgd4V3BAIdBb4jvBO4/uIAfQKAJ3Gh7sC6/X7ogBUIL0BCwJ3ChHoO4QeCO4YHBXAQCBO4xQBJoYVBNwIBBhWq0HDwEOCIPuoDtIH4LuCAAOwMIR3BUATnIfgZ9BFYKHBd5nQKwICBBYWAPoJ3B///d5HM5jvD4DxBd4PQGwIBCHIMAeAQAEhQIC4GIboQABB4ifBW4ZeCAAO+EwJyBNQV2sDvCAAw6DAAaLFDgPwB4kNGIUJ5I3CcooAHO4OZzILH+AABFgcKeAa+Dd4p3Jd4+Ld4juChnMuz0DNQQABBAMOM4RqDuFwY4IUEGpLwB8DjB+ACBC4kJyAEC93uyAABDoxLB8HwFYTlBAIMMFIJlEQQJ3BCoIYBDgULCIpZCQ4YGBu5pBhn/u4UExB2BNoMO9wBB9xqDO4JeEEQKTFxABBwHJh3ex2P9+JxncZAJcBhMJO4mZO4dgXYRPCWQQzF4AABRIhHB5gACBYPeSAcAxOAAYICCdwK0CQYfc/I6BNYeAOwIAKBgMMQIIHC8EP///AoLkBgH4+AMCd4uoxWI1B3EAAOQzIDBswCBcIwAGBosOh7dChuNAYXvL4IPChGYgEP+AnFFox3B9vtO4LvBG47/CcofOPoYABWIIzCd4bYCB4NwgwFBd4IBBhI0Bh64CdwIHBdwJIBdAq7BEgTwDAgaxBAQMJhvdBALuBBAIQDeAMPh/ADQOH2+IhpeDfgbvDZAMP54ACMoJcCsAYC5nOV4OXcgQADd4QADs8HsF2g1QSwQAE+AcGRILhD/5cDE4ySDAgcGwGdxqvDd4j3BCIMP5iSCvfQcA6SB9wLBxBmBAAX/H4LkDSAcOFoOXgG72AgEd4IADqEFAQkL93rhzHCLgRIBCwbwCBgSFBOoLvBwEMg6XBBgIXDO4WJhuNHQyOF+DvCu+w2/QHoQACBYPt7qsCAAPgOQLvJAAeXhYdCZYIBBKYOAAII/I3yMB6CoBd4UDgbvDO44gBPIQ+BW4YADD4TvBOoI2FKA0A0AABAwfu9oOFOwPgAQLgBDoqwBAQIJFO5QACJIP/JQIDC+AVCO4LrBdgjuE24uB/7uFd4nwQob0DxEN7uIVxJ3E1R3Bh0ONoZ+E93gAIIPCVQ7fDgENAwRhC8AWBE4LvNAAXdaQsAmAHEO4QABhOZyB6BxB3BIg3QH4PQ/GIEIIAGQIMPTQMAhTuB1DaE9xNCAQTvCLgQACyDcDAAWIFARbD3ew9ycEKILvCABkMAAMAgZKCAAYlBHog8BAArqDO4mPx5bBuCTDCYWfh/P6AeFNgVwg7FEaITvC4BIB4B3HMgXdEwP/VwyCBO4QpB8A4GABiUCACB2COoIBCxH4wEM28A5hYCgEGszvC6F3NojKBuF3O4g+DPQPAAAWQ/7GB5nMH48D+AsCAAZDBF4YFCP4OAwD4GJgQCBhkJBYg8BBQJeBCgoABBAQCBNgIABd4UL5dwBASZQxGAKQcNAgPuQgJuBhnAz8A/kM553GFwMwO4PPhYfFTYjvBhAwBfAQABuA/GVAKKCTgxdR/GI+EM3gXCSIZeBg8Au7vEO4vQJgIAB+BTB8DvI//8FQLzBFYPL5YDBKQvQd5Z3FYoUPO4ZUBCQOf/5YDVoIFDIwNw+CUHBgQADEAOIUQnHg9wg+8714zUQCYbvBO4pDFXwRPBd4UOfwIzB5e7U4gAMO4R4BA4S4HhgiBO452DRQcP54ECyEJzJ3DkYXDGIIABRQTvCVoI0EhvcZghFCu4QBaQhKEdYIIFO4m7hewGIIRFEJAAFMYRQCRQZ3FXYUOCYXgd4cJhJ5BBIMOgE9mAYCxGAd4kAdwJ3DzIYBhu9OwbvDPwqTCcI8LAYU83gEC2B4BCoP85ns4Z6BO5UP/5lCAAz+DF4kPOoIBBC4eggGpdoJeBh3ggEDkLvGHROeDAMI7rFETYLVB3ew6AMDJwxKEgcAQgZ3D5//53Onk8O4a+BAIO62DvIKQMJKIMIZofQh3uOQIABR4X/BgLtBd4h3B4+QiF2gzjCeggAB5vmwGrd4YADSYMGy2Wd4jODd4j5EAA52BMwLvB53uO4MNTIUBgIRB1TOBAAJlBABkHJAXgHYI9CXAK6Cbwvghx3BAoNgAQI1BiMAw53ExJ3BAAUMhWQhptCd4T3DNwzGBhh5BhnMPoQEDBAnM5jvB4YIBFQUQ+EQd4plBFYZLCGgvQuDvCO4/gdoWZzIWDO4TvDGYIBBxGLw+HO4OKO4nA1WQ4GwFYMGBIML3a6I/53CgEOZxoAFO4MPgPxSwIAE93gSIQACqsFqEMF4MLeAbjFW4UA0ABCAAmOSwp3Dxe7hAiGha3BhOQhANCd4W/l7EDyGQzILBG4L4GP4Z3ODgKVBLgYhBL4MM/kA/LcBoHwoCAF6HueALdBh3+eAQABuEHcgKdFbgQBB4JtD3YAGgGwUoIiDAYTdB2Xy2DiCOgJ4BO4vQPYfMGQJdB5nM55rELYg9CA4fvO4cIxEAzJoBh4uBO4sLH4QOBC4X/PAMHAAQSCg/ud4UMAAYMCzOIwB2GO4oABJQbvFAAg3BHAPgFIKpDO4TgB//5zML1cAjUAhUQeAYABxAeC7qWDAALvCAAfAK4bbB92QAAJCFg93d4gGBAgSVBO4sJxbvI2EIBwPYAQOqVoYOBXAICDbI5YDO4cJzOZznMhQiCKYXQO4PMCQLCBLYorIABGQhp3CewTvDKIbvB54TBd453Hd4sNPQWZGITnDbQMPX4jLFABEONQMK3QGBFAR3Cg8Gd4JwRDYRwDUQJHC8HgCg2wd4XA+B3DeYO/BgMJxDvHhYMBd4l3agRCI7sNAAJEEFgLtCJ4nM5gbGhqRBg9gMgUPdoYBDfwIaExAABwDvEAIUOhIBBQAMJAYJ3D93Ah7RDAAO7+ARBEQgADBAbvBAoPuO48OW4R2FAAZ2GCoPOEAMLX4gDCNYTvB+Hw/8AuAIBAQScBDQQBBG4SoBF4OQAALvDO4ZQCd4eZOwbDCd4WZwEPGwQAL7p3BhOQDALMBQQPgNY/bO4R4DCAXx/DOGAAZnBAAMPd4JCBg4ABTgo4BAIPuEwXteAhlDJgOQd4UL3YMC/PwAgW52EJ/grDh//O4IpDeQ0A5iLBGIOwc4ZBB5nAG4OZm71BIoR3DhyrC/8QEgYiBu50BRIdwUwLvBAAp3DdwYlBEwS3CACLvGO4fM5h3CBQIpDgEIxAFDqoeCD4PdhvQRYOA//w8CsBMIML7zaCMoYACiMfF4PwX4OQuFwdgZ3B6BgBeAMAd4oRB3cLVgLFFhoEBha7Ch8PhAABAgJ4G+ycCd4vHvjBBVIZ5Ed4gABSoQxChsIdYWQ8HphOnVw4iCT4hQBO4TvDMYR3DdQVwBIR3ChcLPALvDHwXAFQQSCABXwPoP/sBCHO4SMCwBxEhAFB5ncDYIsMEoKFCa4YDC8DCBAQOZ5nMBILvIAoPdH4UPdgIBDSAQACJgMIHYzvDdoQADBweZzMAsx3CKgZIBIofAMAoMBwBKB6AMELAQCBIIIAKXRGZ/6YDIQNwg7vBO4buBABewAAK+DGh4AEz3pegZtBGwLyC4C1DOwj/DO5BYBhOQ3JCBh7LBgHuAAMA5vgvI9HVAKpCABDkBO4ztDgEEdwYAJd4TqDgwFEO4sP95ABO4TiBbYp4EKoncgEKAIPdRoMJCoJCDbYQjBDQPA8Fw0BQLAYyYBAAuIwAABg75DCAISE+DVBAQTvHsFgZQ2Zd45TCGwgIC8HuAQINDd4Wg0HQ5j4ByAaEHoTvFO4OwMouYmcwh//AIIKDYgYADh4IBPIMHg7dBgxoFCAMAwACBEIgACdwMGAwYWDhvLD4sOeoMHAwWJwDvIO4JxBeALvB5jdKABf4RAOImCNBKoVQAQOOG4YAC/5UBd4Y7BBYQ4Sd4sPj6OCLQIAHO4cIH4R2BPAwAChcOXYMMgYNHhpODAA7XBO4rvBMwMI9HoeYZBC5kM4AGBd4TPC4D5Cu+Zh5iB3ew2HP5nAdAbwBAocP+J3ChItCOIYtCAoYOBgHgOwUMdYIADBIOw8Fw6GQLwIAG6GZzLvKFYJ6Bd4arC7qRCO4cM5gABAwIyB8DvDCARKC+C8BAgP//4GBABEBiJ3BqAcCuF3O4l3AwgAF4AABIQJ3Ch7wDyYIB1MK7gOCYwOQDgcMNYP/NwQMCyDtBBAQHBhv9/p3FOwTZBXQcJx3ugF3uEHvKnDO4LvDdQYADL4kP81wdA14KQmwcoq3CAQP8BYfweATvCyGQ6EMI4J3Bd5UAhQEDxEIdoOgO4MPDQJ3GMIPILQhEB8BXCJQR3EGpIAFh/g8AtCLwQlBHoIgCAQbwFPQcAggLEd4SUB6ARBuF96EAhML3YABDYMJCwQwCNYWAAQJVB7vw/oaBO4Y0B5iuD4+Qhx3Kh4DCWoIGBh7tCAgIUE+HuAYJ3D/8A7iTDhgeCegQAEBIdEoBoB9IIDO4PcDQNwuDvD2CaC4HACALuEd4iRB7vzO4JTBg5JCeAXohEMvLvGAgMD//yOALVBBgIDCAA8OBYLvDAAVQ+ABBcooBBeQ54CggABEgKZCQYgABO4QXDO4wAJdQMN7vddwOIg93XIXMh3gwDuBLgQ3CNoJdB+Hw/7iChnsFIkNhsMHoUOCAJ3BegQABgtVNQwnBAYMLWYIADNgVAOwNAd4UN5pfFKwR3GgEJgBkBLIX/VoKoCXQgAHB4QAFOAPwLYIBBO4QDBAIIjBSIPMDYxyDhaCBb4zvJ9wAE2C4BeAKlFO40AtvM5wdBO4O7fgg+BH4JJCM5ByEhjjEAA4KBBg4XCh//UoRsBNoXdPIWw2HQ2G9BAIYBhcJYYIFBD4TRCAAiWDO4sAyALCUgZ3DAA94vEO70AzOQK4JmH6BfEhvdFAUDmEzmDkCAAe72BTBKosHu93VYIAENwKOBd4R6CVYXA2GQgyLCfhTvHLYJ3Bd5IAD997SoNwhCJDEgPuCIn/MwItBAQR3BhoWCOgIBBAA2q0BaBKRLvCGggABCZTqEAwsIDojvGaYTvGAA0Ph33uELg94BYjKECIP/boMNAwPe6HMd4Q8BxGAAIKFBeAgIBh2OMoXgcYIAJ5jvCfQvdeIQANh7vLGRbvEvOQW4JeBwGA5jLG/+IMgXtOwImHmDvFyB5ExAkCIQIbCNYNwg93hGIgHA4CIBg4gETYdAA4SHBEAIXBAIIRCC4h3EgyOKhi6CBIsIaIICCO4cIQYP/d4S8B9x3HmZ4BIIcM/IMDd4sNDIsHg6uBO4QJCeAl3AoJiBRIUO9wLBYoJOBAAOwPAoAD8C2EAAY8BVIJEC7oPHwBBEbwQmEaYXnSgwAGHAojFHwbuBd4QHB5iBEGwzaCN4MMCQTvF34qFhyDCO4MJ/kAx2wBAP8hvQ5h2CPoLXD9ns8GIwEMKYcLeAR2EJooAHXAR3CDQMMAATvFh1w87vCLobuDAIJ3EXwaJBxBIBdwKSCh5CCu4ZBAAMIzOAO4h/CgxxBPAJ2BL4XQhoGBYxI/F9x4BDIPgEwUA3YABNwToDyB4B2CvCACihGg8GKwLvCxjvGVgVwTYIYDBgIYBd4Z3Cd4JxBOALwD7tOMYQ3EUAMJeAQKE9ylCqA4CNQIACIQcM/IaBAAIZCgjADJANgAIQAIuEDmEwmZPBDIsM5iPKO4tAgGQMIbvEAAMOAATuCBATvCg93uB3BNAQAEhzvDmDdEAgLuEAALuBd5JABwFng53JdwsIWINwCYuIMAQACQAV3AAJBCHoZ3EBQTvB7vQc4UOhqlDd4R1BO4X/O44FEfgLvEO4JuHQIQoBd4Z3Du5jBh8PdwwDCmDBB8BKEDwYfCA4bNBSQ+IhMJhSWBACp3CAoSfBIoXuCpLvH5n5eASQBSIuIaIMPvIGBh/wE5J3Bd4RlCLoeIBQOIO5sIO4WoFQ7xBdgICBhrdFuAhC/4ABA4IABDotm5nMgBXBhe7gG7dwSrH8AABaAgBBg6gBABGgAwruEdYQDCAoX8HgJ3CAAnwd4qLD1orGAAbDFAAUP/4rBP4J3E5/8s3uO4IAIwB7CFQgrFO4QoBGw6aB1QoJbIKiBNwR3C4HAhhABJYkP94UB6GQD4vbTgXuAATJC8BABYgwAHeoI1Bhh3DQwIABoBNDhbwINAZ3EGgpUBh8LmfuYhRxBhg7BhgIC/gDCg8HgGIFIRGBA4IAGd4hxCgF3uB3GhB3IhOZFALvC5h3DoFPgjkB7sA2AcCHYkPSYVwYokOKIbvF126AoNEgigB9RHCUAJ1BdARsCewVwwF4WAYvBMoI/Cu4zBxwGB3cL2BxBFAJNBO4v3+/wVAOQJYJNChP5c4sDgEwgGEwB3B93QJoUHNoICCXYb7BeAIADYYvA53u93qeAVAAAJWB1wRDd4wAEsEIHIMGs1mu4ABHQQCBhHIAoOwAALvDAoI3B9x3Cv9/CwPPyGN6ABBd4h3HppOBhzvCMoR2BAQKxBO4TvGIwQAD5nA8Hg92u1QuCAILwEd4Z3Hg0GgGIgB2BO4d2sw+Bd4mwAIJ3FEQqRCd48P/+QO4kAkQFCojGCRQLdDGwJwCDYJTBdxZlBgB2BA==")),0,y+19); +g.drawString(NRF.getAddress(),g.getWidth()/2,g.getHeight()-8,true); g.flip(); setWatch(_=>load(), BTN1); diff --git a/apps/aclock/ChangeLog b/apps/aclock/ChangeLog index 9687bc58f..77d4ea646 100644 --- a/apps/aclock/ChangeLog +++ b/apps/aclock/ChangeLog @@ -8,3 +8,4 @@ 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) diff --git a/apps/aclock/clock-analog.js b/apps/aclock/clock-analog.js index 951145c4e..7516a0eb7 100644 --- a/apps/aclock/clock-analog.js +++ b/apps/aclock/clock-analog.js @@ -113,9 +113,6 @@ const onMinute = () => { g.setColor(1, 1, 0.9); // Minute hand((360 * currentDate.getMinutes()) / 60, -8, faceWidth - 10); - if (currentDate.getHours() >= 0 && currentDate.getMinutes() === 0) { - Bangle.buzz(); - } drawDate(); }; diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index 23b8ee562..96e1490ab 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -8,3 +8,4 @@ 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 diff --git a/apps/alarm/alarm.js b/apps/alarm/alarm.js index 28261110a..26345e887 100644 --- a/apps/alarm/alarm.js +++ b/apps/alarm/alarm.js @@ -38,6 +38,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/arrow/ChangeLog b/apps/arrow/ChangeLog new file mode 100644 index 000000000..2f1b2b4c4 --- /dev/null +++ b/apps/arrow/ChangeLog @@ -0,0 +1,4 @@ +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 LCD1.write() as it was keeping LCD on diff --git a/apps/arrow/README.md b/apps/arrow/README.md new file mode 100644 index 000000000..3b439711c --- /dev/null +++ b/apps/arrow/README.md @@ -0,0 +1,41 @@ +# 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) + + +## 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..2cb3a42ad --- /dev/null +++ b/apps/arrow/app.js @@ -0,0 +1,187 @@ +var pal1color = new Uint16Array([0x0000,0xFFC0],0,1); +var pal2color = new Uint16Array([0x0000,0xffff],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 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() { + 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}; + 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}); + },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)=>{ + require("Storage").write("magnav.json",r); + Bangle.buzz(); + CALIBDATA = r; + startdraw(); + setButtons(); + }); + } else { + startdraw(); + setTimeout(setButtons,1000); + } + } + 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); +} + +function startdraw(){ + g.clear(); + g.setColor(1,1,1); + Bangle.drawWidgets(); + candraw = true; + intervalRef = setInterval(reading,500); +} + +function stopdraw() { + candraw=false; + if(intervalRef) {clearInterval(intervalRef);} +} + +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 (on) { + startdraw(); + } else { + stopdraw(); + } +}); + +Bangle.on('kill',()=>{Bangle.setCompassPower(0);}); + +Bangle.loadWidgets(); +Bangle.setCompassPower(1); +startdraw(); +setButtons(); 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/banglerun/ChangeLog b/apps/banglerun/ChangeLog index 5820f9fbf..c778588cc 100755 --- a/apps/banglerun/ChangeLog +++ b/apps/banglerun/ChangeLog @@ -7,3 +7,7 @@ 0.07: Fixed GPS update, added guards against NaN values 0.08: Fix issue with GPS coordinates being wrong after the first one 0.09: Another GPS fix (log raw coordinates - not filtered ones) +0.10: Removed kalman filtering to allow distance log to work + Only log data every 5 seconds (not 1 sec) + Don't create a file until the first log entry is ready + Add labels for buttons diff --git a/apps/banglerun/app.js b/apps/banglerun/app.js index 97287e61b..b79255171 100644 --- a/apps/banglerun/app.js +++ b/apps/banglerun/app.js @@ -1 +1 @@ -!function(){"use strict";const t={STOP:63488,PAUSE:65504,RUN:2016};function n(t,n,r){g.setColor(0),g.fillRect(n-60,r,n+60,r+30),g.setColor(65535),g.drawString(t,n,r)}function r(r){var e;g.setFontVector(30),g.setFontAlign(0,-1,0),n((r.distance/1e3).toFixed(2),60,55),n(function(t){const n=Math.round(t),r=Math.floor(n/3600),e=Math.floor(n/60)%60,o=n%60;return(r?r+":":"")+("0"+e).substr(-2)+":"+("0"+o).substr(-2)}(r.duration),180,55),n(function(t){if(t<.1667)return"__'__\"";const n=Math.round(1e3/t),r=Math.floor(n/60),e=n%60;return("0"+r).substr(-2)+"'"+("0"+e).substr(-2)+'"'}(r.speed),60,115),n(r.hr.toFixed(0),180,115),n(r.steps.toFixed(0),60,175),n(r.cadence.toFixed(0),180,175),g.setFont("6x8",2),g.setColor(r.gpsValid?2016:63488),g.fillRect(0,216,80,240),g.setColor(0),g.drawString("GPS",40,220),g.setColor(65535),g.fillRect(80,216,160,240),g.setColor(0),g.drawString(("0"+(e=new Date).getHours()).substr(-2)+":"+("0"+e.getMinutes()).substr(-2),120,220),g.setColor(t[r.status]),g.fillRect(160,216,240,240),g.setColor(0),g.drawString(r.status,200,220)}function e(t){g.clear(),g.setColor(50712),g.setFont("6x8",2),g.setFontAlign(0,-1,0),g.drawString("DIST (KM)",60,32),g.drawString("TIME",180,32),g.drawString("PACE",60,92),g.drawString("HEART",180,92),g.drawString("STEPS",60,152),g.drawString("CADENCE",180,152),r(t),Bangle.drawWidgets()}var o;function a(t){t.status===o.Stopped&&function(t){const n=(new Date).toISOString().replace(/[-:]/g,""),r=`banglerun_${n.substr(2,6)}_${n.substr(9,6)}`;t.file=require("Storage").open(r,"w"),t.file.write(["timestamp","latitude","longitude","altitude","duration","distance","heartrate","steps"].join(",")+"\n")}(t),t.status===o.Running?t.status=o.Paused:t.status=o.Running,r(t)}!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(o||(o={}));const s={fix:NaN,lat:NaN,lon:NaN,alt:NaN,vel:NaN,dop:NaN,gpsValid:!1,x:NaN,y:NaN,z:NaN,v:NaN,t:NaN,dt:NaN,pError:NaN,vError:NaN,hr:60,hrError:100,file:null,drawing:!1,status:o.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var i;i=s,Bangle.on("GPS",t=>function(t,n){t.lat=n.lat,t.lon=n.lon,t.alt=n.alt,t.vel=n.speed/3.6,t.fix=n.fix,t.dop=n.hdop,t.gpsValid=t.fix>0&&t.dop<=5,function(t){const n=Date.now();let r=(n-t.t)/1e3;if(isFinite(r)||(r=0),t.t=n,t.dt+=r,t.status===o.Running&&(t.duration+=r),!t.gpsValid)return;const e=6371008.8+t.alt,a=t.lat*Math.PI/180,s=t.lon*Math.PI/180,i=e*Math.cos(a)*Math.cos(s),d=e*Math.cos(a)*Math.sin(s),u=e*Math.sin(a),g=t.vel;if(!t.x)return t.x=i,t.y=d,t.z=u,t.v=g,t.pError=2.5*t.dop,void(t.vError=.05*t.dop);const l=i-t.x,c=d-t.y,p=u-t.z,f=g-t.v,N=Math.sqrt(l*l+c*c+p*p),h=Math.abs(f);t.pError+=t.v*t.dt,t.dt=0;const S=N+2.5*t.dop,E=h+.05*t.dop,w=t.pError/(t.pError+S)||0,x=t.vError/(t.vError+E)||0;t.x+=l*w,t.y+=c*w,t.z+=p*w,t.v+=f*x,t.pError+=(S-t.pError)*w,t.vError+=(E-t.vError)*x,t.status===o.Running&&(t.distance+=N*w,t.speed=t.distance/t.duration||0,t.cadence=60*t.steps/t.duration||0)}(t),r(t),t.gpsValid&&t.status===o.Running&&function(t){t.file.write([Date.now().toFixed(0),t.lat.toFixed(6),t.lon.toFixed(6),t.alt.toFixed(2),t.duration.toFixed(0),t.distance.toFixed(2),t.hr.toFixed(0),t.steps.toFixed(0)].join(",")+"\n")}(t)}(i,t)),Bangle.setGPSPower(1),function(t){Bangle.on("HRM",n=>function(t,n){if(0===n.confidence)return;const r=n.bpm-t.hr,e=Math.abs(r)+101-n.confidence,o=t.hrError/(t.hrError+e)||0;t.hr+=r*o,t.hrError+=(e-t.hrError)*o}(t,n)),Bangle.setHRMPower(1)}(s),function(t){Bangle.on("step",()=>function(t){t.status===o.Running&&(t.steps+=1)}(t))}(s),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&e(t)}),e(t)}(s),setWatch(()=>a(s),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(t){t.status===o.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(t),t.status===o.Running?t.status=o.Paused:t.status=o.Stopped,r(t)}(s),BTN3,{repeat:!0,edge:"falling"})}(); +!function(){"use strict";var t;!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(t||(t={}));const n={STOP:63488,PAUSE:65504,RUN:2016};function e(t,n,e){g.setColor(0),g.fillRect(n-60,e,n+60,e+30),g.setColor(65535),g.drawString(t,n,e)}function i(i){var s;g.setFontVector(30),g.setFontAlign(0,-1,0),e((i.distance/1e3).toFixed(2),60,55),e(function(t){const n=Math.round(t),e=Math.floor(n/3600),i=Math.floor(n/60)%60,s=n%60;return(e?e+":":"")+("0"+i).substr(-2)+":"+("0"+s).substr(-2)}(i.duration),172,55),e(function(t){if(t<.1667)return"__'__\"";const n=Math.round(1e3/t),e=Math.floor(n/60),i=n%60;return("0"+e).substr(-2)+"'"+("0"+i).substr(-2)+'"'}(i.speed),60,115),e(i.hr.toFixed(0),172,115),e(i.steps.toFixed(0),60,175),e(i.cadence.toFixed(0),172,175),g.setFont("6x8",2),g.setColor(i.gpsValid?2016:63488),g.fillRect(0,216,80,240),g.setColor(0),g.drawString("GPS",40,220),g.setColor(65535),g.fillRect(80,216,160,240),g.setColor(0),g.drawString(("0"+(s=new Date).getHours()).substr(-2)+":"+("0"+s.getMinutes()).substr(-2),120,220),g.setColor(n[i.status]),g.fillRect(160,216,230,240),g.setColor(0),g.drawString(i.status,200,220),g.setFont("6x8").setFontAlign(0,0,1).setColor(-1),i.status===t.Paused?g.drawString("START",236,60,1).drawString(" CLEAR ",236,180,1):i.status===t.Running?g.drawString(" PAUSE ",236,60,1).drawString(" PAUSE ",236,180,1):g.drawString("START",236,60,1).drawString(" ",236,180,1)}function s(t){g.clear(),g.setColor(50712),g.setFont("6x8",2),g.setFontAlign(0,-1,0),g.drawString("DIST (KM)",60,32),g.drawString("TIME",180,32),g.drawString("PACE",60,92),g.drawString("HEART",180,92),g.drawString("STEPS",60,152),g.drawString("CADENCE",180,152),i(t),Bangle.drawWidgets()}function a(n){n.status===t.Stopped&&function(t){const n=(new Date).toISOString().replace(/[-:]/g,""),e=`banglerun_${n.substr(2,6)}_${n.substr(9,6)}`;t.file=require("Storage").open(e,"w"),t.fileWritten=!1}(n),n.status===t.Running?n.status=t.Paused:n.status=t.Running,i(n)}const r={fix:NaN,lat:NaN,lon:NaN,alt:NaN,vel:NaN,dop:NaN,gpsValid:!1,x:NaN,y:NaN,z:NaN,t:NaN,timeSinceLog:0,hr:60,hrError:100,file:null,fileWritten:!1,drawing:!1,status:t.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var o;o=r,Bangle.on("GPS",n=>function(n,e){n.lat=e.lat,n.lon=e.lon,n.alt=e.alt,n.vel=e.speed/3.6,n.fix=e.fix,n.dop=e.hdop,n.gpsValid=n.fix>0,function(n){const e=Date.now();let i=(e-n.t)/1e3;if(isFinite(i)||(i=0),n.t=e,n.timeSinceLog+=i,n.status===t.Running&&(n.duration+=i),!n.gpsValid)return;const s=6371008.8+n.alt,a=n.lat*Math.PI/180,r=n.lon*Math.PI/180,o=s*Math.cos(a)*Math.cos(r),g=s*Math.cos(a)*Math.sin(r),d=s*Math.sin(a);if(!n.x)return n.x=o,n.y=g,void(n.z=d);const u=o-n.x,l=g-n.y,c=d-n.z,f=Math.sqrt(u*u+l*l+c*c);n.x=o,n.y=g,n.z=d,n.status===t.Running&&(n.distance+=f,n.speed=n.distance/n.duration||0,n.cadence=60*n.steps/n.duration||0)}(n),i(n),n.gpsValid&&n.status===t.Running&&n.timeSinceLog>5&&(n.timeSinceLog=0,function(t){t.fileWritten||(t.file.write(["timestamp","latitude","longitude","altitude","duration","distance","heartrate","steps"].join(",")+"\n"),t.fileWritten=!0),t.file.write([Date.now().toFixed(0),t.lat.toFixed(6),t.lon.toFixed(6),t.alt.toFixed(2),t.duration.toFixed(0),t.distance.toFixed(2),t.hr.toFixed(0),t.steps.toFixed(0)].join(",")+"\n")}(n))}(o,n)),Bangle.setGPSPower(1),function(t){Bangle.on("HRM",n=>function(t,n){if(0===n.confidence)return;const e=n.bpm-t.hr,i=Math.abs(e)+101-n.confidence,s=t.hrError/(t.hrError+i)||0;t.hr+=e*s,t.hrError+=(i-t.hrError)*s}(t,n)),Bangle.setHRMPower(1)}(r),function(n){Bangle.on("step",()=>function(n){n.status===t.Running&&(n.steps+=1)}(n))}(r),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&s(t)}),s(t)}(r),setWatch(()=>a(r),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(n){n.status===t.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(n),n.status===t.Running?n.status=t.Paused:n.status=t.Stopped,i(n)}(r),BTN3,{repeat:!0,edge:"falling"})}(); diff --git a/apps/banglerun/src/display.ts b/apps/banglerun/src/display.ts index baa370860..528890c35 100644 --- a/apps/banglerun/src/display.ts +++ b/apps/banglerun/src/display.ts @@ -1,4 +1,4 @@ -import { AppState } from './state'; +import { ActivityStatus, AppState } from './state'; declare var Bangle: any; declare var g: any; @@ -26,11 +26,11 @@ function drawBackground(): void { g.setFont('6x8', 2); g.setFontAlign(0, -1, 0); g.drawString('DIST (KM)', 60, 32); - g.drawString('TIME', 180, 32); + g.drawString('TIME', 172, 32); g.drawString('PACE', 60, 92); - g.drawString('HEART', 180, 92); + g.drawString('HEART', 172, 92); g.drawString('STEPS', 60, 152); - g.drawString('CADENCE', 180, 152); + g.drawString('CADENCE', 172, 152); } function drawValue(value: string, x: number, y: number) { @@ -45,11 +45,11 @@ function draw(state: AppState): void { g.setFontAlign(0, -1, 0); drawValue(formatDistance(state.distance), 60, 55); - drawValue(formatTime(state.duration), 180, 55); + drawValue(formatTime(state.duration), 172, 55); drawValue(formatPace(state.speed), 60, 115); - drawValue(state.hr.toFixed(0), 180, 115); + drawValue(state.hr.toFixed(0), 172, 115); drawValue(state.steps.toFixed(0), 60, 175); - drawValue(state.cadence.toFixed(0), 180, 175); + drawValue(state.cadence.toFixed(0), 172, 175); g.setFont('6x8', 2); @@ -64,9 +64,18 @@ function draw(state: AppState): void { g.drawString(formatClock(new Date()), 120, 220); g.setColor(STATUS_COLORS[state.status]); - g.fillRect(160, 216, 240, 240); + g.fillRect(160, 216, 230, 240); g.setColor(0x0000); g.drawString(state.status, 200, 220); + + g.setFont("6x8").setFontAlign(0,0,1).setColor(-1); + if (state.status === ActivityStatus.Paused) { + g.drawString("START",236,60,1).drawString(" CLEAR ",236,180,1); + } else if (state.status === ActivityStatus.Running) { + g.drawString(" PAUSE ",236,60,1).drawString(" PAUSE ",236,180,1); + } else { + g.drawString("START",236,60,1).drawString(" ",236,180,1); + } } function drawAll(state: AppState) { diff --git a/apps/banglerun/src/gps.ts b/apps/banglerun/src/gps.ts index 3cab561a7..1886ecfb2 100644 --- a/apps/banglerun/src/gps.ts +++ b/apps/banglerun/src/gps.ts @@ -14,8 +14,6 @@ interface GpsEvent { } const EARTH_RADIUS = 6371008.8; -const POS_ACCURACY = 2.5; -const VEL_ACCURACY = 0.05; function initGps(state: AppState): void { Bangle.on('GPS', (gps: GpsEvent) => readGps(state, gps)); @@ -29,13 +27,17 @@ function readGps(state: AppState, gps: GpsEvent): void { state.vel = gps.speed / 3.6; state.fix = gps.fix; state.dop = gps.hdop; - - state.gpsValid = state.fix > 0 && state.dop <= 5; + state.gpsValid = state.fix > 0; updateGps(state); draw(state); - if (state.gpsValid && state.status === ActivityStatus.Running) { + /* Only log GPS data every 5 secs if we + have a fix and we're running. */ + if (state.gpsValid && + state.status === ActivityStatus.Running && + state.timeSinceLog > 5) { + state.timeSinceLog = 0; updateLog(state); } } @@ -44,9 +46,8 @@ function updateGps(state: AppState): void { const t = Date.now(); let dt = (t - state.t) / 1000; if (!isFinite(dt)) dt=0; - state.t = t; - state.dt += dt; + state.timeSinceLog += dt; if (state.status === ActivityStatus.Running) { state.duration += dt; @@ -62,52 +63,25 @@ function updateGps(state: AppState): void { const x = r * Math.cos(lat) * Math.cos(lon); const y = r * Math.cos(lat) * Math.sin(lon); const z = r * Math.sin(lat); - const v = state.vel; if (!state.x) { state.x = x; state.y = y; state.z = z; - state.v = v; - state.pError = state.dop * POS_ACCURACY; - state.vError = state.dop * VEL_ACCURACY; return; } const dx = x - state.x; const dy = y - state.y; const dz = z - state.z; - const dv = v - state.v; const dpMag = Math.sqrt(dx * dx + dy * dy + dz * dz); - const dvMag = Math.abs(dv); - state.pError += state.v * state.dt; - state.dt = 0; - - const pError = dpMag + state.dop * POS_ACCURACY; - const vError = dvMag + state.dop * VEL_ACCURACY; - - const pGain = (state.pError / (state.pError + pError)) || 0; - const vGain = (state.vError / (state.vError + vError)) || 0; - - state.x += dx * pGain; - state.y += dy * pGain; - state.z += dz * pGain; - state.v += dv * vGain; - state.pError += (pError - state.pError) * pGain; - state.vError += (vError - state.vError) * vGain; - -/*// we're not currently updating lat/lon with the kalman filter - // as it seems not to update them correctly at the moment - // and we only use them for logging (where it makes sense to use - // raw GPS coordinates) - const pMag = Math.sqrt(state.x * state.x + state.y * state.y + state.z * state.z); - state.lat = (Math.asin(state.z / pMag) * 180 / Math.PI) || 0; - state.lon = (Math.atan2(state.y, state.x) * 180 / Math.PI) || 0; - state.alt = pMag - EARTH_RADIUS;*/ + state.x = x; + state.y = y; + state.z = z; if (state.status === ActivityStatus.Running) { - state.distance += dpMag * pGain; + state.distance += dpMag; state.speed = (state.distance / state.duration) || 0; state.cadence = (60 * state.steps / state.duration) || 0; } diff --git a/apps/banglerun/src/log.ts b/apps/banglerun/src/log.ts index 2ab7b4191..b6714e407 100644 --- a/apps/banglerun/src/log.ts +++ b/apps/banglerun/src/log.ts @@ -8,19 +8,23 @@ function initLog(state: AppState): void { const time = datetime.substr(9, 6); const filename = `banglerun_${date}_${time}`; state.file = require('Storage').open(filename, 'w'); - state.file.write([ - 'timestamp', - 'latitude', - 'longitude', - 'altitude', - 'duration', - 'distance', - 'heartrate', - 'steps', - ].join(',') + '\n'); + state.fileWritten = false; } function updateLog(state: AppState): void { + if (!state.fileWritten) { + state.file.write([ + 'timestamp', + 'latitude', + 'longitude', + 'altitude', + 'duration', + 'distance', + 'heartrate', + 'steps', + ].join(',') + '\n'); + state.fileWritten = true; + } state.file.write([ Date.now().toFixed(0), state.lat.toFixed(6), diff --git a/apps/banglerun/src/state.ts b/apps/banglerun/src/state.ts index ddb60e1ad..14ef2dc5d 100644 --- a/apps/banglerun/src/state.ts +++ b/apps/banglerun/src/state.ts @@ -14,15 +14,14 @@ interface AppState { dop: number; gpsValid: boolean; - // GPS Kalman data + // Absolute position data x: number; y: number; z: number; - v: number; + // Last fix time t: number; - dt: number; - pError: number; - vError: number; + // Last time we saved log info + timeSinceLog : number; // HRM data hr: number, @@ -30,6 +29,7 @@ interface AppState { // Logger data file: File; + fileWritten: boolean; // Drawing data drawing: boolean; @@ -62,16 +62,14 @@ function initState(): AppState { x: NaN, y: NaN, z: NaN, - v: NaN, t: NaN, - dt: NaN, - pError: NaN, - vError: NaN, + timeSinceLog : 0, hr: 60, hrError: 100, file: null, + fileWritten: false, drawing: false, diff --git a/apps/batclock/ChangeLog b/apps/batclock/ChangeLog new file mode 100644 index 000000000..5d221b4c4 --- /dev/null +++ b/apps/batclock/ChangeLog @@ -0,0 +1 @@ +0.01: App Created! diff --git a/apps/batclock/README.md b/apps/batclock/README.md new file mode 100644 index 000000000..4a7b8e1f9 --- /dev/null +++ b/apps/batclock/README.md @@ -0,0 +1,25 @@ +
+

BatClock

+

+ +

+

Based on Morphing Clock, with an awesome "The Dark Knight" themed logo. +

+
+Made with JavaScipt Open Source Hardware Espruino Built with Love + + + +
+ +
+ +## Requests + +Please leave bug reports and requests by raising an issue [here](https://github.com/ra101/BangleApps). + +
+ +## Creator + +[< RA >](https://github.com/ra101) \ No newline at end of file diff --git a/apps/batclock/bat-clock.app.js b/apps/batclock/bat-clock.app.js new file mode 100644 index 000000000..abb5fbd3a --- /dev/null +++ b/apps/batclock/bat-clock.app.js @@ -0,0 +1,263 @@ +// Initailize Variables + +var is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; +var locale = require("locale"); +var CHARW = 9; // how tall are digits? +var CHARP = 1; // how chunky are digits? +var Y = 105; // start height + +// Offscreen buffer +var buf = Graphics.createArrayBuffer(CHARW + CHARP * 2, CHARW * 2 + CHARP * 2, 1, { + msb: true +}); +var bufimg = { + width: buf.getWidth(), + height: buf.getHeight(), + buffer: buf.buffer +}; + +// The last time that we displayed +var lastTime = "-----"; + +// If animating, this is the interval's id +var animInterval; +var timeInterval; + +// Background Image +const bg_crack = require("heatshrink").decompress(atob("+HwgJC/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/gEIDrkYDrkwDrlgDrnADvMBwAdcLDkCBhcQDp8EfdMMDrkGDrkHDrkDCB8wDpj8PuD8M4AdO8ANaFp/wgEIDrV4gEEQ5gtBDprhN/wOMngCBLRcEj7EMj5qBBxcMgamMFYWEBAgzFjEA/AdMUgMGDpUQgEeDA0gAgd/DoNCDpUwS4J4GhgED/ZnBkIdKsACBaYxRDgPxBgMRDpVAYogADgJUBfoPgQoQYEwECAoYSCKQgACuD9CwE4d4ISCEQ0BdpRDCh4ECghOCDoUEAgUDZpQaBgEcYIUCRQQdGLogAGgZHBK4MfCQLaEKYj6FAAzNBPQMPwEBiAdEhDkHAA84WwUfGgOYDopCCEIYAIjxbCj4gBCYhZEjAdLg/GAYMfLwNMDogZDmAdLgP8AYM/+EAkgdEPoafEABF8AQP/DoMWDpDbEPBJcB//4bIJ3IQYJPBDpMPOwPPLoMLDo8BAgQqEAAqzBv8H/wUEDpEGSxXB/kD/4ZFAYY3Dgh4KOoIdBC4JuCDog3Dhg7K//gAQIbBVQYdDhwDChA7Kv/AEAUMDo8ejgDBvBZLwBZB+0cmAdGn+eFYL+BAAj9DAAscBQYdDnw3KDpMYDo1uDqiJDDofGDqceYo0BwwdTh0EHYsBd4YdRgQHFgQlDDqIHGggdUh4HGhB/GDqK2DgAdUg4dESwMQDqqvCHYcgIAgdXoEQDqcDDoQAD4EYDq47DwEwDqcCDoTqCgMA+AdTKQICBg04IQMD8AdXAAUGg4dXLAUAh0PDijKBgMeAoUeAgYASsEBx4EBkEengdViEAw4dCn14DqsMgHHDoUzuAdVgOAsYEBmF3WSoABvExDoeADq0P/ADBjH8DiwABDIUPeoIA/AH4A/AAUgDvVgC60DAonwDq0MAgcBDq84OYn4DisB8EAg4CBgF+DqsHwADB/+DwE8DqseEIf/8AdWvAEDn/wDqsfOYR8C//+Oqn/OwQACh//DqcPGY8DDqZwBRqodG4AdcDjcAcywAGsAdcAH4A/AH4A/AH4AnmBA/AEUBw//+Adaj///AOLFRsD//vCBk8DpkH85ZBg/ABxGAjwdMBoP/8JbJgeAg4dMsEAv49COxHAgfQQpeAAQOD//gBo4dBgCjLggED/53HgcAE4N8ap5tBBA0PDoSWNW4eAGgKiGDoIMBDp0GKQQAEjkAuAKBbxIdHAIIAE/gdDUQ4AHjEAhiyFV4M4DoLeIAAwSBDosAvAKCgU8Dp1gHoQADiEwDoS0JoBQFQ4IUCIYkAj/+WgL8IaA0AGgJYFDoJ6BWg8IAwsPDoJbBDo0PDQMBWgwTFgE/BI4dDAwpvDGYIAEvYCBGIMQBAV8NQMOAwS0Fgh+GEoInCMod8KgIdDH4bACDpbxDvkA/gGDg6WELAwICXgQWCiFmQQIdDgPwDA4ACjACBgQCBKQQdCj/8CIbVBABLrCg0AoEHBAViKgIdEnwdKZoQdCUQUQscAgYdEh5zIZoZ0CoClDsIDBvymE8AdJFAUcDovhKg94LRQdDcIngFAMeBIk8Dpi2CDofADoK6CAAQFFDp6fDAAcHSxQdICYUDBIkBE4QAJiAjFDoUBCAq0KDpAQJnAdQggdKYgodIhAdNg4dNhgdNga0LC4IsDDpUAWhaEBcIYdLuALKmDnEDpYpRVBaHCABD8EiAdLgSDMaIUYchgAJgKSBJIUYNZYAKSQQdDH4QATgwdFDisAggCBG4R2WHA0fTYIAUDQQ7CDq8wdgQdZkAdFa4IAUoAFEDoqhCABxSFjIdVgJSFd4odRXJAACgz1WDosCDv4dYgYdWhiiLDv4dSgGADv4dYoAdzgitWDpkgLLgddACwd/Dv4d/Dv4drADsIDv6z/LOQA/AH4A/AH4A/AGw")); +const batman = require("heatshrink").decompress(atob("+HwwJC/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4Ash//AA/gCQ8BCRH8DrsHDrf4BRIdSCQN/BQ/wDqOAgEfDqEDCI/+BYJ4IDqP4BZQdRCIYdMgYdPSw5HCgFwDpnABgSWHDoqmBRJJMDDpcwDocPWRIqJ/gLCvA7LCAZmIBgZZEHY6mEgI7Lg4dK8AdLMwd5DoaIHDojSHDoYFBDpQKCAAU/YBLjEB5TSKFYQdLaAiFJDo5pGaAjfJQoQdDUo/4DorwHb4QdDBxQdLNAQdDQw4dGJQ54CDoZ2GDo4TCB458CJI6GDDpn/DIXwdo69DAAhLHABrgCAAguIABgcGDrzDHABjfCDscPDqf8DssHDqf4DssDDuPwDssBHePgHbgdIgAdx4Ad/Dv4dywBZ/Dv6z/aP4d/Dv4drgIdT8Adm/gDBh4ZLB4YdIAAkfDhP+DBhePGxoAFn4dIDiUAg4cH/AdTepDpIABlfSbAADagzOCACcDDovwDqp4GOyodHoAdWeIocWSwqUWAAMHDof4Dq8BDofgDq6WEWS4ABv4dCwAd2j4cB/wcYaQbQYaQjQYAAMDDoPwDrjuZgEBDrkADoPADvF/Dr2ADrU//4caDoP+DrcfDrkPDrv8DvMH/Ad5gfwDv4AXgIdd8Ad/ADHADv4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHY")); + + +/* Get array of lines from digit d to d+1. + n is the amount (0..1) + maxFive is true is this digit only counts 0..5 */ +const DIGITS = { + " ": n => [], + "0": n => [ + [n, 0, 1, 0], + [1, 0, 1, 1], + [1, 1, 1, 2], + [n, 2, 1, 2], + [n, 1, n, 2], + [n, 0, n, 1] + ], + "1": n => [ + [1 - n, 0, 1, 0], + [1, 0, 1, 1], + [1 - n, 1, 1, 1], + [1 - n, 1, 1 - n, 2], + [1 - n, 2, 1, 2] + ], + "2": n => [ + [0, 0, 1, 0], + [1, 0, 1, 1], + [0, 1, 1, 1], + [0, 1 + n, 0, 2], + [1, 2 - n, 1, 2], + [0, 2, 1, 2] + ], + "3": n => [ + [0, 0, 1 - n, 0], + [0, 0, 0, n], + [1, 0, 1, 1], + [0, 1, 1, 1], + [1, 1, 1, 2], + [n, 2, 1, 2] + ], + "4": n => [ + [0, 0, 0, 1], + [1, 0, 1 - n, 0], + [1, 0, 1, 1 - n], + [0, 1, 1, 1], + [1, 1, 1, 2], + [1 - n, 2, 1, 2] + ], + "5to0": n => [ // 5 -> 0 + [0, 0, 0, 1], + [0, 0, 1, 0], + [n, 1, 1, 1], + [1, 1, 1, 2], + [0, 2, 1, 2], + [0, 2, 0, 2], + [1, 1 - n, 1, 1], + [0, 1, 0, 1 + n] + ], + "5to6": n => [ // 5 -> 6 + [0, 0, 0, 1], + [0, 0, 1, 0], + [0, 1, 1, 1], + [1, 1, 1, 2], + [0, 2, 1, 2], + [0, 2 - n, 0, 2] + ], + "6": n => [ + [0, 0, 0, 1 - n], + [0, 0, 1, 0], + [n, 1, 1, 1], + [1, 1 - n, 1, 1], + [1, 1, 1, 2], + [n, 2, 1, 2], + [0, 1 - n, 0, 2 - 2 * n] + ], + "7": n => [ + [0, 0, 0, n], + [0, 0, 1, 0], + [1, 0, 1, 1], + [1 - n, 1, 1, 1], + [1, 1, 1, 2], + [1 - n, 2, 1, 2], + [1 - n, 1, 1 - n, 2] + ], + "8": n => [ + [0, 0, 0, 1], + [0, 0, 1, 0], + [1, 0, 1, 1], + [0, 1, 1, 1], + [1, 1, 1, 2], + [0, 2, 1, 2], + [0, 1, 0, 2 - n] + ], + "9": n => [ + [0, 0, 0, 1], + [0, 0, 1, 0], + [1, 0, 1, 1], + [0, 1, 1 - n, 1], + [0, 1, 0, 1 + n], + [1, 1, 1, 2], + [0, 2, 1, 2] + ], + ":": n => [ + [0.4, 0.4, 0.6, 0.4], + [0.6, 0.4, 0.6, 0.6], + [0.6, 0.6, 0.4, 0.6], + [0.4, 0.4, 0.4, 0.6], + [0.4, 1.4, 0.6, 1.4], + [0.6, 1.4, 0.6, 1.6], + [0.6, 1.6, 0.4, 1.6], + [0.4, 1.4, 0.4, 1.6] + ] +}; + +/* Draw a transition between lastText and thisText. + 'n' is the amount - 0..1 */ +function drawDigits(lastText, thisText, n) { + "ram" + const p = CHARP; // padding around digits + const s = CHARW; // character size + var x = 80; // x offset + g.reset(); + g.setColor(0, 0, 0); + g.setBgColor(1, 1, 1); + for (var i = 0; i < lastText.length; i++) { + var lastCh = lastText[i]; + var thisCh = thisText[i]; + if (thisCh == ":") x -= 4; + if (lastCh != thisCh) { + var ch, chn = n; + if ((thisCh - 1 == lastCh || + (thisCh == 0 && lastCh == 5) || + (thisCh == 0 && lastCh == 9))) + ch = lastCh; + else { + ch = thisCh; + chn = 0; + } + buf.clear(); + if (ch == "5") ch = (lastCh == 5 && thisCh == 0) ? "5to0" : "5to6"; + var l = DIGITS[ch](chn); + l.forEach(c => { + if (c[0] != c[2]) // horiz + buf.fillRect(p + c[0] * s, c[1] * s, p + c[2] * s, 2 * p + c[3] * s); + else if (c[1] != c[3]) // vert + buf.fillRect(c[0] * s, p + c[1] * s, 2 * p + c[2] * s, p + c[3] * s); + }); + g.drawImage(bufimg, x, Y); + } + if (thisCh == ":") x -= 4; + x += s + p + 7; + } +} + +function drawEverythingElse() { + var x = (CHARW + CHARP + 6) * 5 + 80; + var y = Y + 2 * CHARW + CHARP; + var d = new Date(); + g.reset(); + g.setBgColor(1, 1, 1); + g.setColor(1, 0, 0); + g.setFont("6x8"); + g.setFontAlign(-1, -1); + g.drawString(("0" + d.getSeconds()).substr(-2), x, y - 8, true); + // meridian + if (is12Hour) g.drawString((d.getHours() < 12) ? "AM" : "PM", x, + +4, true); + // date + g.setFontAlign(0, -1); + var date = locale.date(d, false); + g.drawString(date, g.getWidth() / 2, y + 8, true); +} + +/* Show the current time, and animate if needed */ +function showTime() { + if (animInterval) return; // in animation - quit + var d = new Date(); + var hours = d.getHours(); + if (is12Hour) hours = ((hours + 11) % 12) + 1; + var t = (" " + hours).substr(-2) + ":" + + ("0" + d.getMinutes()).substr(-2); + var l = lastTime; + // same - don't animate + if (t == l || l == "-----") { + drawDigits(l, t, 0); + drawEverythingElse(); + lastTime = t; + return; + } + var n = 0; + animInterval = setInterval(function () { + n += 1 / 10; + if (n >= 1) { + n = 1; + clearInterval(animInterval); + animInterval = undefined; + } + drawDigits(l, t, n); + }, 20); + lastTime = t; +} + +Bangle.on('lcdPower', function (on) { + if (animInterval) { + clearInterval(animInterval); + animInterval = undefined; + } + if (timeInterval) { + clearInterval(timeInterval); + timeInterval = undefined; + } + if (on) { + showTime(); + timeInterval = setInterval(showTime, 1000); + } else { + lastTime = "-----"; + } +}); + +g.clear(); + +// Draw Backgound before displaying time +g.setColor(0, 0.5, 0).drawImage(bg_crack); +g.setColor(1, 1, 1).drawImage(batman); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// Update time once a second +timeInterval = setInterval(showTime, 1000); +showTime(); + +// Show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2, { + repeat: false, + edge: "falling" +}); diff --git a/apps/batclock/bat-clock.icon.js b/apps/batclock/bat-clock.icon.js new file mode 100644 index 000000000..e10240b46 --- /dev/null +++ b/apps/batclock/bat-clock.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgJC/ACc//8AgP//kf/+AAoP4h4FD8ED/+Dwf/4AFB8fHBwYACwEAAoYqB/2vxAFD1uuAoePAont13+AoXvxwLCEYsAv4QCAAMf/AFDh/gAocH4AFDgYFEgJLBAH4AaA==")) diff --git a/apps/batclock/bat-clock.png b/apps/batclock/bat-clock.png new file mode 100644 index 000000000..6853a0ca6 Binary files /dev/null and b/apps/batclock/bat-clock.png differ diff --git a/apps/batclock/screenshot.png b/apps/batclock/screenshot.png new file mode 100644 index 000000000..84f4122fc Binary files /dev/null and b/apps/batclock/screenshot.png differ diff --git a/apps/battleship/ChangeLog b/apps/battleship/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/battleship/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/battleship/README.md b/apps/battleship/README.md new file mode 100644 index 000000000..765692d77 --- /dev/null +++ b/apps/battleship/README.md @@ -0,0 +1,18 @@ +# Battleship + +The classic game of battleship. + +## Usage + +In the beginning, each player is required to place +all ships in his fleet on the field. +Navigation of the cursor is performed using BTN1 and +BTN3 as well as left and right on the touch screen. +To place a ship use BTN2 to initialize a placement +and BTN2 again to complete it. + +In the next phase the players take alternating turns +in trying to hit an opposing ship. + +After a player succeeds in sinking the entire opposing +fleet the game ends. diff --git a/apps/battleship/battleship-icon.js b/apps/battleship/battleship-icon.js new file mode 100644 index 000000000..0878a4b28 --- /dev/null +++ b/apps/battleship/battleship-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwyBC/AH4A/AH4A/AH4A/AH4AEgeGAIVmAogBHBoRV/LZQBaLf4BK9EMlMMpQBClIJBMf5dM08Utcdh0luABNCIMUpYZBMO5bK1hZPAJYdBMecDswxFhkqktQLrYBEqAlBMI1mXdq5dYpxhqFYsc5pdnAIM16VeuQ1FLs67pAIM9+WP7GHrFN2JhjEYsMlRdtv9Yu34v8YlnNHolmL8GnktQLtkXt3XuwBB/ADBhfIYLq9FimsLtd2+9m21u25hFouQIIq9eLtVu+1eu1myxhIYLi9GtZdps3Wq11u83w94t3WMIZfDmsOL78dhxdo2tOmhZBy/5AIILCYYhfBr0TIopfUswZDLsc+iRRBr2Vp0UL4X2L4teL4JhEAIMD05fYPIfoLstWuk9+dGiZhDu83w+Ys3Wr11MI8UlJfbhkqLsdOqk16U96ZhHAINWqoBBMI8kxZfcpRddmvRLoNGmct2M12RhLqxhKlmsL/ZhDkuRloBBL4JhRL4JhCkhfdlJffAIhhajmLL7cD9BfkuBfCMJlGMJEUhJfcwxflMLFUgenL7sdhxhpnvxp0RnvSMIXzMI89yJFFL7MUpZfnmuxq0xAIbDEMI0k1hdXMJGnL9HRL5BhD+RhBovzHoJfcszBE1hhnovxp0RYoMtyJhG+ck9qhEL7DBIqBhnAIlxMInSNIK9dL5GGhkqLMstyEt2BhJilKXr5hJimsLsdGiABBnvwMIsc5hdjMJXNL7812BfDopfEFoJdnL4VmYc2QXYJdBMoJdC1gxFL8phJhkqktQYr4hBEoJdtMJcD07FdXIWnLuJjGG4xjCpcc9xZPCIMMpZb5YpwBF9EMlMMpQBClIJBC5hd2YpwBWLfZldKf4A/AH4A/AH4A/AH4A/AAo")) diff --git a/apps/battleship/battleship-icon.png b/apps/battleship/battleship-icon.png new file mode 100644 index 000000000..514492d2f Binary files /dev/null and b/apps/battleship/battleship-icon.png differ diff --git a/apps/battleship/battleship-icon.svg b/apps/battleship/battleship-icon.svg new file mode 100644 index 000000000..bd23abf25 --- /dev/null +++ b/apps/battleship/battleship-icon.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/apps/battleship/battleship.js b/apps/battleship/battleship.js new file mode 100644 index 000000000..3661ef494 --- /dev/null +++ b/apps/battleship/battleship.js @@ -0,0 +1,321 @@ +const FIELD_WIDTH = [11, 11, 15]; // for each phase +const FIELD_HEIGHT = FIELD_WIDTH; +const FIELD_LINE_WIDTH = 2; +const FIELD_MARGIN = 2; +const FIELD_COUNT_X = 10; +const FIELD_COUNT_Y = FIELD_COUNT_X; +const MARGIN_LEFT = 16; +const MARGIN_TOP = 42; +const HEADING_COLOR = ['#FF7070', '#7070FF']; // for each player +const FIELD_LINE_COLOR = '#FFFFFF'; +const FIELD_BG_COLOR_REGULAR = '#808080'; +const FIELD_BG_COLOR_SELECTED = '#FFFFFF'; +const SHIP_COLOR_PLACED = '#507090'; +const SHIP_COLOR_AVAIL = '#204070'; +const STATE_HIT_COLOR = ['#B00000', '#0000B0']; // for each player +const STATE_MISS_COLOR = '#404040'; +const SHIP_CAPS = [ + 1, // Carrier (type 0, size 5) + 2, // Battleship (type 1, size 4) + 3, // Destroyer (type 2, size 3) + 4 // Patrol Boat (type 3, size 2) +]; +const FULL_HITS = SHIP_CAPS.reduce((a, c, i) => a + c*(5 -i), 0); +const INDICATOR_LAYOUT = [ + [0, 1, 1, 3], + [2, 2, 2, 3, 3, 3] +]; +const INDICATORS = INDICATOR_LAYOUT.reduce((a, c, i) => { + let y = FIELD_COUNT_Y + 1 + i; + let x1 = 0; + c.forEach(type => { + let size = 5 - type; + let x2 = x1 + size - 1; + a.push({ "type": type, "position": [x1, y, x2, y] }); + x1 += size; + }); + return a; +}, []).sort((l, r) => (l.type - r.type)*FIELD_COUNT_X*FIELD_COUNT_Y + + (l.position[0] + l.position[1]*FIELD_COUNT_X + - (r.position[0] + r.position[1]*FIELD_COUNT_X))); + +let phase = 0; +let player = 0; +let selected = [-10, -10]; +let to_add = null; +let to_rem = null; +let placements = [[],[]]; +let field_states = [new Array(100).fill(0), new Array(FIELD_COUNT_X*FIELD_COUNT_Y).fill(0)]; +let current = [[0, 0],[0, 0]]; +let behaviours = []; // depending on phase + +function getLeftOffset(x) { + return MARGIN_LEFT + x*(FIELD_WIDTH[phase] + FIELD_MARGIN + 1); +} + +function getTopOffset(y) { + return MARGIN_TOP + y*(FIELD_HEIGHT[phase] + FIELD_MARGIN + 1); +} + +function getFieldState(x, y) { + return field_states[player][x + FIELD_COUNT_X*y]; +} + +function setFieldState(x, y, value) { + field_states[player][x + FIELD_COUNT_X*y] = value; +} + +function updateFieldStates() { + placements.forEach((ps, i) => { + ps.forEach(p => { + let pos = p.position; + for (let x = pos[0]; x <= pos[2]; x++) + for (let y = pos[1]; y <= pos[3]; y++) { + field_states[i][x + FIELD_COUNT_X*y] = 1; + } + }); + }); +} + +function getHitCount() { + return field_states[player].reduce( + (v, state) => state == 3 ? v + 1 : v, + 0); +} + +function drawField(x, y, selected) { + let x1 = getLeftOffset(x); + let y1 = getTopOffset(y); + let x2 = x1 + FIELD_WIDTH[phase]; + let y2 = y1 + FIELD_HEIGHT[phase]; + let field_state = getFieldState(x, y); + g.setColor(selected ? FIELD_BG_COLOR_SELECTED : FIELD_BG_COLOR_REGULAR); + g.fillRect(x1, y1, x2, y2); + g.setColor(FIELD_LINE_COLOR); + g.drawRect(x1, y1, x2, y2); + switch (field_state) { + case 2: + g.setColor(STATE_MISS_COLOR); + g.fillCircle(x1 + FIELD_WIDTH[phase]/2 + 1, y1 + FIELD_HEIGHT[phase]/2 + 1, FIELD_WIDTH[phase]/2 - 3); + break; + case 3: + g.setColor(STATE_HIT_COLOR[player]); + g.fillCircle(x1 + FIELD_WIDTH[phase]/2 + 1, y1 + FIELD_HEIGHT[phase]/2 + 1, FIELD_WIDTH[phase]/2 - 1); + break; + default: + break; + } +} + +function drawFields(x1, y1, x2, y2) { + let l = getLeftOffset(x1); + let t = getTopOffset(y1); + let r = getLeftOffset(x2) + FIELD_WIDTH[phase] + FIELD_MARGIN; + let b = getTopOffset(y2) + FIELD_HEIGHT[phase] + FIELD_MARGIN; + g.clearRect(l, t, r, b); + for (let x = x1; x <= x2; x++) + for (let y = y1; y <= y2; y++) { + drawField(x, y, x == current[player][0] && y == current[player][1]); + } +} + +function drawShip(x1, y1, x2, y2, color) { + g.setColor(color); + let diam = Math.min(FIELD_HEIGHT[phase], FIELD_WIDTH[phase]) - 3; + let rad = diam/2; + let cx1 = getLeftOffset(x1) + FIELD_WIDTH[phase]/2 + 1; + let cy1 = getTopOffset(y1) + FIELD_HEIGHT[phase]/2 + 1; + let cx2 = getLeftOffset(x2) + FIELD_WIDTH[phase]/2 + 1; + let cy2 = getTopOffset(y2) + FIELD_HEIGHT[phase]/2 + 1; + if (x1 == x2) { + g.fillRect(cx1 - rad, cy1, cx1 + rad, cy2); + } else { + g.fillRect(cx1, cy1 - rad, cx2, cy1 + rad); + } + g.fillCircle(cx1, cy1, rad); + g.fillCircle(cx2, cy2, rad); +} + +function hasCollision(pos) { + return placements[player].some( + p => pos[0] <= p.position[2] + && pos[2] >= p.position[0] + && pos[1] <= p.position[3] + && pos[3] >= p.position[1]); +} + +function isAvailable(type) { + let count = placements[player].reduce( + (v, p) => p.type == type ? v + 1 : v, + 0); + return count < SHIP_CAPS[type]; +} + +function determineChanges() { + to_rem = to_add; + to_add = null; + if (selected[0] == current[player][0] && selected[1] == current[player][1]) return; + if (selected[0] == current[player][0]) { + let size = Math.abs(selected[1] - current[player][1]) + 1; + if (size < 2 || size > 5 ) return; + let y1 = Math.min(selected[1], current[player][1]); + let y2 = Math.max(selected[1], current[player][1]); + let pos = [current[player][0], y1, current[player][0], y2]; + let type = 5 - size; + if (!hasCollision(pos) && isAvailable(type)) { + to_add = { "type": type, "position": pos }; + } + } + if (selected[1] == current[player][1]) { + let size = Math.abs(selected[0] - current[player][0]) + 1; + if (size < 2 || size > 5 ) return; + let x1 = Math.min(selected[0], current[player][0]); + let x2 = Math.max(selected[0], current[player][0]); + let pos = [x1, current[player][1], x2, current[player][1]]; + let type = 5 - size; + if (!hasCollision(pos) && isAvailable(type)) { + to_add = { "type": type, "position": pos }; + } + } +} + +function addPlacement(descriptor) { + placements[player].push(descriptor); + placements[player].sort((l, r) => l.type - r.type); +} + +function drawShipPlacements() { + if (to_rem) { + drawFields.apply(null, to_rem.position); + } + placements[player].forEach( + p => drawShip.apply(null, p.position.concat([SHIP_COLOR_PLACED]))); + if (to_add) { + drawShip.apply(null, to_add.position.concat([SHIP_COLOR_PLACED])); + } +} + +function drawShipIndicator() { + let p = to_add + ? placements[player].concat(to_add).sort((l, r) => l.type - r.type) + : placements[player]; + let pi = 0; + INDICATORS.forEach(indicator => { + let color = SHIP_COLOR_AVAIL; + if (pi < p.length && p[pi].type == indicator.type) { + pi += 1; + color = SHIP_COLOR_PLACED; + } + drawShip.apply(null, indicator.position.concat(color)); + }); +} + +function drawHeading(text) { + g.clearRect(0, 20, 100, 32); + g.setColor(HEADING_COLOR[player]); + g.setFont('4x6', 2.8); + g.drawString(text, MARGIN_LEFT, 20); +} + +function reset() { + g.clear(); + drawHeading('Player ' + (player + 1)); + drawFields(0, 0, 9, 9); +} + +function showResults() { + let text1 = 'Player ' + (player + 1) + ' won!'; + let text2 = 'Congratulations!'; + g.clear(); + g.clearRect(0, 20, 100, 32); + g.setColor(HEADING_COLOR[player]); + g.setFont('Vector', 20); + g.drawString(text1, MARGIN_LEFT, 80); + g.drawString(text2, MARGIN_LEFT, 120); +} + +function moveSelection(dx, dy) { + let x = current[player][0]; + let y = current[player][1]; + drawField(x, y, false); + current[player][0] = x = (x + dx + FIELD_COUNT_X)%FIELD_COUNT_X; + current[player][1] = y = (y + dy + FIELD_COUNT_Y)%FIELD_COUNT_Y; + drawField(x, y, true); +} + +behaviours.push({ + "move": (dx, dy) => { + moveSelection(dx, dy); + determineChanges(); + drawShipPlacements(); + drawShipIndicator(); + }, + "action": _ => { + if (to_add) { + addPlacement(to_add); + to_add = null; + selected = [-10, -10]; + if (placements[player].length == 10) { + behaviours[phase].transition(); + } + } else { + selected = [current[player][0], current[player][1]]; + } + }, + "transition": _ => { + current[0] = [0, 0]; + player = 1; + phase = 1; + reset(); + drawShipIndicator(); + } +}); + +behaviours.push({ + "move": behaviours[0].move, + "action": behaviours[0].action, + "transition": _ => { + current[1] = [0, 0]; + player = 0; + phase = 2; + updateFieldStates(); + reset(); + } +}); + +behaviours.push({ + "move": (dx, dy) => moveSelection(dx, dy), + "action": _ => { + let x = current[player][0]; + let y = current[player][1]; + let field_state = getFieldState(x, y); + if (field_state > 1) return; + setFieldState(x, y, field_state + 2); + drawField(x, y, true); + Bangle.buzz(200 + field_state*800, 0.5 + field_state*0.5); + if (getHitCount() < FULL_HITS) { + player = (player + 1)%2; + setTimeout(reset, 1000); + } else { + setTimeout(behaviours[phase].transition, 1000); + } + }, + "transition": _ => { + phase = 3; + showResults(); + } +}); + +behaviours.push({ + "move": _ => {}, + "action": _ => {} +}); + +reset(); +drawShipIndicator(); + +setWatch(_ => behaviours[phase].move(0, -1), BTN1, {repeat: true, debounce: 100}); +setWatch(_ => behaviours[phase].move(0, 1), BTN3, {repeat: true, debounce: 100}); +setWatch(_ => behaviours[phase].move(-1, 0), BTN4, {repeat: true, debounce: 100}); +setWatch(_ => behaviours[phase].move(1, 0), BTN5, {repeat: true, debounce: 100}); +setWatch(_ => behaviours[phase].action(), BTN2, {repeat: true, debounce: 100}); diff --git a/apps/beebclock/ChangeLog b/apps/beebclock/ChangeLog index 14dd12220..ac1c58c29 100644 --- a/apps/beebclock/ChangeLog +++ b/apps/beebclock/ChangeLog @@ -1,2 +1,3 @@ 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) diff --git a/apps/beebclock/beebclock.js b/apps/beebclock/beebclock.js index 6ed4f532e..bbf65697f 100644 --- a/apps/beebclock/beebclock.js +++ b/apps/beebclock/beebclock.js @@ -262,12 +262,6 @@ Graphics.prototype.drawRotLine = function (sina, cosa, cx, cy, r1, r2) { g.drawRotLine(Math.sin(a), Math.cos(a), CX, CY+TM, RC1, R1); } - // Clock chime on the hour. - if (hours >= 0 && minutes === 0) - try { - Bangle.buzz(); - } catch (e) { } - // And draw widgets if we're in that mode if (with_widgets) Bangle.drawWidgets(); diff --git a/apps/blobclk/ChangeLog b/apps/blobclk/ChangeLog index 9715fc4ab..10983d7e1 100644 --- a/apps/blobclk/ChangeLog +++ b/apps/blobclk/ChangeLog @@ -3,3 +3,4 @@ Remove 'faceUp' check as it's automatic 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 diff --git a/apps/blobclk/clock-blob.js b/apps/blobclk/clock-blob.js index 76f10865f..9b68bd4bd 100644 --- a/apps/blobclk/clock-blob.js +++ b/apps/blobclk/clock-blob.js @@ -86,9 +86,9 @@ function clearTimers() { } function startTimers() { g.clear(); + redraw(); Bangle.drawWidgets(); intervalRef = setInterval(redraw,1000); - redraw(); } Bangle.loadWidgets(); startTimers(); diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 7e9fd4a81..48e1baa48 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -21,3 +21,6 @@ 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 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..138258c5a 100644 --- a/apps/boot/bootloader.js +++ b/apps/boot/bootloader.js @@ -14,11 +14,7 @@ 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"});) -`; +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"); diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js new file mode 100644 index 000000000..9dc90cc9a --- /dev/null +++ b/apps/boot/bootupdate.js @@ -0,0 +1,133 @@ +/* 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 boot = ""; +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}) { eval(require('Storage').read('bootupdate.js'));} else {\n`; +boot += `E.setFlags({pretokenise:1});\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 += `NRF.setServices({}, {uart:true, hid:Bangle.HID});\n`; + } +} +if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth + 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) 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") 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.quiet && s.qmOptions) boot+=`Bangle.setOptions(${E.toJS(s.qmOptions)});\n`; +if (s.quiet && s.qmBrightness) { + if (s.qmBrightness!=1) boot+=`Bangle.setLCDBrightness(${s.qmBrightness});\n`; +} else { + if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; +} +if (s.quiet && s.qmTimeout) boot+=`Bangle.setLCDTimeout(${s.qmTimeout});\n`; +if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${s.passkey}, 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 +if (!g.theme) { + boot += `g.theme={fg:-1,bg:0,fg2:-1,bg2:7,fgH:-1,bgH:0x02F7};\n`; +} +if (!Bangle.setUI) { + 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.touchandler) { + Bangle.removeListener("touch", Bangle.touchHandler); + delete Bangle.touchHandler; +} +function b() { + try{Bangle.buzz(20);}catch(e){} +} +if (!mode) return; +else if (mode=="updown") { + Bangle.btnWatches = [ + setWatch(function() { b();cb(-1); }, BTN1, {repeat:1}), + setWatch(function() { b();cb(1); }, BTN3, {repeat:1}), + setWatch(function() { b();cb(); }, BTN2, {repeat:1}) + ]; +} else if (mode=="leftright") { + Bangle.btnWatches = [ + setWatch(function() { b();cb(-1); }, BTN1, {repeat:1}), + setWatch(function() { b();cb(1); }, BTN3, {repeat:1}), + setWatch(function() { b();cb(); }, BTN2, {repeat:1}) + ]; + Bangle.swipeHandler = d => {b();cb(d);}; + Bangle.on("swipe", Bangle.swipeHandler); + Bangle.touchHandler = d => {b();cb();}; + Bangle.on("touch", Bangle.touchHandler); +} else + throw new Error("Unknown UI mode"); +};\n`; +} +// Append *.boot.js files +require('Storage').list(/\.boot\.js/).forEach(bootFile=>{ + boot += "//"+bootFile+"\n"+require('Storage').read(bootFile)+"\n"; +}); +boot += "}\n";// initial 'if' +var s = require('Storage').write('.boot0',boot); +delete boot; +E.showMessage("Reloading..."); +eval(require('Storage').read('.boot0')); +eval(require('Storage').read('.bootcde')); diff --git a/apps/cliock/ChangeLog b/apps/cliock/ChangeLog index 07b38e189..53616638b 100644 --- a/apps/cliock/ChangeLog +++ b/apps/cliock/ChangeLog @@ -3,3 +3,4 @@ 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 diff --git a/apps/cliock/app.js b/apps/cliock/app.js index ca48bb26f..d9541f545 100644 --- a/apps/cliock/app.js +++ b/apps/cliock/app.js @@ -2,7 +2,6 @@ var fontsize = 3; 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"; @@ -26,19 +25,14 @@ function drawAll(){ } 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) diff --git a/apps/color_catalog/Changelog b/apps/color_catalog/Changelog new file mode 100644 index 000000000..b79d0c85b --- /dev/null +++ b/apps/color_catalog/Changelog @@ -0,0 +1 @@ +0.01: 1st ver,RGB565 and RGB888 colors in a common UI/UX diff --git a/apps/color_catalog/README.md b/apps/color_catalog/README.md new file mode 100644 index 000000000..c2d9aeb00 --- /dev/null +++ b/apps/color_catalog/README.md @@ -0,0 +1,43 @@ +# Color Catalog + +This is a very basic app that displays RGB565 and RGB888 colors, its name and code in screen. +This apps is based in the "common UI/UX". + + +Launcher icon + +![](color_catalog.png) + +1st screen - page 1 + +![](color_catalog_s1.png) + +2nd screen - page + +![](color_catalog_s2.png) + +## Usage + +Open and 2 rows of colors per page +Interact with a horizontal swipe/slide to move to next or previos page in order to display other colors + +## Features + +Colours, font, user input, load widgets + + +## Controls +Press left area - +Press righ area - +Press center area - +Swipe Left - Load the previous page and its colors +Swipe Right - Load the next page and its colors +BTN1 - Prints Button1 +BTN2 - Prints Button2 +BTN3 - Quit to Launcher + + +## Support + +This app is so basic that probably the easiest is to just edit the code +Otherwise you can contact me [here](https://github.com/dapgo) \ No newline at end of file diff --git a/apps/color_catalog/app-icon.js b/apps/color_catalog/app-icon.js new file mode 100644 index 000000000..ad28d4dd6 --- /dev/null +++ b/apps/color_catalog/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AHczAAYXyh3jC6s+93gC6nuAAMwC6UDC4QYMC40OC4fuC6JeBC53/AAZeEAAKSLC4sPmYXDSJfdAAZNB/4YDR5YXFIQP/MILZMC4iNCmfznxGCgbKDC5PtIgQYBXwUykTDHC4ndOoc/CwcikAXQIwMCCwMikYXRh4uCAAIXQgUT+YXDMAzXEnoWC8kRiQXEJAwXEma8B93hiMRn5IK5gADka8B91BC4IGBF5IXE7s/+fuCwJIFL4wXFDAMxC4ZICa44XG6cyC4URl8yCw4XH7szkIXCiYWIC4/UkZIDag4XJ61jJAUSCxIXHi1i+YWMa4pGBszUBkUq1UicIIACC5XWsMRj8yCwIXPmVGC4MT1WiC6EzIwMRmYWCC5gMBF4IXBjUzCwQXQsMa1R3BC5xHDRQOqD4IXRnQWBC6gWCC84A/AGg=")) \ No newline at end of file diff --git a/apps/color_catalog/app.js b/apps/color_catalog/app.js new file mode 100644 index 000000000..58951d1c6 --- /dev/null +++ b/apps/color_catalog/app.js @@ -0,0 +1,182 @@ +/* Color show for Bangle.js +grey +RGB888:#404040 / 0x404040 +RGB565:#4208 / 0x4208 +grey RGB888:#5c5c5c / 0x5c5c5c +RGB565:#5AEB / 0x5AEB +*/ + + +var v_model=process.env.BOARD; + console.log("device="+v_model); + + var x_max_screen=g.getWidth();//240; + var y_max_screen=g.getHeight(); //240; + var y_wg_bottom=g.getHeight()-25; + var y_wg_top=25; + if (v_model=='BANGLEJS') { + var x_btn_area=215; + var x_max_usable_area=x_btn_area;//Pend! only for bangle.js + var y_btn2=124; //harcoded for bangle.js cuz it is not the half of + } else x_max_usable_area=240; + + var contador=1; + var cont_items=0; + var cont_row=0; + var v_boxes_row=4; + var cont_page=1; + var v_boxwidth=40; + var v_boxheight=10; + var v_acolorpos=0; + var v_font1size=11; + var v_fontsize=13; + var v_color_b_area='#111111';//black + var v_color_b_area2=0x5AEB;//Dark + var v_color_text='#FB0E01'; + var v_color_statictxt='#e56e06'; //orange RGB format rrggbb + //RGB565 requires only 16 (5+6+5) bits/2 bytes + var a_colors_str= Array('White RGB565 0x','Orange','DarkGreen','Yellow', + 'Maroon','Blue','green','Purple', + 'cyan','olive','DarkCyan','DarkGrey', + 'Navy','Red','Magenta','GreenYellow', + 'Blush RGB888','pure red','Orange','Grey green', + 'D. grey','Almond','Amber','Bone', + 'Canary','Aero blue','Camel','Baby pink', + 'Y.Corn','Cultured','Eigengrau','Citrine'); + var a_colors= Array(0xFFFF,0xFD20,0x03E0,0xFFE0, + 0x7800,0x001F,0x07E0,0x780F, + 0x07FF,0x7BE0,0x03EF,0x7BEF, + 0x000F,0xF800,0xF81F,0xAFE5, + '#DE5D83','#FB0E01','#E56E06','#7E795C', + '#404040','#EFDECD','#FFBF00','#E3DAC9', + '#FFFF99','#C0E8D5','#C19A6B','#F4C2C2', + '#FBEC5D','#F5F5F5','#16161D','#E4D00A'); + var v_color_lines=0xFFFF; //White hex format + + + //the biggest usable area, button area not included +function ClearActiveArea(){ + g.setColor(v_color_b_area); + g.fillRect(0,y_wg_top,x_max_usable_area,y_wg_bottom); //fill all screen except widget areas + g.flip(); +} + + + function UserInput(){ + Bangle.on('touch', function(button){ + switch(button){ + case 1: + console.log("Touch 1");//left + break; + case 2: + console.log("Touch 2");//right + break; + case 3: + console.log("Touch 3");//center 1+2 + break; + } + }); + + if (v_model=='BANGLEJS') { + //only the name of the function + setWatch(Bangle.showLauncher, BTN3, { repeat: true }); + } + Bangle.on('swipe', dir => { + if(dir == 1) { + console.log("v_acolorpos"+v_acolorpos+"a_colors.length"+a_colors.length); + if (v_acolorpos0) cont_page--; + console.log("swipe page"+cont_page); + PrintScreen(cont_page); + } + }); + console.log("Log: Input conditions loaded"); +} //end of UserInput + +function DrawBangleButtons(){ + g.setFontVector(v_font1size); + g.setColor(v_color_lines);//White + + //g.drawString("Dwn", x_max_screen-g.stringWidth("Dwn"),y_wg_top+v_font1size+1); + //above Btn2 + //g.setFontVector(v_font1size).drawString("Off", x_max_screen-g.stringWidth("Off"),y_btn2-(2*v_font1size)); + //g.drawString("Set", x_max_screen-g.stringWidth("Set"),y_btn2-v_font1size); + //above Btn3 + g.drawString("Quit", x_max_screen-g.stringWidth("Quit"),y_wg_bottom-(2*v_font1size)); + g.flip(); + g.setColor(v_color_text); //green + g.setFontVector(v_font1size); + g.drawString("B1", x_max_screen-g.stringWidth("B1"),y_wg_top); + g.drawString("B2", x_max_screen-g.stringWidth("B2"),y_btn2); + g.drawString("B3",x_max_screen-g.stringWidth("B3"),y_wg_bottom-v_font1size); + g.flip(); +} + + function PrintScreen(page){ + ClearActiveArea(); + g.setColor(v_color_statictxt); + g.setFont("Vector",v_fontsize); + g.drawString("Page "+page,10,y_wg_top+5); + g.flip(); + + v_acolorpos=page*(v_boxes_row*2); + + console.log("page"+cont_page+"arraypos"+v_acolorpos); + for (cont_row=0;cont_row<2;cont_row++){ + console.log("row"+cont_row); + for (cont_items=0;cont_items { + remainingSeconds += 60; + + if (remainingSeconds >= 5999) { + remainingSeconds = 5999; + } + + drawRemainingSecondsPanel(); + }, 250); + + isIncreasingRemainingSeconds = true; + + drawRemainingSecondsPanel(); +} + +function incremementRemainingSeconds() { + if (remainingSeconds >= 5999) return; + ++remainingSeconds; +} + +function onIncreaseRemainingSecondsReleased() { + if (currentState == timerState.RUNNING) return; + clearInterval(increasingInterval); + isIncreasingRemainingSeconds = false; + drawRemainingSecondsPanel(); +} + +function onDecreaseRemainingSecondsPressed() { + if (currentState == timerState.RUNNING) return; + decreaseRemainingSeconds(); + + decreasingInterval = setInterval(() => { + remainingSeconds -= 60; + + if (remainingSeconds < 0) { + remainingSeconds = 0; + } + + drawRemainingSecondsPanel(); + }, 250); + + isDecreasingRemainingSeconds = true; + + drawRemainingSecondsPanel(); +} + +function decreaseRemainingSeconds() { + if (remainingSeconds <= 0) return; + --remainingSeconds; +} + +function onDecreaseRemainingSecondsReleased() { + if (currentState == timerState.RUNNING) return; + + clearInterval(decreasingInterval); + + isDecreasingRemainingSeconds = false; + draw(); +} + +main(); \ No newline at end of file diff --git a/apps/countdowntimer/countdowntimer.png b/apps/countdowntimer/countdowntimer.png new file mode 100644 index 000000000..fe59fab3b Binary files /dev/null and b/apps/countdowntimer/countdowntimer.png differ diff --git a/apps/counter/ChangeLog b/apps/counter/ChangeLog index 8d0f821fd..f3f1c4eac 100644 --- a/apps/counter/ChangeLog +++ b/apps/counter/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Added decrement and touch functions +0.03: Set color - ensures widgets don't end up coloring the counter's text diff --git a/apps/counter/counter.js b/apps/counter/counter.js index 86db23ba9..3e0687944 100644 --- a/apps/counter/counter.js +++ b/apps/counter/counter.js @@ -1,9 +1,8 @@ var counter = 0; -g.setColor(0xFFFF); - function updateScreen() { g.clearRect(0, 50, 250, 150); + g.setColor(0xFFFF); g.setFont("Vector",40).setFontAlign(0,0); g.drawString(Math.floor(counter), g.getWidth()/2, 100); g.drawString('-', 45, 100); @@ -44,7 +43,7 @@ g.drawString('Tap right or BTN1 to increase\nTap left or BTN3 to decrease\nPress Bangle.loadWidgets(); Bangle.drawWidgets(); +updateScreen(); // TODO: Enable saving counts to file -// Does not work if widgets are not visible // Add small watch diff --git a/apps/dane/ChangeLog b/apps/dane/ChangeLog index 48bb5c4be..fa37446bb 100644 --- a/apps/dane/ChangeLog +++ b/apps/dane/ChangeLog @@ -10,4 +10,5 @@ 0.12: Move code to Arwes Module 0.13: Improve icon 0.14: Switch Icon back to 8bit web palette to fix it -0.15: Hotfix: Remove var declaration from app image \ No newline at end of file +0.15: Hotfix: Remove var declaration from app image +0.16: Revert: Change Counter back to button control \ No newline at end of file diff --git a/apps/dane/app.js b/apps/dane/app.js index 841ecdfeb..3663d92a1 100644 --- a/apps/dane/app.js +++ b/apps/dane/app.js @@ -67,7 +67,7 @@ function levelColor(l) { function drawBattery() { const l = E.getBattery(), c = levelColor(l); - count = l; + // count = l; const xl = 45 + l * (194 - 46) / 100; g.clearRect(46, 58 + 80 + yOffset + 37, 193, height - 5); g.setColor(c).fillRect(46, 58 + 80 + yOffset + 37, xl, height - 5); @@ -124,14 +124,14 @@ drawClock(); setWatch(Bangle.showLauncher, BTN2, {repeat: false, edge: "falling"}); -// setWatch(function () { -// count++; -// drawCounterText(); -// }, BTN1, {repeat: true, edge: "falling"}); -// setWatch(function () { -// count--; -// drawCounterText(); -// }, BTN3, {repeat: true, edge: "falling"}); +setWatch(function () { + count++; + drawCounterText(); +}, BTN1, {repeat: true, edge: "falling"}); +setWatch(function () { + count--; + drawCounterText(); +}, BTN3, {repeat: true, edge: "falling"}); // refesh every 100 milliseconds setInterval(updateClock, 500); diff --git a/apps/dotclock/ChangeLog b/apps/dotclock/ChangeLog index 26f95bbde..c9658afb8 100644 --- a/apps/dotclock/ChangeLog +++ b/apps/dotclock/ChangeLog @@ -1 +1,2 @@ -0.01: Based on the Analog Clock app, minimal dot interface \ No newline at end of file +0.01: Based on the Analog Clock app, minimal dot +0.02: Remove hardcoded hour buzz (you can install widchime if you miss it) diff --git a/apps/dotclock/clock-dot.js b/apps/dotclock/clock-dot.js index a4b3f260f..c4a8be921 100644 --- a/apps/dotclock/clock-dot.js +++ b/apps/dotclock/clock-dot.js @@ -128,9 +128,6 @@ const onMinute = () => { g.setColor(1, 0.9, 0.9); // Minute minDot((360 * currentDate.getMinutes()) / 60,3); - if (currentDate.getHours() >= 0 && currentDate.getMinutes() === 0) { - Bangle.buzz(); - } drawDate(); }; diff --git a/apps/doztime/ChangeLog b/apps/doztime/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/doztime/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/doztime/README.md b/apps/doztime/README.md new file mode 100644 index 000000000..075b2f66a --- /dev/null +++ b/apps/doztime/README.md @@ -0,0 +1,14 @@ +Dozenal Time +============ + +A dozenal Holocene calendar and a dozenal diurnal clock. For information about them, go to https://dozenal.ae-web.ca/pdf/dozenal-calendar.pdf and https://dozenal.ae-web.ca/pdf/about-short.pdf. They've been in use for some years. + +In the dozenal number base, ten and eleven are single digits, and 10 is a dozen. The clock simply divides the day by successive powers of a dozen. The day or parts of it may be divided easily into halves, thirds, quarters, sixths, or twelfths (dozenths). There is no conglomeration of bases two, ten, twelve, and sixty, as in the current system of time measurement. + +The annual calendar has a dozen months of 5 weeks each, each week having 6 days. The 5 or 6 days beyond 360 (dozenal 260) are added where they keep the season beginnings the most accurate. + +The year itself begins on the December solstice. Because that always happens, there is no need of a leap-year rule to keep the seasons from drifting. + +The epoch (year numbering) begins in the last year when the perihelion coincided with the June solstice, near the beginning of the Holocene era. That astronomical basis makes the calendar free from politics, religion, or geography. + +While the year number remains cardinal, BTN5 toggles between cardinal and ordinal for the rest of the calendar segments. BTN4 adds or removes a quickly changing digit to or from the clock. diff --git a/apps/doztime/app-icon.js b/apps/doztime/app-icon.js new file mode 100644 index 000000000..19e81c45d --- /dev/null +++ b/apps/doztime/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lEowggdkUiCKIADCJcCkUjmYACmUikAlKB4ImDAoQSJkYhBFAQECAQI5HBQU//4AC+YUCHowzBCQfzAYYKCEw8vEgYqD+QoGgQbBHAYADCwIoBCYkiEwhPEBAIoBHgY6BExHyHwQhBFAQ6BkYTHDgcyHgcCHRZlDCYQsBTYg6GDAJQDPoI6LAAIPBCYRiHHQhkDCYRiHHQhkCCYKKBCYzzBA4yMBCYTVEGYITEBYITZHY5PHUAJjITIJjHRZINBIYoTDWZAoFWYbbJFALbHgUyX4oPDXIcjMQITBmZkHFYszCYZkJMQoTCKAQ8IHQZOCHgYoKkQ6DHgYoEcIgmBHQg8CFAIPCCYfzBQQSEFAbrFCQImHFAQUCkczmYECAQISGHoYzBAAQFCCRA9BEwYoDHI4pFAAgRLCooRPABg=")) diff --git a/apps/doztime/app.js b/apps/doztime/app.js new file mode 100644 index 000000000..83f536018 --- /dev/null +++ b/apps/doztime/app.js @@ -0,0 +1,225 @@ +// Positioning values for graphics buffers +const g_height = 80; // total graphics height +const g_x_off = 16; // position from left +const g_y_off = (240 - g_height)/2; // vertical center for graphics region +const g_width = 240 - 2 * g_x_off; // total graphics width +const g_height_d = 32; // height of date region +const g_y_off_d = 0; // y position of date region within graphics region +const spacing = 0; // space between date and time in graphics region +const g_y_off_t = g_y_off_d + g_height_d + spacing; // y position of time within graphics region +const g_height_t = 48; // height of time region + +// Other vars +const A1 = [30,30,30,30,31,31,31,31,31,31,30,30]; +const B1 = [30,30,30,30,30,31,31,31,31,31,30,30]; +const B2 = [30,30,30,30,31,31,31,31,31,30,30,30]; +const timeColour = "#f2f2f2"; +const dateColours = ["#ff0000","#ffa500","#ffff00","#00b800","#0000ff","#ff00ff","#ff0080"]; +const calen10 = {"size":32,"pt0":[32-g_x_off,16],"step":[20,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line +const calen7 = {"size":32,"pt0":[62-g_x_off,16],"step":[20,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line +const time5 = {"size":48,"pt0":[64-g_x_off,24],"step":[30,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line; was 64 +const time6 = {"size":48,"pt0":[48-g_x_off,24],"step":[30,0],"dx":-6.5,"dy":-6.5}; // positioning for twinkling time line +const baseYear = 11584; +const baseDate = Date(2020,11,21); // month values run from 0 to 11 +let accum = new Date(baseDate.getTime()); +let sequence = []; +let timeActiveUntil; +let addTimeDigit = false; +let dateFormat = false; +let lastX = 999999999; +let res = {}; +//var last_time_log = 0; + +// Date and time graphics buffers +var dateColour = "#ffffff"; // override later +var g_d = Graphics.createArrayBuffer(g_width,g_height_d,1,{'msb':true}); +var g_t = Graphics.createArrayBuffer(g_width,g_height_t,1,{'msb':true}); +// Set screen mode and function to write graphics buffers +Bangle.setLCDMode(); +g.clear(); // start with blank screen +g.flip = function() +{ + g.setColor(dateColour); + g.drawImage( + { + width:g_width, + height:g_height_d, + buffer:g_d.buffer + }, g_x_off, g_y_off + g_y_off_d); + g.setColor(timeColour); + g.drawImage( + { + width:g_width, + height:g_height_t, + buffer:g_t.buffer + }, g_x_off, g_y_off + g_y_off_t); +}; + +setWatch(function(){ modeTime(); }, BTN1, {repeat:true} ); +setWatch(function(){ Bangle.showLauncher(); }, BTN2, { repeat: false, edge: "falling" }); +setWatch(function(){ modeWeather(); }, BTN3, {repeat:true}); +setWatch(function(){ toggleTimeDigits(); }, BTN4, {repeat:true}); +setWatch(function(){ toggleDateFormat(); }, BTN5, {repeat:true}); + +function buildSequence(targ){ + for(let i=0;i n > dt)-1; + let year = baseYear+parseInt(index/12); + let month = index % 12; + let day = parseInt((dt-sequence[index])/86400000); + let colour = dateColours[day % 6]; + if(day==30){ colour=dateColours[6]; } + return({"year":year,"month":month,"day":day,"colour":colour}); +} +function toggleTimeDigits(){ + addTimeDigit = !addTimeDigit; + modeTime(); +} +function toggleDateFormat(){ + dateFormat = !dateFormat; + modeTime(); +} +function formatDate(res,dateFormat){ + let yyyy = res.year.toString(12); + calenDef = calen10; + if(!dateFormat){ //ordinal format + let mm = ("0"+(res.month+1).toString(12)).substr(-2); + let dd = ("0"+(res.day+1).toString(12)).substr(-2); + if(res.day==30){ + calenDef = calen7; + let m = ((res.month+1).toString(12)).substr(-2); + return(yyyy+"-"+"S"+m); // ordinal format + } + return(yyyy+"-"+mm+"-"+dd); + } + let m = res.month.toString(12); // cardinal format + let w = parseInt(res.day/6); + let d = res.day%6; + //return(yyyy+"-"+res.month+"-"+w+"-"+d); + return(yyyy+"-"+m+"-"+w+"-"+d); +} + +function writeDozTime(text,def,colour){ + let pts = def.pts; + let x=def.pt0[0]; + let y=def.pt0[1]; + g_t.clear(); + g_t.setFont("Vector",def.size); + for(let i in text){ + if(text[i]=="a"){ g_t.setFontAlign(0,0,2); g_t.drawString("2",x+def.dx,y+def.dy); } + else if(text[i]=="b"){ g_t.setFontAlign(0,0,2); g_t.drawString("3",x+def.dx,y+def.dy); } + else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); } + x = x+def.step[0]; + y = y+def.step[1]; + } +} +function writeDozDate(text,def,colour){ + dateColour = colour; + let pts = def.pts; + let x=def.pt0[0]; + let y=def.pt0[1]; + g_d.clear(); + g_d.setFont("Vector",def.size); + for(let i in text){ + if(text[i]=="a"){ g_d.setFontAlign(0,0,2); g_d.drawString("2",x+def.dx,y+def.dy); } + else if(text[i]=="b"){ g_d.setFontAlign(0,0,2); g_d.drawString("3",x+def.dx,y+def.dy); } + else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); } + x = x+def.step[0]; + y = y+def.step[1]; + } +} + +// Functions for time mode +function drawTime() +{ + let dt = new Date(); + let date = ""; + let timeDef; + let x = 0; + dt.setDate(dt.getDate()); + if(addTimeDigit){ + x = + 10368*dt.getHours()+172.8*dt.getMinutes()+2.88*dt.getSeconds()+0.00288*dt.getMilliseconds(); + let msg = "00000"+Math.floor(x).toString(12); + let time = msg.substr(-5,3)+"."+msg.substr(-2); + let wait = 347*(1-(x%1)); + timeDef = time6; + } else { + x = + 864*dt.getHours()+14.4*dt.getMinutes()+0.24*dt.getSeconds()+0.00024*dt.getMilliseconds(); + let msg = "0000"+Math.floor(x).toString(12); + let time = msg.substr(-4,3)+"."+msg.substr(-1); + let wait = 4167*(1-(x%1)); + timeDef = time5; + } + if(lastX > x){ res = getDate(dt); } // calculate date once at start-up and once when turning over to a new day + date = formatDate(res,dateFormat); + if(dt 13) { + hours = hours - 12; + } + + var meridian; + + if (typeof locale.meridian === "function") { + meridian = locale.meridian(new Date()); + } else { + meridian = ""; + } + + var timestr; + + 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); + + //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); + }; + + //setInterval for HR visualisation + const newBeats = function (hr) { + if (id != 0) { + changeInterval(id, 6e3 / hr.bpm); + } else { + id = setInterval(drawHR, 6e3 / hr.bpm); + } + }; + + //visualize HR with circles pulsating + const drawHR = function () { + if (grow && size < settings.hr.size) { + size++; + } + + if (!grow && size > 3) { + size--; + } + + if (size == settings.hr.size || size == 3) { + grow = !grow; + } + + 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); + } + }; + + // clean app screen + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + //manage when things should be enabled and not + Bangle.on('lcdPower', function (on) { + if (on) { + Bangle.setHRMPower(1); + } else { + Bangle.setHRMPower(0); + } + }); + + // refesh every second + setInterval(drawClock, 1E3); + + //start HR monitor and update frequency of update + Bangle.setHRMPower(1); + Bangle.on('HRM', function (d) { + newBeats(d); + }); + + // draw now + drawClock(); + + // Show launcher when middle button pressed + setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); + +} \ No newline at end of file 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/gbmusic/ChangeLog b/apps/gbmusic/ChangeLog new file mode 100644 index 000000000..ecbca5fb6 --- /dev/null +++ b/apps/gbmusic/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial version +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 diff --git a/apps/gbmusic/README.md b/apps/gbmusic/README.md new file mode 100644 index 000000000..4bad9b8c8 --- /dev/null +++ b/apps/gbmusic/README.md @@ -0,0 +1,48 @@ +# Gadgetbridge Music Controls + +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) + +Download the [latest Gadgetbridge for Android here](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/). + +## Features + +* Dynamic colors based on Track/Artist/Album name +* Scrolling display for long titles +* Automatic start when music plays +* Time and date display + +## Settings + +You can change these under `Settings`->`App/Widget Settings`->`Music Controls`. + +**Auto start**: +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. +(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 + - Long-press: open application launcher +* Button 3: Volume down + +### Touch +* Left: pause/previous song +* Right: next song/resume +* Center: toggle play/pause +* Swipe: next/previous song + +## Creator + +Richard de Boer diff --git a/apps/gbmusic/app.js b/apps/gbmusic/app.js new file mode 100644 index 000000000..5f95868bb --- /dev/null +++ b/apps/gbmusic/app.js @@ -0,0 +1,670 @@ +/* jshint esversion: 6 */ +/** + * Control the music on your Gadgetbridge-connected phone + **/ +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; +} + +/** + * @param {string} text + * @return {number} Maximum font size to make text fit on screen + */ +function fitText(text) { + if (!text.length) { + 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! + return best; + } + if (test(best)<240) { + do { + best++; + } while(test(best)<=240); + return best-1; + } + // width > 240 + do { + best--; + } while(test(best)>240); + return best; +} + +/** + * @param {string} text + * @return {number} Randomish but deterministic number from 0-360 for text + */ +function textCode(text) { + "ram"; + let code = 0; + for(let i = 0; i { + 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); +} +/** + * @param {string} name - musicinfo property "num"/"artist"/"album"/"track" + * @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; + } + v = brightness(); + const rgb = hsv2rgb(h, s, v); + return "#"+f2hex(rgb.r)+f2hex(rgb.g)+f2hex(rgb.b); +} +/** + * Remember track color until info changes + * Because we need this every time we move the scroller + * @return {string} + */ +function trackColor() { + if (!("track_color" in info) || fade) { + info.track_color = infoColor("track"); + } + return info.track_color; +} + +//////////////////// +// Drawing functions +//////////////////// +/** + * Draw date and time + */ +function drawDateTime() { + 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(); + if (hour===0) { + d12.setHours(12); + } else if (hour>12) { + d12.setHours(hour-12); + } + time = l.time(d12, true)+l.meridian(now); + } else { + time = 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); +} + +/** + * Draw track number and total count + * @param {boolean} clr - Clear area before redrawing? + */ +function drawNum(clr) { + let num = ""; + if ("n" in info && info.n>0) { + num = "#"+info.n; + if ("c" in info && info.c>0) { // I've seen { c:-1 } + 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); +} + +/** + * 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 + */ +function musicInfo(e) { + info = e; + delete (info.t); + offset = null; + if (Bangle.isLCDOn()) { + drawMusic(); + } + if (tIxt) { + clearTimeout(tIxt); + tIxt = null; + } + if (auto && stat==="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); + } +} + +let tPxt, tIxt; // Timeouts to eXiT when Paused/Inactive for too long +/** + * Update music state + * @param {Object} e - Gadgetbridge musicstate event + */ +function musicState(e) { + stat = e.state; + // if paused for five minutes, load the clock + // (but timeout resets if we get new info, even while paused) + if (tPxt) { + clearTimeout(tPxt); + tPxt = null; + } + if (tIxt) { + clearTimeout(tIxt); + tIxt = null; + } + fade = null; + delete info.track_color; + if (auto) { // auto opened -> auto close + switch(stat) { + case "stop": // never actually happens with my phone :-( + load(); + break; + 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); + break; + case "pause": + default: + // quit when paused for a long time + // also fade out track info while waiting for this + tPxt = setTimeout(load, POUT); + fade = Date.now(); + fadeOut(); + break; + } + } + if (Bangle.isLCDOn()) { + drawMusic(false); // redraw in case we were fading out but resumed play + drawControls(); + } +} + +//////////////////// +// Events +//////////////////// + +// we put starting of watches inside a function, so we can defer it until +// we asked the user about autoStart +/** + * Start watching for BTN2 presses + */ +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"}); + + // BTN2: long-press for launcher, otherwise depends on number of presses + setWatch(() => { + if (nPress===0) { + tPress = setTimeout(() => {Bangle.showLauncher();}, 3000); + } + }, BTN2, {repeat: true, edge: "rising"}); + const s = require("Storage").readJSON("gbmusic.json", 1) || {}; + if (s.simpleButton) { + setWatch(() => { + clearTimeout(tPress); + togglePlay(); + }, BTN2, {repeat: true, edge: "falling"}); + } else { + setWatch(() => { + nPress++; + clearTimeout(tPress); + tPress = setTimeout(handleButton2Press, 500); + }, BTN2, {repeat: true, edge: "falling"}); + } +} +function handleButton2Press() { + tPress = null; + switch(nPress) { + case 1: + togglePlay(); + break; + case 2: + sendCommand("next"); + break; + case 3: + sendCommand("previous"); + break; + default: // invalid + Bangle.buzz(50); + } + nPress = 0; +} + +let tCommand = {}; +/** + * Send command and highlight corresponding control + * @param {string} command - "play"/"pause"/"next"/"previous"/"volumeup"/"volumedown" + */ +function sendCommand(command) { + Bluetooth.println(JSON.stringify({t: "music", n: command})); + // for controlColor + if (command in tCommand) { + clearTimeout(tCommand[command]); + } + tCommand[command] = setTimeout(function() { + delete tCommand[command]; + drawControls(); + }, 200); + drawControls(); +} + +// touch/swipe: navigation +function togglePlay() { + sendCommand(stat==="play" ? "pause" : "play"); +} +function startTouchWatches() { + Bangle.on("touch", side => { + if (!Bangle.isLCDOn()) {return;} // for <2v10 firmware + switch(side) { + case 1: + sendCommand(stat==="play" ? "pause" : "previous"); + break; + case 2: + sendCommand(stat==="play" ? "next" : "play"); + break; + case 3: + togglePlay(); + } + }); + Bangle.on("swipe", dir => { + if (!Bangle.isLCDOn()) {return;} // for <2v10 firmware + sendCommand(dir===1 ? "previous" : "next"); + }); +} +function startLCDWatch() { + Bangle.on("lcdPower", (on) => { + if (on) { + // redraw and resume scrolling + tick(); + drawMusic(); + drawControls(); + fadeOut(); + if (offset!==null) { + drawScroller(); + if (!iScroll) { + iScroll = setInterval(scroll, 200); + } + } + } else { + // pause scrolling + if (iScroll) { + clearInterval(iScroll); + iScroll = null; + } + } + }); +} + +///////////////////// +// 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! + Bluetooth = { + println: (line) => {console.log("Bluetooth:", line);}, + }; + // some example info + GB({"t": "musicinfo", "artist": "Some Artist Name", "album": "The Album Name", "track": "The Track Title Goes Here", "dur": 241, "c": 2, "n": 2}); + GB({"t": "musicstate", "state": "play", "position": 0, "shuffle": 1, "repeat": 1}); + } +} +function startWatches() { + startButtonWatches(); + startTouchWatches(); + startLCDWatch(); +} + +function start() { + // start listening for music updates + const _GB = global.GB; + global.GB = (event) => { + // we eat music events! + switch(event.t) { + case "musicinfo": + musicInfo(event); + break; + case "musicstate": + musicState(event); + break; + default: + // pass on other events + if (_GB) { + setTimeout(_GB, 0, event); + } + return; + } + }; + drawMusic(); + drawControls(); + startWatches(); + tick(); + startEmulator(); +} + +function init() { + 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(); + } + } +} +init(); + diff --git a/apps/gbmusic/icon.js b/apps/gbmusic/icon.js new file mode 100644 index 000000000..5a83430a9 --- /dev/null +++ b/apps/gbmusic/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4AihvQCynd7oXThoWBC6YVCC6QVEC6BCDC6QVHC5wWJC/4VHC6oJCC6QSDC6QJFC54JHC5oNIC/4X/BpkNA4IXTCwL0GC5z1EC8JVHIwgXJKpAXOBpAXlBpQJELxgXdBQaONBwyxCaZQ9LdZYXWKpgYNCygA/AGYA==")) diff --git a/apps/gbmusic/icon.png b/apps/gbmusic/icon.png new file mode 100644 index 000000000..43d24afa2 Binary files /dev/null and b/apps/gbmusic/icon.png differ diff --git a/apps/gbmusic/screenshot.png b/apps/gbmusic/screenshot.png new file mode 100644 index 000000000..569a6a2c5 Binary files /dev/null and b/apps/gbmusic/screenshot.png differ diff --git a/apps/gbmusic/screenshot_2.png b/apps/gbmusic/screenshot_2.png new file mode 100644 index 000000000..f19f8f428 Binary files /dev/null and b/apps/gbmusic/screenshot_2.png differ diff --git a/apps/gbmusic/settings.js b/apps/gbmusic/settings.js new file mode 100644 index 000000000..ae013fda5 --- /dev/null +++ b/apps/gbmusic/settings.js @@ -0,0 +1,45 @@ +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const SETTINGS_FILE = "gbmusic.json", + storage = require("Storage"), + translate = require("locale").translate; + + // initialize with default settings... + let s = { + autoStart: true, + simpleButton: false, + }; + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const saved = storage.readJSON(SETTINGS_FILE, 1) || {}; + for(const key in saved) { + s[key] = saved[key]; + } + + function save(key) { + return function (value) { + s[key] = value; + storage.write(SETTINGS_FILE, s); + } + } + + const yesNo = (v) => translate(v ? "Yes" : "No"); + let menu = { + "": {"title": "Music Control"}, + }; + menu[translate("< Back")] = back; + menu[translate("Auto start")] = { + value: !!s.autoStart, + format: yesNo, + onchange: save("autoStart"), + }; + menu[translate("Simple button")] = { + value: !!s.simpleButton, + format: yesNo, + onchange: save("simpleButton"), + }; + + E.showMenu(menu); +}); diff --git a/apps/gbmusic/widget.js b/apps/gbmusic/widget.js new file mode 100644 index 000000000..86bda99a1 --- /dev/null +++ b/apps/gbmusic/widget.js @@ -0,0 +1,44 @@ +(() => { + if (global.gbmusic_active || !(require("Storage").readJSON("gbmusic.json", 1) || {}).autoStart) { + return; + } + if (typeof __FILE__ === 'string') { // only exists since 2v09 + const info = require("Storage").readJSON(__FILE__.split(".")[0]+".info", 1) || false; + if (info && info.type!=="clock") { // info can have no type (but then it isn't a clock) + return; + } + } + + let state, info; + function checkMusic() { + if (state!=="play" || !info) { + return; + } + // playing music: launch music app + require("Storage").writeJSON("gbmusic.load.json", { + state: state, + info: info, + }); + load("gbmusic.app.js"); + } + + const _GB = global.GB; + global.GB = (event) => { + // we eat music events! + switch(event.t) { + case "musicinfo": + info = event; + delete (info.t); + checkMusic(); + break; + case "musicstate": + state = event.state; + checkMusic(); + break; + default: + if (_GB) { + setTimeout(_GB, 0, event); + } + } + }; +})(); diff --git a/apps/gbridge/ChangeLog b/apps/gbridge/ChangeLog index bec2d305a..f4837d60a 100644 --- a/apps/gbridge/ChangeLog +++ b/apps/gbridge/ChangeLog @@ -18,3 +18,7 @@ Nicer display of alarm clock notifications 0.17: Modified music notification for updated 'notify' library 0.18: Added reporting of step count and HRM (new Gadgetbridges can now log this) +0.19: Support for call incoming/start/end +0.20: Reduce memory usage +0.21: Fix HRM setting +0.22: Respect Quiet Mode diff --git a/apps/gbridge/settings.js b/apps/gbridge/settings.js index 42fa84a3e..afd0be4fb 100644 --- a/apps/gbridge/settings.js +++ b/apps/gbridge/settings.js @@ -31,7 +31,7 @@ }, "Find Phone" : function() { E.showMenu(findPhone); }, "Record HRM" : { - value: settings().hrm, + value: !!settings().hrm, format: v => v?"Yes":"No", onchange: v => updateSetting('hrm', v) }, diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js index d616e8816..b4ce71907 100644 --- a/apps/gbridge/widget.js +++ b/apps/gbridge/widget.js @@ -2,20 +2,18 @@ // Music handling const state = { music: "stop", - musicInfo: { artist: "", album: "", track: "" }, - scrollPos: 0 }; // activity reporting var currentSteps = 0, lastSentSteps=0; var activityInterval; var hrmTimeout; - + function settings() { let settings = require('Storage').readJSON("gbridge.json", true) || {}; if (!("showIcon" in settings)) { @@ -43,14 +41,6 @@ return event; } } - function handleNotificationEvent(event) { - if (event.t === "notify") { - require("notify").show(prettifyNotificationEvent(event)); - Bangle.buzz(); - } else { // notify- - require("notify").hide(event); - } - } function updateMusic(options){ if (state.music === "play") { @@ -117,38 +107,7 @@ require("notify").hide("music"); } } - function handleMusicStateUpdate(event) { - if (state.music !== event.state) { - state.music = event.state - updateMusic({on: true}); - } - } - function handleMusicInfoUpdate(event) { - state.musicInfo = event; - updateMusic({on: false}); - } - function handleCallEvent(event) { - if (event.cmd === "accept") { - require("notify").show({ - size: 55, title: event.name, id: "call", - body: event.number, icon:require("heatshrink").decompress(atob("jEYwIMJj4CCwACJh4CCCIMOAQMGAQMHAQMDAQMBCIMB4PwgHz/EAn4CBj4CBg4CBgACCAAw="))}); - Bangle.buzz(); - } - } - - function handleFindEvent(event) { - if (state.find) { - clearInterval(state.find); - delete state.find; - } - if (event.n) - state.find = setInterval(_=>{ - Bangle.buzz(); - setTimeout(_=>Bangle.beep(), 1000); - },2000); - } - function handleActivityEvent(event) { var s = settings(); // handle setting activity interval @@ -193,19 +152,57 @@ switch (event.t) { case "notify": case "notify-": - handleNotificationEvent(event); + 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); + } break; case "musicinfo": - handleMusicInfoUpdate(event); + state.musicInfo = event; + updateMusic({on: false}); break; case "musicstate": - handleMusicStateUpdate(event); + if (state.music !== event.state) { + state.music = event.state + updateMusic({on: true}); + } break; case "call": - handleCallEvent(event); + var note = { size: 55, title: event.name, id: "call", + body: event.number, icon:require("heatshrink").decompress(atob("jEYwIMJj4CCwACJh4CCCIMOAQMGAQMHAQMDAQMBCIMB4PwgHz/EAn4CBj4CBg4CBgACCAAw="))} + if (event.cmd === "incoming") { + require("notify").show(note); + if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.buzz(); + } + } else if (event.cmd === "start") { + require("notify").show(Object.assign(note, { + bgColor : "#008000", titleBgColor : "#00C000", + body: "In progress: "+event.number})); + } else if (event.cmd === "end") { + require("notify").show(Object.assign(note, { + bgColor : "#800000", titleBgColor : "#C00000", + body: "Ended: "+event.number})); + setTimeout(function() { + require("notify").hide({ id: "call" }); + }, 2000); + } break; case "find": - handleFindEvent(event); + if (state.find) { + clearInterval(state.find); + delete state.find; + } + if (event.n) + // Ignore quiet mode: we always want to find our watch + state.find = setInterval(_=>{ + Bangle.buzz(); + setTimeout(_=>Bangle.beep(), 1000); + },2000); break; case "act": handleActivityEvent(event); @@ -251,7 +248,7 @@ function sendBattery() { gbSend({ t: "status", bat: E.getBattery() }); } - + // Send a summary of activity to Gadgetbridge function sendActivity(hrm) { var steps = currentSteps - lastSentSteps; @@ -279,7 +276,7 @@ } }); handleActivityEvent({}); // kicks off activity reporting - + // Finally add widget WIDGETS["gbridgew"] = {area: "tl", width: 24, draw: draw, reload: reload}; reload(); 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 new file mode 100644 index 000000000..bd718a5b1 --- /dev/null +++ b/apps/geissclk/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: BTN2->launcher, use smaller text to allow "20:00" to fit on screen diff --git a/apps/geissclk/clock-icon.js b/apps/geissclk/clock-icon.js new file mode 100644 index 000000000..723d17cc0 --- /dev/null +++ b/apps/geissclk/clock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AoMF/3G/2m/wOCmoaKmtAAgVQBYsFoECwALMAA0JBZQABBYIbBBaVUBYMVBYkBIIILDgEKGYUBqkA6oLB6sAlQLDqsAioLBCAOq4AkCqtAivCBYMC1YwDitQgtCwNUhWsOwlUBYVQlWwJAlVgtSxNQFwnwGANBqWFqQuE1QwBqNewtKFwgLBGANWytq2EFWoU6gWBqoLBFwNVYIZZBqtXyurioLF1W1q/W1tVPIIxE1NV72VqpTDDAleEIgADnWq3qgBTIYADvQuBAAVUBYla1XVBYYbDyt632qd40Fqulquq0ALFOwO16t6BYpxBvWX6tW0aoDXwNU1XXNQMD1T7CAwPC1abBegPqFodQdQILCO4hxBlWxq2FoqaFBYOBcYNxQggLCToOBqAwBTgwLCiqoGgtCJ4L0BBYsVBYRmBBY0CwEUJ4IwFBYkVBYsMBYI0CJAwLDAA4L2A=")) diff --git a/apps/geissclk/clock.js b/apps/geissclk/clock.js new file mode 100644 index 000000000..7d63b815e --- /dev/null +++ b/apps/geissclk/clock.js @@ -0,0 +1,151 @@ +var W = 79, H = 64; +/*var compiled = E.compiledC(` +// void transl(int, int, int ) +int transl(unsigned char *map, unsigned char *imgfrom, unsigned char *imgto) { + int n = 0; + const int W = 79; + const int H = 64; + for (int y=0;y>4)&0x0F) - 8; + int ax = nx&7; + int ay = ny&7; + int a = (nx>>3) + ((ny>>3)*W); + int c = 0; + if (a>=0 && a<(W*H-(W+1))) { + c = imgfrom[a]*(8-ax)*(8-ay) + + imgfrom[a+1]*(ax)*(8-ay) + + imgfrom[a+W]*(8-ax)*(ay) + + imgfrom[a+W+1]*(ax)*(ay); + c = (c>>6) - 4; + if (c<0) c=0; + } + imgto[n] = c; + n++; + } +} +`);*/ +var compiled = (function(){ + var bin=atob("Len3TwAnT/BPCPsAAJMI+wfzAOsDCdMYAZMAJhn4BkAAnQTwDwMD68YDBesUFAg7CDzdEE/q5AoI+wpaQfJvNapFItgB6woOA/AHAxH4CsCe+AGww/EIBQX7DPwD+wvMnvhPsATwBwQF+wv1ZUPE8QgKCvsMXJ74UFBrQwT7A8SkEQQ8JOrkdADgACQBm5xVATZPLsLRATdAL7bRA7C96PCPAAA="); + return { + transl:E.nativeCall(1, "void(int, int, int )", bin), + }; +})(); + +//require("Font5x9Numeric7Seg").add(Graphics); +Graphics.prototype.setFont5x9Numeric7Seg = function() { + this.setFontCustom(atob("AAAAAAAAAAIAAAQCAQAAAd0BgMBdwAAAAAAAdwAB0RiMRcAAAERiMRdwAcAQCAQdwAcERiMRBwAd0RiMRBwAAEAgEAdwAd0RiMRdwAcERiMRdwAFAAd0QiEQdwAdwRCIRBwAd0BgMBAAABwRCIRdwAd0RiMRAAAd0QiEQAAAAAAAAAA="), 32, atob("BgAAAAAAAAAAAAAAAAYCAAYGBgYGBgYGBgYCAAAAAAAABgYGBgYG"), 9); + } + +// Allocate the data +var dataa = new Uint8Array(W*H); +var datab = new Uint8Array(W*H); +var map = new Uint8Array(W*H); +var pal = new Uint16Array(256); +var PALETTES = 3; +var MAPS = 6; + +// If we're missing any maps, compute them! +(function() { + var files = require("Storage").list(/^geissclk/); + var allOk = true; + for (var n=0;n>3) | ((g&0xFC)<<3) | ((r&0xF8)<<8); + }pal[255] = 65535; +})() +require("Storage").write("geissclk.0.pal",pal.buffer); +E.showMessage("Precomputing\npalettes\n\nPlease wait...\n1 / 3"); +(function() { // gunge + for (var i=0;i<256;i++) { + var r = 0; + var g = Math.min(i*3,255); + var b = Math.min(i,255); + pal[i] = (b>>3) | ((g&0xFC)<<3) | ((r&0xF8)<<8); + }pal[255] = 65535; +})() +require("Storage").write("geissclk.1.pal",pal.buffer); +E.showMessage("Precomputing\npalettes\n\nPlease wait...\n2 / 3"); +(function() { // rainbow + for (var i=0;i<256;i++) { + var cl = E.HSBtoRGB((48+i)/128,1,Math.min(i/16,0.9),true); + var r = cl[0]; + var g = cl[1]; + var b = cl[2]; + pal[i] = (b>>3) | ((g&0xFC)<<3) | ((r&0xF8)<<8); + }pal[255] = 65535;pal[255] = 65535; +})() +require("Storage").write("geissclk.2.pal",pal.buffer); + + +// MAPS ---------------------------------------------- +E.showMessage("Precomputing\nmaps\n\nPlease wait...\n0 / 5"); +// straight out +(function() { "ram"; var n = 0; for (var y=0;y3)?0.5:-0.5); + dy = (-dy/d) + (((x&7)>3)?0.5:-0.5); + map[n++] = ((dx*3 + 8) & 0x0F) | (((dy*3 + 8) & 0x0F)<<4); + } +}})() +require("Storage").write("geissclk.2.map",map); +E.showMessage("Precomputing\nmaps\n\nPlease wait...\n3 / 5"); +// spiral +(function() { "ram"; var n = 0; for (var y=0;yv+"s", onchange: v => { settings.recording = false; settings.period = v; updateSettings(); } }, - 'View Tracks': viewTracks, + 'View Tracks': ()=>{viewTracks();}, '< Back': ()=>{load();} }; return E.showMenu(mainmenu); @@ -64,13 +65,13 @@ function viewTracks() { for (var n=0;n<36;n++) { var f = require("Storage").open(getFN(n),"r"); if (f.readLine()!==undefined) { - menu["Track "+n] = viewTrack.bind(null,n,false); + menu["Track "+n] = (n=>viewTrack(n)).bind(null,n,false); found = true; } } if (!found) menu["No Tracks found"] = function(){}; - menu['< Back'] = showMainMenu; + menu['< Back'] = () => { showMainMenu(); }; return E.showMenu(menu); } @@ -160,7 +161,7 @@ function viewTrack(n, info) { viewTrack(n, info); }); }; - menu['< Back'] = viewTracks; + menu['< Back'] = () => { viewTracks(); }; return E.showMenu(menu); } diff --git a/apps/gpsrec/interface.html b/apps/gpsrec/interface.html index 837162b74..11b53164f 100644 --- a/apps/gpsrec/interface.html +++ b/apps/gpsrec/interface.html @@ -98,11 +98,10 @@ function getTrackList() { for (var n=0;n<36;n++) { var f = require("Storage").open(".gpsrc"+n.toString(36),"r"); var l = f.readLine(); - if (l!==undefined) - Bluetooth.println(n+","+l.trim()); + Bluetooth.println((l!==undefined) ? (n + "," + l.trim()) : ""); } })()\n`,tracklist=>{ - var trackLines = tracklist.trim().split("\n"); + var trackLines = tracklist.trim().split("\n").filter(l=>l!=""); var html = `
\n`; trackLines.forEach(l => { diff --git a/apps/gpsrec/widget.js b/apps/gpsrec/widget.js index 4ddd0e669..8e4286db5 100644 --- a/apps/gpsrec/widget.js +++ b/apps/gpsrec/widget.js @@ -3,8 +3,8 @@ var hasFix = false; var fixToggle = false; // toggles once for each reading var gpsTrack; // file for GPS track - var periodCtr = 0; var gpsOn = false; + var lastFixTime; // draw your widget function draw() { @@ -26,9 +26,11 @@ fixToggle = !fixToggle; WIDGETS["gpsrec"].draw(); if (hasFix) { - periodCtr--; - if (periodCtr<=0) { - periodCtr = settings.period; + var period = 1000000; + if (lastFixTime!==undefined) + period = fix.time.getTime() - lastFixTime; + if (period > settings.period*1000) { + lastFixTime = fix.time.getTime(); try { if (gpsTrack) gpsTrack.write([ fix.time.getTime(), diff --git a/apps/gpssetup/ChangeLog b/apps/gpssetup/ChangeLog new file mode 100644 index 000000000..e57d53d8e --- /dev/null +++ b/apps/gpssetup/ChangeLog @@ -0,0 +1,2 @@ +0.01: First version of GPS Setup app +0.02: Created gppsetup module diff --git a/apps/gpssetup/README.md b/apps/gpssetup/README.md new file mode 100644 index 000000000..8e64c6a30 --- /dev/null +++ b/apps/gpssetup/README.md @@ -0,0 +1,109 @@ +# GPS Setup + +An App and module to enable the GPS to be configured into low power mode. + +## Goals + +To develop an app that configures the GPS to run with the lowest +possible power consumption. + +Example power consumption of the GPS while powered on: + +* 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 with 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 App + +The Settings App enables you set the options below. Either start the +App from the launcher or go to Settings, select App/Widgets and then +'GPS Setup'. + +When you exit the setup app, the settings will be stored in the +gpssetup.settings.json file, the GPS will be switched on and the +necessary commands sent to the GPS to configure it. The GPS is then +powered off. The GPS configuration is stored in the GPS non-volatile +memory so that next time the GPS is powered, that configuration is +used. These settings will remain for all apps that use the GPS. + + +- Power Mode: + + - **SuperE** - the factory default setup for the GPS. The recommended + power saving mode. If you need frequent (every second) updates on + position, then this is the mode for you. + + - **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. + +## Module + +A module is provided that'll allow you to set GPS configuration from your own +app. To use it: + +``` +// This will set up the GPS to current saved defaults. It's not normally +// needed unless the watch's battery has run down +require("gpssetup").setPowerMode(); + +// This sets up the PSMOO mode. update/search are optional in seconds +require("gpssetup").setPowerMode({ + power_mode:"PSMOO", + update:optional (default 120), + search:optional (default 5)}) + +// This sets up SuperE +require("gpssetup").setPowerMode({power_mode:"SuperE"}) +``` + +`setPowerMode` returns a promise, which is completed when the GPS is set up. + +So you can for instance do the following to turn the GPS off once it +has been configured: + +``` +require("gpssetup").setPowerMode().then(function() { + Bangle.setGPSPower(0); +}); +``` + +**Note:** It's not guaranteed that the user will have installed the `gpssetup` +app/module. It might be worth checking for its existence by surrounding the +`require` call with `try...catch` block. + +``` +var gpssetup; +try { + gpssetup = require("gpssetup") +} catch(e) { + E.showMessage("gpssetup\nnot installed"); +} +``` + +## 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 can 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/gpssetup/app.js b/apps/gpssetup/app.js new file mode 100644 index 000000000..e0d188af5 --- /dev/null +++ b/apps/gpssetup/app.js @@ -0,0 +1,109 @@ +/* + * GPS Setup app, hughbarney AT googlemail DOT com + * With thanks to Gordon Williams for support and advice + * + * UBLOX power modes: + * SuperE - will provide updates every second and consume 35mA, 75mA with LCD on + * PSMOO - will sleep for update time and consume around 7mA during that period + * after acquiring satelite fixes the GPS will settle into a cycle of + * obtaining fix, sleeping for update seconds, wake up, get fix and sleep. + * + * See README file for more details + * + */ + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +function log_debug(o) { + //let timestamp = new Date().getTime(); + //console.log(timestamp + " : " + o); +} + +const SETTINGS_FILE = "gpssetup.settings.json"; +let settings = undefined; +let settings_changed = false; + +function updateSettings() { + require("Storage").write(SETTINGS_FILE, settings); + settings_changed = true; +} + +function loadSettings() { + log_debug("loadSettings()"); + settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; + settings.update = settings.update||120; + settings.search = settings.search||5; + settings.power_mode = settings.power_mode||"SuperE"; + log_debug(settings); +} + +/*********** GPS Power and Setup Functions ******************/ + +function setupGPS() { + Bangle.setGPSPower(1); + setTimeout(function() { + require("gpssetup").setPowerMode().then(function() { + Bangle.setGPSPower(0); + }); + }, 100); +} + +/*********** GPS Setup Menu App *****************************/ + +function showMainMenu() { + var power_options = ["SuperE","PSMOO"]; + + const mainmenu = { + '': { 'title': 'GPS Setup' }, + '< Back': ()=>{exitSetup();}, + '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.update = v; + updateSettings(); + } + }, + 'Search (s)': { + value: settings.search, + min: 1, + max: 65, + step: 1, + onchange: v => { + settings.search = v; + updateSettings(); + } + } + }; + + return E.showMenu(mainmenu); +} + +function exitSetup() { + log_debug("exitSetup()"); + if (settings_changed) { + log_debug(settings); + E.showMessage("Configuring GPS"); + setTimeout(function() { + setupGPS(); + setTimeout(function() { load() }, 750); + }, 500); + } else { + load(); + } +} + +loadSettings(); +showMainMenu(); diff --git a/apps/gpssetup/gpssetup.js b/apps/gpssetup/gpssetup.js new file mode 100644 index 000000000..f8fed68ff --- /dev/null +++ b/apps/gpssetup/gpssetup.js @@ -0,0 +1,179 @@ +const SETTINGS_FILE = "gpssetup.settings.json"; + +function log_debug(o) { + //let timestamp = new Date().getTime(); + //console.log(timestamp + " : " + o); +} + +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; + } + + 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() { + log_debug("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() { + log_debug("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() { + log_debug("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]); +} + +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function setupSuperE() { + log_debug("setupGPS() Super-E"); + return Promise.resolve().then(function() { + UBX_CFG_RESET(); + return delay(100); + }).then(function() { + UBX_CFG_PMS(); + return delay(20); + }).then(function() { + UBX_CFG_SAVE(); + return delay(20); + }).then(function() { + log_debug("Powering GPS Off"); + /* + * must be part of the promise chain to ensure that + * setup does not return and powerOff before config functions + * have run + */ + return delay(20); + }); +} + +function setupPSMOO(settings) { + log_debug("setupGPS() PSMOO"); + return Promise.resolve().then(function() { + UBX_CFG_RESET(); + return delay(100); + }).then(function() { + UBX_CFG_PM2(settings.update, settings.search); + return delay(20); + }).then(function() { + UBX_CFG_RXM(); + return delay(20); + }).then(function() { + UBX_CFG_SAVE(); + return delay(20); + }).then(function() { + log_debug("Powering GPS Off"); + /* + * must be part of the promise chain to ensure that + * setup does not return and powerOff before config functions + * have run + */ + return delay(20); + }); +} + +/** Set GPS power mode (assumes GPS on), returns a promise. +Either: + +require("gpssetup").setPowerMode() // <-- set up GPS to current saved defaults +require("gpssetup").setPowerMode({power_mode:"PSMOO", update:optional, search:optional}) // <-- PSMOO mode +require("gpssetup").setPowerMode({power_mode:"SuperE"}) // <-- Super E mode + +See the README for more information + */ +exports.setPowerMode = function(options) { + settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; + if (options) { + if (options.update) settings.update = options.update; + if (options.search) settings.search = options.search; + if (options.power_mode) settings.power_mode = options.power_mode; + } + settings.update = settings.update||120; + settings.search = settings.search||5; + settings.power_mode = settings.power_mode||"SuperE"; + if (options) require("Storage").write(SETTINGS_FILE, settings); + if (settings.power_mode === "PSMOO") { + return setupPSMOO(settings); + } else { + return setupSuperE(); + } +}; diff --git a/apps/gpssetup/gpssetup.png b/apps/gpssetup/gpssetup.png new file mode 100644 index 000000000..ca5899983 Binary files /dev/null and b/apps/gpssetup/gpssetup.png differ diff --git a/apps/gpssetup/icon.js b/apps/gpssetup/icon.js new file mode 100644 index 000000000..fbe544b8b --- /dev/null +++ b/apps/gpssetup/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/ACmsAAIss1mBwIfZqwABFpwuYC4IADGBYuaDQwuDF5ouYLo4vJIYousCYQOIHJIuUCo4uIHJIuUf4wlGEIQaHFywvHGAguiF5GBEIgbHFzAvJDwwuRvV6F6xLJFxmkAAQuKqwtKPQ4RKFwgwICgobHVRJAJFQOr0owIE5AtNDIYRI0ulGBAuNapYABCRGkGBAuGAoIpNGBIIFGBF6FwuBFygAKGBAulGBgujGBWAF0jpBehIvlqwws1jnCGFOs1heBGFQuBFoYwoFxFWwAwjFw+BAAIMBGEIuMGEIuIeQIQFGDouJ1gSHGDYuSGDYuUGDSzBFYP+dAQuNGBN6F6RcBFyAwHwAvQGAYuSGAheCF54wCAAYuRGAQABwGAC6QuWGAV6FyYA/AH4A2A=")) diff --git a/apps/gpssetup/settings.js b/apps/gpssetup/settings.js new file mode 100644 index 000000000..0e3c621d1 --- /dev/null +++ b/apps/gpssetup/settings.js @@ -0,0 +1,4 @@ +(function(back) { + // just go right to our app + load("gpssetup.app.js"); +})(); diff --git a/apps/gpssetup/settings.json b/apps/gpssetup/settings.json new file mode 100644 index 000000000..631ececdc --- /dev/null +++ b/apps/gpssetup/settings.json @@ -0,0 +1 @@ +{"power_mode":"SuperE", "update":120, "search":6} diff --git a/apps/hardalarm/ChangeLog b/apps/hardalarm/ChangeLog index b8b4561b8..7e9b17f2a 100644 --- a/apps/hardalarm/ChangeLog +++ b/apps/hardalarm/ChangeLog @@ -1 +1,2 @@ 0.01: Add a number to match to turn off alarm +0.02: Respect Quiet Mode diff --git a/apps/hardalarm/hardalarm.js b/apps/hardalarm/hardalarm.js index c3623a193..e33bd39cc 100644 --- a/apps/hardalarm/hardalarm.js +++ b/apps/hardalarm/hardalarm.js @@ -62,6 +62,7 @@ function showPrompt(msg, buzzCount, alarm) { } function showAlarm(alarm) { + if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence var msg = formatTime(alarm.hr); var buzzCount = 20; if (alarm.msg) diff --git a/apps/heart/ChangeLog b/apps/heart/ChangeLog index 70134af27..083ecec85 100644 --- a/apps/heart/ChangeLog +++ b/apps/heart/ChangeLog @@ -1,3 +1,13 @@ 0.01: New App! 0.02: Don't overwrite existing settings on app update Clean up recordings on app removal +0.03: added graphing feature of 164 latest measurements +0.04: Fix memory usage when viewing HRM traces +0.05: Add loading screen for viewRecord + List average, minimum & maximum measurement in viewRecord + Disable recording only when current recording file is erased + Fix timezone offset + Draw chart based on height and width of display instead of hard-coded limits + Reduce memory usage by ~30% + Generate scale based on defined minimum and maximum measurement + Added background line on 50% to ease estimation of drawn values diff --git a/apps/heart/app.js b/apps/heart/app.js index 366a1068d..77a1c2106 100644 --- a/apps/heart/app.js +++ b/apps/heart/app.js @@ -1,3 +1,12 @@ +E.setFlags({pretokenise:1}); + +function log(msg) { + console.log("heart: " + msg + "; mem used: " + process.memory().usage / process.memory().blocksize); + return; +} + +log("start"); + Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -11,6 +20,7 @@ function updateSettings() { require("Storage").write("heart.json", settings); if (WIDGETS["heart"]) WIDGETS["heart"].reload(); + return; } function showMainMenu() { @@ -35,66 +45,268 @@ function showMainMenu() { updateSettings(); } }, - 'View Records': viewRecords, + 'View Records': ()=>{createRecordMenu(viewRecord.bind());}, + 'Graph Records': ()=>{createRecordMenu(graphRecord.bind());}, '< Back': ()=>{load();} }; return E.showMenu(mainMenu); } -function viewRecords() { +// Date().as().str cannot be used as it always returns UTC time +function getDateString(timestamp) { + var date = new Date(timestamp); + var day = date.getDate() < 10 ? "0" + date.getDate().toString() : date.getDate().toString(); + var month = date.getMonth() < 10 ? "0" + date.getMonth().toString() : date.getMonth().toString(); + return day + "." + month + "." + date.getFullYear(); +} + +// Date().as().str cannot be used as it always returns UTC time +function getTimeString(timestamp) { + var date = new Date(timestamp); + var hour = date.getHours() < 10 ? '0' + date.getHours().toString() : date.getHours().toString(); + var minute = date.getMinutes() < 10 ? '0' + date.getMinutes().toString() : date.getMinutes().toString(); + return hour + ':' + minute; +} + +function createRecordMenu(func) { const menu = { '': { 'title': 'Heart Records' } }; var found = false; for (var n=0;n<36;n++) { - var f = require("Storage").open(getFileNbr(n),"r"); - if (f.readLine()!==undefined) { - menu["Record "+n] = viewRecord.bind(null,n); + var line = require("Storage").open(getFileNbr(n),"r").readLine(); + if (line!==undefined) { + menu["#" + n + " " + getDateString(line.split(",")[0]*1000) + " " + getTimeString(line.split(",")[0]*1000)] = func.bind(null, n); found = true; } } if (!found) menu["No Records Found"] = function(){}; - menu['< Back'] = showMainMenu; + menu['< Back'] = ()=>{showMainMenu();}; return E.showMenu(menu); } function viewRecord(n) { + E.showMenu({'': 'Heart Record '+n}); + E.showMessage( + "Loading Data ...\n\nMay take a while,\nwill vibrate\nwhen done.", + 'Heart Record '+n + ); const menu = { '': { 'title': 'Heart Record '+n } }; - var heartCount = 0; var heartTime; var f = require("Storage").open(getFileNbr(n),"r"); var l = f.readLine(); - if (l!==undefined) { - var c = l.split(","); - heartTime = new Date(c[0]*1000); - } + // using arrays for memory optimization + var limits = Uint8Array(2); + // using arrays for memory optimization + var avg = Uint32Array(2); + // minimum + limits[0] = 2000; + // maximum + limits[1] = 0; + // count + avg[0] = 0; + // average sum + avg[1] = 0; + var count = 0; + var value = 0; + if (l!==undefined) + heartTime = new Date(l.split(",")[0]*1000); + log("parsing records"); while (l!==undefined) { - heartCount++; - // TODO: min/max/average of heart rate? + count++; + if (parseInt(l.split(',')[2]) >= 70) { + avg[0]++; + value = parseInt(l.split(',')[1]); + if (value < limits[0]) { + limits[0] = value; + } else if (value > limits[1]) { + limits[1] = value; + } + avg[1] += value; + } l = f.readLine(); } + l = undefined; + value = undefined; + log("finished parsing"); if (heartTime) menu[" "+heartTime.toString().substr(4,17)] = function(){}; - menu[heartCount+" records"] = function(){}; - // TODO: option to draw it? Just scan through, project using min/max - menu['Erase'] = function() { + menu[count + " records"] = function(){}; + menu["Min: " + limits[0]] = function(){}; + menu["Max: " + limits[1]] = function(){}; + menu["Avg: " + Math.round(avg[1] / avg[0])] = function(){}; + menu["Erase"] = function() { E.showPrompt("Delete Record?").then(function(v) { if (v) { - settings.isRecording = false; - updateSettings(); - var f = require("Storage").open(getFileNbr(n),"r"); - f.erase(); - viewRecords(); + if (n == settings.fileNbr) { + settings.isRecording = false; + updateSettings(); + } + require("Storage").open(getFileNbr(n),"r").erase(); + E.showMenu(); + createRecordMenu(viewRecord.bind()); } else - viewRecord(n); + return viewRecord(n); }); }; - menu['< Back'] = viewRecords; - print(menu); + menu['< Back'] = ()=>{createRecordMenu(viewRecord.bind());}; + Bangle.buzz(200, 0.3); return E.showMenu(menu); } +function stop() { + E.showMenu(); + load(); +} + +function graphRecord(n) { + var headline = "Heart Record " + n; + E.showMenu({'': headline}); + E.showMessage( + "Loading Data ...\n\nMay take a while,\nwill vibrate\nwhen done.", + headline + ); + + const MinMeasurement = 30; + const MaxMeasurement = 150; + const GraphXLabel = 35; + const GraphXZero = 40; + const GraphY100 = 60; + const GraphMarkerOffset = 5; + // calculate number of pixels based on display width + const MaxValueCount = g.getWidth() - GraphXZero - ( g.getWidth() - 220 ) - GraphMarkerOffset; + // calculate Y axis "0" pixel + const GraphYZero = g.getHeight() - g.setFont("Vector", 10).getFontHeight() - GraphMarkerOffset * 2; + // calculate X axis max drawable pixel + const GraphXMax = GraphXZero + MaxValueCount; + // calculate space between labels of scale + const LabelOffset = (GraphYZero - GraphY100) / (MaxMeasurement - MinMeasurement); + + var lineCount = 0; + var startLine = 1; + var f = require("Storage").open(getFileNbr(n),"r"); + var line = f.readLine(); + + log("Counting lines"); + + while (line !== undefined) { + lineCount++; + line = f.readLine(); + } + + log(`lineCount: ${lineCount}`); + if (lineCount > MaxValueCount) + startLine = lineCount - MaxValueCount; + f = undefined; + line = undefined; + lineCount = undefined; + log(`startLine: ${startLine}`); + + f = require("Storage").open(getFileNbr(n),"r"); + line = f.readLine(); + + var times = Uint32Array(2); + var tempCount = 0; + var positionX = GraphXZero; + var positionY = GraphYZero; + var measure; + + while (line !== undefined) { + currentLine = line; + line = f.readLine(); + tempCount++; + if (tempCount == startLine) { + // generating rgaph in loop when reaching startLine to keep loading + // message on screen until graph can be drawn + g.clear(). + // Home for Btn2 + setColor(1, 1, 1). + drawLine(220, 118, 227, 110). + drawLine(227, 110, 234, 118). + drawPoly([222,117,222,125,232,125,232,117], false). + drawRect(226,120,229,125). + + // headline + setFontAlign(0, -1, 0). + setFont("6x8", 2). + drawString(headline, g.getWidth()/2 - headline.length/2, GraphY100 - g.getFontHeight() - GraphMarkerOffset). + + // Chart + setColor(1, 1, 0). + // horizontal bottom line + drawLine(GraphXZero, GraphYZero + GraphMarkerOffset, GraphXZero, GraphY100). + // vertical left line + drawLine(GraphXZero - GraphMarkerOffset, GraphYZero, GraphXMax + GraphMarkerOffset, GraphYZero). + // scale indicator line for 100% + drawLine(GraphXZero - GraphMarkerOffset, GraphY100, GraphXZero, GraphY100). + // scale indicator line for 50% + drawLine(GraphXZero - GraphMarkerOffset, GraphY100 + (GraphYZero - GraphY100)/2, GraphXZero, GraphY100 + (GraphYZero - GraphY100)/2). + // background line for 50% + setColor(1, 1, 1). + drawLine(GraphXZero + 1, GraphY100 + (GraphYZero - GraphY100)/2, GraphXMax, GraphY100 + (GraphYZero - GraphY100)/2). + setFontAlign(1, -1, 0). + setFont("Vector", 10); + + // scale text + for (var i = MaxMeasurement; i >= MinMeasurement; i-=10) { + g.drawString(i, GraphXLabel, GraphY100 + LabelOffset * ( MaxMeasurement - i ) - GraphMarkerOffset); + } + + log("Finished drawing chart"); + } else if (tempCount > startLine) { + positionX++; + if (parseInt(currentLine.split(",")[2]) >= 70) { + g.setColor(1, 0.3, 0.3); + oldPositionY = positionY; + measure = parseInt(currentLine.split(",")[1]); + positionY = GraphYZero - measure + MinMeasurement; + if (positionY > GraphYZero) { + positionY = GraphYZero; + } + if (positionY < GraphY100) { + positionY = GraphY100; + } + + if (times[0] === 0) { + times[0] = parseInt(currentLine.split(",")[0]); + } + if (tempCount == startLine + 1) { + g.setPixel(positionX, positionY); + } else { + g.drawLine(positionX - 1, oldPositionY, positionX, positionY); + times[1] = parseInt(currentLine.split(",")[0]); + } + } + } + } + + g.setColor(1, 1, 0).setFont("Vector", 10); + log('startTime: ' + times[0]); + log('endTime: ' + times[1]); + + if (times[0] !== 0) { + g.setFontAlign(-1, -1, 0). + drawString(getTimeString(times[0]*1000), 15, GraphYZero + 12); + } + + if (times[1] !== 0) { + var dateStr = getDateString(times[1]*1000); + g.setFontAlign(-1, -1, 0). + drawString(dateStr, GraphXMax/2 - dateStr.length/2 - GraphMarkerOffset, GraphYZero + 12). + setFontAlign(1, -1, 0). + drawString(getTimeString(times[1]*1000), GraphXMax, GraphYZero + 12); + } + + log("Finished rendering data"); + Bangle.buzz(200, 0.3); + g.flip(); + setWatch(stop, BTN2, {edge:"falling", debounce:50, repeat:false}); + return; +} + showMainMenu(); + +// vim: et ts=2 sw=2 diff --git a/apps/helloworld/ChangeLog b/apps/helloworld/ChangeLog new file mode 100644 index 000000000..777e555f1 --- /dev/null +++ b/apps/helloworld/ChangeLog @@ -0,0 +1,2 @@ +0.01: 1st version ! +0.02: Supports bottom widgets and UI based in UI4swatch! diff --git a/apps/helloworld/README.md b/apps/helloworld/README.md new file mode 100644 index 000000000..370ec349b --- /dev/null +++ b/apps/helloworld/README.md @@ -0,0 +1,47 @@ +# hello, world! + +A cross cultural hello world!/hola mundo! app +The most common testing sentence in several languages ;) + + +## Pictures: + +Launcher icon + +![](helloworld_icon.png) + +Screen - Spanish + +![](helloworld_es.png) + +Screen - English + +![](helloworld_en.png) + +Screen - Japanese + +![](helloworld_jp.png) + + + +## Usage + +Open and see a hello, World! in the screen +interact to change language, color or quit. + +## Features + +Colours, all inputs , graph, widgets loaded +Counter for Times Display + + +## Controls + +finger swipe +button 1,2 and 3 +touch screen left, center or right + + +## Creator + +Daniel Perez \ No newline at end of file diff --git a/apps/helloworld/app-icon.js b/apps/helloworld/app-icon.js new file mode 100644 index 000000000..24cb8fabc --- /dev/null +++ b/apps/helloworld/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4MA///t0uChkD4AFDg/guAFCh/4/AFCj/xAoc/8fwAoV/4/gAoePEgd/j+AAoV+n4vDv1+Aoc//gFDj4uDHYPwjBHE/4LD4P/FQUfwf/GwU/g//IQU+Aok8h4FDnAFEwAFE4AFE/gdEAo0DGoYFCIIReBJoYFBngFEAAYFF/wFEZQIjDAoQ1CAoSuCAoSLCv4FBEwU/AoiEB/4sDAsYAJ")) diff --git a/apps/helloworld/app.js b/apps/helloworld/app.js new file mode 100644 index 000000000..4803fc423 --- /dev/null +++ b/apps/helloworld/app.js @@ -0,0 +1,186 @@ +//HolaMundo v202103 +// place your const, vars, functions or classes here +{ + var contador=1; + var v_color_statictxt='#e56e06'; + var v_color_b_area='#111111'; //orange RGB format rrggbb //white,Orange,DarkGreen,Yellow,Maroon,Blue,green,Purple,cyan,olive,DarkCyan,pink + var a_colors= Array(0xFFFF,0xFD20,0x03E0,0xFFE0,0x7800,0x001F,0x07E0,0x780F,0x07FF,0x7BE0,0x03EF,0xF81F); + var v_color_lines=0xFFFF; //White hex format + //new + var v_color_text=0x07E0; + var v_font1size=10; //out of quotes + var v_font2size=12; + var v_font_banner_size=30; + var v_font3size=12; + var v_arraypos=0; + var v_acolorpos=0; + var a_string1 = Array('hola', 'hello', 'saluton', 'ola','ciao', 'salut','czesc','konnichiwa'); + var a_string2 = Array('mundo!', 'world!', 'mondo!','mundo!','mondo!','monde!','swiat!','sekai!'); + var mem=process.memory(); + var v_model=process.env.BOARD; + console.log("device="+v_model); + + var x_max_screen=g.getWidth();//240; + var y_max_screen=g.getHeight(); //240; + var y_wg_bottom=g.getHeight()-25; + var y_wg_top=25; + if (v_model=='BANGLEJS') { + var x_btn_area=215; + var x_max_usable_area=x_btn_area;//Pend! only for bangle.js + var y_btn2=124; //harcoded for bangle.js cuz it is not the half of display height + } else x_max_usable_area=240; +} + + console.log("*** UI dimensions***"); + console.log("x="+x_max_screen); + console.log("y_wg_bottom="+y_wg_bottom); + + + // special function to handle display switch on + Bangle.on('lcdPower', (on) => { + if (on) { + contador=contador+1; + PrintHelloWorld(); + // call your app function here + // If you clear the screen, do Bangle.drawWidgets(); + } + }); + + //Clear/fill dynamic area except widget area, right and bottom status line + function ClearActiveArea(){ + g.setColor(v_color_b_area); + //harcoded values to avoid clearing areas + g.fillRect(0,y_wg_top,195,150); + g.flip(); + } + + function DrawBangleButtons(){ + g.setFontVector(v_font1size); + g.setColor(v_color_lines);//White + + + g.drawString("Lang", x_max_screen-g.stringWidth("Lang"),y_wg_top+v_font1size+1); + //above Btn2 + //g.setFontVector(v_font1size).drawString("Off", x_max_screen-g.stringWidth("Off"),y_btn2-(2*v_font1size)); + g.drawString("Color", x_max_screen-g.stringWidth("Color"),y_btn2-v_font1size); + //above Btn3 + g.drawString("Quit", x_max_screen-g.stringWidth("Quit"),y_wg_bottom-(2*v_font1size)); + g.flip(); + g.setColor(v_color_text); //green + g.setFontVector(v_font1size); + g.drawString("B1", x_max_screen-g.stringWidth("B1"),y_wg_top); + g.drawString("B2", x_max_screen-g.stringWidth("B2"),y_btn2); + g.drawString("B3",x_max_screen-g.stringWidth("B3"),y_wg_bottom-v_font1size); + g.flip(); +} + +function DrawBottomInfoBanner(){ +/* External Vars:v_color_text,v_font2size,x_max_usable_area,y_wg_bottom +*/ + g.setColor(v_color_text); + var info_text1="Swipe <- -> (Lang)"; + //var info_text2="Touch: Left=Up Right=Down"; + //aligned left of max usable area + g.setFontVector(v_font2size); + //g.drawString(info_text2, x_max_usable_area-g.stringWidth(info_text2)-2 ,y_wg_bottom-(2*v_font2size)); + g.drawString(info_text1, x_max_usable_area-g.stringWidth(info_text1)-2 ,y_wg_bottom-v_font2size); + g.flip(); +} + + //function Graphics.setColor(r, g, b) binary + // banglejs.com/reference#l_Graphics_setColor + + function PrintHelloWorld(){ + ClearActiveArea(); //except widgets and bottom + + console.log("drawing a "+a_string1[v_arraypos]+" "+a_string2[v_arraypos]); + + g.setColor(a_colors[v_acolorpos]); //dynamic color + g.setFont("Vector",v_font_banner_size); + g.drawString(a_string1[v_arraypos],2,55); + //line below 2nd string + g.drawLine(10, 149, 150, 149); + g.flip(); + + g.setColor(a_colors[v_acolorpos+1]); //dynamic color + g.drawString(a_string2[v_arraypos],5,85); + g.flip(); + + g.setFont("Vector",v_font3size); + g.setColor(0,0,1); //blue + g.drawString("Display count: "+contador ,10,115); + mem=process.memory(); + //console.log("Mem free/total: "+mem.free+"/"+mem.total); + g.drawString("Free mem: "+mem.free+"/"+mem.total,10,135); + g.flip(); + } + + function PrintMainStaticArea(){ + g.setColor(v_color_statictxt); + g.setFont("Vector",v_font3size); + g.drawString("#by DPG #bangle.js",10,170); + g.drawString("#javascript #espruino",10,185); + + var img_obj_RedHi = { + width : 40, height : 40, bpp : 4, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AFkM7vd4EAhoTNhvQhvcgHdAQIAL5oWCFIPdExo+CEoIZCABI0DhvADIZhJL4IXDHRkMEAQmOCYgmOAAIOBHwImNRQgmPHgYmCUIIXMJobfB3jgCWZJNDEga1JYQQQCMYZoJJAJNDBwgTICQPdCY7lDRQx4DVIwTIHYZzEHZATFBwblDCZRKEO5ITFWAbIJCYrHBAAImICYwEB5raKCYwAMCYXc5gADE5hLDAAgTIBJLkBBJAyKHw5hKBRJJKKJSuII5Q0IhqPKCbjRKCc4AgA==")) + } + g.drawImage(img_obj_RedHi,155,160); + g.flip(); + } + + + //inc var postion for text array + function ChangeLang(){ + if (v_arraypos { + if(dir == 1) ChangeLang(); //func load() to quit + else ChangeLang(); + }); + } + + console.log("**************************"); + console.log("Log: *** hola mundo app"); + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + if (v_model=='BANGLEJS') DrawBangleButtons(); + DrawBottomInfoBanner(); + UserInput(); + PrintMainStaticArea(); + PrintHelloWorld(); + \ No newline at end of file diff --git a/apps/helloworld/app.png b/apps/helloworld/app.png new file mode 100644 index 000000000..ebbd0525a Binary files /dev/null and b/apps/helloworld/app.png differ diff --git a/apps/helloworld/helloworld.img b/apps/helloworld/helloworld.img new file mode 100644 index 000000000..b17c4877d --- /dev/null +++ b/apps/helloworld/helloworld.img @@ -0,0 +1 @@ +00>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> \ No newline at end of file diff --git a/apps/helloworld/helloworld_en.png b/apps/helloworld/helloworld_en.png new file mode 100644 index 000000000..5a97440f1 Binary files /dev/null and b/apps/helloworld/helloworld_en.png differ diff --git a/apps/helloworld/helloworld_es.png b/apps/helloworld/helloworld_es.png new file mode 100644 index 000000000..131babc63 Binary files /dev/null and b/apps/helloworld/helloworld_es.png differ diff --git a/apps/helloworld/helloworld_icon.png b/apps/helloworld/helloworld_icon.png new file mode 100644 index 000000000..369929072 Binary files /dev/null and b/apps/helloworld/helloworld_icon.png differ diff --git a/apps/helloworld/helloworld_jp.png b/apps/helloworld/helloworld_jp.png new file mode 100644 index 000000000..0b97f0352 Binary files /dev/null and b/apps/helloworld/helloworld_jp.png differ diff --git a/apps/hourstrike/ChangeLog b/apps/hourstrike/ChangeLog new file mode 100644 index 000000000..73b8cb168 --- /dev/null +++ b/apps/hourstrike/ChangeLog @@ -0,0 +1,7 @@ +0.01: New App +0.02: Add different strike intervals and support for quiet time +0.03: Bug fixes for setting attributes +0.04: Add more time to strike and the strength +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 diff --git a/apps/hourstrike/README.md b/apps/hourstrike/README.md new file mode 100644 index 000000000..67a131f8a --- /dev/null +++ b/apps/hourstrike/README.md @@ -0,0 +1,25 @@ +# Hour Strike + +![icon](app-icon.png) + +Time passes too fast! + +This app configures your `Bangle.js` so that it buzzes on the hour or on the half hour. + +This app is slightly different from [Hour Chime](https://github.com/espruino/BangleApps/tree/master/apps/widchime). `Hour Chimee` runs as a widget but `Hour Strike` runs as a background task, without showing a widget. + +## Features + +- Strike the hour, the half hour, the quarter hour, and more +- Set up a range of hours that clock will strike +- Set up the strength of the strike +- Preview when the next strike will happen + +## Known Issues + +- This app does not know or check whether your clock already chimes on the hour. + +## Creator + +[Weiming Hu](https://weiming-hu.github.io/), using coding from the [Default Alarm](https://github.com/espruino/BangleApps/tree/master/apps/alarm). + diff --git a/apps/hourstrike/app-icon.js b/apps/hourstrike/app-icon.js new file mode 100644 index 000000000..7f2040745 --- /dev/null +++ b/apps/hourstrike/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkGswAHogAEBxAAHsgXFowXPCwowQFwwwQCIUjn/zmQwPFwUj/4ACDAQwMBwNDCgPwh4DBmgwMFwU/C4vzGBgMBoRECC4f/kgwLIwgXFJBgLBl4XH+QXNLwQXFMAQXPmEDC6K8DiEBAoYXSgA1DI6MxgETL6gYBgIGBC5ynDCYMQAwKnOa4YABmTXQoQXEAAUkC5dkMAx2EowXJJBBGNAAMUBwMjMAgHBoIXLiIPBDAM/+YWCokRC5gwCAAtBC5owDAAgJBC5dBiAwGoMBigXLokQgIXFA4QXMoEAAANNqAECggXR7oXXAYQXSgvUC6sEC60N6oXW6AXUinu8IXTgPuAAMQC6UEC4SsDC58OC4XgC9RHXO66nCoLXVAAoXmABQX1A")) diff --git a/apps/hourstrike/app-icon.png b/apps/hourstrike/app-icon.png new file mode 100644 index 000000000..f7ca232cf Binary files /dev/null and b/apps/hourstrike/app-icon.png differ diff --git a/apps/hourstrike/app.js b/apps/hourstrike/app.js new file mode 100644 index 000000000..c70fa2d41 --- /dev/null +++ b/apps/hourstrike/app.js @@ -0,0 +1,48 @@ +const storage = require('Storage'); +let settings; + +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]; + const mainmenu = {'': { 'title': 'Hour Strike' }}; + mainmenu['Next strike '+settings.next_hour+':'+settings.next_minute] = function(){}; + mainmenu['Notify every'] = { + value: mode_interval.indexOf(settings.interval), + min: 0, max: 6, format: v => mode_txt[v], + onchange: v => { + settings.interval = mode_interval[v]; + if (v===0) {settings.next_hour = -1; settings.next_minute = -1;} + updateSettings();}}; + mainmenu.Start = { + value: settings.start, min: 0, max: 23, format: v=>v+':00', + onchange: v=> {settings.start = v; updateSettings();}}; + mainmenu.End = { + value: settings.end, min: 0, max: 23, format: v=>v+':59', + onchange: v=> {settings.end = v; updateSettings();}}; + mainmenu.Strength = { + value: settings.vlevel*10, min: 1, max: 10, format: v=>v/10, + onchange: v=> {settings.vlevel = v/10; updateSettings();}}; + mainmenu['< Back'] = ()=>load(); + return E.showMenu(mainmenu); +} + +showMainMenu(); diff --git a/apps/hourstrike/boot.js b/apps/hourstrike/boot.js new file mode 100644 index 000000000..8ddad31af --- /dev/null +++ b/apps/hourstrike/boot.js @@ -0,0 +1,39 @@ +(function() { + function setup () { + 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; + if (wait_msec>0) { + t.setMilliseconds(t.getMilliseconds()+wait_msec); + var t_hour = t.getHours(); + if (t_hoursettings.end) { + var strike = new Date(t.getTime()); + strike.setHours(settings.start); + strike.setMinutes(0); + if (t_hour>settings.end) { + strike.setDate(strike.getDate()+1); + } + wait_msec += strike-t; + settings.next_hour = strike.getHours(); + settings.next_minute = strike.getMinutes(); + } else { + settings.next_hour = t_hour; + settings.next_minute = t.getMinutes(); + } + setTimeout(strike_func, wait_msec); + } else { + settings.next_hour = -1; + settings.next_minute = -1; + } + require('Storage').write('hourstrike.json', settings); + } + function strike_func () { + var setting = require('Storage').readJSON('hourstrike.json',1)||[]; + Bangle.buzz(200, setting.vlevel||0.5) + .then(() => new Promise(resolve => setTimeout(resolve,200))) + .then(() => Bangle.buzz(200, setting.vlevel||0.5)); + setup(); + } + setup(); +})(); diff --git a/apps/hrm/ChangeLog b/apps/hrm/ChangeLog index 5715e07c7..6cedf8f1b 100644 --- a/apps/hrm/ChangeLog +++ b/apps/hrm/ChangeLog @@ -1,2 +1,4 @@ 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 diff --git a/apps/hrm/heartrate.js b/apps/hrm/heartrate.js index 6f0a176d3..09e8a826e 100644 --- a/apps/hrm/heartrate.js +++ b/apps/hrm/heartrate.js @@ -4,15 +4,25 @@ Bangle.setHRMPower(1); var hrmInfo, hrmOffset = 0; var hrmInterval; 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(); } 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 = setInterval(readHRM,40); + hrmInterval = undefined; + if (hrmInfo.raw) { + hrmOffset = 0; + setTimeout(function() { + hrmInterval = setInterval(readHRM,41); + }, 40); + } var px = g.getWidth()/2; g.setFontAlign(0,0); @@ -25,17 +35,35 @@ 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) { + var a = v.raw; + hrmOffset++; + if (hrmOffset>g.getWidth()) { + hrmOffset=0; + g.clearRect(0,90,239,239); + g.moveTo(-100,0); + } + + y = E.clip(170 - (v.raw*2),100,230); + g.setColor(1,1,1); + g.lineTo(hrmOffset, y); +}); // 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().setFont("6x8",2).setFontAlign(0,0); +g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16); countDown(); -var min=0,max=0; var wasHigh = 0, wasLow = 0; var lastHigh = getTime(); var hrmList = []; @@ -51,9 +79,7 @@ function readHRM() { for (var i=0;i<2;i++) { var a = hrmInfo.raw[hrmOffset]; hrmOffset++; - min=Math.min(min*0.97+a*0.03,a); - max=Math.max(max*0.97+a*0.03,a); - y = E.clip(170 - (a*4),100,230); + y = E.clip(170 - (a*2),100,230); g.setColor(1,1,1); g.lineTo(hrmOffset, y); } diff --git a/apps/kitchen/ChangeLog b/apps/kitchen/ChangeLog new file mode 100644 index 000000000..71548ec30 --- /dev/null +++ b/apps/kitchen/ChangeLog @@ -0,0 +1,10 @@ +0.01: First version +0.02: compass disable BTN1,BTN2 while waiting for GPS to reach RUNNING status +0.03: Don't buzz for GPS fix in Quiet Mode +0.04: Added stopwatch face +0.05: Stopwatch, hide hours if 0, fixed flicker when stopped, updated README issues +0.06: Reduced memory footprint of compass, used direct screen access rather than arrayBuffer +0.07: Added error codes if dependancies are missing +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 diff --git a/apps/kitchen/README.md b/apps/kitchen/README.md new file mode 100644 index 000000000..a829a39b0 --- /dev/null +++ b/apps/kitchen/README.md @@ -0,0 +1,256 @@ +# Kitchen Combo - a multiclock format of the waypointer, walkersclock, stepo, stopwatch, heartrate and arrow apps. + +![](screenshot_kitchen.jpg) + +*...everything but the kitchen sink..* + +NOTE: This app requires Bangle firmware 2.08.187 or later. + +The app is aimed at navigation whilst walking. Please note that it +would be foolish in the extreme to rely on this as your only +navigation aid! + +Please refer to the section on calibration of the compass. This +should be done each time the app is going to be used. + +The app has 4 faces that can quickly be switched from one to another. +* Stepo - a large font clock that displays the current steps in a doughnut gauge +* GPS - when the GPS is on displays current grid ref, lat, lon, speed, altitude and course +* Digi - a digital clock with day and date, displays battery and memory status (click BTN1) +* Swatch - a simple stopwatch that times in seconds, minutes and up to 999 hours, with lap times +* Waypointer - a compass arrow that points to a selected waypoint when the GPS is on. + - enables you to mark waypoints and cycle through a list of waypoints + - shows distance and bearing to currently selected waypoint + +## Common buttons used to navigate through the app + +* BTN3 - short press, next app/clock face +* BTN3 - long press, reset the watch +* BTN2 - long press, start the app launcher + +The following buttons depend on which face is currently in use + +* BTN1 - Short press + - Digi : Cycle the battery, memory display on the mode line + - GPS : Cycle through the GPS data displays (grid ref, lat lon, speed, alt, course) + - Waypointer : Select previous waypoint +* BTN1 - long press + - GPS : switch GPS on or off + - Waypointer : set or unset the current waypoint +* BTN2 - short press + - Waypointer : select next waypoint + + +## Stepo +![](screenshot_stepo.jpg) + +- Requires one of the pedominter widgets to be installed +- Displays the time in large font +- Display current step count in a doughnut gauge +- Show step count in the middle of the 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 BTN3 to switch to the next app + + +## GPS +![](screenshot_gps.jpg) +- Use BTN1 long press to switch the GPS on or off +- Use BTN1 short press to switch between the display of the Os grid refernce, lat lon, speed, alt, course. +- Use BTN3 to switch to the next app + +## Digi +![](screenshot_digi.jpg) +- 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 +- BTN1 - start, stop +- BTN2 - lap if the timer is running, reset if the timer is stopped + +## Heart +![](screenshot_heart.jpg) +- A simple heart rate monitor, at present the app is just showing the raw value from HRM.bpm +- BTN1, long press, turn heart rate monitor on / off + +## Waypointer +- Use BTN1 to select previous waypoint (when GPS is on) +- Use BTN2 to select the next waypoint (when GPS is on) +- Use BTN3 to switch to the next app +- Use BTN1 long press to clear a waypoint or to record the current position + +When the GPS is off this screen acts as a compass and points +North. The white digits below the arrow show your current heading +with reference to North. + +When the GPS in on the screen points to the selected waypoint which +are loaded from the waypoints.json file. The compass arrow now points +in the direction you need to walk in. Once you have selected a +waypoint a bearing from your current position (received from a GPS +fix) is calculated and the compass is set to point in that direction. +If the arrow is pointing to the left, turning left should straighten +the arrow up so that it is pointing straight ahead. + +The large digits are the bearing from the current position. On the +left is the distance to the waypoint in local units. When the +selected waypoint has a lat/lon recorded the text of the distance and +waypoint name will be shown in blue. If the waypoint name is shown +in white it means it is available to record a waypoint. + +Use BTN1 and BTN2 to select the previous or next waypoint +respectively. In the screen shot below a waypoint giving the location +of Stone Henge has been selected. + +![](screenshot_stone.jpg) + +The screenshot above shows that Stone Henge is 259.9 miles from the +current location. To travel towards Stone Henge I need to turn +slightly right until the arrow is pointing straight ahead. As you +continue to walk in the pointed direction you should see the distance +to the waypoint reduce. The frequency of updates will depend on +which settings you have used in the GPS. + +At the top of the screen you can see two widgets. These are the [GPS +Power +Widget](https://github.com/espruino/BangleApps/tree/master/apps/widgps) +and the [Compass Power Indicator Widget]. These can be installed +seperately and provide you a indication of when the GPS and Compass +are switched on and drawing power. + + +### Marking Waypoints + +The app lets you mark your current location as follows. There are +vacant slots in the waypoint file which can be allocated a +location. In the distributed waypoint file these are labelled WP0 to +WP4. Select one of these - WP2 is shown below. + +![](screenshot_wp2_cleared.jpg) + +Bearing and distance are both zero as WP2 has currently no GPS +location associated with it. To mark the location, long press BTN1. + +![](screenshot_wp2_set.jpg) + +The app indicates that WP2 is now marked by changing the color to +blue. The distance should be small as shown in the screen shot as you +have just marked your current location. + +You can free the waypoint by long pressing BTN1 again. + + +### Waypoint JSON file + +When the app is loaded from the app loader, a file named +`waypoints.json` is loaded along with the javascript etc. The file +has the following contents: + + +``` +[ + { + "name":"NONE" + }, + { + "name":"No10", + "lat":51.5032, + "lon":-0.1269 + }, + { + "name":"Stone", + "lat":51.1788, + "lon":-1.8260 + }, + { "name":"WP0" }, + { "name":"WP1" }, + { "name":"WP2" }, + { "name":"WP3" }, + { "name":"WP4" } +] +``` + +The file contains the initial NONE waypoint which is useful if you +just want to display course and speed. The next two entries are +waypoints to No 10 Downing Street and to Stone Henge - obtained from +Google Maps. The last five entries are entries which can be *marked*. + +You add and delete entries using the Web IDE to load and then save +the file from and to watch storage. The app itself does not limit the +number of entries although it does load the entire file into RAM +which will obviously limit this. + + +### Waypoint Editor + +Clicking on the download icon of gpsnav in the app loader invokes the +waypoint editor. The editor downloads and displays the current +`waypoints.json` file. Clicking the `Edit` button beside an entry +causes the entry to be deleted from the list and displayed in the +edit boxes. It can be restored - by clicking the `Add waypoint` +button. A new markable entry is created by using the `Add name` +button. The edited `waypoints.json` file is uploaded to the Bangle by +clicking the `Upload` button. + + +### Calibration of the Compass + +The Compass should be calibrated before using the App to navigate to +a waypoint (or a series of waypoints). To do this use either the +Arrow Compass or the [Navigation +Compass](https://github.com/espruino/BangleApps/tree/master/apps/magnav). +Open the compass app and clicking on BTN3. The calibration process +takes 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`. + + +### Technical Notes on Memory Management + +v0.06: The stepo watch face uses an ArrayBuffer to setup the doughnut +gauge before it is displayed. This is necessary as the drawing of +the doughnut shape is quite slow. However I found that when an +ArrayBuffer was also used for the compass arrow part of the App it +could result in LOW_MEMORY errors due to the memory fragmentation +caused when switching multiple times between the watch faces. It is +possible to call Bangle.defrag() when switching to a new watch face +but this causes an annoying delay in the time it takes to switch. So +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.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 +reduces chance of getting a memory error on switching watch faces. + +### Error Codes + +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 + +### Issues / Future enhancements + +* 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 diff --git a/apps/kitchen/app.kit.js b/apps/kitchen/app.kit.js new file mode 100644 index 000000000..416aa43c4 --- /dev/null +++ b/apps/kitchen/app.kit.js @@ -0,0 +1,55 @@ +// simple template +(() => { + function getFace(){ + var intervalRefSec; + var prevTime; + + const Y_TIME = 30; + const Y_ACTIVITY = 116; + + function init(gps,sw) { + prevTime = ""; + g.clear(); + } + + function freeResources() { + prevTime = undefined; + } + + function startTimer() { + draw(); + intervalRefSec = setInterval(draw, 5000); + } + + function stopTimer() { + if (intervalRefSec) { intervalRefSec = clearInterval(intervalRefSec); } + } + + function onButtonShort(btn) {} + function onButtonLong(btn) {} + + function draw() { + var d = new Date(); + var da = d.toString().split(" "); + var time = da[4].substr(0,5); + + if (time !== prevTime) { + prevTime = time; + g.setColor(0); + g.fillRect(0, Y_TIME, 239, Y_ACTIVITY -1); + g.setColor(1,1,1); + g.setFont("Vector",80); + g.setFontAlign(0,-1); + g.drawString(time, 120, Y_TIME); + + g.setFont("Vector",26); + g.drawString("Hello World", 120, Y_ACTIVITY); + } + } + + return {init:init, freeResources:freeResources, startTimer:startTimer, stopTimer:stopTimer, + onButtonShort:onButtonShort, onButtonLong:onButtonLong}; + } + + return getFace; +})(); diff --git a/apps/kitchen/compass.kit.js b/apps/kitchen/compass.kit.js new file mode 100644 index 000000000..530ba021c --- /dev/null +++ b/apps/kitchen/compass.kit.js @@ -0,0 +1,295 @@ +(() => { + function getFace(){ + var intervalRefSec; + var bearing; + var heading; + var oldHeading; + var CALIBDATA; + var previous; + var wp; + var wp_distance; + var wp_bearing; + var loc; + var gpsObject; + + function log_debug(o) { + //console.log(o); + } + + 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}; + loc = require("locale"); + CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; + getWaypoint(); + + /* + * compass should be powered on before startDraw is called + * otherwise compass power widget will not come on + */ + if (!Bangle.isCompassOn()) Bangle.setCompassPower(1); + gps.determineGPSState(); + + showMem("compass init() END"); + } + + function freeResources() { + showMem("compass freeResources() START"); + gpsObject = undefined; + intervalRefSec = undefined; + previous = undefined; + bearing = 0; + heading = 0; + oldHeading = 0; + loc = undefined; + CALIBDATA = undefined; + wp = undefined; + if (Bangle.isCompassOn !== undefined && Bangle.isCompassOn()) Bangle.setCompassPower(0); + showMem("compass freeResources() END"); + } + + function startTimer() { + log_debug("startTimer()"); + if (Bangle.isCompassOn !== undefined && !Bangle.isCompassOn()) Bangle.setCompassPower(1); + resetPrevious(); + draw(); + intervalRefSec = setInterval(draw, 500); + } + + function stopTimer() { + log_debug("stopTimer()"); + if (intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} + 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; + switch(btn) { + case 1: + log_debug("prev waypoint"); + gpsObject.nextWaypoint(-1); + break; + case 2: + log_debug("next waypoint"); + gpsObject.nextWaypoint(1); + break; + case 3: + default: + break; + } + resetPrevious(); + getWaypoint(); + drawGPSData(); + } + + function onButtonLong(btn) { + log_debug("markWaypoint()"); + if (btn !== 1) return; + if (gpsObject.getState() !== gpsObject.GPS_RUNNING) return; + log_debug("markWaypoint()"); + + gpsObject.markWaypoint(); + resetPrevious(); + getWaypoint(); + drawGPSData(); + } + + function getWaypoint() { + log_debug("getWaypoint()"); + wp = gpsObject.getCurrentWaypoint(); + wp_distance = gpsObject.getWPdistance(); + wp_bearing = gpsObject.getWPbearing(); + + if (gpsObject.getState() === gpsObject.GPS_RUNNING) + bearing = wp_bearing; + else + bearing = 0; + + log_debug(wp); + log_debug("wp_distance:" + wp_distance); + log_debug("wp_bearing:" + wp_bearing); + } + + // takes 16-20ms, will be called twice + function drawCompass(angle, col) { + angle = angle * Math.PI/180; + var p = [0, 1.1071, Math.PI/4, 2.8198, 3.4633, 7*Math.PI/4 , 5.1760]; + + var poly = [ + 120+60*Math.sin(angle+p[0]), 120-60*Math.cos(angle+p[0]), + 120+44.7214*Math.sin(angle+p[1]), 120-44.7214*Math.cos(angle+p[1]), + 120+28.2843*Math.sin(angle+p[2]), 120-28.2843*Math.cos(angle+p[2]), + 120+63.2455*Math.sin(angle+p[3]), 120-63.2455*Math.cos(angle+p[3]), + 120+63.2455*Math.sin(angle+p[4]), 120-63.2455*Math.cos(angle+p[4]), + 120+28.2843*Math.sin(angle+p[5]), 120-28.2843*Math.cos(angle+p[5]), + 120+44.7214*Math.sin(angle+p[6]), 120-44.7214*Math.cos(angle+p[6]) + ]; + + g.setColor(col); + g.fillPoly(poly); + } + + // stops violent compass swings and wobbles, takes 3ms + function newHeading(m,h){ + //log_debug("newHeading()"); + 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; + if (s<3) 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){ + //log_debug("tiltfixread()"); + 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 draw() { + log_debug("draw()"); + + g.setFontAlign(0,0); + g.setColor(1,1,1); + g.setFont("Vector", 24); + + if (Bangle.isCompassOn === undefined) { + g.drawString("E-FW", 120, 120); + return + } + + if (CALIBDATA === undefined || CALIBDATA === null) { + g.drawString("E-CALIB", 120, 120); + return + } + + var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale); + heading = newHeading(d,heading); + // sets bearing to waypoint bearing if GPS on else sets to 0 (north) + getWaypoint(); + + // make the compass point in the direction we need to go + var dir = bearing - heading; + if (dir < 0) dir += 360; + if (dir > 360) dir -= 360; + + if (dir !== oldHeading) { + drawCompass(oldHeading, 0); + drawCompass(dir, 0xFFC0); // yellow + oldHeading = dir; + } + + if (gpsObject.getState() === gpsObject.GPS_RUNNING) { + drawGPSData(); + } else { + drawCompassHeading(); + } + } + + // only used when acting as compass with GPS off + function drawCompassHeading() { + //log_debug("drawCompassHeading()"); + var hding = Math.round(heading); + var hd = hding.toString(); + hd = hding < 10 ? "00"+hd : hding < 100 ? "0"+hd : hd; + + if (previous.hding !== hd) { + previous.hding = hd; + g.setColor(1,1,1); + g.setFontAlign(-1,-1); + g.setFont("Vector",38); + g.clearRect(80, 200, 159, 239); + g.drawString(hd, 80, 200); + } + } + + function drawGPSData() { + log_debug("drawGPSData()"); + var bs = wp_bearing.toString(); + bs = wp_bearing<10?"00"+bs : wp_bearing<100 ?"0"+bs : bs; + var dst = loc.distance(wp_distance); + + log_debug(bs); + log_debug(dst); + + // -1=left (default), 0=center, 1=right + + // show distance on the left + if (previous.dst !== dst) { + previous.dst = dst + g.setFontAlign(-1,-1); // left, bottom + g.setFont("Vector", 20); + g.clearRect(0, 200, 79, 239); + + if (gpsObject.waypointHasLocation()) + g.setColor(0x07ff); + else + g.setColor(1,1,1); + g.drawString(dst, 0, 200); + } + + // bearing, place in middle at bottom of compass + if (previous.bs !== bs) { + previous.bs = bs; + g.setColor(1,1,1); + g.setFontAlign(0,-1); // middle, bottom + g.setFont("Vector",38); + g.clearRect(80, 200, 159, 239); + g.drawString(bs, 119, 200); + } + + // waypoint name on right + if (previous.wp_name !== wp.name) { + g.setFontAlign(1,-1); // right, bottom + g.setFont("Vector", 20); + g.clearRect(160, 200, 239, 239); + + if (gpsObject.waypointHasLocation()) + g.setColor(0x07ff); + else + g.setColor(1,1,1); + g.drawString(wp.name, 239, 200); + } + } + + // clear the attributes that control the display refresh + function resetPrevious() { + log_debug("resetPrevious()"); + previous = {hding:"-", bs:"-", dst:"-", wp_name:"-", course:999}; + } + + return {init:init, freeResources:freeResources, startTimer:startTimer, stopTimer:stopTimer, + onButtonShort:onButtonShort, onButtonLong:onButtonLong}; + } + + return getFace; + +})(); diff --git a/apps/kitchen/digi.kit.js b/apps/kitchen/digi.kit.js new file mode 100644 index 000000000..91ae70905 --- /dev/null +++ b/apps/kitchen/digi.kit.js @@ -0,0 +1,154 @@ +(() => { + function getFace(){ + var intervalRefSec; + var buf; + var days; + var prevInfo; + var prevDate; + var prevTime; + var infoMode; + + const INFO_NONE = 0; + const INFO_BATT = 1; + const INFO_MEM = 2; + const INFO_FW = 3; + const Y_TIME = 30; + const Y_ACTIVITY = 116; + const Y_MODELINE = 200; + + function init(gps,sw,hrm) { + showMem("digi init 1"); + days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday","Friday", "Saturday"]; + prevInfo = ""; + prevTimeStr = ""; + prevDateStr = ""; + infoMode = INFO_NONE; + g.clear(); + showMem("digi init 2"); + } + + function freeResources() { + showMem("digi free 1"); + days = undefined; + prevInfo = undefined; + prevTime = undefined; + prevDate = undefined; + showMem("digi free 2"); + } + + function showMem(msg) { + var val = process.memory(); + var str = msg + " " + Math.round(val.usage*100/val.total) + "%"; + //console.log(str); + } + + function startTimer() { + draw(); + intervalRefSec = setInterval(draw, 5000); + } + + function stopTimer() { + if(intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} + } + + function onButtonShort(btn) { + if (btn === 1) cycleInfoMode(); + } + + function onButtonLong(btn) {} + function getGPSfix() { return undefined; } + function setGPSfix(f) {} + + function draw() { + var d = new Date(); + var da = d.toString().split(" "); + var time = da[4].substr(0,5); + + if (time !== prevTime) { + prevTime = time; + g.setColor(0); + g.fillRect(0, Y_TIME, 239, Y_ACTIVITY -1); + g.setColor(1,1,1); + g.setFont("Vector",80); + g.setFontAlign(0,-1); + g.drawString(time, 120, Y_TIME); + } + + var day = days[d.getDay()]; + var dateStr = da[2] + " " + da[1] + " " + da[3]; + + if (dateStr !== prevDate) { + prevDate = dateStr; + g.setColor(0); + g.fillRect(0, Y_ACTIVITY, 239, Y_MODELINE - 3); + g.setColor(1,1,1); + g.setFont("Vector",26); + g.drawString(day, 120, Y_ACTIVITY); + g.drawString(dateStr, 120, Y_ACTIVITY + 40); + } + + drawInfo(); + } + + function cycleInfoMode() { + switch(infoMode) { + case INFO_NONE: + infoMode = INFO_BATT; + break; + case INFO_BATT: + infoMode = INFO_MEM + break; + case INFO_MEM: + infoMode = INFO_FW + break; + case INFO_FW: + default: + infoMode = INFO_NONE; + break; + } + drawInfo(); + } + + function drawInfo() { + let val; + let str = ""; + let col = 0x07FF; // cyan + + switch(infoMode) { + case INFO_MEM: + val = process.memory(); + str = "Memory: " + Math.round(val.usage*100/val.total) + "%"; + break; + case INFO_BATT: + str = "Battery: " + E.getBattery() + "%"; + break; + case INFO_FW: + str = "Fw: " + process.env.VERSION; + break; + case INFO_NONE: + default: + col = 0x0000; + str = ""; + break; + } + + // check if we need to draw, avoid flicker + if (str == prevInfo) + return; + + prevInfo = str; + //g.setFont("6x8", 3); + g.setFont("Vector",26); + g.setColor(col); + g.fillRect(0, Y_MODELINE - 3, 239, Y_MODELINE + 25); + g.setColor(0,0,0); + g.setFontAlign(0, -1); + g.drawString(str, 120, Y_MODELINE); + } + + return {init:init, freeResources:freeResources, startTimer:startTimer, stopTimer:stopTimer, + onButtonShort:onButtonShort, onButtonLong:onButtonLong}; + } + + return getFace; +})(); diff --git a/apps/kitchen/gps.kit.js b/apps/kitchen/gps.kit.js new file mode 100644 index 000000000..d6c936226 --- /dev/null +++ b/apps/kitchen/gps.kit.js @@ -0,0 +1,188 @@ +(() => { + function getFace(){ + var intervalRefSec; + + const GDISP_OS = 4; + const GDISP_LATLN = 5; + const GDISP_SPEED = 6; + const GDISP_ALT = 7; + const GDISP_COURSE = 8; + + const Y_TIME = 30; + const Y_ACTIVITY = 120; + const Y_MODELINE = 200; + + let gpsDisplay = GDISP_OS; + let clearActivityArea = true; + let gpsObject = undefined; + + function log_debug(o) { + //console.log(o); + } + + function init(gps, sw, hrm) { + log_debug("gps init"); + //log_debug(gps); + gpsObject = gps; + gpsDisplay = GDISP_OS; + clearActivityArea = true; + gpsObject.determineGPSState(); + } + + function freeResources() {} + + function startTimer() { + draw(); + intervalRefSec = setInterval(draw, 5000); + } + + function stopTimer() { + if(intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} + } + + function onButtonShort(btn) { + if (btn === 1) cycleGPSDisplay(); + } + + function onButtonLong(btn) { + if (btn === 1) toggleGPSPower(); + } + + function draw(){ + drawGPSTime(); + drawGPSData(); + } + + function drawGPSTime() { + var time = gpsObject.getGPSTime(); + + g.reset(); + g.clearRect(0,Y_TIME, 239, Y_ACTIVITY - 1); + g.setColor(1,1,1); + g.setFontAlign(0, -1); + + if (time.length > 5) + g.setFont("Vector", 56); + else + g.setFont("Vector", 80); + + g.drawString(time, 120, Y_TIME); + } + + function drawGPSData() { + if (clearActivityArea) { + g.clearRect(0, Y_ACTIVITY, 239, Y_MODELINE - 1); + clearActivityArea = false; + } + + g.setFontVector(26); + g.setColor(0xFFC0); + g.setFontAlign(0, -1); + + if (Bangle.isGPSOn === undefined) { + g.setColor(1,1,1); + g.drawString("E-FW", 120, Y_ACTIVITY); + return; + } + + if (gpsObject.getState() === gpsObject.GPS_OFF) { + g.drawString("GPS off", 120, Y_ACTIVITY); + return; + } + + if (gpsObject.getState() === gpsObject.GPS_TIME) { + g.drawString("Waiting for", 120, Y_ACTIVITY); + g.drawString("GPS", 120, Y_ACTIVITY + 36); + return; + } + + let fx = gpsObject.getLastFix(); + + log_debug("gpsObject.getState()= " + gpsObject.getState()); + + if (gpsObject.getState() === gpsObject.GPS_SATS) { + g.drawString("Satellites", 120, Y_ACTIVITY); + g.drawString(fx.satellites, 120, Y_ACTIVITY + 36); + return; + } + + if (gpsObject.getState() === gpsObject.GPS_RUNNING) { + let time = gpsObject.formatTime(fx.time); + let age = gpsObject.timeSince(time); + let os = gpsObject.getOsRef(); + //let ref = to_map_ref(6, os.easting, os.northing); + let speed; + let activityStr = ""; + + if (age < 0) age = 0; + g.setFontVector(40); + g.setColor(0xFFC0); + + switch(gpsDisplay) { + case GDISP_OS: + activityStr = os; + break; + case GDISP_LATLN: + g.setFontVector(26); + activityStr = fx.lat.toFixed(4) + ", " + fx.lon.toFixed(4); + break; + case GDISP_SPEED: + speed = fx.speed; + speed = speed.toFixed(1); + activityStr = speed + "kph"; + break; + case GDISP_ALT: + activityStr = fx.alt + "m"; + break; + case GDISP_COURSE: + activityStr = fx.course; + break; + } + + 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); + } + } + + function toggleGPSPower() { + gpsObject.toggleGPSPower(); + clearActivityArea = true; + draw(); + } + + function cycleGPSDisplay() { + if (gpsObject.getState() !== gpsObject.GPS_RUNNING) return; + + switch (gpsDisplay) { + case GDISP_OS: + gpsDisplay = GDISP_SPEED; + break; + case GDISP_SPEED: + gpsDisplay = GDISP_ALT; + break; + case GDISP_ALT: + gpsDisplay = GDISP_LATLN; + break; + case GDISP_LATLN: + gpsDisplay = GDISP_COURSE; + break; + case GDISP_COURSE: + default: + gpsDisplay = GDISP_OS; + break; + } + + clearActivityArea = true; + drawGPSData(); + } + + return {init:init, freeResources:freeResources, startTimer:startTimer, stopTimer:stopTimer, + onButtonShort:onButtonShort, onButtonLong:onButtonLong}; + } + + return getFace; + +})(); diff --git a/apps/kitchen/heart.kit.js b/apps/kitchen/heart.kit.js new file mode 100644 index 000000000..aedbadf9f --- /dev/null +++ b/apps/kitchen/heart.kit.js @@ -0,0 +1,116 @@ +(() => { + function getFace(){ + const Y_TIME = 30; + const Y_ACTIVITY = 116; + let prevTime; + let prevBpm; + let toggle = 1; + let redrawHrmPower = true; + let intervalRefSec; + let img; + let hrmObject; + + function log_debug(o) { + //console.log(o); + } + + function init(gps, sw, hrm) { + img = require("heatshrink").decompress(atob("mEwwRC/ABf/+ADBh//BQgGB//AgYDBCAQWCA4QPCDAYSC//8n4EC4AiEAAo1EBZIeDAAn8BZoKHJAYL7L64LLTa6/DAAi/CKhDjGBZBIGIwQ8IHQQ8IHQYwHBQgwFFwgwGFwgwGFwowFBQwwDFwwwEFwwwEFw4wDBRAkBERAkCERIA/AAYA=")); + prevTime = "-"; + prevBpm = "-"; + toggle = 1; + redrawHrmPower = true; + hrmObject = hrm; + intervalRefSec; + g.clear(); + } + + function freeResources() { + prevTime = undefined; + img = undefined; + } + + function startTimer() { + draw(); + intervalRefSec = setInterval(draw, 1000); + } + + function stopTimer() { + if (intervalRefSec) { intervalRefSec = clearInterval(intervalRefSec); } + } + + function onButtonShort(btn) {} + + function onButtonLong(btn) { + log_debug("toggleHRM"); + if (btn !== 1) return; + if (!Bangle.isHRMOn) return; // old firmware + hrmObject.toggleHRMPower(); + prevBpm = '-'; + toggle = 1; // ensure we draw the heart first + redrawHrmPower = true; + } + + function draw() { + let d = new Date(); + let da = d.toString().split(" "); + let time = da[4].substr(0,5); + + if (time !== prevTime) { + prevTime = time; + g.setColor(0); + g.fillRect(0, Y_TIME, 239, Y_ACTIVITY -1); + g.setColor(1,1,1); + g.setFont("Vector",80); + g.setFontAlign(0,-1); + g.drawString(time, 120, Y_TIME); + } + + let bpm = hrmObject.getBpm(); + + if (!Bangle.isHRMOn()) { + if (!redrawHrmPower) return; + redrawHrmPower = false; + g.setColor(0); + g.drawImage(img, 12, 132, {scale:2}); + g.fillRect(120,120,239,239); + g.setColor(255,0,0); + //g.setColor(0xFFC0); // yellow + g.drawImage(img, 12, 132, {scale:2}); + + g.setFont("Vector",40); + g.setFontAlign(0,0); + g.setColor(1,1,1); + g.drawString("OFF", 180, 180); + return; + } + + // draw the heart + if (++toggle % 2 === 0) { + g.setColor(0); + g.fillRect(12, 132, 108, 228); + } else { + g.setColor(255,0,0); + //g.setColor(0xFFC0); // yellow + g.drawImage(img, 12, 132, {scale:2}); + } + + // draw the bpm + if (bpm !== prevBpm) { + prevBpm = bpm; + g.setColor(0); + g.fillRect(120, 120, 239, 239); + g.setColor(1,1,1); + //g.setColor(0xFFC0); // yellow + g.setFont("Vector",52); + g.setFontAlign(0,0); + g.drawString(bpm, 180, 180); + } + } + + return {init:init, freeResources:freeResources, startTimer:startTimer, stopTimer:stopTimer, + onButtonShort:onButtonShort, onButtonLong:onButtonLong}; + } + + return getFace; +})(); diff --git a/apps/kitchen/kitchen.app.js b/apps/kitchen/kitchen.app.js new file mode 100644 index 000000000..c3f7bd74d --- /dev/null +++ b/apps/kitchen/kitchen.app.js @@ -0,0 +1,789 @@ +// read in the faces +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 face = FACES[iface](); +var firstPress +var pressTimer; + +function stopdraw() { + face.stopTimer(); +} + +function startdraw() { + Bangle.drawWidgets(); + face.startTimer(); +} + +function nextFace(){ + stopdraw(); + face.freeResources(); + + iface += 1 + iface = iface % FACES.length; + face = FACES[iface](); + + g.clear(); + g.reset(); + face.init(gpsObj, swObj, hrmObj, tripObject); + startdraw(); +} + +// when you feel the buzzer you know you have done a long press +function longPressCheck() { + Bangle.buzz(); + debug_log("long PressCheck() buzz"); + if (pressTimer) { + clearInterval(pressTimer); + debug_log("clear pressTimer 2"); + pressTimer = undefined; + } +} + +// start a timer and buzz when held long enough +function buttonPressed(btn) { + if (btn === 3) { + nextFace(); + } else { + firstPress = getTime(); + if (pressTimer) { + debug_log("clear pressTimer 1"); + clearInterval(pressTimer); + } + debug_log("set pressTimer 1"); + pressTimer = setInterval(longPressCheck, 1500); + } +} + +// if you release too soon there is no buzz as timer is cleared +function buttonReleased(btn) { + var dur = getTime() - firstPress; + if (pressTimer) { + debug_log("clear pressTimer 3"); + clearInterval(pressTimer); + pressTimer = undefined; + } + + if ( dur >= 1.5 ) { + switch(btn) { + case 1: + face.onButtonLong(btn); + break; + case 2: + Bangle.showLauncher(); + break; + case 3: + // do nothing + break; + } + return; + } + + if (btn !== 3) face.onButtonShort(btn); +} + +function setButtons(){ + setWatch(buttonPressed.bind(null,1), BTN1, {repeat:true,edge:"rising"}); + setWatch(buttonPressed.bind(null,2), BTN2, {repeat:true,edge:"rising"}); + setWatch(nextFace, BTN3, {repeat:true,edge:"rising"}); + + setWatch(buttonReleased.bind(null,1), BTN1, {repeat:true,edge:"falling"}); + setWatch(buttonReleased.bind(null,2), BTN2, {repeat:true,edge:"falling"}); + // BTN 3 long press should always reset the bangle +} + +Bangle.on('kill',()=>{ + Bangle.setCompassPower(0); + Bangle.setGPSPower(0); +}); + +Bangle.on('lcdPower',function(on) { + if (on) { + startdraw(); + } else { + stopdraw(); + } +}); + +/***************************************************************************** + +Start of GPS object code so we can share it between faces + +******************************************************************************/ + + +function log_debug(o) { + //console.log(o); +} + +function radians(a) { + return a*Math.PI/180; +} + +function degrees(a) { + var d = a*180/Math.PI; + return (d+360)%360; +} + +function GPS() { + this.wp; + this.wp_index = 0; + this.wp_current = undefined; + this.GPS_OFF = 0; + this.GPS_TIME = 1; + this.GPS_SATS = 2; + this.GPS_RUNNING = 3; + this.gpsState = this.GPS_OFF; + this.gpsPowerState = false; + this.last_fix = { + fix: 0, + alt: 0, + lat: 0, + lon: 0, + speed: 0, + time: 0, + satellites: 0 + }; + this.listenerCount = 0; + this.loadFirstWaypoint(); +} + +GPS.prototype.log_debug = function(o) { + //console.log(o); +}; + +GPS.prototype.getState = function() { + return this.gpsState; +} + +GPS.prototype.getLastFix = function() { + return this.last_fix; +} + +GPS.prototype.determineGPSState = function() { + this.log_debug("determineGPSState"); + gpsPowerState = Bangle.isGPSOn(); + + //this.log_debug("last_fix.fix " + this.last_fix.fix); + //this.log_debug("gpsPowerState " + this.gpsPowerState); + //this.log_debug("last_fix.satellites " + this.last_fix.satellites); + + if (!gpsPowerState) { + this.gpsState = this.GPS_OFF; + this.resetLastFix(); + } else if (this.last_fix.fix && this.gpsPowerState && this.last_fix.satellites > 0) { + this.gpsState = this.GPS_RUNNING; + } else { + this.gpsState = this.GPS_SATS; + } + + this.log_debug("gpsState=" + this.gpsState); + + if (this.gpsState !== this.GPS_OFF) { + if (this.listenerCount === 0) { + Bangle.on('GPS', processFix); + this.listenerCount++; + this.log_debug("listener added " + this.listenerCount); + } + } else { + if (this.listenerCount > 0) { + Bangle.removeListener("GPS", processFix); + this.listenerCount--; + this.log_debug("listener removed " + this.listenerCount); + } + } +}; + +GPS.prototype.getGPSTime = function() { + var time; + + if (this.last_fix !== undefined && this.last_fix.time !== undefined && this.last_fix.time.toUTCString !== undefined && + (this.gpsState == this.GPS_SATS || this.gpsState == this.GPS_RUNNING)) { + time = this.last_fix.time.toUTCString().split(" "); + return time[4]; + } else { + var d = new Date(); + var da = d.toString().split(" "); + time = da[4].substr(0,5); + return time; + } +}; + +GPS.prototype.toggleGPSPower = function() { + this.log_debug("toggleGPSPower()"); + this.gpsPowerState = Bangle.isGPSOn(); + this.gpsPowerState = !this.gpsPowerState; + Bangle.setGPSPower(this.gpsPowerState ? 1 : 0); + + this.resetLastFix(); + this.determineGPSState(); + + // poke the gps widget indicator to change + if (WIDGETS.gps !== undefined) { + WIDGETS.gps.draw(); + } +}; + +GPS.prototype.resetLastFix = function() { + this.last_fix = { + fix: 0, + alt: 0, + lat: 0, + lon: 0, + speed: 0, + time: 0, + satellites: 0 + }; +}; + +function processFix(fix) { + //log_debug("processFix()"); + gpsObj.processFix(fix); +} + +GPS.prototype.processFix = function(fix) { + //this.log_debug("GPS:processFix()"); + //this.log_debug(fix); + this.last_fix.time = fix.time; + + if (this.gpsState == this.GPS_TIME) { + this.gpsState = this.GPS_SATS; + } + + if (fix.fix) { + //this.log_debug("Got fix - setting state to GPS_RUNNING"); + 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"); + } + this.last_fix = fix; + } +}; + +GPS.prototype.formatTime = function(now) { + var fd = now.toUTCString().split(" "); + return fd[4]; +}; + +GPS.prototype.timeSince = function(t) { + var hms = t.split(":"); + var now = new Date(); + + var sn = 3600*(now.getHours()) + 60*(now.getMinutes()) + 1*(now.getSeconds()); + var st = 3600*(hms[0]) + 60*(hms[1]) + 1*(hms[2]); + + return (sn - st); +}; + +GPS.prototype.getOsRef = function() { + let os = OsGridRef.latLongToOsGrid(this.last_fix); + let ref = to_map_ref(6, os.easting, os.northing); + return ref; +}; + +GPS.prototype.calcBearing = function(a,b) { + var delta = radians(b.lon-a.lon); + var alat = radians(a.lat); + var blat = radians(b.lat); + var y = Math.sin(delta) * Math.cos(blat); + var x = Math.cos(alat)*Math.sin(blat) - + Math.sin(alat)*Math.cos(blat)*Math.cos(delta); + return Math.round(degrees(Math.atan2(y, x))); +} + +GPS.prototype.calcDistance = function(a,b) { + var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); + var y = radians(b.lat-a.lat); + return Math.round(Math.sqrt(x*x + y*y) * 6371000); +} + +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) + return 0; + else + return this.calcDistance(this.last_fix, this.wp_current); +} + +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) + 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"}]; + this.wp_index = 0; + this.wp_current = waypoints[this.wp_index]; + log_debug(this.wp_current); + return this.wp_current; +} + +GPS.prototype.getCurrentWaypoint = function() { + return this.wp_current; +} + +GPS.prototype.waypointHasLocation = function() { + if (this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) + return false; + else + return true; +} + +GPS.prototype.markWaypoint = function() { + + if(this.wp_current.name === "NONE") + return; + + log_debug("GPS::markWaypoint()"); + + var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; + this.wp_current = waypoints[this.wp_index]; + + if (this.waypointHasLocation()) { + waypoints[this.wp_index] = {name:this.wp_current.name, lat:0, lon:0}; + } else { + waypoints[this.wp_index] = {name:this.wp_current.name, lat:this.last_fix.lat, lon:this.last_fix.lon}; + } + + this.wp_current = waypoints[this.wp_index]; + require("Storage").writeJSON("waypoints.json", waypoints); + log_debug("GPS::markWaypoint() written"); +} + +GPS.prototype.nextWaypoint = function(inc) { + var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; + 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; + this.wp_current = waypoints[this.wp_index]; + log_debug(this.wp_current); + return this.wp_current; +} + +var gpsObj = new GPS(); + + +/***************************************************************************** + +Start of OS lat lon to grid ref code + +******************************************************************************/ + +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}`; +} + + +/***************************************************************************** + +Stopwatch Class Code + +******************************************************************************/ + + +function STOPWATCH() { + this.tTotal = Date.now(); + this.tStart = Date.now(); + this.tCurrent = Date.now(); + this.running = false; + this.timeY = 45; + this.TtimeY = 75; + this.lapTimes = []; + this.displayInterval; + this.redrawButtons = true; + this.redrawLaps = true; + this.redrawTime = true; +} + +STOPWATCH.prototype.log_debug = function(o) { + //console.log(o); +} + +STOPWATCH.prototype.timeToText = function(t) { + let hrs = Math.floor(t/3600000); + let mins = Math.floor(t/60000)%60; + let secs = Math.floor(t/1000)%60; + let text; + + if (hrs === 0) + text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2); + else + text = (""+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2); + + this.log_debug(text); + return text; +} + +STOPWATCH.prototype.getLapTimesArray = function() { + this.lapTimes.push(tCurrent-tTotal); + return this.lapTimes.map(timeToText).reverse(); +} + +STOPWATCH.prototype.stopStart = function() { + this.log_debug("stopStart()"); + this.running = !this.running; + + if (this.running) + this.tStart = Date.now() + this.tStart - this.tCurrent; + + this.tTotal = Date.now() + this.tTotal - this.tCurrent; + this.tCurrent = Date.now(); + this.redrawButtons = true; + this.redrawLaps = true; + this.redrawTime = true; + this.draw(); +} + +STOPWATCH.prototype.lap = function() { + this.log_debug("lap()"); + if (this.running) { + this.tCurrent = Date.now(); + this.lapTimes.unshift(this.tCurrent - this.tStart); + log_debug(this.tCurrent - this.tStart); + } + + this.tStart = this.tCurrent; + this.redrawButtons = true; + this.redrawLaps = true; + this.draw(); +} + +STOPWATCH.prototype.reset = function() { + this.log_debug("reset()"); + if (this.running === false) { + this.tStart = this.tCurrent = this.tTotal = Date.now(); + this.lapTimes = []; + } + g.clear(); + this.draw(); +} + +// lap or reset +STOPWATCH.prototype.lapOrReset = function() { + this.redrawButtons = true; + this.redrawLaptimes = true; + this.redrawTime = true; + + this.log_debug("lapReset()"); + if (this.running) + this.lap() + else + this.reset(); +} + +STOPWATCH.prototype.draw = function() { + if (this.running) this.tCurrent = Date.now(); + this.log_debug("draw()" + getTime()); + + g.setColor(1,1,1); + if (this.redrawButtons) this.drawButtons(); + if (this.running || this.redrawTime) this.drawTime(); + if (this.redrawLaps) this.drawLaptimes(); +} + +STOPWATCH.prototype.drawButtons = function() { + this.log_debug("drawButtons()"); + + g.clearRect(0,23,g.getWidth()-1,g.getHeight()-24); + g.setColor(1,1,1); + g.setFont("Vector", 20); + g.setFontAlign(0,0,3); + g.drawString(this.running? "STOP" : "GO", 230, 50); // BTN1 + g.drawString(this.running? "LAP" : "RESET", 230, 120); // BTN2 + this.redrawButtons = false; +} + +STOPWATCH.prototype.drawLaptimes = function() { + g.setFont("Vector",24); + g.setFontAlign(-1,-1); + g.clearRect(4, 205, 239, 229); // clear the last line of the lap times + + let laps = 0; + for (let i in this.lapTimes) { + g.drawString(this.lapTimes.length-i + ": " + this.timeToText(this.lapTimes[i]), 4, this.timeY + 40 + i*24); + if (++laps > 5) break; + } + this.redrawLaps = false; +} + +STOPWATCH.prototype.drawTime = function() { + this.log_debug("drawTime()"); + let tLap = this.tCurrent - this.tStart; + let tTotal = this.tCurrent - this.tTotal; + let txtLap = this.timeToText(tLap); + let txtTotal = this.timeToText(tTotal); + let xTotal = 100; + let xLap = 125; + + // total time + g.setFont("Vector",38); + g.setFontAlign(0,0); + g.clearRect(0, this.timeY-21, 200, this.timeY+21); + g.setColor(0xFFC0); + g.drawString(txtTotal, xTotal, this.timeY); + + // current lap time + g.setFont("Vector", 20); + g.clearRect(0, this.TtimeY-7, 200, this.TtimeY+7); + g.setColor(1,1,1); + g.drawString(txtLap, xLap, this.TtimeY); + + this.redrawTime = false; +} + +STOPWATCH.prototype.startTimer = function() { + this.log_debug("startTimer()"); + this.redrawButtons = true; + this.redrawLaps = true; + this.redrawTime = true; + this.draw(); + this.displayInterval = setInterval(stopwatchDraw, 1000); +} + +STOPWATCH.prototype.stopTimer = function() { + this.log_debug("stopTimer()"); + if (this.displayInterval) { + clearInterval(this.displayInterval); + this.displayInterval = undefined; + } +} + +let swObj = new STOPWATCH(); + +function stopwatchDraw() { + swObj.draw(); +} + + +/***************************************************************************** + +Heart Rate Monitor + +******************************************************************************/ + +function HRM() { + this.bpm = 0; + this.confidence = 0; +} + +HRM.prototype.log_debug = function(o) { + //console.log(o); +} + +HRM.prototype.toggleHRMPower = function() { + this.log_debug("HRM.toggleHRMPower()"); + if (!Bangle.isHRMOn) return; // old firmware + + if (!Bangle.isHRMOn()) { + this.log_debug("HRM.toggleHRMPower(powerOn)"); + Bangle.removeListener('HRM', onHRM); + Bangle.setHRMPower(1); + Bangle.on('HRM', onHRM); + } else { + this.log_debug("HRM.toggleHRMPower(powerOff)"); + Bangle.removeListener('HRM', onHRM); + Bangle.setHRMPower(0); + } + + // poke the hrt widget indicator to change + if (WIDGETS.widhrt !== undefined) { + WIDGETS.widhrt.draw(); + } +} + +HRM.prototype.getBpm = function() { + return this.bpm; +} + +HRM.prototype.getConfidence = function() { + return this.confidence; +} + +HRM.prototype.onHRM = function(hrm) { + this.bpm = hrm.bpm; + this.confidence = hrm.confidence; + this.log_debug("onHRM:(bpm)" + this.bpm); + this.log_debug("onHRM:(conf) " + this.confidence); +} + +let hrmObj = new HRM(); + +function onHRM(hrm) { + hrmObj.onHRM(hrm); +} + + +/***************************************************************************** + +Trip Counter + +******************************************************************************/ + +function TRIP() { + this.showTrip = false; + this.tripStart = 0; +} + +TRIP.prototype.resetTrip = function(steps) { + this.tripStart = (0 + steps); + console.log("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); + return tripSteps; +} + +TRIP.prototype.getTripState = function() { + return this.showTrip; +} + +TRIP.prototype.setTripState = function(t) { + this.showTrip = t; +} + +let tripObject = new TRIP(); + +/***************************************************************************** + +Debug Object + +******************************************************************************/ + +/* +function DEBUG() { + this.logfile = require("Storage").open("debug.log","a"); +} + +DEBUG.prototype.log = function(msg) { + let timestamp = new Date().toString().split(" ")[4]; + let line = timestamp + ", " + msg + "\n"; + this.logfile.write(line); +} + +debugObj = new DEBUG(); +*/ + +function debug_log(m) { + //debugObj.log(m); +} + +/***************************************************************************** + +Start App + +******************************************************************************/ + +g.clear(); +Bangle.loadWidgets(); +face.init(gpsObj,swObj, hrmObj, tripObject); +startdraw(); +setButtons(); diff --git a/apps/kitchen/kitchen.icon.js b/apps/kitchen/kitchen.icon.js new file mode 100644 index 000000000..32b01e4bd --- /dev/null +++ b/apps/kitchen/kitchen.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4AVgAACF1crnNkF9QuBLoMHF9cyGIYuprovBX1cAsgABrovYJSBeBF4SPBFy8HmQbOLwdkFzAtBmYcNCIIuCYAIvXLgJ/BF5pecAAy9OdqgmCg8rmUznMyAAJkCEI5eEdqQgBU4YAJrsHEYheErwuSFpoxEEoa9WIwgwTgFeAwQvQLqQwFaYa7TLyYuBmQYBdKQuCIoUrlcyABcrKwJcWXgZJBEIQiBAAQlEEwI+BmU5CgINDF6ZgFGQI3BAAkzLw8OLygvBg8VhwaBLY5eEg8OisVjgABhwvVhwaCACgv/F78VF/4v/F8sHd9owBF68HFyhgYRyowDYQMVFqMHFy4xEAAMHg41BAAIrFiq6BAAIuZGhIAIFj4A/AH4A/AH4AWA==")) diff --git a/apps/kitchen/kitchen.info.js b/apps/kitchen/kitchen.info.js new file mode 100644 index 000000000..d800a60ea --- /dev/null +++ b/apps/kitchen/kitchen.info.js @@ -0,0 +1,8 @@ +require("Storage").write("kitchen.info",{ + "id":"kitchen", + "name":"Kitchen", + "src":"kitchen.app.js", + "icon":"kitchen.img", + "type":"clock", + "files":"kitchen.info, kitchen.app.js, kitchen.img, stepo.kit.js, digi.kit.js, compass.kit.js, gps.kit.js" +}); diff --git a/apps/kitchen/kitchen.png b/apps/kitchen/kitchen.png new file mode 100644 index 000000000..83de52285 Binary files /dev/null and b/apps/kitchen/kitchen.png differ diff --git a/apps/kitchen/screenshot_digi.jpg b/apps/kitchen/screenshot_digi.jpg new file mode 100644 index 000000000..b900d9d04 Binary files /dev/null and b/apps/kitchen/screenshot_digi.jpg differ diff --git a/apps/kitchen/screenshot_gps.jpg b/apps/kitchen/screenshot_gps.jpg new file mode 100644 index 000000000..8d3f9796c Binary files /dev/null and b/apps/kitchen/screenshot_gps.jpg differ diff --git a/apps/kitchen/screenshot_heart.jpg b/apps/kitchen/screenshot_heart.jpg new file mode 100644 index 000000000..f0951a9b0 Binary files /dev/null and b/apps/kitchen/screenshot_heart.jpg differ diff --git a/apps/kitchen/screenshot_kitchen.jpg b/apps/kitchen/screenshot_kitchen.jpg new file mode 100644 index 000000000..d4abdadde Binary files /dev/null and b/apps/kitchen/screenshot_kitchen.jpg differ diff --git a/apps/kitchen/screenshot_stepo.jpg b/apps/kitchen/screenshot_stepo.jpg new file mode 100644 index 000000000..4e5c88a62 Binary files /dev/null and b/apps/kitchen/screenshot_stepo.jpg differ diff --git a/apps/kitchen/screenshot_stone.jpg b/apps/kitchen/screenshot_stone.jpg new file mode 100644 index 000000000..831e34dc9 Binary files /dev/null and b/apps/kitchen/screenshot_stone.jpg differ diff --git a/apps/kitchen/screenshot_swatch.jpg b/apps/kitchen/screenshot_swatch.jpg new file mode 100644 index 000000000..e8518d586 Binary files /dev/null and b/apps/kitchen/screenshot_swatch.jpg differ diff --git a/apps/kitchen/screenshot_wp2.jpg b/apps/kitchen/screenshot_wp2.jpg new file mode 100644 index 000000000..1c27af927 Binary files /dev/null and b/apps/kitchen/screenshot_wp2.jpg differ diff --git a/apps/kitchen/screenshot_wp2_cleared.jpg b/apps/kitchen/screenshot_wp2_cleared.jpg new file mode 100644 index 000000000..1ee523f87 Binary files /dev/null and b/apps/kitchen/screenshot_wp2_cleared.jpg differ diff --git a/apps/kitchen/screenshot_wp2_set.jpg b/apps/kitchen/screenshot_wp2_set.jpg new file mode 100644 index 000000000..48ef3d6ad Binary files /dev/null and b/apps/kitchen/screenshot_wp2_set.jpg differ diff --git a/apps/kitchen/stepo.kit.js b/apps/kitchen/stepo.kit.js new file mode 100644 index 000000000..9fa34e8ab --- /dev/null +++ b/apps/kitchen/stepo.kit.js @@ -0,0 +1,148 @@ +(() => { + function getFace(){ + var intervalRefSec; + var trip; + var prevSteps; + + function init(g,sw,hrm,tr) { + trip = tr; + } + + function freeResources() { + trip = undefined; + prevSteps = -1; + } + + function onButtonShort(btn) { + trip.setTripState(!trip.getTripState()); + drawStepText(); + } + + function onButtonLong(btn) { + trip.resetTrip(getSteps()); + trip.setTripState(true); + drawStepText(); + } + + function radians(a) { + return a*Math.PI/180; + } + + function startTimer() { + draw(); + intervalRefSec = setInterval(draw, 5000); + } + + function stopTimer() { + if(intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} + } + + function drawSteps() { + var i = 0; + var cx = 60 + 60; + var cy = 60 + 115; + var r = 56; + var steps = getSteps(); + + 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(0xAFE5); // greenyellow + + // draw guauge + for (i = startrot; i > midrot; i -= 4) { + x = cx + r * Math.sin(radians(i)); + y = cy + r * Math.cos(radians(i)); + g.fillCircle(x,y,4); + } + + // change the remaining color to RED if battery is below 25% + if (E.getBattery() > 25) + g.setColor(0x7BEF); // grey + else + g.setColor(0xF800); // red + + // draw remainder of guage in grey or red + for (i = midrot; i > endrot; i -= 4) { + x = cx + r * Math.sin(radians(i)); + y = cy + r * Math.cos(radians(i)); + g.fillCircle(x,y,4); + } + } + + function drawStepText() { + var cx = 60 + 60; + var cy = 60 + 115; + var r = 56; + var steps = getSteps(); + + /* + * 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 + */ + if (trip.getTrip(steps) < 0) + trip.resetTrip(steps); + + // show trip count or total steps today + g.setFontAlign(0,0); + g.setFont("Vector", 24); + + // clear the space for the text + g.clearRect(cx - (r - 12), cy - 16, cx + (r - 12), cy + 16); + + if (trip.getTripState() == true) { + g.setColor(0x7BEF); // grey + //g.setColor(1,0,0); // red + g.drawString(trip.getTrip(steps), cx, cy); + } else { + g.setColor(1,1,1); // white + g.drawString(steps, cx, cy); + } + } + + function draw() { + var d = new Date(); + var da = d.toString().split(" "); + var time = da[4].substr(0,5); + + g.clearRect(0, 30, 239, 99); + g.setColor(1,1,1); + g.setFontAlign(0, -1); + g.setFont("Vector", 80); + g.drawString(time, 120, 30, true); + + drawSteps(); + drawStepText(); + } + + function getSteps() { + if (stepsWidget() !== undefined) + return stepsWidget().getSteps(); + return "E-STEPS"; + } + + 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 new file mode 100644 index 000000000..b88518cbd --- /dev/null +++ b/apps/kitchen/swatch.kit.js @@ -0,0 +1,41 @@ +(() => { + function getFace(){ + let swObject = undefined; + + function init(gps, sw, hrm) { + swObject = sw; + g.clear(); + } + + function freeResources() {} + + function startTimer() { + swObject.startTimer(); + } + + function stopTimer() { + swObject.stopTimer(); + } + + function onButtonShort(btn) { + switch (btn) { + case 1: + swObject.stopStart(); + break; + case 2: + swObject.lapOrReset(); + break; + case 3: + default: + return; + } + } + + function onButtonLong(btn) {} + + return {init:init, freeResources:freeResources, startTimer:startTimer, stopTimer:stopTimer, + onButtonShort:onButtonShort, onButtonLong:onButtonLong}; + } + + return getFace; +})(); diff --git a/apps/kitchen/waypoints.html b/apps/kitchen/waypoints.html new file mode 100644 index 000000000..d02260732 --- /dev/null +++ b/apps/kitchen/waypoints.html @@ -0,0 +1,170 @@ + + + + + + + +

List of waypoints

+ + + + + + + + + + + + +
NameLat.Long.Actions
+
+

Add a new waypoint

+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + + + + + + diff --git a/apps/kitchen/waypoints.json b/apps/kitchen/waypoints.json new file mode 100644 index 000000000..98a670c0d --- /dev/null +++ b/apps/kitchen/waypoints.json @@ -0,0 +1,20 @@ +[ + { + "name":"NONE" + }, + { + "name":"No10", + "lat":51.5032, + "lon":-0.1269 + }, + { + "name":"Stone", + "lat":51.1788, + "lon":-1.8260 + }, + { "name":"WP0" }, + { "name":"WP1" }, + { "name":"WP2" }, + { "name":"WP3" }, + { "name":"WP4" } +] \ No newline at end of file diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog index 7e7ea65ab..b56c9f6bb 100644 --- a/apps/launch/ChangeLog +++ b/apps/launch/ChangeLog @@ -2,3 +2,5 @@ 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 diff --git a/apps/launch/app.js b/apps/launch/app.js index 9795d8901..ab1a89fc0 100644 --- a/apps/launch/app.js +++ b/apps/launch/app.js @@ -12,55 +12,53 @@ var menuScroll = 0; var menuShowing = false; function drawMenu() { - g.setFont("6x8",2); - g.setFontAlign(-1,0); - var n = 3; + g.reset().setFont("6x8",2).setFontAlign(-1,0); + var w = g.getWidth(); + var h = g.getHeight(); + var m = w/2; + var n = (h-48)/64; if (selected>=n+menuScroll) menuScroll = 1+selected-n; if (selectedn+menuScroll) ? -1 : 0); - g.fillPoly([120,233,106,219,134,219]); + g.setColor(menuScroll ? g.theme.fg : g.theme.bg); + g.fillPoly([m,6,m-14,20,m+14,20]); + g.setColor((apps.length>n+menuScroll) ? g.theme.fg : g.theme.bg); + g.fillPoly([m,h-7,m-14,h-21,m+14,h-21]); // draw - g.setColor(-1); + g.setColor(g.theme.fg); 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); +Bangle.setUI("updown",dir=>{ + if (dir) { + selected += dir; + if (selected<0) selected = apps.length-1; + if (selected>=apps.length) selected = 0; + drawMenu(); } else { - E.showMessage("Loading..."); - load(apps[selected].src); + 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/lazyclock/ChangeLog b/apps/lazyclock/ChangeLog index 1fc732a04..984d29869 100644 --- a/apps/lazyclock/ChangeLog +++ b/apps/lazyclock/ChangeLog @@ -1 +1,2 @@ -0.01: Launch app \ No newline at end of file +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 diff --git a/apps/lazyclock/lazyclock-app.js b/apps/lazyclock/lazyclock-app.js index cd7edf329..400e26ede 100644 --- a/apps/lazyclock/lazyclock-app.js +++ b/apps/lazyclock/lazyclock-app.js @@ -1,5 +1,6 @@ let secondInterval; let showRealTime = false; +let currentFormatter; const utils = { random: function(items) { @@ -68,13 +69,14 @@ const words = { approx: ['\'Bout', 'About', 'Around', `Summat\nlike`, 'Near', 'Close to'], approach: ['Nearly', `Coming\nup to`, 'Approaching', `A touch\nbefore`], past: [`A shade\nafter`, `A whisker\nafter`, 'Just gone'], - quarter: ['Quarter', `Fifteen\nminutes`], + quarter: ['A quarter', 'Quarter'], half: ['Half', 'Half past'], exactly: ['exactly', 'on the dot', 'o\' clock'], - ish: ['-ish', `\n(ish)`] + ish: ['-ish', `\n(ish)`, `\nand change`, `\nand some`, `\nor`, `\nor\nthereabouts`] }; function switchMode() { + currentFormatter = null; showRealTime = !showRealTime; refreshTime(); } @@ -94,76 +96,69 @@ function drawRealTime(date) { } -function drawDumbTime(time) { - const hours = time.getHours(); - const minutes = time.getMinutes(); +const makeApprox = (str, template) => { + let _template = template || 'approx'; + if (utils.oneIn(2)) { + _template = 'approx'; - function formatTime(hours, minutes) { - const makeApprox = (str, template) => { - let _template = template || 'approx'; - if (utils.oneIn(2)) { - _template = 'approx'; - - if (utils.oneIn(words.approx.length)) { - const ish = utils.random(words.ish); - return `${str}${ish}`; - } - } - - const approx = `${utils.random(words[_template])} `; - - return `${approx}\n${str.toLowerCase()}`; - }; - - const formatters = { - 'onTheHour': (hoursAsWord) => { - const exactly = utils.random(words.exactly); - - return `${hoursAsWord}\n${exactly}`; - }, - 'nearTheHour': (hoursAsWord) => { - const template = (minutes < 10) ? 'past' : 'approach'; - - return makeApprox(hoursAsWord, template); - }, - 'nearQuarter': (hoursAsWord, minutes) => { - const direction = (minutes > 30) ? 'to' : 'past'; - const quarter = utils.random(words.quarter); - - const formatted = `${quarter} ${direction}\n${hoursAsWord}`; - - return (minutes === 15 || minutes === 45) ? formatted : makeApprox(formatted); - }, - 'nearHalf': (hoursAsWord, minutes) => { - const half = utils.random(words.half); - - const formatted = `${half}\n${hoursAsWord}`; - - const template = (minutes > 30) ? 'past' : 'approach'; - return (minutes === 30) ? formatted : makeApprox(formatted, template); - }, - }; - - function getFormatter(hours, minutes) { - if (minutes === 0) { - return formatters.onTheHour; - } else if (minutes > 50 || minutes < 10) { - return formatters.nearTheHour; - } else if (minutes > 40|| minutes < 20) { - return formatters.nearQuarter; - } else { - return formatters.nearHalf; - } + if (utils.oneIn(words.approx.length)) { + const ish = utils.random(words.ish); + return `${str}${ish}`; } + } + const approx = `${utils.random(words[_template])} `; + + return `${approx}\n${str.toLowerCase()}`; +}; + +const formatters = { + 'onTheHour': (hoursAsWord) => { + const exactly = utils.random(words.exactly); + + return `${hoursAsWord}\n${exactly}`; + }, + 'nearTheHour': (hoursAsWord, minutes) => { + const template = (minutes < 10) ? 'past' : 'approach'; + + return makeApprox(hoursAsWord, template); + }, + 'nearQuarter': (hoursAsWord, minutes) => { + const direction = (minutes > 30) ? 'to' : 'past'; + const quarter = utils.random(words.quarter); + const formatted = `${quarter} ${direction}\n${hoursAsWord}`; + + return (minutes === 15 || minutes === 45) ? formatted : makeApprox(formatted); + }, + 'nearHalf': (hoursAsWord, minutes) => { + const half = utils.random(words.half); + const formatted = `${half}\n${hoursAsWord}`; + const template = (minutes > 30) ? 'past' : 'approach'; + + return (minutes === 30) ? formatted : makeApprox(formatted, template); + }, +}; + +function getFormatter(hours, minutes) { + if (minutes === 0) { + return 'onTheHour'; + } else if (minutes > 50 || minutes < 10) { + return 'nearTheHour'; + } else if (minutes > 40|| minutes < 20) { + return 'nearQuarter'; + } + + return 'nearHalf'; +} + +function drawDumbTime(hours, minutes, formatter) { + function formatTime(hours, minutes, formatter) { const hoursAsWord = utils.hours2Word(hours, minutes); - const formatter = getFormatter(hours, minutes); - return formatter(hoursAsWord, minutes); } - utils.print(formatTime(hours, minutes)); + utils.print(formatTime(hours, minutes, formatter)); } function cancelTimeout() { @@ -174,22 +169,42 @@ function cancelTimeout() { secondInterval = undefined; } -function refreshTime() { - cancelTimeout(); +function refreshTime(force) { + function clearForRefresh() { + g.clearRect(0, 24, g.getWidth(), g.getHeight()-24); + g.reset(); + g.setFontAlign(0,0); + } - g.clearRect(0, 24, g.getWidth(), g.getHeight()-24); - g.reset(); - g.setFontAlign(0,0); + function setRefreshInterval(time) { + const secondsTillRefresh = 60 - time.getSeconds(); + secondInterval = setTimeout(refreshTime, secondsTillRefresh * 1000); + + return secondInterval; + } + + force = force === true; + cancelTimeout(); const time = new Date(); - const method = showRealTime ? drawRealTime : drawDumbTime; + if (showRealTime) { + clearForRefresh(); + drawRealTime(time); + return setRefreshInterval(time); + } - method(time); + const hours = time.getHours(); + const minutes = time.getMinutes(); + const formatter = getFormatter(hours, minutes); - const secondsTillRefresh = 60 - time.getSeconds(); + if (formatter !== currentFormatter) { + clearForRefresh(); + currentFormatter = formatter; + drawDumbTime(hours, minutes, formatters[formatter]); + } - secondInterval = setTimeout(refreshTime, secondsTillRefresh * 1000); + return setRefreshInterval(time); } @@ -200,6 +215,7 @@ function startClock() { function addEvents() { Bangle.on('lcdPower', (on) => { cancelTimeout(); + currentFormatter = null; if (on) { startClock(); } @@ -215,8 +231,10 @@ function addEvents() { edge: "falling" }); - - setWatch(refreshTime, BTN3, { + setWatch(() => { + currentFormatter = null; + refreshTime(); + }, BTN3, { repeat: true, edge: "falling" }); @@ -232,5 +250,4 @@ function init() { addEvents(); } - -init(); +init(); \ No newline at end of file diff --git a/apps/lifeclk/app.js b/apps/lifeclk/app.js index d0288ad2f..6064aa162 100644 --- a/apps/lifeclk/app.js +++ b/apps/lifeclk/app.js @@ -264,7 +264,6 @@ function improveLetter(textPreBuf, char, x,y){ return;} default: return; } - return; } function fillMinTextBuf(date){ diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index c6779bf27..3d64cf8d7 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -8,3 +8,4 @@ Ensure 'on' is always supplied for translations 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 diff --git a/apps/locale/locales.js b/apps/locale/locales.js index 0861bc907..34f259498 100644 --- a/apps/locale/locales.js +++ b/apps/locale/locales.js @@ -245,6 +245,24 @@ var locales = { day: "söndag,måndag,tisdag,onsdag,torsdag,fredag,lördag", trans: { yes: "ja", Yes: "Ja", no: "nej", No: "Nej", ok: "ok", on: "on", off: "off" } }, + "en_NZ": { + lang: "en_NZ", + decimal_point: ".", + thousands_sep: ",", + currency_symbol: "$", + int_curr_symbol: "NZD", + speed: "kph", + distance: { 0: "m", 1: "km" }, + temperature: "°C", + ampm: { 0: "am", 1: "pm" }, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + datePattern: { 0: "%A, %B %d, %Y", "1": "%d/%m/%y" }, // Sunday, 1 March 2020 // 1/3/20 + 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", + // No translation for english... + }, "en_AU": { lang: "en_AU", decimal_point: ".", @@ -531,7 +549,26 @@ var locales = { abday: "ned.,pon.,tor.,sre.,čet.,pet.,sob.", day: "nedelja,ponedeljek,torek,sreda,četrtek,petek,sobota", trans: { yes: "da", Yes: "Da", no: "ne", No: "Ne", ok: "ok", on: "Vklj.", off: "Izklj.", "< Back": "< Nazaj" } - }/*, + }, + "pt_PT": { + lang: "pt_PT", + decimal_point: ",", + thousands_sep: " ", + currency_symbol: "€", + int_curr_symbol: "EUR", + speed: "kmh", + distance: { 0: "m", 1: "km" }, + temperature: "°C", + ampm: { 0: "am", 1: "pm" }, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + datePattern: { 0: "%d %b %Y", 1: "%d/%m/%y" }, + abmonth: "Jan,Fev,Mar,Abr,Mai,Jun,Jul,Ago,Set,Out,Nov,Dez", + month: "Janeiro,Fevereiro,Março,Abril,Maio,Junho,Julho,Agosto,Setembro,Outubro,Novembro,Dezembro", + abday: "Dom,Seg,Ter,Qua,Qui,Sex,Sab", + 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" } + }, +/*, "he_IL": { // This won't work until we get a font - see https://github.com/espruino/BangleApps/issues/399 codePage : "ISO8859-8", lang: "he_IL", diff --git a/apps/marioclock/ChangeLog b/apps/marioclock/ChangeLog index 66e4ab800..276c65c6b 100644 --- a/apps/marioclock/ChangeLog +++ b/apps/marioclock/ChangeLog @@ -12,3 +12,4 @@ 0.12: Add info banner message when phone (dis)connects. Display low-battery warning (<=10%) 0.13: Fix drawPyramid function so pyramids are drawn in correct Y position 0.14: Add jumping frame for characters +0.15: Disable notification buzz during Quiet Mode diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js index 20d1dfd85..6289a2568 100644 --- a/apps/marioclock/marioclock-app.js +++ b/apps/marioclock/marioclock-app.js @@ -1,718 +1,720 @@ -/** - * BangleJS MARIO CLOCK - * - * + Original Author: Paul Cockrell https://github.com/paulcockrell - * + Created: April 2020 - * + Based on Espruino Mario Clock V3 https://github.com/paulcockrell/espruino-mario-clock - * + Online Image convertor: https://www.espruino.com/Image+Converter, Use transparency + compression + 8bit Web + export as Image String - * + Images must be drawn as PNGs with transparent backgrounds - */ - -const locale = require("locale"); -const storage = require('Storage'); -const settings = (storage.readJSON('setting.json', 1) || {}); -const timeout = settings.timeout || 10; -const is12Hour = settings["12hour"] || false; - -// Screen dimensions -let W, H; -// Screen brightness -let brightness = 1; - -let intervalRef, displayTimeoutRef = null; - -// Colours -const LIGHTEST = "#effedd"; -const LIGHT = "#add795"; -const DARK = "#588d77"; -const DARKEST = "#122d3e"; -const NIGHT = "#001818"; - -// Character names -const DAISY = "daisy"; -const TOAD = "toad"; -const MARIO = "mario"; - -const characterSprite = { - frameIdx: 0, - x: 33, - y: 55, - jumpCounter: 0, - jumpIncrement: Math.PI / 6, - isJumping: false, - character: MARIO, -}; - -const coinSprite = { - frameIdx: 0, - x: 34, - y: 18, - isAnimating: false, - yDefault: 18, -}; - -const pyramidSprite = { - x: 90, - height: 34, -}; - -const ONE_SECOND = 1000; -const DATE_MODE = "date"; -const BATT_MODE = "batt"; -const TEMP_MODE = "temp"; -const PHON_MODE = "gbri"; - -let timer = 0; -let backgroundArr = []; -let nightMode = false; -let infoMode = DATE_MODE; - -// Used to stop values flapping when displayed on screen -let lastBatt = 0; -let lastTemp = 0; - -const phone = { - get status() { - return NRF.getSecurityStatus().connected ? "Yes" : "No"; - }, - message: null, - messageTimeout: null, - messageScrollX: null, - messageType: null, -}; - -const SETTINGS_FILE = "marioclock.json"; - -function readSettings() { - return require('Storage').readJSON(SETTINGS_FILE, 1) || {}; -} - -function writeSettings(newSettings) { - require("Storage").writeJSON(SETTINGS_FILE, newSettings); -} - -function phoneOutbound(msg) { - Bluetooth.println(JSON.stringify(msg)); -} - -function phoneClearMessage() { - if (phone.message === null) return; - - if (phone.messageTimeout) { - clearTimeout(phone.messageTimeout); - phone.messageTimeout = null; - } - phone.message = null; - phone.messageScrollX = null; - phone.messageType = null; -} - -function phoneNewMessage(type, msg) { - Bangle.buzz(); - - phoneClearMessage(); - phone.messageTimeout = setTimeout(() => phone.message = null, ONE_SECOND * 30); - phone.message = msg; - phone.messageType = type; - - // Notify user and active screen - if (!Bangle.isLCDOn()) { - clearTimers(); - Bangle.setLCDPower(true); - } -} - -function truncStr(str, max) { - if (str.length > max) { - return str.substr(0, max) + '...'; - } - return str; -} - -function phoneInbound(evt) { - switch (evt.t) { - case 'notify': - const sender = truncStr(evt.sender, 10); - const subject = truncStr(evt.subject, 15); - phoneNewMessage("notify", `${sender} - '${subject}'`); - break; - case 'call': - if (evt.cmd === "accept") { - let nameOrNumber = "Unknown"; - if (evt.name !== null || evt.name !== "") { - nameOrNumber = evt.name; - } else if (evt.number !== null || evt.number !== "") { - nameOrNumber = evt.number; - } - phoneNewMessage("call", nameOrNumber); - } - break; - default: - return null; - } -} - -function genRanNum(min, max) { - return Math.floor(Math.random() * (max - min + 1) + min); -} - -function switchCharacter() { - const curChar = characterSprite.character; - - let newChar; - switch(curChar) { - case DAISY: - newChar = MARIO; - break; - case TOAD: - newChar = DAISY; - break; - case MARIO: - default: - newChar = TOAD; - } - - characterSprite.character = newChar; -} - -function toggleNightMode() { - if (!nightMode) { - nightMode = true; - return; - } - - brightness -= 0.30; - if (brightness <= 0) { - brightness = 1; - nightMode = false; - } - Bangle.setLCDBrightness(brightness); -} - -function incrementTimer() { - if (timer > 100) { - timer = 0; - } - else { - timer += 10; - } -} - -function drawBackground() { - "ram" - - // Clear screen - const bgColor = (nightMode) ? NIGHT : LIGHTEST; - g.setColor(bgColor); - g.fillRect(0, 10, W, H); - - // set cloud colors and draw clouds - const cloudColor = (nightMode) ? DARK : LIGHT; - g.setColor(cloudColor); - g.fillRect(0, 10, W, 15); - g.fillRect(0, 17, W, 17); - g.fillRect(0, 19, W, 19); - g.fillRect(0, 21, W, 21); - - // Date bar - g.setColor(DARKEST); - g.fillRect(0, 0, W, 9); -} - -function drawFloor() { - const fImg = require("heatshrink").decompress(atob("ikDxH+rgATCoIBQAQYDP")); // Floor image - for (let x = 0; x < 4; x++) { - g.drawImage(fImg, x * 20, g.getHeight() - 5); - } -} - -function drawPyramid() { - "ram" - - const pPol = [pyramidSprite.x + 10, H - 5, pyramidSprite.x + 50, pyramidSprite.height, pyramidSprite.x + 90, H - 5]; // Pyramid poly - - const color = (nightMode) ? DARK : LIGHT; - g.setColor(color); - g.fillPoly(pPol); - - pyramidSprite.x -= 1; - // Reset and randomize pyramid if off-screen - if (pyramidSprite.x < - 100) { - pyramidSprite.x = 90; - pyramidSprite.height = genRanNum(25, 60); - } -} - -function drawTreesFrame(x, y) { - const tImg = require("heatshrink").decompress(atob("h8GxH+AAMHAAIFCAxADEBYgDCAQYAFCwobOAZAEFBxo=")); // Tree image - - g.drawImage(tImg, x, y); - g.setColor(DARKEST); - g.drawLine(x + 6 /* Match stalk to palm tree */, y + 6 /* Match stalk to palm tree */, x + 6, H - 6); -} - -function generateTreeSprite() { - return { - x: 90, - y: genRanNum(30, 60) - }; -} - -function drawTrees() { - // remove first sprite if offscreen - let firstBackgroundSprite = backgroundArr[0]; - if (firstBackgroundSprite) { - if (firstBackgroundSprite.x < -15) backgroundArr.splice(0, 1); - } - - // set background sprite if array empty - let lastBackgroundSprite = backgroundArr[backgroundArr.length - 1]; - if (!lastBackgroundSprite) { - const newSprite = generateTreeSprite(); - lastBackgroundSprite = newSprite; - backgroundArr.push(lastBackgroundSprite); - } - - // add random sprites - if (backgroundArr.length < 2 && lastBackgroundSprite.x < (16 * 7)) { - const randIdx = Math.floor(Math.random() * 25); - if (randIdx < 2) { - const newSprite = generateTreeSprite(); - backgroundArr.push(newSprite); - } - } - - for (x = 0; x < backgroundArr.length; x++) { - let scenerySprite = backgroundArr[x]; - scenerySprite.x -= 5; - drawTreesFrame(scenerySprite.x, scenerySprite.y); - } -} - -function drawCoinFrame(x, y) { - const cImg = require("heatshrink").decompress(atob("hkPxH+AAcHAAQIEBIXWAAQNEBIWHAAdcBgQLBA4IODBYQKEBAQMDBelcBaJUBM4QRBNYx1EBQILDR4QHBBISdIBIoA==")); // Coin image - g.drawImage(cImg, x, y); -} - -function drawCoin() { - if (!coinSprite.isAnimating) return; - - coinSprite.y -= 8; - if (coinSprite.y < (0 - 15 /*Coin sprite height*/)) { - coinSprite.isAnimating = false; - coinSprite.y = coinSprite.yDefault; - return; - } - - drawCoinFrame(coinSprite.x, coinSprite.y); -} - -function drawDaisyFrame(idx, x, y) { - var frame; - - switch(idx) { - case 2: - frame = require("heatshrink").decompress(atob("h0UxH+AAkrAIgAH60rAIQNIBQIABDZErAAwMMBwo0CBxQNEHAQGCBpIPCBoQJCDRIXDBpA7DBIQACw5yCJQgZDP4gNErlcJAZ6GAgNcw+HRI4CCDgNcU44ZDDYSYGDIYACB4QaEDYgMFJAg3DFQ5mFBQYA==")); // daisy jumping - break; - case 0: - frame = require("heatshrink").decompress(atob("h8UxH+AAsHAIgAI60HAIQOJBYIABDpMHAAwNNB4wOJB4gIEHgQBBBxYQCBwYLDDhIaEBxApEw4qDAgIOHDwiIEBwtcFIRWIUgWHw6TIAQXWrlcWZAqBDQIeBBxQaBDxIcCHIQ8JDAIAFWJLPHA==")); - break; - case 1: - default: - frame = require("heatshrink").decompress(atob("h8UxH+AAsHAIgAI60HAIQOJBYIABDpMHAAwNNB4wOJB4gIEHgQBBBxYQCBwYLDDhIaEBxApEw4qDAgIOHDwiIEBwtcFIRWIUgQvBSZACCBwNcWZQcCAAIPIDgYACFw4YBDYIOCD4waEDYI+HaBQ=")); - } - - g.drawImage(frame, x, y); -} - -function drawMarioFrame(idx, x, y) { - var frame; - - switch(idx) { - case 2: - frame = require("heatshrink").decompress(atob("h8UxH+AAkrAAYFCBo9cAAIEB63WB4gMDB4YOFBowfDw4xDBAYADA4YcDGwYACDoYAEBYYBBw4NDCoYOFDIweFFwoZFAQYIDLAQWGEwqgECI6ECJ4JeGQYS9EB4QTHBwImCBYRtDSAwrFawqkFWY7PEBxoMFKoZaELoYICAAg")); // Mario frame jumping - break; - case 0: - frame = require("heatshrink").decompress(atob("h8UxH+AAkrAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGGQgNCw4ACLwgFBBwgKECQpZCCgRqDFQikEJIriIBgzwIdxjiGBxIuEBIo=")); // Mario Frame 1 - break; - case 1: - default: - frame = require("heatshrink").decompress(atob("h8UxH+AAkrAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGHQpMDw+HCQYEBSowOBBQIeJDAQODSwaVHUhwOLfg4FHe4wASA=")); // Mario frame 2 - } - - g.drawImage(frame, x, y); -} - -function drawToadFrame(idx, x, y) { - var frame; - - switch(idx) { - case 2: - frame = require("heatshrink").decompress(atob("iEUxH+ACkrAAoNJrnWAAQRGlfWrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPENwoTEH4crw4EDAAgGDB4YABAYIBDP4YLEAAIPHCAQHCCAQTDD4gHDEA4PFGAY3EbooPECob8IPooPFCATGEf44hFAAYLDA==")); // toad jumping - break; - case 0: - frame = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYYrCCAwbFFwgQEM4gAEeA4OIH4ghFAAYLD")); // Toad Frame 1 - break; - case 1: - default: - frame = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYQrDb4wcGFxYLDMoYgHRYgwKABAMBA")); // Mario frame 2 - } - - g.drawImage(frame, x, y); -} - -// Mario speach bubble -function drawNotice(x, y) { - if (phone.message === null) return; - - let img; - switch (phone.messageType) { - case "call": - img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo=")); - break; - case "notify": - img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA=")); - break; - case "lowBatt": - img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INFrgABB4oOEBoQPFBwwDGB0uHAAIOLJRB3OSpApDBoQAHB4INLByANNAwo")); - break; - } - - if (img) g.drawImage(img, characterSprite.x, characterSprite.y - 16); -} - -function drawCharacter(date, character) { - "ram" - - // calculate jumping - const seconds = date.getSeconds(), - milliseconds = date.getMilliseconds(); - - if (seconds == 59 && milliseconds > 800 && !characterSprite.isJumping) { - characterSprite.isJumping = true; - } - - if (characterSprite.isJumping) { - characterSprite.y = (Math.sin(characterSprite.jumpCounter) * -12) + 50 /* Character Y base value */; - characterSprite.jumpCounter += characterSprite.jumpIncrement; - - if (parseInt(characterSprite.jumpCounter) === 2 && !coinSprite.isAnimating) { - coinSprite.isAnimating = true; - } - - if (characterSprite.jumpCounter.toFixed(1) >= 4) { - characterSprite.jumpCounter = 0; - characterSprite.isJumping = false; - } - } - - // calculate animation timing - if (timer % 20 === 0) { - // shift to next frame - if (characterSprite.isJumping) { - characterSprite.frameIdx = 2; - } else { - characterSprite.frameIdx = characterSprite.frameIdx == 0 ? 1 : 0; - } - } - - switch(characterSprite.character) { - case DAISY: - drawDaisyFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); - break; - case TOAD: - drawToadFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); - break; - case MARIO: - default: - drawMarioFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); - } -} - -function drawBrickFrame(x, y) { - const brk = require("heatshrink").decompress(atob("ikQxH+/0HACASB6wAQCoPWw4AOrgT/Cf4T/Cb1cAB8H/wVBAB/+A")); - g.drawImage(brk, x, y); -} - -function drawTime(date) { - // draw hour brick - drawBrickFrame(20, 25); - // draw minute brick - drawBrickFrame(42, 25); - - const h = date.getHours(); - const hours = ("0" + ((is12Hour && h > 12) ? h - 12 : h)).substr(-2); - const mins = ("0" + date.getMinutes()).substr(-2); - - g.setFont("6x8"); - g.setColor(DARKEST); - g.drawString(hours, 25, 29); - g.drawString(mins, 47, 29); -} - -function buildDateStr(date) { - let dateStr = locale.date(date, true); - dateStr = dateStr.replace(date.getFullYear(), "").trim().replace(/\/$/i,""); - dateStr = locale.dow(date, true) + " " + dateStr; - - return dateStr; -} - -function buildBatStr() { - let batt = parseInt(E.getBattery()); - const battDiff = Math.abs(lastBatt - batt); - - // Suppress flapping values - // Only update batt if it moves greater than +-2 - if (battDiff > 2) { - lastBatt = batt; - } else { - batt = lastBatt; - } - - const battStr = `Bat: ${batt}%`; - - return battStr; -} - -function buildTempStr() { - let temp = parseInt(E.getTemperature()); - const tempDiff = Math.abs(lastTemp - temp); - - // Suppress flapping values - // Only update temp if it moves greater than +-2 - if (tempDiff > 2) { - lastTemp = temp; - } else { - temp = lastTemp; - } - const tempStr = `Temp: ${temp}'c`; - - return tempStr; -} - -function buildPhonStr() { - return `Phone: ${phone.status}`; -} - -function drawInfo(date) { - let xPos; - let str = ""; - - if (phone.message !== null) { - str = phone.message; - const strLen = g.stringWidth(str); - if (strLen > W) { - if (phone.messageScrollX === null || (phone.messageScrollX <= (strLen * -1))) { - phone.messageScrollX = W; - resetDisplayTimeout(); - } else { - phone.messageScrollX -= 2; - } - xPos = phone.messageScrollX; - } else { - xPos = (W - g.stringWidth(str)) / 2; - } - } else { - switch(infoMode) { - case PHON_MODE: - str = buildPhonStr(); - break; - case TEMP_MODE: - str = buildTempStr(); - break; - case BATT_MODE: - str = buildBatStr(); - break; - case DATE_MODE: - default: - str = buildDateStr(date); - } - xPos = (W - g.stringWidth(str)) / 2; - } - - g.setFont("6x8"); - g.setColor(LIGHTEST); - g.drawString(str, xPos, 1); -} - -function changeInfoMode() { - phoneClearMessage(); - - switch(infoMode) { - case BATT_MODE: - infoMode = TEMP_MODE; - break; - case TEMP_MODE: - infoMode = PHON_MODE; - break; - case PHON_MODE: - infoMode = DATE_MODE; - break; - case DATE_MODE: - default: - infoMode = BATT_MODE; - } -} - -function redraw() { - const date = new Date(); - - // Update timers - incrementTimer(); - - // Draw frame - drawBackground(); - drawFloor(); - drawPyramid(); - drawTrees(); - drawTime(date); - drawInfo(date); - drawCharacter(date); - drawNotice(); - drawCoin(); - - // Render new frame - g.flip(); -} - -function clearTimers(){ - if(intervalRef) { - clearInterval(intervalRef); - intervalRef = null; - } - - if(displayTimeoutRef) { - clearInterval(displayTimeoutRef); - displayTimeoutRef = null; - } -} - -function resetDisplayTimeout() { - if (displayTimeoutRef) clearInterval(displayTimeoutRef); - - displayTimeoutRef = setInterval(() => { - if (Bangle.isLCDOn()) Bangle.setLCDPower(false); - clearTimers(); - }, ONE_SECOND * timeout); -} - -function startTimers(){ - if(intervalRef) clearTimers(); - intervalRef = setInterval(redraw, 50); - - resetDisplayTimeout(); - - redraw(); -} - -function loadSettings() { - const settings = readSettings(); - if (!settings) return; - - if (settings.character) characterSprite.character = settings.character; - if (settings.nightMode) nightMode = settings.nightMode; - if (settings.brightness) { - brightness = settings.brightness; - Bangle.setLCDBrightness(brightness); - } -} - -function updateSettings() { - const newSettings = { - character: characterSprite.character, - nightMode: nightMode, - brightness: brightness, - }; - writeSettings(newSettings); -} - -function checkBatteryLevel() { - if (Bangle.isCharging()) return; - if (E.getBattery() > 10) return; - if (phone.message !== null) return; - - phoneNewMessage("lowBatt", "Warning, battery is low"); -} - -// Main -function init() { - loadSettings(); - - clearInterval(); - - // Initialise display - Bangle.setLCDMode("80x80"); - - // Store screen dimensions - W = g.getWidth(); - H = g.getHeight(); - - // Get Mario to jump! - setWatch(() => { - if (intervalRef && !characterSprite.isJumping) characterSprite.isJumping = true; - resetDisplayTimeout(); - phoneClearMessage(); // Clear any phone messages and message timers - }, BTN3, {repeat: true}); - - // Close watch and load launcher app - setWatch(() => { - Bangle.setLCDMode(); - Bangle.showLauncher(); - }, BTN2, {repeat: false, edge: "falling"}); - - // Change info mode - setWatch(() => { - changeInfoMode(); - }, BTN1, {repeat: true}); - - Bangle.on('lcdPower', (on) => on ? startTimers() : clearTimers()); - - Bangle.on('faceUp', (up) => { - if (up && !Bangle.isLCDOn()) { - clearTimers(); - Bangle.setLCDPower(true); - } - }); - - Bangle.on('swipe', (sDir) => { - resetDisplayTimeout(); - - switch(sDir) { - // Swipe right (1) - change character (on a loop) - case 1: - switchCharacter(); - break; - // Swipe left (-1) - change day/night mode (on a loop) - case -1: - default: - toggleNightMode(); - } - - updateSettings(); - }); - - // Phone connectivity - try { NRF.wake(); } catch (e) {} - - NRF.on('disconnect', () => { - phoneNewMessage(null, "Phone disconnected"); - }); - - NRF.on('connect', () => { - setTimeout(() => { - phoneOutbound({ t: "status", bat: E.getBattery() }); - }, ONE_SECOND * 2); - phoneNewMessage(null, "Phone connected"); - }); - - GB = (evt) => phoneInbound(evt); - - startTimers(); - - setInterval(checkBatteryLevel, ONE_SECOND * 60 * 10); - checkBatteryLevel(); -} - -// Initialise! -init(); \ No newline at end of file +/** + * BangleJS MARIO CLOCK + * + * + Original Author: Paul Cockrell https://github.com/paulcockrell + * + Created: April 2020 + * + Based on Espruino Mario Clock V3 https://github.com/paulcockrell/espruino-mario-clock + * + Online Image convertor: https://www.espruino.com/Image+Converter, Use transparency + compression + 8bit Web + export as Image String + * + Images must be drawn as PNGs with transparent backgrounds + */ + +const locale = require("locale"); +const storage = require('Storage'); +const settings = (storage.readJSON('setting.json', 1) || {}); +const timeout = settings.timeout || 10; +const is12Hour = settings["12hour"] || false; + +// Screen dimensions +let W, H; +// Screen brightness +let brightness = 1; + +let intervalRef, displayTimeoutRef = null; + +// Colours +const LIGHTEST = "#effedd"; +const LIGHT = "#add795"; +const DARK = "#588d77"; +const DARKEST = "#122d3e"; +const NIGHT = "#001818"; + +// Character names +const DAISY = "daisy"; +const TOAD = "toad"; +const MARIO = "mario"; + +const characterSprite = { + frameIdx: 0, + x: 33, + y: 55, + jumpCounter: 0, + jumpIncrement: Math.PI / 6, + isJumping: false, + character: MARIO, +}; + +const coinSprite = { + frameIdx: 0, + x: 34, + y: 18, + isAnimating: false, + yDefault: 18, +}; + +const pyramidSprite = { + x: 90, + height: 34, +}; + +const ONE_SECOND = 1000; +const DATE_MODE = "date"; +const BATT_MODE = "batt"; +const TEMP_MODE = "temp"; +const PHON_MODE = "gbri"; + +let timer = 0; +let backgroundArr = []; +let nightMode = false; +let infoMode = DATE_MODE; + +// Used to stop values flapping when displayed on screen +let lastBatt = 0; +let lastTemp = 0; + +const phone = { + get status() { + return NRF.getSecurityStatus().connected ? "Yes" : "No"; + }, + message: null, + messageTimeout: null, + messageScrollX: null, + messageType: null, +}; + +const SETTINGS_FILE = "marioclock.json"; + +function readSettings() { + return require('Storage').readJSON(SETTINGS_FILE, 1) || {}; +} + +function writeSettings(newSettings) { + require("Storage").writeJSON(SETTINGS_FILE, newSettings); +} + +function phoneOutbound(msg) { + Bluetooth.println(JSON.stringify(msg)); +} + +function phoneClearMessage() { + if (phone.message === null) return; + + if (phone.messageTimeout) { + clearTimeout(phone.messageTimeout); + phone.messageTimeout = null; + } + phone.message = null; + phone.messageScrollX = null; + phone.messageType = null; +} + +function phoneNewMessage(type, msg) { + + phoneClearMessage(); + phone.messageTimeout = setTimeout(() => phone.message = null, ONE_SECOND * 30); + phone.message = msg; + phone.messageType = type; + + // Notify user and active screen + if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.buzz(); + if (!Bangle.isLCDOn()) { + clearTimers(); + Bangle.setLCDPower(true); + } + } +} + +function truncStr(str, max) { + if (str.length > max) { + return str.substr(0, max) + '...'; + } + return str; +} + +function phoneInbound(evt) { + switch (evt.t) { + case 'notify': + const sender = truncStr(evt.sender, 10); + const subject = truncStr(evt.subject, 15); + phoneNewMessage("notify", `${sender} - '${subject}'`); + break; + case 'call': + if (evt.cmd === "accept") { + let nameOrNumber = "Unknown"; + if (evt.name !== null || evt.name !== "") { + nameOrNumber = evt.name; + } else if (evt.number !== null || evt.number !== "") { + nameOrNumber = evt.number; + } + phoneNewMessage("call", nameOrNumber); + } + break; + default: + return null; + } +} + +function genRanNum(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +function switchCharacter() { + const curChar = characterSprite.character; + + let newChar; + switch(curChar) { + case DAISY: + newChar = MARIO; + break; + case TOAD: + newChar = DAISY; + break; + case MARIO: + default: + newChar = TOAD; + } + + characterSprite.character = newChar; +} + +function toggleNightMode() { + if (!nightMode) { + nightMode = true; + return; + } + + brightness -= 0.30; + if (brightness <= 0) { + brightness = 1; + nightMode = false; + } + Bangle.setLCDBrightness(brightness); +} + +function incrementTimer() { + if (timer > 100) { + timer = 0; + } + else { + timer += 10; + } +} + +function drawBackground() { + "ram" + + // Clear screen + const bgColor = (nightMode) ? NIGHT : LIGHTEST; + g.setColor(bgColor); + g.fillRect(0, 10, W, H); + + // set cloud colors and draw clouds + const cloudColor = (nightMode) ? DARK : LIGHT; + g.setColor(cloudColor); + g.fillRect(0, 10, W, 15); + g.fillRect(0, 17, W, 17); + g.fillRect(0, 19, W, 19); + g.fillRect(0, 21, W, 21); + + // Date bar + g.setColor(DARKEST); + g.fillRect(0, 0, W, 9); +} + +function drawFloor() { + const fImg = require("heatshrink").decompress(atob("ikDxH+rgATCoIBQAQYDP")); // Floor image + for (let x = 0; x < 4; x++) { + g.drawImage(fImg, x * 20, g.getHeight() - 5); + } +} + +function drawPyramid() { + "ram" + + const pPol = [pyramidSprite.x + 10, H - 5, pyramidSprite.x + 50, pyramidSprite.height, pyramidSprite.x + 90, H - 5]; // Pyramid poly + + const color = (nightMode) ? DARK : LIGHT; + g.setColor(color); + g.fillPoly(pPol); + + pyramidSprite.x -= 1; + // Reset and randomize pyramid if off-screen + if (pyramidSprite.x < - 100) { + pyramidSprite.x = 90; + pyramidSprite.height = genRanNum(25, 60); + } +} + +function drawTreesFrame(x, y) { + const tImg = require("heatshrink").decompress(atob("h8GxH+AAMHAAIFCAxADEBYgDCAQYAFCwobOAZAEFBxo=")); // Tree image + + g.drawImage(tImg, x, y); + g.setColor(DARKEST); + g.drawLine(x + 6 /* Match stalk to palm tree */, y + 6 /* Match stalk to palm tree */, x + 6, H - 6); +} + +function generateTreeSprite() { + return { + x: 90, + y: genRanNum(30, 60) + }; +} + +function drawTrees() { + // remove first sprite if offscreen + let firstBackgroundSprite = backgroundArr[0]; + if (firstBackgroundSprite) { + if (firstBackgroundSprite.x < -15) backgroundArr.splice(0, 1); + } + + // set background sprite if array empty + let lastBackgroundSprite = backgroundArr[backgroundArr.length - 1]; + if (!lastBackgroundSprite) { + const newSprite = generateTreeSprite(); + lastBackgroundSprite = newSprite; + backgroundArr.push(lastBackgroundSprite); + } + + // add random sprites + if (backgroundArr.length < 2 && lastBackgroundSprite.x < (16 * 7)) { + const randIdx = Math.floor(Math.random() * 25); + if (randIdx < 2) { + const newSprite = generateTreeSprite(); + backgroundArr.push(newSprite); + } + } + + for (x = 0; x < backgroundArr.length; x++) { + let scenerySprite = backgroundArr[x]; + scenerySprite.x -= 5; + drawTreesFrame(scenerySprite.x, scenerySprite.y); + } +} + +function drawCoinFrame(x, y) { + const cImg = require("heatshrink").decompress(atob("hkPxH+AAcHAAQIEBIXWAAQNEBIWHAAdcBgQLBA4IODBYQKEBAQMDBelcBaJUBM4QRBNYx1EBQILDR4QHBBISdIBIoA==")); // Coin image + g.drawImage(cImg, x, y); +} + +function drawCoin() { + if (!coinSprite.isAnimating) return; + + coinSprite.y -= 8; + if (coinSprite.y < (0 - 15 /*Coin sprite height*/)) { + coinSprite.isAnimating = false; + coinSprite.y = coinSprite.yDefault; + return; + } + + drawCoinFrame(coinSprite.x, coinSprite.y); +} + +function drawDaisyFrame(idx, x, y) { + var frame; + + switch(idx) { + case 2: + frame = require("heatshrink").decompress(atob("h0UxH+AAkrAIgAH60rAIQNIBQIABDZErAAwMMBwo0CBxQNEHAQGCBpIPCBoQJCDRIXDBpA7DBIQACw5yCJQgZDP4gNErlcJAZ6GAgNcw+HRI4CCDgNcU44ZDDYSYGDIYACB4QaEDYgMFJAg3DFQ5mFBQYA==")); // daisy jumping + break; + case 0: + frame = require("heatshrink").decompress(atob("h8UxH+AAsHAIgAI60HAIQOJBYIABDpMHAAwNNB4wOJB4gIEHgQBBBxYQCBwYLDDhIaEBxApEw4qDAgIOHDwiIEBwtcFIRWIUgWHw6TIAQXWrlcWZAqBDQIeBBxQaBDxIcCHIQ8JDAIAFWJLPHA==")); + break; + case 1: + default: + frame = require("heatshrink").decompress(atob("h8UxH+AAsHAIgAI60HAIQOJBYIABDpMHAAwNNB4wOJB4gIEHgQBBBxYQCBwYLDDhIaEBxApEw4qDAgIOHDwiIEBwtcFIRWIUgQvBSZACCBwNcWZQcCAAIPIDgYACFw4YBDYIOCD4waEDYI+HaBQ=")); + } + + g.drawImage(frame, x, y); +} + +function drawMarioFrame(idx, x, y) { + var frame; + + switch(idx) { + case 2: + frame = require("heatshrink").decompress(atob("h8UxH+AAkrAAYFCBo9cAAIEB63WB4gMDB4YOFBowfDw4xDBAYADA4YcDGwYACDoYAEBYYBBw4NDCoYOFDIweFFwoZFAQYIDLAQWGEwqgECI6ECJ4JeGQYS9EB4QTHBwImCBYRtDSAwrFawqkFWY7PEBxoMFKoZaELoYICAAg")); // Mario frame jumping + break; + case 0: + frame = require("heatshrink").decompress(atob("h8UxH+AAkrAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGGQgNCw4ACLwgFBBwgKECQpZCCgRqDFQikEJIriIBgzwIdxjiGBxIuEBIo=")); // Mario Frame 1 + break; + case 1: + default: + frame = require("heatshrink").decompress(atob("h8UxH+AAkrAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGHQpMDw+HCQYEBSowOBBQIeJDAQODSwaVHUhwOLfg4FHe4wASA=")); // Mario frame 2 + } + + g.drawImage(frame, x, y); +} + +function drawToadFrame(idx, x, y) { + var frame; + + switch(idx) { + case 2: + frame = require("heatshrink").decompress(atob("iEUxH+ACkrAAoNJrnWAAQRGlfWrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPENwoTEH4crw4EDAAgGDB4YABAYIBDP4YLEAAIPHCAQHCCAQTDD4gHDEA4PFGAY3EbooPECob8IPooPFCATGEf44hFAAYLDA==")); // toad jumping + break; + case 0: + frame = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYYrCCAwbFFwgQEM4gAEeA4OIH4ghFAAYLD")); // Toad Frame 1 + break; + case 1: + default: + frame = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYQrDb4wcGFxYLDMoYgHRYgwKABAMBA")); // Mario frame 2 + } + + g.drawImage(frame, x, y); +} + +// Mario speach bubble +function drawNotice(x, y) { + if (phone.message === null) return; + + let img; + switch (phone.messageType) { + case "call": + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo=")); + break; + case "notify": + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA=")); + break; + case "lowBatt": + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INFrgABB4oOEBoQPFBwwDGB0uHAAIOLJRB3OSpApDBoQAHB4INLByANNAwo")); + break; + } + + if (img) g.drawImage(img, characterSprite.x, characterSprite.y - 16); +} + +function drawCharacter(date, character) { + "ram" + + // calculate jumping + const seconds = date.getSeconds(), + milliseconds = date.getMilliseconds(); + + if (seconds == 59 && milliseconds > 800 && !characterSprite.isJumping) { + characterSprite.isJumping = true; + } + + if (characterSprite.isJumping) { + characterSprite.y = (Math.sin(characterSprite.jumpCounter) * -12) + 50 /* Character Y base value */; + characterSprite.jumpCounter += characterSprite.jumpIncrement; + + if (parseInt(characterSprite.jumpCounter) === 2 && !coinSprite.isAnimating) { + coinSprite.isAnimating = true; + } + + if (characterSprite.jumpCounter.toFixed(1) >= 4) { + characterSprite.jumpCounter = 0; + characterSprite.isJumping = false; + } + } + + // calculate animation timing + if (timer % 20 === 0) { + // shift to next frame + if (characterSprite.isJumping) { + characterSprite.frameIdx = 2; + } else { + characterSprite.frameIdx = characterSprite.frameIdx == 0 ? 1 : 0; + } + } + + switch(characterSprite.character) { + case DAISY: + drawDaisyFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); + break; + case TOAD: + drawToadFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); + break; + case MARIO: + default: + drawMarioFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y); + } +} + +function drawBrickFrame(x, y) { + const brk = require("heatshrink").decompress(atob("ikQxH+/0HACASB6wAQCoPWw4AOrgT/Cf4T/Cb1cAB8H/wVBAB/+A")); + g.drawImage(brk, x, y); +} + +function drawTime(date) { + // draw hour brick + drawBrickFrame(20, 25); + // draw minute brick + drawBrickFrame(42, 25); + + const h = date.getHours(); + const hours = ("0" + ((is12Hour && h > 12) ? h - 12 : h)).substr(-2); + const mins = ("0" + date.getMinutes()).substr(-2); + + g.setFont("6x8"); + g.setColor(DARKEST); + g.drawString(hours, 25, 29); + g.drawString(mins, 47, 29); +} + +function buildDateStr(date) { + let dateStr = locale.date(date, true); + dateStr = dateStr.replace(date.getFullYear(), "").trim().replace(/\/$/i,""); + dateStr = locale.dow(date, true) + " " + dateStr; + + return dateStr; +} + +function buildBatStr() { + let batt = parseInt(E.getBattery()); + const battDiff = Math.abs(lastBatt - batt); + + // Suppress flapping values + // Only update batt if it moves greater than +-2 + if (battDiff > 2) { + lastBatt = batt; + } else { + batt = lastBatt; + } + + const battStr = `Bat: ${batt}%`; + + return battStr; +} + +function buildTempStr() { + let temp = parseInt(E.getTemperature()); + const tempDiff = Math.abs(lastTemp - temp); + + // Suppress flapping values + // Only update temp if it moves greater than +-2 + if (tempDiff > 2) { + lastTemp = temp; + } else { + temp = lastTemp; + } + const tempStr = `Temp: ${temp}'c`; + + return tempStr; +} + +function buildPhonStr() { + return `Phone: ${phone.status}`; +} + +function drawInfo(date) { + let xPos; + let str = ""; + + if (phone.message !== null) { + str = phone.message; + const strLen = g.stringWidth(str); + if (strLen > W) { + if (phone.messageScrollX === null || (phone.messageScrollX <= (strLen * -1))) { + phone.messageScrollX = W; + resetDisplayTimeout(); + } else { + phone.messageScrollX -= 2; + } + xPos = phone.messageScrollX; + } else { + xPos = (W - g.stringWidth(str)) / 2; + } + } else { + switch(infoMode) { + case PHON_MODE: + str = buildPhonStr(); + break; + case TEMP_MODE: + str = buildTempStr(); + break; + case BATT_MODE: + str = buildBatStr(); + break; + case DATE_MODE: + default: + str = buildDateStr(date); + } + xPos = (W - g.stringWidth(str)) / 2; + } + + g.setFont("6x8"); + g.setColor(LIGHTEST); + g.drawString(str, xPos, 1); +} + +function changeInfoMode() { + phoneClearMessage(); + + switch(infoMode) { + case BATT_MODE: + infoMode = TEMP_MODE; + break; + case TEMP_MODE: + infoMode = PHON_MODE; + break; + case PHON_MODE: + infoMode = DATE_MODE; + break; + case DATE_MODE: + default: + infoMode = BATT_MODE; + } +} + +function redraw() { + const date = new Date(); + + // Update timers + incrementTimer(); + + // Draw frame + drawBackground(); + drawFloor(); + drawPyramid(); + drawTrees(); + drawTime(date); + drawInfo(date); + drawCharacter(date); + drawNotice(); + drawCoin(); + + // Render new frame + g.flip(); +} + +function clearTimers(){ + if(intervalRef) { + clearInterval(intervalRef); + intervalRef = null; + } + + if(displayTimeoutRef) { + clearInterval(displayTimeoutRef); + displayTimeoutRef = null; + } +} + +function resetDisplayTimeout() { + if (displayTimeoutRef) clearInterval(displayTimeoutRef); + + displayTimeoutRef = setInterval(() => { + if (Bangle.isLCDOn()) Bangle.setLCDPower(false); + clearTimers(); + }, ONE_SECOND * timeout); +} + +function startTimers(){ + if(intervalRef) clearTimers(); + intervalRef = setInterval(redraw, 50); + + resetDisplayTimeout(); + + redraw(); +} + +function loadSettings() { + const settings = readSettings(); + if (!settings) return; + + if (settings.character) characterSprite.character = settings.character; + if (settings.nightMode) nightMode = settings.nightMode; + if (settings.brightness) { + brightness = settings.brightness; + Bangle.setLCDBrightness(brightness); + } +} + +function updateSettings() { + const newSettings = { + character: characterSprite.character, + nightMode: nightMode, + brightness: brightness, + }; + writeSettings(newSettings); +} + +function checkBatteryLevel() { + if (Bangle.isCharging()) return; + if (E.getBattery() > 10) return; + if (phone.message !== null) return; + + phoneNewMessage("lowBatt", "Warning, battery is low"); +} + +// Main +function init() { + loadSettings(); + + clearInterval(); + + // Initialise display + Bangle.setLCDMode("80x80"); + + // Store screen dimensions + W = g.getWidth(); + H = g.getHeight(); + + // Get Mario to jump! + setWatch(() => { + if (intervalRef && !characterSprite.isJumping) characterSprite.isJumping = true; + resetDisplayTimeout(); + phoneClearMessage(); // Clear any phone messages and message timers + }, BTN3, {repeat: true}); + + // Close watch and load launcher app + setWatch(() => { + Bangle.setLCDMode(); + Bangle.showLauncher(); + }, BTN2, {repeat: false, edge: "falling"}); + + // Change info mode + setWatch(() => { + changeInfoMode(); + }, BTN1, {repeat: true}); + + Bangle.on('lcdPower', (on) => on ? startTimers() : clearTimers()); + + Bangle.on('faceUp', (up) => { + if (up && !Bangle.isLCDOn()) { + clearTimers(); + Bangle.setLCDPower(true); + } + }); + + Bangle.on('swipe', (sDir) => { + resetDisplayTimeout(); + + switch(sDir) { + // Swipe right (1) - change character (on a loop) + case 1: + switchCharacter(); + break; + // Swipe left (-1) - change day/night mode (on a loop) + case -1: + default: + toggleNightMode(); + } + + updateSettings(); + }); + + // Phone connectivity + try { NRF.wake(); } catch (e) {} + + NRF.on('disconnect', () => { + phoneNewMessage(null, "Phone disconnected"); + }); + + NRF.on('connect', () => { + setTimeout(() => { + phoneOutbound({ t: "status", bat: E.getBattery() }); + }, ONE_SECOND * 2); + phoneNewMessage(null, "Phone connected"); + }); + + GB = (evt) => phoneInbound(evt); + + startTimers(); + + setInterval(checkBatteryLevel, ONE_SECOND * 60 * 10); + checkBatteryLevel(); +} + +// Initialise! +init(); diff --git a/apps/multiclock/ChangeLog b/apps/multiclock/ChangeLog index 3ed2ed6b4..2f27f7f28 100644 --- a/apps/multiclock/ChangeLog +++ b/apps/multiclock/ChangeLog @@ -9,3 +9,5 @@ 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.13: Localised digi.js and timdat.js \ No newline at end of file diff --git a/apps/multiclock/apps_entry.json b/apps/multiclock/apps_entry.json index 19b6bc43d..6383609c1 100644 --- a/apps/multiclock/apps_entry.json +++ b/apps/multiclock/apps_entry.json @@ -14,8 +14,6 @@ {"name":"digi.face.js","url":"digi.min.js"}, {"name":"txt.face.js","url":"txt.min.js"}, {"name":"ped.face.js","url":"ped.js"}, - {"name":"osref.face.js","url":"osref.js"}, - {"name":"gps.face.js","url":"pgs.js"}, {"name":"multiclock.img","url":"multiclock-icon.js","evaluate":true} ] }, diff --git a/apps/multiclock/digi.js b/apps/multiclock/digi.js index 4422e6b62..0b2ca4aaa 100644 --- a/apps/multiclock/digi.js +++ b/apps/multiclock/digi.js @@ -1,5 +1,7 @@ (() => { +var locale = require("locale"); + function getFace(){ var buf = Graphics.createArrayBuffer(240,92,1,{msb:true}); @@ -19,7 +21,7 @@ function getFace(){ buf.drawString(time,buf.getWidth()/2,0); buf.setFont("6x8",2); buf.setFontAlign(0,-1); - var date = d.toString().substr(0,15); + var date = locale.dow(d, 1) + " " + locale.date(d, 1); buf.drawString(date, buf.getWidth()/2, 70); flip(); } diff --git a/apps/multiclock/gps.js b/apps/multiclock/gps.js deleted file mode 100644 index 3e756fc5e..000000000 --- a/apps/multiclock/gps.js +++ /dev/null @@ -1,95 +0,0 @@ -(() => { - - function getFace(){ - - //var img = require("heatshrink").decompress(atob("mEwghC/AH4AKg9wC6t3u4uVC6wWBI6t3uJeVuMQCqcBLisAi4XLxAABFxAXKgc4DBAuBRhQXEDAq7MmYXEwBHEXZYXFGAOqAAKDMmczC4mIC62CC50PC4JIBkQABiIvRmURAAUSjQXSFwMoxGKC6CRFwUSVYgXLPIgXXwMYegoXLJAYXCGBnzGA0hPQIwMgYwGC6gwCC4ZIMC4gYBC604C4ZISmcRVgapQAAMhC6GIJIwXCMBcIxGDDBAuLC4IwGAARGMAAQWGmAXPJQoWMC4pwCCpoXJAB4XXAH4A/ABQA=")); - var nofix = 0; - - function formatTime(now) { - var fd = now.toUTCString().split(" "); - return fd[4]; - } - - - function timeSince(t) { - var hms = t.split(":"); - var now = new Date(); - - var sn = 3600*(now.getHours()) + 60*(now.getMinutes()) + 1*(now.getSeconds()); - var st = 3600*(hms[0]) + 60*(hms[1]) + 1*(hms[2]); - - return (sn - st); - } - - function draw() { - var gps_on = false; - - var fix = { - fix: 0, - alt: 0, - lat: 0, - lon: 0, - speed: 0, - time: 0, - satellites: 0 - }; - - var y_line = 26; - var y_start = 46; - var x_start = 10; - - // only attempt to get gps fix if gpsservuce is loaded - if (WIDGETS.gpsservice !== undefined) { - fix = WIDGETS.gpsservice.gps_get_fix(); - gps_on = WIDGETS.gpsservice.gps_get_status(); - } - - g.reset(); - g.clearRect(0,24,239,239); - - if (fix.fix) { - var time = formatTime(fix.time); - var age = timeSince(time); - - g.setFontAlign(-1, -1); - g.setFont("6x8"); - g.setFontVector(22); - g.drawString("Alt: " + fix.alt +" m", x_start, y_start, true); - g.drawString("Lat: "+ fix.lat, x_start, y_start + y_line, true); - g.drawString("Lon: " + fix.lon, x_start, y_start + 2*y_line, true); - g.drawString("Time: " + time, x_start, y_start + 3*y_line, true); - g.drawString("Age(s): " + age, x_start, y_start + 4*y_line, true); - g.drawString("Satellites: " + fix.satellites, x_start, y_start + 5*y_line, true); - - } else if (gps_on) { - - g.setFontAlign(0, 1); - g.setFont("6x8", 2); - g.drawString("GPS Watch", 120, 60); - g.drawString("Waiting for GPS", 120, 80); - nofix = (nofix+1) % 4; - g.drawString(".".repeat(nofix) + " ".repeat(4-nofix), 120, 120); - g.setFontAlign(0,0); - g.drawString(fix.satellites + " satellites", 120, 100); - - } else if (!gps_on) { - - g.setFontAlign(0, 0); - g.setFont("6x8", 3); - g.drawString("GPS Watch", 120, 80); - g.drawString("GPS is off",120, 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/osref.js b/apps/multiclock/osref.js deleted file mode 100644 index c98b73134..000000000 --- a/apps/multiclock/osref.js +++ /dev/null @@ -1,202 +0,0 @@ -(() => { - - function getFace(){ - var nofix = 0; - - function formatTime(now) { - var fd = now.toUTCString().split(" "); - return fd[4]; - } - - function timeSince(t) { - var hms = t.split(":"); - var now = new Date(); - - var sn = 3600*(now.getHours()) + 60*(now.getMinutes()) + 1*(now.getSeconds()); - var st = 3600*(hms[0]) + 60*(hms[1]) + 1*(hms[2]); - - return (sn - st); - } - - - 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}`; - } - - function draw() { - var gps_on = false; - - var fix = { - fix: 0, - alt: 0, - lat: 0, - lon: 0, - speed: 0, - time: 0, - satellites: 0 - }; - - var y_line = 26; - var y_start = 46; - var x_start = 10; - - // only attempt to get gps fix if gpsservuce is loaded - if (WIDGETS.gpsservice !== undefined) { - fix = WIDGETS.gpsservice.gps_get_fix(); - gps_on = WIDGETS.gpsservice.gps_get_status(); - } - - g.reset(); - g.clearRect(0,24,239,239); - - if (fix.fix) { - var time = formatTime(fix.time); - var age = timeSince(time); - var os = OsGridRef.latLongToOsGrid(fix); - var ref = to_map_ref(6, os.easting, os.northing); - age = age >=0 ? age : 0; // avoid -1 etc - - g.reset(); - g.clearRect(0,24,239,239); - g.setFontAlign(0,0); - g.setFont("Vector"); - g.setColor(1,1,1); - g.setFontVector(40); - g.drawString(time, 120, 80); - - g.setFontVector(40); - g.setColor(0xFFC0); - g.drawString(ref, 120,140, true); - - g.setFont("6x8",2); - g.setColor(1,1,1); - g.drawString(age, 120, 194); - - } else if (gps_on) { - - g.setFontAlign(0, 1); - g.setFont("6x8", 2); - g.drawString("Gridref Watch", 120, 60); - g.drawString("Waiting for GPS", 120, 80); - nofix = (nofix+1) % 4; - g.drawString(".".repeat(nofix) + " ".repeat(4-nofix), 120, 120); - g.setFontAlign(0,0); - g.drawString(fix.satellites + " satellites", 120, 100); - - } else if (!gps_on) { - - g.setFontAlign(0, 0); - g.setFont("6x8", 3); - g.drawString("Gridref Watch", 120, 80); - g.drawString("GPS is off", 120, 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/timdat.js b/apps/multiclock/timdat.js index ff1bdf000..a4a93a691 100644 --- a/apps/multiclock/timdat.js +++ b/apps/multiclock/timdat.js @@ -1,16 +1,16 @@ (() => { + var locale = require("locale"); + var dayFirst = ["en_GB", "en_IN", "en_NAV", "de_DE", "nl_NL", "fr_FR", "en_NZ", "en_AU", "de_AT", "en_IL", "es_ES", "fr_BE", "de_CH", "fr_CH", "it_CH", "it_IT", "tr_TR", "pt_BR", "cs_CZ", "pt_PT"]; + var withDot = ["de_DE", "nl_NL", "de_AT", "de_CH", "hu_HU", "cs_CZ", "sl_SI"]; + 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; + var tm=d.toString().split(' ')[4].substring(0,5); + lastmin=d.getMinutes(); g.reset(); g.clearRect(0,24,239,239); var w=g.getWidth(); @@ -19,7 +19,16 @@ 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]; + var dt=locale.dow(d, 1) + " "; + if (dayFirst.includes(locale.name)) { + dt+=d.getDate(); + if (withDot.includes(locale.name)) { + dt+="."; + } + dt+=" " + locale.month(d, 1); + } else { + dt+=locale.month(d, 1) + " " + d.getDate(); + } g.drawString(dt,(w-g.stringWidth(dt))/2,160); g.flip(); } diff --git a/apps/notify/ChangeLog b/apps/notify/ChangeLog index a1e8e4418..2b7a4f990 100644 --- a/apps/notify/ChangeLog +++ b/apps/notify/ChangeLog @@ -2,3 +2,6 @@ 0.02: Add notification ID option 0.03: Pass `area{x,y,w,h}` to render callback instead of just `y` 0.05: Adjust position of notification src text +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 diff --git a/apps/notify/README.md b/apps/notify/README.md index cef9f2124..11c493102 100644 --- a/apps/notify/README.md +++ b/apps/notify/README.md @@ -9,14 +9,14 @@ other applications or widgets to display messages. ```JS options = { - on : bool, // turn screen on, default true - size : int, // height of notification, default 80 (max) + on : bool, // turn screen on, default true (But not if Quiet Mode is enabled) + size : int, // height of notification, default is fit to height (80 max) title : string, // optional title id // optional notification ID, used with hide() src : string, // optional source name body : string, // optional body text icon : string, // optional icon (image string) - render function(area) {} // function callback to render in area{x,y,w,h} + render function(area) {} // function callback to render in area{x,y,w,h} }; // eg... show notification require("notify").show({title:"Test", body:"Hello"}); diff --git a/apps/notify/notify.js b/apps/notify/notify.js index 6f5261de1..b5ef32d8b 100644 --- a/apps/notify/notify.js +++ b/apps/notify/notify.js @@ -41,7 +41,9 @@ function fitWords(text,rows,width) { src : string // optional source name body : string // optional body text icon : string // optional icon (image string) - render function(y) // function callback to render + 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) } */ /* @@ -68,8 +70,18 @@ exports.show = function(options) { options = options || {}; if (options.on===undefined) options.on = true; id = ("id" in options)?options.id:null; - let size = options.size || 80; - if (size>80) {size = 80} + let w = 240; + let text = []; + let size = options.size; + if (options.body) { + const bh = (size || 80) - 20, + maxRows=Math.floor((bh-4)/8), // font=6x8 + maxChars=Math.floor(w/6)-2; + text=fitWords(options.body, maxRows, maxChars); + // set size based on newlines + if (!size) size = 28 + (text.match(/\n/g).length+1)*8; + } else size = 20; + if (size>80) size = 80; const oldMode = Bangle.getLCDMode(); // TODO: throw exception if double-buffered? // TODO: throw exception if size>80? @@ -78,18 +90,17 @@ exports.show = function(options) { // drawing area let x = 0, y = 320-size, - w = 240, h = size, b = y+h-1, r = x+w-1; // bottom,right g.setClipRect(x,y, r,b); // clear area - g.setColor(0).fillRect(x,y, r,b); + g.setColor(options.bgColor||0).fillRect(x,y, r,b); // bottom border g.setColor(0x39C7).fillRect(0,b-1, r,b); b -= 2;h -= 2; // title bar if (options.title || options.src) { - g.setColor(0x39C7).fillRect(x,y, r,y+20); + 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.drawString(title.trim().substring(0, 13), x+25,y+3); @@ -97,20 +108,18 @@ exports.show = function(options) { g.setFont("6x8", 1).setFontAlign(1, 1, 0); g.drawString(options.src.substring(0, 10), g.getWidth()-23,y+18); } - y += 20;h -= 20; } + // we always need to pad because of the curved edges of the screen + y += 20; h -= 20; if (options.icon) { let i = options.icon, iw; g.drawImage(i, x,y+4); - if ("string"==typeof i) {iw = i.charCodeAt(0)} - else {iw = i[0]} + if ("string"==typeof i) iw = i.charCodeAt(0); + else iw = i[0]; x += iw;w -= iw; } // body text if (options.body) { - const maxRows=Math.floor((h-4)/8), // font=6x8 - maxChars=Math.floor(w/6)-2, - text=fitWords(options.body, maxRows, maxChars); g.setColor(-1).setFont("6x8", 1).setFontAlign(-1, -1, 0).drawString(text, x+6,y+4); } @@ -118,7 +127,9 @@ exports.show = function(options) { options.render({x:x, y:y, w:w, h:h}); } - if (options.on) Bangle.setLCDPower(1); // light up + if (options.on && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.setLCDPower(1); // light up + } Bangle.setLCDMode(oldMode); // clears cliprect function anim() { @@ -131,7 +142,7 @@ exports.show = function(options) { } anim(); Bangle.on("touch", exports.hide); -} +}; /** options = { @@ -150,4 +161,4 @@ exports.hide = function(options) { if (pos < 0) setTimeout(anim, 10); } anim(); -} +}; diff --git a/apps/notifyfs/ChangeLog b/apps/notifyfs/ChangeLog index 16bc0ebb3..974e138f7 100644 --- a/apps/notifyfs/ChangeLog +++ b/apps/notifyfs/ChangeLog @@ -4,3 +4,5 @@ 0.04: Pass `area{x,y,w,h}` to render callback instead of just `y` 0.05: Fix `g` corruption issue if .hide gets called twice 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 diff --git a/apps/notifyfs/notify.js b/apps/notifyfs/notify.js index 2c622f624..07801cedb 100644 --- a/apps/notifyfs/notify.js +++ b/apps/notifyfs/notify.js @@ -37,7 +37,9 @@ function fitWords(text,rows,width) { src : string // optional source name body : string // optional body text icon : string // optional icon (image string) - render function(y) // function callback to render + 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) } */ exports.show = function(options) { @@ -53,11 +55,11 @@ exports.show = function(options) { w = 240, h = size; // clear screen - g.clear(1); + g.setColor(options.bgColor||0).fillRect(0,0,g.getWidth(),g.getHeight()); // top bar if (options.title||options.src) { const title = options.title || options.src - g.setColor(0x39C7).fillRect(x, y, x+w-1, y+30); + g.setColor(options.titleBgColor||0x39C7).fillRect(x, y, x+w-1, y+30); g.setColor(-1).setFontAlign(-1, -1, 0).setFont("6x8", 3); g.drawString(title.trim().substring(0, 13), x+5, y+3); if (options.title && options.src) { @@ -88,8 +90,9 @@ exports.show = function(options) { const area={x:x, y:y, w:w, h:h} options.render(area); } - - if (options.on) Bangle.setLCDPower(1); // light up + if (options.on && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.setLCDPower(1); // light up + } Bangle.on("touch", exports.hide); // Create a fake graphics to hide draw attempts oldg = g; @@ -113,9 +116,11 @@ exports.hide = function(options) { Bangle.removeListener("touch", exports.hide); g.clear(); Bangle.drawWidgets(); - // flipping the screen off then on often triggers a redraw - it may not! - Bangle.setLCDPower(0); - Bangle.setLCDPower(1); + if (Bangle.isLCDOn() || !(require('Storage').readJSON('setting.json',1)||{}).quiet) { + // flipping the screen off then on often triggers a redraw - it may not! + Bangle.setLCDPower(0); + Bangle.setLCDPower(1); + } // hack for E.showMenu/showAlert/showPrompt - can force a redraw by faking next/back if (Bangle.btnWatches) { global["\xff"].watches[Bangle.btnWatches[0]].callback(); diff --git a/apps/omnitrix/ChangeLog b/apps/omnitrix/ChangeLog new file mode 100644 index 000000000..5d221b4c4 --- /dev/null +++ b/apps/omnitrix/ChangeLog @@ -0,0 +1 @@ +0.01: App Created! diff --git a/apps/omnitrix/README.md b/apps/omnitrix/README.md new file mode 100644 index 000000000..211426014 --- /dev/null +++ b/apps/omnitrix/README.md @@ -0,0 +1,23 @@ +
+

Omnitrix

+

+ +

+

This App is a portable library of intergalactic genetic data that allows the wielder to alter xer DNA at will and transform into the selected pre-scanned alien species.

+
+Made with JavaScipt Open Source Hardware Espruino Built with Love + + +
+ +
+ +## Requests + +Please leave bug reports and requests by raising an issue [here](https://github.com/ra101/BangleApps). + +
+ +## Creator + +[< RA >](https://github.com/ra101) diff --git a/apps/omnitrix/omnitrix.app.js b/apps/omnitrix/omnitrix.app.js new file mode 100644 index 000000000..c53cd7844 --- /dev/null +++ b/apps/omnitrix/omnitrix.app.js @@ -0,0 +1,81 @@ +g.clear(); + +// Draw the Backgound, that will not change +g.setColor(0.7, 0.7, 0.7).fillPoly([0, 0, 70, 0, 20, 120, 70, 240, 0, 240]); +g.setColor(0.7, 0.7, 0.7).fillPoly([240, 0, 170, 0, 220, 120, 170, 240, 240, 240]); +g.setColor(0, 1, 0).fillPoly([90, 0, 38, 120, 90, 240, 150, 240, 202, 120, 150, 0]); + +var avatar = { + width: 109, height: 140, bpp: 1, + transparent: 0, +}; + +// Shadows for each Alien +const avatar_shadow = [ + require("heatshrink").decompress(atob("AH4A/ADcwBhcB4gNLh0QFBnABpf8BhcH8BDL/+ABpUPFBk/+QoM+BDLGxYaBGxcP/56Lv/+IZn4FDE/FBcD/6jLn//DRhsLj4oLLwIoLGoJsKV4JsLv4NBXxIaCKJQaCc5IaCGxRQB/6jJNYRfKZQIoLBgQoJDQZRJLwQoJZQIoLn/fFBUB/wbCUZEP85sLv70BepMD/BSCIZPDFBUD/goCNhE/8AMB355JbQP9FBEf+DaB94oI/+BFAM/PJH+X4Px/gaI8BsBx56HIIOD//8PRE//BsB84oHgJ2BIYQoHIYIqB/IoIv/4NgPPPQ6HB4P//1/PQ8fNgXzFBBsDz4oHIIIqBNgIoHNgn+FBJsBYQIoINgeANg/8bISVJ8YoBv6VIGYP//aVKDQPfNg4oB86VBNhAoBIYP3NhCTBIQJwBXxBCC54aHJ4KvB904FBKvBuJDHFAXn/eOFBHxKQPcWA6+Bx/5/1nWA8HGgPP+BDInnn/l/gZDHgPnn/j/EeDQ8P3P+j/BQ48Anvv8H+DREA9//g/4DRED3/4n5CHAAMc//BZRAABkf8h4nIKQXgv4MJgF+gf4BhMH+EfIZLaBgP8BhImBj5DKNQP/IZX8G4JDK8E/co5eDIZnwh5eKj+AnxDLgJDKgfgh5DKj8AvgoK/EBIZUH4EODRUegFwBpXggZDKg8AjgaKnAcBBhJPBDgIAJJ4IcBABNwNhZPBNhZPBvBsLRIJsLn5sM/xsM/gNJh4qBBpX4dIICBDRChBv6kJ/44B/7NIh4zBgfwDRAlKUQWPBhSiBuANLkYMLAH4A2")), require("heatshrink").decompress(atob("AFkgBhcBwADFAAsMAYwAFuADCsAMHgfAAYMHDRE+gF+gE4IZHwgf/gIeCAAsP4Ef/8eFBAmBn//+AoI/EA///NhAoBGwP8FBF/gA2B8BsIFAN//4aIh/gFAIQBFBJRBNhED/kB//+DREf8BDBNhAoBwBsKFAMPNhUf+BeBNhCTBw4oBBhEH/gMBNhB3BAAXBBw4zBAAU/PYyEBAAhgFLYIAERIwMFIo0HBoxuFIQgACLpIACS4pQGYArVBAAxfELwxfGIY4oNSwgoaNg56FNg6jEIaqVFIY6+EZY5DSeg5DFFAP9Q5QoB46wL/xhFNg3gIwgoFAwRvC/JRFIYPwTAePPQoXBPof8FgIaFEII2C8CjFBIPAFAYvCcwhDBL4X4CgQNCj4TCKIXBL4gkCwAeBPwYoDAoI8BDwIKBDwIoDAoJlBIYWDFAhoCwBsDDwKIDDQIoBNggoDH4XgAYRsCFAY/CAYaVCKIZ1CQ4fhHoRDE4AaC/+HUYggBAoIMCFATMDIYSiCGYIoHUQZ6CFAv4LwZ6CFARNCIYh6CWAhyBBgR6CSovwHITnGbIaiCc4zZCIYbnGbIRDB+7nDNgQXCwACBFQJsFC4P8IYKFBNgqVDDAKuBNgpbCKgKuCNgqVCJgJcBNhHAv/+DAJsH/5UBHAP+IYJsFJgQgB+LZEFARcBHARmCFAp1BHAP4MwQoEGIKlCw/vNgYoB/IoBUoP8n1/IYYoBx6FBEAPh8JDDPQP8ngVBFAMPh6HDUYPh4YaBj/4uF/DQaCBhk/GAJTBgZeDFAP4EoOAg6SBj5eDKIPAJwJDBDoQaDh/+B4PgDoIHBIYgzBMIItCDoJDDHwSiBDoI4BDQkD/waB4EPHAMfcwRfC8DmBEoJOB3AMDgEcIAPwOIIHBDQgABagOAIYQAHcIJxCBg4lB8BxBDRE/ZYTWDAAglB+BsCBo6HCNgQAHEoU/XYYoGAQYoIDQKgFFApABUAqyEC4RsJC4QoKC4U/SpKECSpRDBCAZsGQgV/FBEPQgLBBUZF/YIhsJSoJEILYTBBFI8DCwV//7NHn5bBgYaBMAzxDh+fFA8/EYcfIgwkBeIb5CXopoDexH/ZAcBGoyRBT5AwDM45eFIYbLLAH4Al")), + require("heatshrink").decompress(atob("AF0B/ANLj/wDRf/wAaL/woL/42Lg//4ANKv/8DRngKBZDL/5DLv4NBFBIMCKJIMDRBEPBgf/JxAAEDY0fBghgGDQxgGGopSHBgpEGDQyyGDQyJFDQwoGQwhRHaoJRLn5DLPIwaGIYyVGDQ3wIbIoFgYoMbArnGLw4oFDQ4oFBgwoFXo4oFDQ4oEDQ56EggMHc4k3Bg7MDNY42FV442FV4PzGxIoCRI34Lwn4DQ3QFAnHBYl/SwYoB/zoE/ZfEFAPxDQnf//gBoQgBS4n+WIhsB/AoEMgJ6DBQPBFAmfIYv8FAn8IYgoB8AaE85DEj/+B4JHBFAb0E+E/Ugf+v6VDDAOLCwQABOQJRDj/4v/8FAeHFAl/4f/8YoDCoIMCgf+LwJ7DYgLnDh/wv/5L4fDSoYoBwf/54NDj5DDgP8n5aBRAd/SoQoB8KsCAAXjSoYoBNwMfKIiVDgbLBJgIACJQIaDh6wBOoJRDIYYoCIoIoEIYcB/EfQ4YoBNgkP4BDD/3/gZDDgF+IoIZC+4hBBgcD+Ef47mD4BsEj/A/xDC/j7BFAn+gbiBQwRsFg4vBx6vDwbZDNgPgegf4/gwBDQZYBNgfD8BsEUIMfIYSGBMoKHD/3AegfwCYIoDh/ng70Dw/AKIl/jE+IYX8nwoEAgPBV4fhRwIoDSYMPBocPwBREAgInCNgUdc4cH+EDDQfH8HfSonAbALXBQwOBDQcAjkBa4X/TQMPBgZuCBoM/YYIZEewf/RIJdBwAMFgYmBRIOAvgaGIYLNB/EDsANGZIQaBnYMGbYJcB/kBIY4oBUoPgj4MGSoLkB/8AuANHLgKiBBY56Cx//ZAYoGLwP8BhC+B84aKIIM/DRJDBUQPgBhB6B45eBABAmBj5eJFAPgIZUH/wBBFBXwAIIMIgP/wLLBFBP4g5sKn/An5sJgP+FQIaJg/wh5sKn+Av5sJgP8gZsKh/gj5sJc4JDBNhMD/EPFBUf4F/PRX+gf/L5JsBj4NKNgP//43INgMHBoIbINgM/BoKyIv6VBBoJuHa4QpBMBE/SYMPDRBqBXgV4DRCTKDQTlKUQIoDABH8vwoL8J2IQ4cPBhUAnEwBpb/JAGo")), + require("heatshrink").decompress(atob("AD8BBpkcBhcD/ANLn/gGpf/wANKh/+FBd/+AMKg//4BDLFBZDBFBcPKJl/FBcDFBkfFBn//hsMUZd//5DMZhc/SpaHBNhaHBNhZDBFBReBFBZDBFBReBSpaiBSpYaBSpReBFBZeBKJReBKJZeBPRReCFBReBZhS9BFBZeCepQMCFBJeCRBR5BFBReCFBR5CKJIaDFBJ5CFBJ5DKJJ5DFBIMDFBAaEKJAMDZhCUDFBN/84oKg/+RAYaI8IoKl/+KQYoGQwPgZgRRHDAKxC+YoHv/wWIWfFAwYBPYX8vwoH+B7C8YoIwBsCh7MGgf8bQX4CQLzG8BsC4JfHv4BBNYMP/goG/BsC8E/GwwoBNgQ5BGwwoBIYUDGwwoBNgXAh5tGj/ANgJBBn6WGvxsCIIIuBFAvwNgTSBL40P4BsBGYMHL40+NgQzBj5fFgPwIYJcCvgaFg+BDQPwCYQoGIYYTBL43wIYcAjwoG4YaBIAV4BoseWAJpCgJfG/CHDEIJfFgfHIYY2Hj5sBXYY2GvpDEGwyUCDQakGPIR2DUgx5Bf4hIDEIReEAwLbFKAIVEn8wIYy6E/iyEDQQ+Dg+mc4m/LwkAmuYAocdLwsBylABofNXoYABg2zAolvLwkAigoEmZ5FgEmFAcBKIJeEgMyAocJPIoHBUYiwBLwkA4iwGXAsgSoqqFgKwGTggAFgIaGAAoaMGo4aHKAoaSj4aLKAKGFAAqUBDRUADRkHDRk/DRZeBDRcfDRZDBUJRDBQxYoB8ANL/wMLgfwBpcPLxcAvgMLgJDMh4MLgF4KJiHLgE+IbMfQ7M/DTJQajyUMDRkDDRkYBplABpgAmA==")), + require("heatshrink").decompress(atob("AH4A/AH4A/AA0B/wNLn/8gfwBhED/4AB4AaJBpcBBgX/DREfBgX8BpAaD8AMHg4NDwBDLFBBDEFBEPBgSKJFAaIINhpRDUZJ6DXxIMC/C+MPJBfDDRMAv4aLL4S9JL4b0JRASUJL4avIL4gaKRAKUJFAZeJKIQaLv5rKIYQnKDQInLg54LDRrLBUJS9CDRheLDQI2KbARsKv4oLZQTYKZQJRLBgP4IZh6KIYKjKgfvNhc+n5sKgPhNhceIgIoK+AoLh+AKJd8FBcH4Ef+56JnyWB355J+EH/pfJj+An/nZhP8gP/n5DJ8EP/LMJC4N/55RIgf8gf+vxDJ8E/+YoJ/5DBz4oIg/4h/8UZM/4F/86jIEwMDNgLMIh/4n/5SpN/8P/54oKh++v4oJ/l9+4oK+fv36+IJ4P+vvPSpP/+fMvCVJ/+euKVJ/v4+OGSpPv4+MmBsJv18FBMP/Pj4eOFBFt78ehlwNhFs/HwkaVIjdnw+AjgoI7c8nkASpEH7Ph8EHDRF+48OgBsIgf3nFwgLZJzPDIZVu5wLBIZEB28wsEGDREN7ODgEgBpEOhkYBZAABE4K8IMAY0JAAUYPBAAD4J4IAAUGhgaLIgIMKgJsBABUMvANLv//NpUD3//WBAABnkPBpUB+Ef/4aJj+Aj/wBpP+gD0JgEHDBReCZZgoBABRBLAAP/ZZcHFBkPDRcAXpgA/AH4AfA==")), + require("heatshrink").decompress(atob("AH4A/AH4ATn4MLg0+BhUBwAaLnAMLh4aLgf4DRf/+AMKj//8AnK///DRV///+J5QaBIhQMBGxQ1CFBRQBL5UBFARDMPRQMMg55O4AaLLpQ1CQxRrQGxM/PJYaD/g1LGxQMC/+AQxQoKQwLmKcoRRKIYaIJDQZ6IIYaxJn/PNhUB/xSCNhEP+JsMG4RsJ/C/CNhPAFBf+FAPvFBEH8AoBn5sJdAP5SpEB/EP//HPRBDBv/+n56I/zNB/JRI5/gZoIDBXpDNBUoJQI/APB/YoIcYK+B75RHg5DC/zMIFAJDB+4oHQoPBWAO/KJH+Q4P8ZhE/+BDB+6jHFAODFAOf4ANGh5DC/n+FBBDC85DHgJDC/8fehBDC/DnIn6HBc4JDHegJDB/0eDQ8P4AaB+CwHgF+ZYP/w6wHgPwn5sBngoIwIaB8JsHgE8NgUOBg8B8BsCmAoINgWBIY8AvCwChgoJDQPgsAoINgUHBg4oBn+//E4Bg8D8f9//DWA8Aj8/56wJgC8Bv/wbJBCB+KwBFBCvBx/8nxsIPAN/8IoIQwPD/zrBIZMf+AoIZQPwWAPwBo7KBwf8h6+HZQP8j/gvgaHia8BWAOgBo+Z/0H/EJSpHP+E/LpEAgV/wP+BhBEBJ4JdIRAXgv4MJRAMDUJAoC/EfIZIoB4BDKgHmg5DKg8wjahHAAU5wHcBhMB4kGFBUOkE3FBVwgMcNhWAhHgBpIYBp5sKDAN4NhQqBKJU4gDnKgPAgP/NhUAh6xKuEAv4NJgfAgf/FJMeFAP/DZEB/ACB/4DBAA0PLgX/MBAyCgP8Q5AkIAAd/XhReBQpQABj4oMuauJAAMCgwaLAH4ApA")), + require("heatshrink").decompress(atob("AH4A/AD8B4ANLngFEhAMFh+AAwlABov4AokCBgsfAwsQAokD+BJFFws/DQsMAokHDQsAsAFEv4MFgZdFDQ0YAon/QAyNEDQ8OKwgaHuB4EDQ0DNgYaIjgaEUIoAB8AaERoqBBDRk4DRb6Ej/8DQxsDDQJ/ENgwaINgYaJngaD/wMGgJsCDQJxDXAgaLgF4DQavGIYIjCh55HFAn/Sg4oDDQJeHgYIC/55HgEeYIReIgBMCv7zHC4IoBg+/Lw8AlyzCLxEBKIMDgZDIc4Uwj5eHc4iiIDAJHCIZEBH4UfIY8AhgQCIZAoDh4oJJoV/FBBRCg5RJuAaCFBEjwEgXwJ6Hn/+gbZBXw4WB+EwegP4BgrUB/8DwbmHgYMB/EcFYInGBoXhAQJrHGoP/z40HSAQNBJ5BEEKYIaboBDIDQUDKQwaDKAMeDQ0/94NB4EDeo1x/4cCwBTGgUH+I3DZg8fMAaIIvE/BhUB4IaCWA8D4AoB/iVJNwWDSpEebYI0IAAI0C8AMIh5QCIQMEBo1wh9//wFBmAcHS4JQBkaHGFIPPFAMBdJCxBXYMPXwwIB+ZRCv58H/xSBbIK+Hg/Bz5RBj5UCAAn4jijBFAKLGg+A4Y2BFBHgh0fLgICCWAoOBL4JsBFAvn4Efg6kB5woFh/4gP4bQP/84oFn+An+D3/v/8fFAkD+EH/k8/+/+F/FAkTM4PD8f+//DPQkBnAcBv4lBv0fUYkBGoIOCbYIoFBwPwn7OB/PBZg0PDQKTB58PZ4IAE+EfSYP8nF/WAy/BgHh4HhXw04X4IQBgcPIY2AvEAjgBBngaFjkDCoJABsJDG4AYBFAQCBAAkGgIVBjEAhk4Boswh4eCFAJDFgcAuAeBJIMODQsYBwIeBFAISBIYozBMQIoBBgsEGYUMB4MIBosgGAVggEIoCHFIYQ4CB4JDIEoIECAAmAIYMAEoI1GIYcCB4ZDFBwIDBB4ZDFBQIDBgEQGwwKCGYwADBRQACHwgoHHwqlBAAoVEKwR7GMogMGCpB7FBpgA/AH4AtA=")), + require("heatshrink").decompress(atob("AH4A/AH4Ash4MLgP+Bpcf+ANL//ABhUHFBk/FBcD/+AIZf8IZngIZYoMn4oLgIoMh//IZn4DRgoLv4oLLwJRLDQIoKUQJ6Ln//ZhQaCZhQaBepQaCL5UfBgKIJV4IABL5IaCL5QMCL5KvBL5aUBL5SvBL5YaDL5AaEL5CUCL5J5DdBJ5DFBAaEFBB5DRBJeDRBEH/oMCv6IHn/PBgP7FA8D/wpC76IHj/xIQQoHLwJSC8YoHh/4FAUfFA9/4a9CFA8H/yWCwbaHn/gFAP8FA8D/4BBV4P/FAwVBFAQQBFAwVBIYQoHIYMHBoIQBFA1/+E/Q4IQBIY+BZQU/FA0//iVCCAPgUQ3ANgQQBUQ3+NgYoHIYJsCEAIoFIYn4FA5DEHIIoFIYiVCPQpDESoL1GIYiVBFAqPBwAMBDwQoFIYKwCDwRDGegQoILgJDCXwQoFJoS+DKIoUCBgKLC+BDFEgWfRYWAIYokB/hHCFAoUBwf/4ZHCFAt//iRBj4oHQIQmBFAX8IZABBCYLnEIYZuBOARDIWQIsBFApDCWQIsCFAhDEFQM/FAoGBCwI6B4K+FBAIWB/AoBHgK+EAwIWBwE/+CVGAwMP/gfBKoKVETgRDBB4JsBSogGBw5MBv6XBNggjB/hDBD4JsGIYPhJgKVBv4oFAwOPEYP/84oFEYP+GYImBQAIoEIYP7IYMfAYKVFAwPfSAIEBNgojBAAPAAgZDEF4I3BAgbZFEYZxB/JDFEYhxB47ZGCoIjBfIKMBSooVB8BxBaAQoE/xuBOITABFAqeBNgQEBIYgIBFAJsC4b0FFAM/NgX+nwaFj/xNgniBggoBx5sD5wMFgE8NIJsB/1gBo3jNgIoBXgh7Dj4aBPQJ4EAAd/BIMDSYgAECxAA/AH4A/ADQ=")), + require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/ABfYBhd/8AMKgf/+ANK///wAMJg4NMv///gMJh4aBIhQaBFBQ1C/waM/BrKGxYMCG5MfBgY3HE4YABS48/FBYaFRI4aFFA0BBgn/4AaL/41LRI5rERI41GL4waGRAoaHGwoaGL4sh//5EwbdBGwcBz//x5PDB4IbDE4P4PYY8BIggIDUIUHIggFBBARqCh5EEEoKkDC4N/FAYlBYoJcDVILNDEASyCcoIfBL4YgCCAIRBEQJDGCwQaBIY+AL4TJBIYwWBp5eCIYooB8BsD4CPBIYZsCXgZDFBQP4KAQtBD4JDDJIQaD+E/HoIoD/w1D//DKgRRD/DiBAARDFDAPHBgf4IYsf/zmD//nDQhRB/YMD/8/DQgoB74MDCQIaEKIKdBv+BCQKiDFAYHCg/9LwkAVIPHGAV/x6cBAAaIBh4VBg/wXog2C/EHToKiCBgg2B4E/D4Pgn4aFGwMDFAMfKQJeEBAP4nwEBvyOBBgixBLIbNCKIwVCFoLmECoYaCfAQMELAIwDboTZFDQY7CIggUCHYjnEAwX8CYqxDAoP3FAo2DDQUfEIc/3+MAwVn/f4Ogdw/g1DCgMPw6NDwPgPQlg8EgAoUEgODPQkGaQsMAwoA/AH4A/AH4A/AH4A/ABg=")), + require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4AIwAMLg/ABpcfBpn/FJcD/4aLh/8Bpc/+AMKgP/IhcH/woM/BfMFDRRNUZhDLh5DLBgJDLBgIoIgg0B//zZhEQg////YExFDBgLLJcgIAB8BPKKBc/v4aKLoIaLGwSvKE4P/NZA1BFBaFBBgM4BhR4BKI4mCGgMDZYwZCAAJ9BQhAAC+E8BgkBBgiHBNoonEDQI2FDQ8HDRX5wEMVgwAB869Hh4HC/5VBUQ1+AQMH+EfUQ0D/CJCwE/Qw0PMoMBUIKIBaowVBh/AgYPBfww+BBQJJDPQg+Bj4dBn4aGIgI1CJIYAEg/gEoRRHGIOADQRDHgE8g4XBj6HGAANwvgaBa4oAD44aBv4MIgT9Bn5rGMAfAh5dHAAUcKgIMJgAlBFQIAIgxUCBpIKBgKGHIYSZCDRM4AQMeBhAlDvxeKbwZeJNoJ7Ij4DCbJDzCAAIoIeYTMKZAb5BPI4/DvBDISRIACIYaiJeJIAChwMLAH4A/AH4A/AH4A/AH4AoA")) +]; + +var blinkFlag = 0; +function blink() { + if (blinkFlag) { + g.clear(); + blinkFlag ^= 1; + } + else { + g.setColor(0, 1, 0).fillPoly([0, 0, 0, 240, 240, 240, 240, 0]); + blinkFlag ^= 1; + } +} + +function morf() { + a = setInterval(blink, 10); + setTimeout(() => { clearTimeout(a); }, 500); + setTimeout(() => { load(); }, 1500); +} + +var i = 0; + +function draw(i) { + // fill the are bw > and < with green + g.setColor(0, 1, 0).fillPoly([90, 0, 38, 120, 90, 240, 150, 240, 202, 120, 150, 0]); + // select the alien's shadow + avatar.buffer = avatar_shadow[i]; + // Draw it + g.setColor(0, 0, 0).drawImage(avatar, 70, 50); + +} + +function next() { + i = (i + 1) % 10; + draw(i); +} + +function prev() { + i = (i + 9) % 10; + draw((i + 10) % 10); +} + +function init() { + draw(0); + + Bangle.on('touch', morf); + Bangle.on('swipe', dir => { + if (dir == 1) prev(); + else next(); + }); + + setWatch(next, BTN1, { repeat: true }); + setWatch(morf, BTN2); + setWatch(prev, BTN3, { repeat: true }); + +} + +init(); diff --git a/apps/omnitrix/omnitrix.icon.js b/apps/omnitrix/omnitrix.icon.js new file mode 100644 index 000000000..45c558f56 --- /dev/null +++ b/apps/omnitrix/omnitrix.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH5TPFtoxdW64ttGKwcJlcxrYAEmMrGDIYIg8xsOLABFhmMHGKoUHg1VwotJAAeFqsGGCQSHmItOGIkxGCIQGrYuSGAVbF55AHFygwCMB7TIF6ryPBgtWGC4cIGA7qFCQjvTF4j0FFxMrqwACAwMqGCGFlQtCAAUrGBAIEqoTDIgUwF58wPQYACqovHLxJEEsAuNsAbLGAheJSIjCNXgYaGMA4GECYwUDlQvLXgRKHJYYvHOQx1FqguJqgaOF47REIpGEFw+EPJQABmIvJChKRDgzCGwsGRpSQGF6B2DmAvFmCNLF7AwDrQuDrQuOF66nCMAheCa5QvZlRfJlQviRwVUX4tUX7B3KqpVCFwoABNIVVU5gvHIxQNCwovHwp6MPAQvHCpIUCXggAFYQRKJFAgvGOw6NCXgzCIDRQvJIo68CRpCREYQSOLF4RgLXhbCOLw5gLA4SNMSIwwELxAwGmIuElQuPAAKRCGAUxFxIvFCYIEDRpyRHDgovHGAwACrQuRAANaDxAuGF5FgFyYABsAvPGA9bRySQCrYuPMBErX6crLyDBKmIxOwsxXqAwNg8xsItJsMxg4uVAAUGGRVbAAgsKgwtPMRYASFyQxaFqoxXFrIxTFroAhA==")) diff --git a/apps/omnitrix/omnitrix.png b/apps/omnitrix/omnitrix.png new file mode 100644 index 000000000..a69f8a20e Binary files /dev/null and b/apps/omnitrix/omnitrix.png differ diff --git a/apps/omnitrix/screenshot.png b/apps/omnitrix/screenshot.png new file mode 100644 index 000000000..2b7dcd8c8 Binary files /dev/null and b/apps/omnitrix/screenshot.png differ diff --git a/apps/planetarium/ChangeLog b/apps/planetarium/ChangeLog index 5ff3fde91..78288f646 100644 --- a/apps/planetarium/ChangeLog +++ b/apps/planetarium/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! -0.02: Major speed improvement. Added more stars. Up to 500! \ No newline at end of file +0.02: Major speed improvement. Added more stars. Up to 500! +0.03: Added more stars and constellations. Now it shows 20 constellations. \ No newline at end of file diff --git a/apps/planetarium/README.md b/apps/planetarium/README.md index 56dc66be9..991fdffd2 100644 --- a/apps/planetarium/README.md +++ b/apps/planetarium/README.md @@ -4,7 +4,7 @@ This planetarium takes your position and time and plots the sky as it is. No planets, or moon, only stars. It can show the 500 most brilliant stars in the sky. -Plan is to show also constellations, but this is work in progress. Now it shows Taurus and Orion as examples. +It shows also constellations. Now it has 20 constellations but this is work in progress. Only northern hemisphere covered now. I think code is quite optimized already. It runs as fast as I could make it run. If someone has some ideas to speed it up, I could plot more stars. @@ -16,4 +16,17 @@ The planetarium plots the stars as if you are looking to the sky (with your watc ## Improvements I plan to add more constellations as soon as I have time. I am adding the constellations that I know of, but the plan is to add all the main ones (at least for North Hemisphere). -Please note that the watch hardware is limited and computing the stars positions is a quite intensive task for this little processor. This is why it plots only stars and no planets or the moon. For plotting the planets, storage will be a limiting factor as well as computing the position for planets needs more initial data compared with stars. \ No newline at end of file +Please note that the watch hardware is limited and computing the stars positions is a quite intensive task for this little processor. This is why it plots only stars and no planets or the moon. For plotting the planets, storage will be a limiting factor as well as computing the position for planets needs more initial data compared with stars. + +## Do you want to contribute? +Maybe you want to add some more constellations to the planetarium. As you can see I didn't cover constellations in the south hemisphere. How to do it? It is a bit tedious but it can be done and you will learn the constellations pretty well at the end of it. Steps: +- Open the file `plantearium.data.csv`. There you have the 500 stars that we have in the planetarium. The number of each star is the line number. For instance for Sirius, the star number will be 1. +- Find the two stars that you want to join in your constellation (a constellation line). For identifying a star you have to have a look at the name, usually something like `AL UMA` (alpha star of constellation ursa major). Lets say that you want to join stars 155 and 8 (this is constellation canis menor, that only has a line joining two stars). +- Go to file `planetarium.const.csv` and add two lines for your new constellation. The first one will be the name of the constellation, the second one the pair of stars that you want to join, separated by coma (see the file for examples). +- Do not forget to add a break line after the last constellation. +- Upload the new `planetarium.const.csv` to your bangle (or emulator) and test it out. + +What is a constellation star is not in the 500 star list in `planetarium.data.csv`? If you need another star to draw the constellation you can find that star in the full star list (`starinfo/planetarium.stars.csv`) and put it in `planetarium.extra.csv`. That stars will be loaded only when constellations are shown. In order to refer to these extra stars in the `planetarium.const.csv` you have to use the code `e_linenumber`, in order to differentiate them from the stars in the normal file. For instance, to refer to the first star in the file `planetarium.extra.csv`, you will refer it as `e_1`. Do not forget to add an extra line at the end as in the other files. + +## Development version +Please check the latest development version [here](https://github.com/pglez82/BangleApps) diff --git a/apps/planetarium/planetarium.app.js b/apps/planetarium/planetarium.app.js index 925a2c0dc..4763efb82 100644 --- a/apps/planetarium/planetarium.app.js +++ b/apps/planetarium/planetarium.app.js @@ -35,14 +35,59 @@ function siderealTime(julianDay) return toRadians(280.46061837 + 360.98564736629 * (julianDay - 2451545.0) + 0.000387933 * T * T - T * T * T / 38710000); } -function drawStars(lat,lon,date){ +/* +* Draws a single star in the sky. +* starPositions is a dictionary that gets modified and it is used later for ploting the constelations +*/ +function drawStar(zeta,theta,z,julianDay,latitude,longitude,starInfo,starNumber,starPositions){ + let starRA = parseFloat(starInfo[0]); + let starDE = parseFloat(starInfo[1]); + let starMag = parseFloat(starInfo[2]); + var dec = Math.asin(Math.sin(theta) * Math.cos(starDE) * Math.cos(starRA + zeta) + Math.cos(theta) * Math.sin(starDE)); + var ascen = Math.atan2(Math.cos(starDE) * Math.sin(starRA + zeta), Math.cos(theta) * Math.cos(starDE) * Math.cos(starRA + zeta) - Math.sin(theta) * Math.sin(starDE)) + z; + var H = siderealTime(julianDay) - longitude - ascen; + //Compute altitude + var alt = Math.asin(Math.sin(latitude) * Math.sin(dec) + Math.cos(latitude) * Math.cos(dec) * Math.cos(H)); + if(alt >= 0) + { + //Compute azimuth + var azi = Math.atan2(Math.sin(H), Math.cos(H) * Math.sin(latitude) - Math.tan(dec) * Math.cos(latitude)); + var x = size / 2 + size / 2 * Math.cos(alt) * Math.sin(azi); + var y = size / 2 + size / 2 * Math.cos(alt) * Math.cos(azi); + starPositions[starNumber] = [x,y]; + var magnitude = starMag<1?2:1; + //Stars between 1.5 and 4 magnitude should get a different colour + var col=1; + if (starMag<=1.5) + col=1; + else if (starMag>1.5 && starMag<2) + col=0.9; + else if (starMag>=2 && starMag<3) + col=0.7; + else if (starMag>=3 && starMag<3.5) + col=0.5; + else + col=0.3; + + g.setColor(col,col,col); + g.fillCircle(x, y, magnitude); + if (starMag<1 && settings.starnames) + g.drawString(starInfo[3],x,y+2); + g.flip(); + + } +} + + + +function plotSky(lat,lon,date){ var longitude = toRadians(-lon); var latitude = toRadians(lat); var julianDay = toJulianDay(date.getFullYear(), date.getMonth()+1,date.getDate(), date.getHours() + date.getTimezoneOffset() / 60, date.getMinutes(), date.getSeconds()); - var size = 240; + storage = require('Storage'); f=storage.read("planetarium.data.csv","r"); @@ -63,64 +108,50 @@ function drawStars(lat,lon,date){ while (lineend>=0) { line = f.substring(linestart,lineend); starNumber++; - //console.log(line); //Process the star starInfo = line.split(','); - //console.log(starInfo[0]); - starRA = parseFloat(starInfo[0]); - starDE = parseFloat(starInfo[1]); - starMag = parseFloat(starInfo[2]); - //var start = new Date().getTime(); - var dec = Math.asin(Math.sin(theta) * Math.cos(starDE) * Math.cos(starRA + zeta) + Math.cos(theta) * Math.sin(starDE)); - var ascen = Math.atan2(Math.cos(starDE) * Math.sin(starRA + zeta), Math.cos(theta) * Math.cos(starDE) * Math.cos(starRA + zeta) - Math.sin(theta) * Math.sin(starDE)) + z; - var H = siderealTime(julianDay) - longitude - ascen; - //Compute altitude - var alt = Math.asin(Math.sin(latitude) * Math.sin(dec) + Math.cos(latitude) * Math.cos(dec) * Math.cos(H)); - if(alt >= 0) - { - //Compute azimuth - var azi = Math.atan2(Math.sin(H), Math.cos(H) * Math.sin(latitude) - Math.tan(dec) * Math.cos(latitude)); - var x = size / 2 + size / 2 * Math.cos(alt) * Math.sin(azi); - var y = size / 2 + size / 2 * Math.cos(alt) * Math.cos(azi); - starPositions[starNumber] = [x,y]; - var magnitude = starMag<1?2:1; - //Stars between 1.5 and 4 magnitude should get a different colour - var col=1; - if (starMag<=1.5) - col=1; - else if (starMag>1.5 && starMag<2) - col=0.9; - else if (starMag>=2 && starMag<3) - col=0.7; - else if (starMag>=3 && starMag<3.5) - col=0.5; - else - col=0.3; - - g.setColor(col,col,col); - g.fillCircle(x, y, magnitude); - if (starMag<1 && settings.starnames) - g.drawString(starInfo[3],x,y+2); - g.flip(); - - } + drawStar(zeta,theta,z,julianDay,latitude,longitude,starInfo,starNumber,starPositions); linestart = lineend+1; lineend = f.indexOf("\n",linestart); } if (settings.constellations){ - //Each star has a number (the position on the file (line number)). These are the lines - //joining each star in the constellations. - constelations=[[[7,68],[10,53],[53,56],[28,68],"Orion"],[[13,172],[13,340],[293,340],[29,293],"Taurus"], - [[155,8],"Canis Menor"],[[36,81],[87,81],[235,87],[33,235],[33,75],[75,40],[36,235],"Ursa Major"],[[67,91],[74,67],[91,110],[110,252],"Cassiopeia"],[[23,166],[16,294],[294,44],[166,149],[230,149],[16,23],"Gemini"],[[88,218],[215,292],[218,292],[245,88],[292,245],[215,218],"Cepheus"],[[150,62],[150,175],[175,35],[403,62],[487,158],[384,487],[384,158],[35,158],[487,403],"Perseus"],[[19,65],[65,90],[65,147],[466,65],[466,189],[147,401],[213,90],"Cygnus"],[[6,42],[168,6],[168,113],[113,29],[104,29],[104,42],"Auriga"],[[1,47],[1,37],[37,22],[22,178],[37,89],"Can Maior"],[[3,118],[118,279],[279,286],[286,180],[180,316],[316,3],"Bootes"]]; - g.setColor(0,255,0); - for (i=0;i=0) { + line = fe.substring(linestart,lineend); + starNumber++; + starInfo = line.split(','); + drawStar(zeta,theta,z,julianDay,latitude,longitude,starInfo,"e_"+starNumber,starPositions); + linestart = lineend+1; + lineend = fe.indexOf("\n",linestart); + } + //End of ploting extra stars + + linenum=linestart = 0; + fc=storage.read("planetarium.const.csv","r"); + lineend = fc.indexOf("\n"); + while (lineend>=0) { + linenum++; + //In this file, each constelation are two lines. The first one the name, the second the lines joining stars + var name = fc.substring(linestart,lineend); + linestart = lineend+1; + lineend = fc.indexOf("\n",linestart); + var lines = fc.substring(linestart,lineend).split(','); + linestart = lineend+1; + lineend = fc.indexOf("\n",linestart); + g.setColor(0,255,0); + constelationShowing=false; - for (j=0;j modeNames[v], + onchange: function(v) { + if (v<0) v = 2; + if (v>2) v = 0; + require("qmsched").setMode(v); + this.value = v; + }, + }, + }; + scheds.sort((a, b) => (a.hr-b.hr)); + scheds.forEach((sched, idx) => { + const name = modeNames[sched.mode]; + const txt = formatTime(sched.hr)+" ".repeat(14-name.length)+name; + menu[txt] = function() { + showEditMenu(idx); + }; + }); + menu["Add Schedule"] = () => showEditMenu(-1); + menu["< Back"] = () => {load();}; + return E.showMenu(menu); +} + +function showEditMenu(index) { + const isNew = index<0; + let hrs = 12, mins = 0; + let mode = 1; + if (!isNew) { + const s = scheds[index]; + hrs = 0|s.hr; + mins = Math.round((s.hr-hrs)*60); + mode = s.mode; + } + const menu = { + "": {"title": (isNew ? "Add" : "Edit")+" Schedule"}, + "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' + }, + "Switch to": { + value: mode, + format: v => modeNames[v], + onchange: function(v) { + if (v<0) v = 2; + if (v>2) v = 0; + mode = v; + this.value = v; + }, // no arrow fn -> preserve 'this' + }, + }; + function getSched() { + const hr = hrs+(mins/60); + let day = 0; + // If schedule is for tomorrow not today (eg, in the past), set day + if (hr Save"] = function() { + if (isNew) { + scheds.push(getSched()); + } else { + scheds[index] = getSched(); + } + require("Storage").writeJSON("qmsched.json", scheds); + showMainMenu(); + }; + if (!isNew) { + menu["> Delete"] = function() { + scheds.splice(index, 1); + require("Storage").writeJSON("qmsched.json", scheds); + showMainMenu(); + }; + } + menu["< Cancel"] = showMainMenu; + return E.showMenu(menu); +} + +showMainMenu(); diff --git a/apps/qmsched/app.png b/apps/qmsched/app.png new file mode 100644 index 000000000..cf1fc29bc Binary files /dev/null and b/apps/qmsched/app.png differ diff --git a/apps/qmsched/boot.js b/apps/qmsched/boot.js new file mode 100644 index 000000000..3c53ef3f7 --- /dev/null +++ b/apps/qmsched/boot.js @@ -0,0 +1,24 @@ +// apply Quiet Mode schedules +(function qm() { + let scheds = require("Storage").readJSON("qmsched.json", 1) || []; + if (!scheds.length) return; + let next,idx; + scheds.forEach(function(s, i) { + if (!next || (s.hr+s.last*24)<(next.hr+next.last*24)) { + next = s; + idx = i; + } + }); + const now = new Date(), + hr = now.getHours()+(now.getMinutes()/60)+(now.getSeconds()/3600); + let t = 3600000*(next.hr-hr); + if (next.last===now.getDate()) t += 86400000; + /* update quiet mode at the correct time. */ + setTimeout(function() { + let scheds = require("Storage").readJSON("qmsched.json", 1) || []; + require("qmsched").setMode(scheds[idx].mode); + scheds[idx].last = (new Date()).getDate(); + require("Storage").writeJSON("qmsched.json", scheds); + qm(); // schedule next update + }, t); +})(); diff --git a/apps/qmsched/icon.js b/apps/qmsched/icon.js new file mode 100644 index 000000000..c0f4e2b66 --- /dev/null +++ b/apps/qmsched/icon.js @@ -0,0 +1,2 @@ +// https://icons8.com/icon/19324/no-reminders +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AElksF1wwtF4YwO0WiGFguBGFovfGB3MAAgwnFooxfGBAuJGEguLGEV/F5owh0YvpGH4vhGCQvd0YwQF7vMGCAveGCAvfGB4vgGBwvhGBouhGFLkIGEouIGEwvKGBguiGEQuNGEHN5owa5ouQ53P5/O5wyOGA3NDAIbBLyAUCAAQzCNBQwF0gVDXiQoBGQgAEEIILE0iSJdiozCFQw1FGBJgSABSVIeg7wQGSDDMFyQ0VCQQwdAAWcAAwPHGD4vPGD+iAAwRJGEgRLGEQRNeTwARF1wA/AH4AX")) \ No newline at end of file diff --git a/apps/qmsched/lib.js b/apps/qmsched/lib.js new file mode 100644 index 000000000..6cdf4f181 --- /dev/null +++ b/apps/qmsched/lib.js @@ -0,0 +1,17 @@ +/** + * Set new Quiet Mode and apply Bangle options + * @param {int} mode Quiet Mode + */ +exports.setMode = function(mode) { + let s = require("Storage").readJSON("setting.json", 1) || {}; + s.quiet = mode; + require("Storage").writeJSON("setting.json", s); + if (s.options) Bangle.setOptions(s.options); + if (mode && s.qmOptions) Bangle.setOptions(s.qmOptions); + if (mode && s.qmBrightness) { + if (s.qmBrightness!=1) Bangle.setLCDBrightness(s.qmBrightness); + } else { + if (s.brightness && s.brightness!=1) Bangle.setLCDBrightness(s.brightness); + } + if (mode && s.qmTimeout) Bangle.setLCDTimeout(s.qmTimeout); +}; \ No newline at end of file diff --git a/apps/qmsched/screenshot_edit.png b/apps/qmsched/screenshot_edit.png new file mode 100644 index 000000000..88b7fcad4 Binary files /dev/null and b/apps/qmsched/screenshot_edit.png differ diff --git a/apps/qmsched/screenshot_main.png b/apps/qmsched/screenshot_main.png new file mode 100644 index 000000000..634abd633 Binary files /dev/null and b/apps/qmsched/screenshot_main.png differ diff --git a/apps/rclock/ChangeLog b/apps/rclock/ChangeLog index fa62e12fb..61bf493c1 100644 --- a/apps/rclock/ChangeLog +++ b/apps/rclock/ChangeLog @@ -1,3 +1,5 @@ 0.01: First published version of app 0.02: Added support for locale and 12H clock 0.03: Added HR indication to clock +0.04: Update font size and alignment +0.05: Changes which circle show minutes and seconds \ No newline at end of file diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js index f9b8a9e6f..ceaffe910 100644 --- a/apps/rclock/rclock.app.js +++ b/apps/rclock/rclock.app.js @@ -23,24 +23,22 @@ // Ssettings const settings = { time: { - color: '#f0af00', - shadow: '#CF7500', + color: '#D6ED17', font: 'Vector', size: 60, - middle: screen.middle - 30, + middle: screen.middle, center: screen.center, }, date: { - color: '#f0af00', - shadow: '#CF7500', + color: '#D6ED17', font: 'Vector', size: 15, - middle: screen.height - 20, // at bottom of screen + middle: screen.height-17, // at bottom of screen center: screen.center, }, circle: { - colormin: '#eeeeee', - colorsec: '#bbbbbb', + colormin: '#ffffff', + colorsec: '#ffffff', width: 10, middle: screen.middle, center: screen.center, @@ -68,18 +66,6 @@ }; const drawMinArc = function (sections, color) { - g.setColor(color); - rad = (settings.circle.height / 2) - 20; - r1 = getArcXY(settings.circle.middle, settings.circle.center, rad, sections * (360 / 60) - 90); - //g.setPixel(r[0],r[1]); - r2 = getArcXY(settings.circle.middle, settings.circle.center, rad - settings.circle.width, sections * (360 / 60) - 90); - //g.setPixel(r[0],r[1]); - g.drawLine(r1[0], r1[1], r2[0], r2[1]); - g.setColor('#333333'); - g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4) - }; - - const drawSecArc = function (sections, color) { g.setColor(color); rad = (settings.circle.height / 2) - 40; r1 = getArcXY(settings.circle.middle, settings.circle.center, rad, sections * (360 / 60) - 90); @@ -88,7 +74,19 @@ //g.setPixel(r[0],r[1]); g.drawLine(r1[0], r1[1], r2[0], r2[1]); g.setColor('#333333'); - g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4) + g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4); + }; + + const drawSecArc = function (sections, color) { + g.setColor(color); + rad = (settings.circle.height / 2) - 20; + r1 = getArcXY(settings.circle.middle, settings.circle.center, rad, sections * (360 / 60) - 90); + //g.setPixel(r[0],r[1]); + r2 = getArcXY(settings.circle.middle, settings.circle.center, rad - settings.circle.width, sections * (360 / 60) - 90); + //g.setPixel(r[0],r[1]); + g.drawLine(r1[0], r1[1], r2[0], r2[1]); + g.setColor('#333333'); + g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4); }; const drawClock = function () { @@ -109,15 +107,13 @@ first = false; } - // Reset seconds + // Reset if (seconds == 59) { g.setColor('#000000'); - g.fillCircle(settings.circle.middle, settings.circle.center, (settings.circle.height / 2) - 40); - } - // Reset minutes - if (minutes == 59 && seconds == 59) { - g.setColor('#000000'); - g.fillCircle(settings.circle.middle, settings.circle.center, (settings.circle.height / 2) - 20); + g.fillCircle(settings.circle.middle, settings.circle.center, (settings.circle.height / 2)); + for (count = 0; count <= minutes; count++) { + drawMinArc(count, settings.circle.colormin); + } } //Get date as a string diff --git a/apps/route/ChangeLog b/apps/route/ChangeLog index 5560f00bc..02779b6ea 100644 --- a/apps/route/ChangeLog +++ b/apps/route/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Change color from red->yellow to ease readability (fix #710) diff --git a/apps/route/custom.html b/apps/route/custom.html index 124c92a31..b958303d8 100644 --- a/apps/route/custom.html +++ b/apps/route/custom.html @@ -143,7 +143,7 @@ var currentDist = 0; function drawMap() { g.clearRect(0,0,239,120); g.setFontAlign(0,0); - g.setColor(1,0,0); + g.setColor(1,1,0); g.setFontVector(40); g.drawString((currentDist===undefined)?"?":(Math.round(currentDist)+"m"), 160, 30); g.setColor(1,1,1); @@ -151,7 +151,7 @@ function drawMap() { g.drawString(Math.round(totalDistance)+"m", 160, 70); g.drawString((nextPtIdx/2)+"/"+coordDistance.length, 50, 20); if (!fix.fix) { - g.setColor(1,0,0); + g.setColor(1,1,0); g.drawString("No GPS", 50, 50); g.setFont("6x8",1); g.drawString(fix.satellites+" Sats", 50, 70); @@ -161,17 +161,17 @@ function drawMap() { g.setColor(0,0,0); g.drawCircle(lastFix.s.x,lastFix.s.y,10); } - for (var i=0;i { - var settings = require('Storage').readJSON('setting.json', true); - if (!settings) return; - if (settings.options) Bangle.setOptions(settings.options); - if (settings.brightness && settings.brightness!=1) Bangle.setLCDBrightness(settings.brightness); - if (settings.passkey!==undefined && settings.passkey.length==6) NRF.setSecurity({passkey:settings.passkey, mitm:1, display:1}); - if (settings.whitelist) NRF.on('connect', function(addr) { if (!settings.whitelist.includes(addr)) NRF.disconnect(); }); - delete settings; -})() diff --git a/apps/setting/settings.js b/apps/setting/settings.js index 8e839bdb1..12448d463 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -6,12 +6,17 @@ let settings; function updateSettings() { //storage.erase('setting.json'); // - not needed, just causes extra writes if settings were the same + if (Object.keys(settings.qmOptions).length === 0) delete settings.qmOptions; storage.write('setting.json', settings); + if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this always exists in this file } function updateOptions() { updateSettings(); Bangle.setOptions(settings.options) + if (settings.quiet) { + Bangle.setOptions(settings.qmOptions) + } } function gToInternal(g) { @@ -29,6 +34,7 @@ function resetSettings() { ble: true, // Bluetooth enabled by default blerepl: true, // Is REPL on Bluetooth - can Espruino IDE be used? log: false, // Do log messages appear on screen? + quiet: 0, // quiet mode: 0: off, 1: priority only, 2: total silence timeout: 10, // Default LCD timeout in seconds vibrate: true, // Vibration enabled by default. App must support beep: "vib", // Beep enabled by default. App must support @@ -48,13 +54,19 @@ function resetSettings() { twistThreshold: 819.2, twistMaxY: -800, twistTimeout: 1000 - } + }, + // Quiet Mode options: + // we only set these if we want to override the default value + // qmOptions: {}, + // qmBrightness: undefined, + // qmTimeout: undefined, }; updateSettings(); } settings = storage.readJSON('setting.json', 1); if (!settings) resetSettings(); +if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this always exists in here const boolFormat = v => v ? "On" : "Off"; @@ -97,12 +109,13 @@ function showMainMenu() { } } }, + "Quiet Mode": ()=>showQuietModeMenu(), 'Locale': ()=>showLocaleMenu(), 'Select Clock': ()=>showClockMenu(), 'Set Time': ()=>showSetTimeMenu(), 'LCD': ()=>showLCDMenu(), 'Reset Settings': ()=>showResetMenu(), - 'Turn Off': ()=>Bangle.off(), + 'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() }, '< Back': ()=>load() }; return E.showMenu(mainmenu); @@ -224,7 +237,9 @@ function showLCDMenu() { onchange: v => { settings.brightness = v || 1; updateSettings(); - Bangle.setLCDBrightness(settings.brightness); + if (!(settings.quiet && "qmBrightness" in settings)) { + Bangle.setLCDBrightness(settings.brightness); + } } }, 'LCD Timeout': { @@ -235,7 +250,9 @@ function showLCDMenu() { onchange: v => { settings.timeout = 0 | v; updateSettings(); - Bangle.setLCDTimeout(settings.timeout); + if (!(settings.quiet && "qmTimeout" in settings)) { + Bangle.setLCDTimeout(settings.timeout); + } } }, 'Wake on BTN1': { @@ -319,6 +336,104 @@ function showLCDMenu() { } return E.showMenu(lcdMenu) } +function showQuietModeMenu() { + // we always keep settings.quiet and settings.qmOptions + // other qm values are deleted when not set + const modes = ["Off", "Alarms", "Silent"]; + const qmDisabledFormat = v => v ? "Off" : "-"; + const qmMenu = { + "": {"title": "Quiet Mode"}, + "< Back": () => showMainMenu(), + "Quiet Mode": { + value: settings.quiet|0, + format: v => modes[v%3], + onchange: v => { + settings.quiet = v%3; + updateSettings(); + updateOptions(); + }, + }, + "LCD Brightness": { + value: settings.qmBrightness || 0, + min: 0, // 0 = use default + max: 1, + step: 0.1, + format: v => (v>0.05) ? v : "-", + onchange: v => { + if (v>0.05) { // prevent v=0.000000000000001 bugs + settings.qmBrightness = v; + } else { + delete settings.qmBrightness; + } + updateSettings(); + if (settings.qmBrightness) { // show result, even if not quiet right now + Bangle.setLCDBrightness(v); + } else { + Bangle.setLCDBrightness(settings.brightness); + } + }, + }, + "LCD Timeout": { + value: settings.qmTimeout || 0, + min: 0, // 0 = use default (no constant on for quiet mode) + max: 60, + step: 5, + format: v => v>1 ? v : "-", + onchange: v => { + if (v>1) { + settings.qmTimeout = v; + } else { + delete settings.qmTimeout; + } + updateSettings(); + if (settings.quiet && v>1) { + Bangle.setLCDTimeout(v); + } else { + Bangle.setLCDTimeout(settings.timeout); + } + }, + }, + // we disable wakeOn* events by overwriting them as false in qmOptions + // not disabled = not present in qmOptions at all + "Wake on FaceUp": { + value: "wakeOnFaceUp" in settings.qmOptions, + format: qmDisabledFormat, + onchange: () => { + if ("wakeOnFaceUp" in settings.qmOptions) { + delete settings.qmOptions.wakeOnFaceUp; + } else { + settings.qmOptions.wakeOnFaceUp = false; + } + updateOptions(); + }, + }, + "Wake on Touch": { + value: "wakeOnTouch" in settings.qmOptions, + format: qmDisabledFormat, + onchange: () => { + if ("wakeOnTouch" in settings.qmOptions) { + delete settings.qmOptions.wakeOnTouch; + } else { + settings.qmOptions.wakeOnTouch = false; + } + updateOptions(); + }, + }, + "Wake on Twist": { + value: "wakeOnTwist" in settings.qmOptions, + format: qmDisabledFormat, + onchange: () => { + if ("wakeOnTwist" in settings.qmOptions) { + delete settings.qmOptions.wakeOnTwist; + } else { + settings.qmOptions.wakeOnTwist = false; + } + updateOptions(); + }, + }, + }; + return E.showMenu(qmMenu); +} function showLocaleMenu() { const localemenu = { diff --git a/apps/setting/settings.min.json b/apps/setting/settings.min.json new file mode 100644 index 000000000..984054c11 --- /dev/null +++ b/apps/setting/settings.min.json @@ -0,0 +1 @@ +{"ble":true,"blerepl":true,"log":false,"timeout":10,"vibrate":true,"beep":"vib","timezone":0,"HID":false,"clock":null,"12hour":false,"brightness":1,"options":{"wakeOnBTN1":true,"wakeOnBTN2":true,"wakeOnBTN3":true,"wakeOnFaceUp":false,"wakeOnTouch":false,"wakeOnTwist":true,"twistThreshold":819.2,"twistMaxY":-800,"twistTimeout":1000}} \ No newline at end of file diff --git a/apps/simplest/README.md b/apps/simplest/README.md new file mode 100644 index 000000000..2fe597234 --- /dev/null +++ b/apps/simplest/README.md @@ -0,0 +1,5 @@ +# Simplest Clock + +The simplest working clock, acts as a tutorial piece + +![](screenshot.jpg) diff --git a/apps/simplest/app.js b/apps/simplest/app.js new file mode 100644 index 000000000..32992534e --- /dev/null +++ b/apps/simplest/app.js @@ -0,0 +1,24 @@ + +function draw() { + var d = new Date(); + var da = d.toString().split(" "); + var time = da[4].substr(0,5); + + g.reset(); + g.clearRect(0, 30, 239, 99); + g.setFontAlign(0, -1); + g.setFont("Vector", 80); + g.drawString(time, 120, 40); +} + +// handle switch display on by pressing BTN1 +Bangle.on('lcdPower', function(on) { + if (on) draw(); +}); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +setInterval(draw, 15000); // refresh every 15s +draw(); +setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); diff --git a/apps/simplest/icon.js b/apps/simplest/icon.js new file mode 100644 index 000000000..06f93e2ef --- /dev/null +++ b/apps/simplest/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV")) diff --git a/apps/simplest/screenshot.jpg b/apps/simplest/screenshot.jpg new file mode 100644 index 000000000..6e91d27f6 Binary files /dev/null and b/apps/simplest/screenshot.jpg differ diff --git a/apps/simplest/simplest.info.js b/apps/simplest/simplest.info.js new file mode 100644 index 000000000..7f7d98783 --- /dev/null +++ b/apps/simplest/simplest.info.js @@ -0,0 +1,7 @@ +require("Storage").write("simplest.info",{ + "id":"simplest", + "name":"Simplest Clock", + "src":"simplest.app.js", + "icon":"simplest.img", + "type":"clock" +}); diff --git a/apps/simplest/simplest.png b/apps/simplest/simplest.png new file mode 100644 index 000000000..cf057046b Binary files /dev/null and b/apps/simplest/simplest.png differ diff --git a/apps/sleepphasealarm/ChangeLog b/apps/sleepphasealarm/ChangeLog index 5560f00bc..47448167e 100644 --- a/apps/sleepphasealarm/ChangeLog +++ b/apps/sleepphasealarm/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Respect Quiet Mode diff --git a/apps/sleepphasealarm/app.js b/apps/sleepphasealarm/app.js index 1f8bf92ae..0de0b9afc 100644 --- a/apps/sleepphasealarm/app.js +++ b/apps/sleepphasealarm/app.js @@ -88,6 +88,7 @@ function drawApp() { var buzzCount = 19; function buzz() { + if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence Bangle.setLCDPower(1); Bangle.buzz().then(()=>{ if (buzzCount--) { diff --git a/apps/slidingtext/ChangeLog b/apps/slidingtext/ChangeLog new file mode 100644 index 000000000..932134ab1 --- /dev/null +++ b/apps/slidingtext/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial Release +0.02: Color Themes, Smoother scrolling +0.03: Added Spanish Language +0.04: Added German Language +0.05: BUGFIX: pedometer widget interfered with the clock Font Alignment diff --git a/apps/slidingtext/README.md b/apps/slidingtext/README.md new file mode 100644 index 000000000..d2d2fb5b6 --- /dev/null +++ b/apps/slidingtext/README.md @@ -0,0 +1,36 @@ +# Sliding Text Clock - See the time in different languages + +Inspired by the Pebble sliding clock, old times are scrolled off the screen and new times on. You are also able to change language on the fly so you can see the time written in other languages using button 1. Please use the upload page to choose which languages you want loaded. + +![](app.png) + +## Usage + +### Button 1 + +Use Button 1 (the top right button) to change the language + +| English | English (Traditional) | French | Japanese (Romanji) | +| ---- | ---- | ---- | ---- | +| ![](./format-01.jpg) | ![](format-02.jpg) | ![](format-03.jpg) |![](format-04.jpg) | +| **German** | **Spanish** | | | +| ![](./format-05.jpg) | ![](format-06.jpg) | | | + +### Button 3 +Button 3 (bottom right button) is used to change the colour + +| Black | Red | Gray | Purple | +| ---- | ---- | ---- | ---- | +| ![](./color-01.jpg) | ![](color-02.jpg) | ![](color-03.jpg) | ![](color-04.jpg) | + +## Further Details + +For further details of design and working please visit [The Project Page](https://www.notion.so/adrianwkirk/Sliding-Text-Clock-a8fe556f03624a619656ddbc4f36f41b) + +## Requests + +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/slidingtext/app.png b/apps/slidingtext/app.png new file mode 100644 index 000000000..3680c3ce6 Binary files /dev/null and b/apps/slidingtext/app.png differ diff --git a/apps/slidingtext/color-01.jpg b/apps/slidingtext/color-01.jpg new file mode 100644 index 000000000..49efb0481 Binary files /dev/null and b/apps/slidingtext/color-01.jpg differ diff --git a/apps/slidingtext/color-02.jpg b/apps/slidingtext/color-02.jpg new file mode 100644 index 000000000..446491cc4 Binary files /dev/null and b/apps/slidingtext/color-02.jpg differ diff --git a/apps/slidingtext/color-03.jpg b/apps/slidingtext/color-03.jpg new file mode 100644 index 000000000..0b26419a5 Binary files /dev/null and b/apps/slidingtext/color-03.jpg differ diff --git a/apps/slidingtext/color-04.jpg b/apps/slidingtext/color-04.jpg new file mode 100644 index 000000000..385c42a90 Binary files /dev/null and b/apps/slidingtext/color-04.jpg differ diff --git a/apps/slidingtext/custom.html b/apps/slidingtext/custom.html new file mode 100644 index 000000000..5e89e230b --- /dev/null +++ b/apps/slidingtext/custom.html @@ -0,0 +1,71 @@ + + + + + + +

Please select watch languages (Max 3, only the first 3 selected will be loaded)

+ + + + + + +
EnabledName
+ +

Click

+ + + + + + diff --git a/apps/slidingtext/format-01.jpg b/apps/slidingtext/format-01.jpg new file mode 100644 index 000000000..b8bc4552e Binary files /dev/null and b/apps/slidingtext/format-01.jpg differ diff --git a/apps/slidingtext/format-02.jpg b/apps/slidingtext/format-02.jpg new file mode 100644 index 000000000..c8b7a5e60 Binary files /dev/null and b/apps/slidingtext/format-02.jpg differ diff --git a/apps/slidingtext/format-03.jpg b/apps/slidingtext/format-03.jpg new file mode 100644 index 000000000..5dfdd8b23 Binary files /dev/null and b/apps/slidingtext/format-03.jpg differ diff --git a/apps/slidingtext/format-04.jpg b/apps/slidingtext/format-04.jpg new file mode 100644 index 000000000..19b01fd64 Binary files /dev/null and b/apps/slidingtext/format-04.jpg differ diff --git a/apps/slidingtext/format-05.jpg b/apps/slidingtext/format-05.jpg new file mode 100644 index 000000000..d6bd2b9aa Binary files /dev/null and b/apps/slidingtext/format-05.jpg differ diff --git a/apps/slidingtext/format-06.jpg b/apps/slidingtext/format-06.jpg new file mode 100644 index 000000000..493777d23 Binary files /dev/null and b/apps/slidingtext/format-06.jpg differ diff --git a/apps/slidingtext/slidingtext-icon.js b/apps/slidingtext/slidingtext-icon.js new file mode 100644 index 000000000..2fc42f023 --- /dev/null +++ b/apps/slidingtext/slidingtext-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lEowkE/4A/AH//+czCSKbBgXziYhKEQXxgMQgERgASIkf/CgM/+UBiISBCZAUBn4DBmcybAUTHZIUEmEQkcgl5gLJ4XygQCBj50MAYUQCZ3/HgXwkcyiZqBJxQoC+UD+cPXBIIBBQcwHIMBNwpNFDocQBoMwM4IUEn4kBMIZ3DAAMgQYoZCCYg6CVhKJFEQJyIn4VBkZrFDAgIJBxCqHO5DmJT4v/mQSIKxA+DDIIADCRJEDZgRCKIgjUHHJAPCPhprFExwSDJRgPDN5oUHCJ4A/AH4AIA==")) diff --git a/apps/slidingtext/slidingtext.dtfmt.js b/apps/slidingtext/slidingtext.dtfmt.js new file mode 100644 index 000000000..865ea47e6 --- /dev/null +++ b/apps/slidingtext/slidingtext.dtfmt.js @@ -0,0 +1,15 @@ +class DateFormatter { + /** + * A pure virtual class which all the other date formatters will + * inherit from. + * The name will be used to declare the date format when selected + * and the date formatDate methid will return the time formated + * to the lines of text on the screen + */ + name(){return "no name";} + formatDate(date){ + return ["no","date","defined"]; + } +} + +module.exports = DateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.js b/apps/slidingtext/slidingtext.js new file mode 100644 index 000000000..9de3c9f44 --- /dev/null +++ b/apps/slidingtext/slidingtext.js @@ -0,0 +1,646 @@ +/** + * Adrian Kirk 2021-02 + * Sliding text clock inspired by the Pebble + * clock with the same name + */ + +const color_schemes = [ + { + name: "black", + background : [0.0,0.0,0.0], + main_bar: [1.0,1.0,1.0], + other_bars: [0.85,0.85,0.85], + }, + { + name: "red", + background : [1.0,0.0,0.0], + main_bar: [1.0,1.0,0.0], + other_bars: [0.85,0.85,0.85] + }, + { + name: "grey", + background : [0.5,0.5,0.5], + main_bar: [1.0,1.0,1.0], + other_bars: [0.0,0.0,0.0], + }, + { + name: "purple", + background : [1.0,0.0,1.0], + main_bar: [1.0,1.0,0.0], + other_bars: [0.85,0.85,0.85] + }, + { + name: "blue", + background : [0.4,0.7,1.0], + main_bar: [1.0,1.0,1.0], + other_bars: [0.9,0.9,0.9] + } +]; + +let color_scheme_index = 0; + + +/** + * The Watch Display + */ + +function bg_color(){ + return color_schemes[color_scheme_index].background; +} + +function main_color(){ + return color_schemes[color_scheme_index].main_bar; +} + +function other_color(){ + return color_schemes[color_scheme_index].other_bars; +} + +let command_stack_high_priority = []; +let command_stack_low_priority = []; + +function next_command(){ + command = command_stack_high_priority.pop(); + if(command == null){ + //console.log("Low priority command"); + command = command_stack_low_priority.pop(); + } else { + //console.log("High priority command"); + } + if(command != null){ + command.call(); + } else { + //console.log("no command"); + } +} + +function reset_commands(){ + command_stack_high_priority = []; + command_stack_low_priority = []; +} + +function has_commands(){ + return command_stack_high_priority.length > 0 || + command_stack_low_priority.lenth > 0; +} + +class ShiftText { + /** + * Class Responsible for shifting text around the screen + * + * This is a object that initializes itself with a position and + * text after which you can tell it where you want to move to + * using the moveTo method and it will smoothly move the text across + * at the selected frame rate and speed + */ + constructor(x,y,txt,font_name, + font_size,speed_x,speed_y,freq_millis, + color, + bg_color){ + this.x = x; + this.tgt_x = x; + this.init_x = x; + this.y = y; + this.tgt_y = y; + this.init_y = y; + this.txt = txt; + this.init_txt = txt; + this.font_name = font_name; + this.font_size = font_size; + this.speed_x = Math.abs(speed_x); + this.speed_y = Math.abs(speed_y); + this.freq_millis = freq_millis; + this.color = color; + this.bg_color = bg_color; + this.finished_callback=null; + this.timeoutId = null; + } + setColor(color){ + this.color = color; + } + setBgColor(bg_color){ + this.bg_color = bg_color; + } + reset(hard_reset) { + //console.log("reset"); + this.hide(); + this.x = this.init_x; + this.y = this.init_y; + if (hard_reset) { + this.txt = this.init_txt; + } + this.show(); + if(this.timeoutId != null){ + clearTimeout(this.timeoutId); + } + } + show() { + g.setFontAlign(-1,-1,0); + g.setFont(this.font_name,this.font_size); + g.setColor(this.color[0],this.color[1],this.color[2]); + g.drawString(this.txt, this.x, this.y); + } + hide(){ + g.setFontAlign(-1,-1,0); + g.setFont(this.font_name,this.font_size); + //console.log("bgcolor:" + this.bg_color); + g.setColor(this.bg_color[0],this.bg_color[1],this.bg_color[2]); + g.drawString(this.txt, this.x, this.y); + /*g.fillPoly([this.x - 1, this.y, + 240, this.y, + 240, this.y + this.font_size, + this.x -1 , this.y + this.font_size, + ]); + */ + } + setText(txt){ + this.txt = txt; + } + setTextPosition(txt,x,y){ + this.hide(); + this.x = x; + this.y = y; + this.txt = txt; + this.show(); + } + setTextXPosition(txt,x){ + this.hide(); + this.x = x; + this.txt = txt; + this.show(); + } + setTextYPosition(txt,y){ + this.hide(); + this.y = y; + this.txt = txt; + this.show(); + } + moveTo(new_x,new_y){ + this.tgt_x = new_x; + this.tgt_y = new_y; + this._doMove(); + } + moveToX(new_x){ + this.tgt_x = new_x; + this._doMove(); + } + moveToY(new_y){ + this.tgt_y = new_y; + this._doMove(); + } + onFinished(finished_callback){ + this.finished_callback = finished_callback; + } + /** + * private internal method for directing the text move. + * It will see how far away we are from the target coords + * and move towards the target at the defined speed. + */ + _doMove(){ + this.hide(); + // move closer to the target in the x direction + var diff_x = this.tgt_x - this.x; + var finished_x = false; + if(Math.abs(diff_x) <= this.speed_x){ + this.x = this.tgt_x; + finished_x = true; + } else { + if(diff_x > 0){ + this.x += this.speed_x; + } else { + this.x -= this.speed_x; + } + } + // move closer to the target in the y direction + var diff_y = this.tgt_y - this.y; + var finished_y = false; + if(Math.abs(diff_y) <= this.speed_y){ + this.y = this.tgt_y; + finished_y = true; + } else { + if(diff_y > 0){ + this.y += this.speed_y; + } else { + this.y -= this.speed_y; + } + } + this.show(); + this.timeoutId = null; + var finished = finished_x & finished_y; + if(!finished){ + this.timeoutId = setTimeout(this._doMove.bind(this), this.freq_millis); + } else if(this.finished_callback != null){ + //console.log("finished - calling:" + this.finished_callback); + this.finished_callback.call(); + this.finished_callback = null; + } + } +} + +const CLOCK_TEXT_SPEED_X = 10; +// a list of display rows +let row_displays = [ + new ShiftText(240,50,'',"Vector",40,CLOCK_TEXT_SPEED_X,1,10,main_color(),bg_color()), + new ShiftText(240,90,'',"Vector",30,CLOCK_TEXT_SPEED_X,1,10,other_color(),bg_color()), + new ShiftText(240,120,'',"Vector",30,CLOCK_TEXT_SPEED_X,1,10,other_color(),bg_color()), + new ShiftText(240,150,'',"Vector",30,CLOCK_TEXT_SPEED_X,1,10,other_color(),bg_color()), + new ShiftText(240,180,'',"Vector",40,CLOCK_TEXT_SPEED_X,1,10,main_color(),bg_color()) +]; + +function nextColorTheme(){ + //console.log("next color theme"); + color_scheme_index += 1; + if(color_scheme_index >= row_displays.length){ + color_scheme_index = 0; + } + setColorScheme(color_schemes[color_scheme_index]); + reset_clock(true); + draw_clock(); +} + +function setColorScheme(color_scheme){ + setColor(color_scheme.main_bar, + color_scheme.other_bars, + color_scheme.background); +} + +function setColor(main_color,other_color,bg_color){ + row_displays[0].setColor(main_color); + row_displays[0].setBgColor(bg_color); + for(var i=1; i= date_formatters.length){ + date_formatter_idx = 0; + } + console.log("changing to formatter " + date_formatter_idx); + date_formatter = date_formatters[date_formatter_idx]; + reset_clock(true); + draw_clock(); + command_stack_high_priority.unshift( + function() { + //console.log("move in new:" + txt); + // first select the top or bottom to display the formatter name + // We choose the first spare row without text + var format_name_display = row_displays[row_displays.length - 1]; + if (format_name_display.txt != '') { + format_name_display = row_displays[0]; + } + if (format_name_display.txt != ''){ + return; + } + format_name_display.speed_x = 3; + format_name_display.onFinished(function(){ + format_name_display.speed_x = CLOCK_TEXT_SPEED_X; + console.log("return speed to:" + format_name_display.speed_x) + next_command(); + }); + format_name_display.setTextXPosition(date_formatter.name(),220); + format_name_display.moveToX(-date_formatter.name().length * format_name_display.font_size); + } + ); + +} + +var DISPLAY_TEXT_X = 20; +function reset_clock(hard_reset){ + console.log("reset_clock hard_reset:" + hard_reset); + + setColorScheme(color_schemes[color_scheme_index]); + if(!hard_reset && last_draw_time != null){ + // If its not a hard reset then we want to reset the + // rows set to the last time. If the last time is too long + // ago then we fast forward to 1 min ago. + // In this way the watch wakes by scrolling + // off the last time and scroll on the new time + var reset_time = last_draw_time; + var last_minute_millis = Date.now() - 60000; + if(reset_time.getTime() < last_minute_millis){ + reset_time = display_time(new Date(last_minute_millis)); + } + var rows = date_formatter.formatDate(reset_time); + for (var i = 0; i < rows.length; i++) { + row_displays[i].hide(); + row_displays[i].speed_x = CLOCK_TEXT_SPEED_X; + row_displays[i].x = DISPLAY_TEXT_X; + row_displays[i].y = row_displays[i].init_y; + if(row_displays[i].timeoutId != null){ + clearTimeout(row_displays[i].timeoutId); + } + row_displays[i].setText(rows[i]); + row_displays[i].show(); + } + } else { + // do a hard reset and clear everything out + for (var i = 0; i < row_displays.length; i++) { + row_displays[i].speed_x = CLOCK_TEXT_SPEED_X; + row_displays[i].reset(hard_reset); + } + } + + reset_commands(); +} + +let last_draw_time = null; +const next_minute_boundary_secs = 10; + +function display_time(date){ + if(date.getSeconds() > 60 - next_minute_boundary_secs){ + console.log("forwarding to next minute"); + return new Date(date.getTime() + next_minute_boundary_secs * 1000); + } else { + return date; + } +} + +function draw_clock(){ + var date = new Date(); + + // we don't want the time to be displayed + // and then immediately be trigger another time + if(last_draw_time != null && + Date.now() - last_draw_time.getTime() < next_minute_boundary_secs * 1000 && + has_commands() ){ + console.log("skipping draw clock"); + return; + } else { + last_draw_time = date; + } + reset_commands(); + date = display_time(date); + console.log("draw_clock:" + last_draw_time.toISOString() + " display:" + date.toISOString()); + // for debugging only + //date.setMinutes(37); + var rows = date_formatter.formatDate(date); + var display; + for (var i = 0; i < rows.length; i++) { + display = row_displays[i]; + var txt = rows[i]; + //console.log(i + "->" + txt); + display_row(display,txt); + } + // If the dateformatter has not returned enough + // rows then treat the reamining rows as empty + for (var j = i; j < row_displays.length; j++) { + display = row_displays[j]; + //console.log(i + "->''(empty)"); + display_row(display,''); + } + next_command(); + //console.log(date); +} + +function display_row(display,txt){ + if(display == null) { + console.log("no display for text:" + txt) + return; + } + + if(display.txt == null || display.txt == ''){ + if(txt != '') { + command_stack_high_priority.unshift( + function () { + //console.log("move in new:" + txt); + display.onFinished(next_command); + display.setTextXPosition(txt, 240); + display.moveToX(DISPLAY_TEXT_X); + } + ); + } + } else if(txt != display.txt && display.txt != null){ + command_stack_high_priority.push( + function(){ + //console.log("move out:" + txt); + display.onFinished(next_command); + display.moveToX(-display.txt.length * display.font_size); + } + ); + command_stack_low_priority.push( + function(){ + //console.log("move in:" + txt); + display.onFinished(next_command); + display.setTextXPosition(txt,240); + display.moveToX(DISPLAY_TEXT_X); + } + ); + } else { + command_stack_high_priority.push( + function(){ + //console.log("move in2:" + txt); + display.setTextXPosition(txt,DISPLAY_TEXT_X); + next_command(); + } + ); + } +} + +/** + * 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; + console.log("match"); + setColorScheme(color_schemes[color_scheme_index]); + break; + } + } +} + +function set_dateformat(dateformat_name){ + console.log("setting date format:" + dateformat_name); + for (var i=0; i < date_formatters.length; i++) { + if(date_formatters[i].name() == dateformat_name){ + date_formatter_idx = i; + date_formatter = date_formatters[date_formatter_idx]; + console.log("match"); + } + } +} + +const PREFERENCE_FILE = "slidingtext.settings.json"; +/** + * Called on startup to set the watch to the last preference settings + */ +function load_settings(){ + try{ + settings = require("Storage").readJSON(PREFERENCE_FILE); + if(settings != null){ + console.log("loaded:" + JSON.stringify(settings)); + if(settings.color_scheme != null){ + set_colorscheme(settings.color_scheme); + } + if(settings.date_format != null){ + set_dateformat(settings.date_format); + } + } else { + console.log("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 = { + date_format : date_formatter.name(), + color_scheme : color_schemes[color_scheme_index].name, + }; + console.log("saving:" + JSON.stringify(settings)); + require("Storage").writeJSON(PREFERENCE_FILE,settings); +} + +function button1pressed() { + changeFormatter(); + save_settings(); +} + +function button3pressed() { + console.log("button3pressed"); + nextColorTheme(); + reset_clock(true); + draw_clock(); + save_settings(); +} + +// The interval reference for updating the clock +let intervalRef = null; + +function clearTimers(){ + if(intervalRef != null) { + clearInterval(intervalRef); + intervalRef = null; + } +} + +function startTimers(){ + var date = new Date(); + var secs = date.getSeconds(); + var nextMinuteStart = 60 - secs; + //console.log("scheduling clock draw in " + nextMinuteStart + " seconds"); + setTimeout(scheduleDrawClock,nextMinuteStart * 1000); + draw_clock(); +} + +/** + * confirms that a redraw is needed by checking the last redraw time and + * the lcd state of the UI + * @returns {boolean|*} + */ +function shouldRedraw(){ + return last_draw_time != null && + Date.now() - last_draw_time.getTime() > next_minute_boundary_secs * 1000 + && Bangle.isLCDOn(); +} + +function scheduleDrawClock(){ + clearTimers(); + if (Bangle.isLCDOn()) { + console.log("schedule draw of clock"); + intervalRef = setInterval(() => { + if (!shouldRedraw()) { + console.log("draw clock callback - skipped redraw"); + } else { + console.log("draw clock callback"); + draw_clock() + } + }, 60 * 1000 + ); + + if (shouldRedraw()) { + draw_clock(); + } else { + console.log("scheduleDrawClock - skipped redraw"); + } + } else { + console.log("scheduleDrawClock - skipped not visible"); + } +} + +Bangle.on('lcdPower', (on) => { + if (on) { + console.log("lcdPower: on"); + Bangle.drawWidgets(); + reset_clock(false); + startTimers(); + } else { + console.log("lcdPower: off"); + reset_clock(false); + 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(); +// Show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2,{repeat:false,edge:"falling"}); + + +// Handle button 1 being pressed +setWatch(button1pressed, BTN1,{repeat:true,edge:"falling"}); + +// Handle button 3 being pressed +setWatch(button3pressed, BTN3,{repeat:true,edge:"falling"}); diff --git a/apps/slidingtext/slidingtext.locale.de.js b/apps/slidingtext/slidingtext.locale.de.js new file mode 100644 index 000000000..11124c24a --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.de.js @@ -0,0 +1,94 @@ +var DateFormatter = require("slidingtext.dtfmt.js"); + +const germanNumberStr = [ ["ZERO",""], // 0 + ["EINS",""], // 1 + ["ZWEI",""], //2 + ["DREI",''], //3 + ["VIER",''], //4 + ["FÜNF",''], //5 + ["SECHS",''], //6 + ["SEIBEN",''], //7 + ["ACHT",''], //8 + ["NUEN",''], // 9, + ["ZEHN",''], // 10 + ["ELF",''], // 11, + ["ZWÖLF",''], // 12 + ["DREI",'ZEHN'], // 13 + ["VIER",'ZEHN'], // 14 + ["FÜNF",'ZEHN'], // 15 + ["SECH",'ZEHN'], // 16 + ["SIEB",'ZEHN'], // 17 + ["ACHT",'ZEHN'], // 18 + ["NEUN",'ZEHN'], // 19 +]; + +const germanTensStr = ["ZERO",//0 + "ZEHN",//10 + "ZWANZIG",//20 + "DREIßIG",//30 + "VIERZIG",//40 + "FÜNFZIG",//50 + "SECHZIG"//60 +] + +const germanUnit = ["",//0 + "EINUND",//1 + "ZWEIUND",//2 + "DREIUND",//3 + "VIERUND", //4 + "FÜNFUND", //5 + "SECHSUND", //6 + "SEIBENUND", //7 + "ACHTUND", //8 + "NEUNUND" //9 +] + +function germanHoursToText(hours){ + hours = hours % 12; + if(hours == 0){ + hours = 12; + } + return germanNumberStr[hours][0]; +} + +function germanMinsToText(mins) { + if (mins < 20) { + return germanNumberStr[mins]; + } else { + var tens = (mins / 10 | 0); + var word1 = germanTensStr[tens]; + var remainder = mins - tens * 10; + var word2 = germanUnit[remainder]; + return [word2, word1]; + } +} + +class GermanDateFormatter extends DateFormatter { + constructor() { super();} + name(){return "German";} + formatDate(date){ + var mins = date.getMinutes(); + var hourOfDay = date.getHours(); + var hours = germanHoursToText(hourOfDay); + //console.log('hourOfDay->' + hourOfDay + ' hours text->' + hours) + // Deal with the special times first + if(mins == 0){ + var hours = germanHoursToText(hourOfDay); + return [hours,"UHR", "","",""]; + } /*else if(mins == 30){ + var hours = germanHoursToText(hourOfDay+1); + return ["", "", "HALB","", hours]; + } else if(mins == 15){ + var hours = germanHoursToText(hourOfDay); + return ["", "", "VIERTEL", "NACH",hours]; + } else if(mins == 45) { + var hours = germanHoursToText(hourOfDay+1); + return ["", "", "VIERTEL", "VOR",hours]; + } */ else { + var mins_txt = germanMinsToText(mins); + return [hours, "UHR", mins_txt[0],mins_txt[1]]; + } + } +} + +module.exports = GermanDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.locale.en.js b/apps/slidingtext/slidingtext.locale.en.js new file mode 100644 index 000000000..7d37fcae1 --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.en.js @@ -0,0 +1,15 @@ +var DateFormatter = require("slidingtext.dtfmt.js"); +const hoursToText = require("slidingtext.utils.en.js").hoursToText; +const numberToText = require("slidingtext.utils.en.js").numberToText; + +class EnglishDateFormatter extends DateFormatter { + constructor() { super();} + name(){return "English";} + formatDate(date){ + var hours_txt = hoursToText(date.getHours()); + var mins_txt = numberToText(date.getMinutes()); + return [hours_txt,mins_txt[0],mins_txt[1]]; + } +} + +module.exports = EnglishDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.locale.en2.js b/apps/slidingtext/slidingtext.locale.en2.js new file mode 100644 index 000000000..cd07e8848 --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.en2.js @@ -0,0 +1,53 @@ +var DateFormatter = require("slidingtext.dtfmt.js"); +const hoursToText = require("slidingtext.utils.en.js").hoursToText; +const numberToText = require("slidingtext.utils.en.js").numberToText; + +class EnglishTraditionalDateFormatter extends DateFormatter { + constructor() { + super(); + } + name(){return "English (Traditional)";} + formatDate(date){ + var mins = date.getMinutes(); + var hourOfDay = date.getHours(); + if(mins > 30){ + hourOfDay += 1; + } + var hours = hoursToText(hourOfDay); + // Deal with the special times first + if(mins == 0){ + return [hours,"", "O'","CLOCK"]; + } else if(mins == 30){ + return ["","HALF", "PAST", "", hours]; + } else if(mins == 15){ + return ["","QUARTER", "PAST", "", hours]; + } else if(mins == 45) { + return ["", "QUARTER", "TO", "", hours]; + } + var mins_txt; + var from_to; + var mins_value; + if(mins > 30){ + mins_value = 60-mins; + from_to = "TO"; + mins_txt = numberToText(mins_value); + } else { + mins_value = mins; + from_to = "PAST"; + mins_txt = numberToText(mins_value); + } + if(mins_txt[1] != '') { + return ['', mins_txt[0], mins_txt[1], from_to, hours]; + } else { + if(mins_value % 5 == 0) { + return ['', mins_txt[0], from_to, '', hours]; + } else if(mins_value == 1){ + return ['', mins_txt[0], 'MINUTE', from_to, hours]; + } else { + return ['', mins_txt[0], 'MINUTES', from_to, hours]; + } + } + } +} + +module.exports = EnglishTraditionalDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.locale.es.js b/apps/slidingtext/slidingtext.locale.es.js new file mode 100644 index 000000000..e1f3bc18b --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.es.js @@ -0,0 +1,77 @@ +var DateFormatter = require("slidingtext.dtfmt.js"); + +const spanishNumberStr = [ ["ZERO"], // 0 + ["UNA",""], // 1 + ["DOS",""], //2 + ["TRES",''], //3 + ["CUATRO",''], //4 + ["CINCO",''], //5 + ["SEIS",''], //6 + ["SEITO",''], //7 + ["OCHO",''], //8 + ["NUEVE",''], // 9, + ["DIEZ",''], // 10 + ["ONCE",''], // 11, + ["DOCE",''], // 12 + ["TRECE",''], // 13 + ["CATORCE",''], // 14 + ["QUINCE",''], // 15 + ["DIECI",'SEIS'], // 16 + ["DIECI",'SIETE'], // 17 + ["DIECI",'OCHO'], // 18 + ["DIECI",'NEUVE'], // 19 + ["VEINTA",''], // 20 + ["VEINTI",'UNO'], // 21 + ["VEINTI",'DOS'], // 22 + ["VEINTI",'TRES'], // 23 + ["VEINTI",'CUATRO'], // 24 + ["VEINTI",'CINCO'], // 25 + ["VEINTI",'SEIS'], // 26 + ["VEINTI",'SIETE'], // 27 + ["VEINTI",'OCHO'], // 28 + ["VEINTI",'NUEVE'] // 29 + ]; + +function spanishHoursToText(hours){ + hours = hours % 12; + if(hours == 0){ + hours = 12; + } + return spanishNumberStr[hours][0]; +} + +function spanishMinsToText(mins){ + return spanishNumberStr[mins]; +} + +class SpanishDateFormatter extends DateFormatter { + constructor() { super();} + name(){return "Spanish";} + formatDate(date){ + var mins = date.getMinutes(); + var hourOfDay = date.getHours(); + if(mins > 30){ + hourOfDay += 1; + } + var hours = spanishHoursToText(hourOfDay); + //console.log('hourOfDay->' + hourOfDay + ' hours text->' + hours) + // Deal with the special times first + if(mins == 0){ + return [hours,"", "","",""]; + } else if(mins == 30){ + return [hours, "Y", "MEDIA",""]; + } else if(mins == 15){ + return [hours, "Y", "CUARTO",""]; + } else if(mins == 45) { + return [hours, "MENOS", "CUARTO",""]; + } else if(mins > 30){ + var mins_txt = spanishMinsToText(60-mins); + return [hours, "MENOS", mins_txt[0],mins_txt[1]]; + } else { + var mins_txt = spanishMinsToText(mins); + return [hours, "Y", mins_txt[0],mins_txt[1]]; + } + } +} + +module.exports = SpanishDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.locale.fr.js b/apps/slidingtext/slidingtext.locale.fr.js new file mode 100644 index 000000000..5844c1a4e --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.fr.js @@ -0,0 +1,70 @@ +var DateFormatter = require("slidingtext.dtfmt.js"); + +/** + * French date formatting + */ +const frenchNumberStr = [ "ZERO", "UNE", "DEUX", "TROIS", "QUATRE", + "CINQ", "SIX", "SEPT", "HUIT", "NEUF", "DIX", + "ONZE", "DOUZE", "TREIZE", "QUATORZE","QUINZE", + "SEIZE", "DIX SEPT", "DIX HUIT","DIX NEUF", "VINGT", + "VINGT ET UN", "VINGT DEUX", "VINGT TROIS", + "VINGT QUATRE", "VINGT CINQ", "VINGT SIX", + "VINGT SEPT", "VINGT HUIT", "VINGT NEUF" +]; + +function frenchHoursToText(hours){ + hours = hours % 12; + if(hours == 0){ + hours = 12; + } + return frenchNumberStr[hours]; +} + +function frenchHeures(hours){ + if(hours % 12 == 1){ + return 'HEURE'; + } else { + return 'HEURES'; + } +} + +class FrenchDateFormatter extends DateFormatter { + constructor() { super(); } + name(){return "French";} + formatDate(date){ + var hours = frenchHoursToText(date.getHours()); + var heures = frenchHeures(date.getHours()); + var mins = date.getMinutes(); + if(mins == 0){ + if(hours == 0){ + return ["MINUIT", "",""]; + } else if(hours == 12){ + return ["MIDI", "",""]; + } else { + return [hours, heures,""]; + } + } else if(mins == 30){ + return [hours, heures,'ET DEMIE']; + } else if(mins == 15){ + return [hours, heures,'ET QUART']; + } else if(mins == 45){ + var next_hour = date.getHours() + 1; + hours = frenchHoursToText(next_hour); + heures = frenchHeures(next_hour); + return [hours, heures,"MOINS",'LET QUART']; + } + if(mins > 30){ + var to_mins = 60-mins; + var mins_txt = frenchNumberStr[to_mins]; + next_hour = date.getHours() + 1; + hours = frenchHoursToText(next_hour); + heures = frenchHeures(next_hour); + return [ hours, heures , "MOINS", mins_txt ]; + } else { + mins_txt = frenchNumberStr[mins]; + return [ hours, heures , mins_txt ]; + } + } +} + +module.exports = FrenchDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.locale.jp.js b/apps/slidingtext/slidingtext.locale.jp.js new file mode 100644 index 000000000..c28780e88 --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.jp.js @@ -0,0 +1,71 @@ +var DateFormatter = require("slidingtext.dtfmt.js"); + +/** + * Japanese date formatting + */ +const japaneseHourStr = [ "ZERO", "ICHII", "NI", "SAN", "YO", + "GO", "ROKU", "SHICHI", "HACHI", "KU", "JUU", + 'JUU ICHI', 'JUU NI']; +const tensPrefixStr = [ "", + "JUU", + 'NIJUU', + 'SAN JUU', + 'YON JUU', + 'GO JUU']; + +const japaneseMinuteStr = [ ["", "PUN"], + ["IP","PUN" ], + ["NI", "FUN"], + ["SAN", "PUN"], + ["YON","FUN"], + ["GO", "HUN"], + ["RO", "PUN"], + ["NANA", "FUN"], + ["HAP", "PUN"], + ["KYU","FUN"], + ["JUP", "PUN"] +]; + +function japaneseHoursToText(hours){ + hours = hours % 12; + if(hours == 0){ + hours = 12; + } + return japaneseHourStr[hours]; +} + +function japaneseMinsToText(mins){ + if(mins == 0){ + return ["",""]; + } else if(mins == 30) + return ["HAN",""]; + else { + var units = mins % 10; + var mins_txt = japaneseMinuteStr[units]; + var tens = mins /10 | 0; + if(tens > 0){ + var tens_txt = tensPrefixStr[tens]; + var minutes_txt; + if(mins_txt[0] != ''){ + minutes_txt = mins_txt[0] + ' ' + mins_txt[1]; + } else { + minutes_txt = mins_txt[1]; + } + return [tens_txt, minutes_txt]; + } else { + return [mins_txt[0], mins_txt[1]]; + } + } +} + +class JapaneseDateFormatter extends DateFormatter { + constructor() { super(); } + name(){return "Japanese (Romanji)";} + formatDate(date){ + var hours_txt = japaneseHoursToText(date.getHours()); + var mins_txt = japaneseMinsToText(date.getMinutes()); + return [hours_txt,"JI", mins_txt[0], mins_txt[1] ]; + } +} + +module.exports = JapaneseDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.png b/apps/slidingtext/slidingtext.png new file mode 100644 index 000000000..9ebf691b3 Binary files /dev/null and b/apps/slidingtext/slidingtext.png differ diff --git a/apps/slidingtext/slidingtext.utils.en.js b/apps/slidingtext/slidingtext.utils.en.js new file mode 100644 index 000000000..a91fcbd16 --- /dev/null +++ b/apps/slidingtext/slidingtext.utils.en.js @@ -0,0 +1,34 @@ +const numberStr = ["ZERO","ONE", "TWO", "THREE", "FOUR", "FIVE", + "SIX", "SEVEN","EIGHT", "NINE", "TEN", + "ELEVEN", "TWELVE", "THIRTEEN", "FOURTEEN", + "FIFTEEN", "SIXTEEN", "SEVENTEEN", "EIGHTEEN", + "NINETEEN", "TWENTY"]; +const tensStr = ["ZERO", "TEN", "TWENTY", "THIRTY", "FOURTY", + "FIFTY"]; + +const hoursToText = (hours)=>{ + hours = hours % 12; + if(hours == 0){ + hours = 12; + } + return numberStr[hours]; +} + +const numberToText = (value)=> { + var word1 = ''; + var word2 = ''; + if(value > 20){ + var tens = (value / 10 | 0); + word1 = tensStr[tens]; + var remainder = value - tens * 10; + if(remainder > 0){ + word2 = numberStr[remainder]; + } + } else if(value > 0) { + word1 = numberStr[value]; + } + return [word1,word2]; +} + +exports.hoursToText = hoursToText; +exports.numberToText = numberToText; \ No newline at end of file diff --git a/apps/speedalt/ChangeLog b/apps/speedalt/ChangeLog index f969e904c..de5c9c221 100644 --- a/apps/speedalt/ChangeLog +++ b/apps/speedalt/ChangeLog @@ -5,3 +5,6 @@ 0.05: Add setting to turn vibrate on/off. 0.06: Tweaks to vibration settings. 0.07: Switch to BTN1 for Max toggle and reset function. +1.00: New features. Added waypoints file and distance to selected waypoint display. Added integration with GPS Setup module to switch GPS to low power mode when screen off. Save display settings and restore when app restarted. +1.01: Add third screen mode with large clock and waypoint selection display to ease visibility in bright daylight. +1.02: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings. diff --git a/apps/speedalt/README.md b/apps/speedalt/README.md index e1d00653e..db3c7e673 100644 --- a/apps/speedalt/README.md +++ b/apps/speedalt/README.md @@ -1,18 +1,151 @@ -Displays the GPS speed and altitude. One is displayed on the watch face using the largest possible characters depending on the number of digits. The other is in a smaller characters below that. Both are always visible. You can display the current or maximum observed speed/altitude values. Current time is always displayed. +# GPS Speed, Altimeter and Distance to Waypoint -Display Tap : Swaps the displays. You can have either speed or altitude on the large primary display. +You can switch between three display modes. One showing speed and altitude (A), one showing speed and distance to waypoint (D) and a large dispay of time and selected waypoint. -BTN1 : Short press < 2 secs toggles the displays between showing the current speed/alt values or the maximum values recorded. +Within the [A]ltitude and [D]istance displays modes one figure is displayed on the watch face using the largest possible characters depending on the number of digits. The other is in a smaller characters below that. Both are always visible. You can display the current or maximum observed speed/altitude values. Current time is always displayed. + +The waypoints list is the same as that used with the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app so the same set of waypoints can be used across both apps. Refer to that app for waypoint file information. + +## Buttons and Controls + +BTN3 : Cycles the modes between Speed+[A]ltitude, Speed+[D]istance and large Time/Waypoint + +### [A]ltitude mode + +BTN1 : Short press < 2 secs toggles the displays between showing the current speed/alt values or the maximum speed/alt values recorded. BTN1 : Long press > 2 secs resets the recorded maximum values. -App Settings : Select the desired display units. Speed can be as per the default locale, kph, knots, mph or m/s. Altitude can be feet or metres. Select one of three colour schemes. Colours, high contrast (all white on black) or night ( all red on black ). Vibration can be used to indicate when a fix is lost or gained. One buzz for a lost fix and a double buzz when a fix is found. +### [D]istance mode -![](screen1.png) -![](screen2.png) -![](screen3.png) -![](screen4.png) +BTN1 : Select next waypoint. Last fix distance from selected waypoint is displayed. + +### Large mode + +BTN1 : Select next waypoint. + +### All modes + +BTN2 : Disables/Restores power saving timeout. Locks the screen on and GPS in SuperE mode to enable reading for longer periods but uses maximum battery drain. Red LED (dot) at top of screen when screen is locked on. Press again to restore power saving timeouts. + +BTN3 : Long press exit and return to watch. + +BTN4 : Left Display Tap : Swaps which figure is in the large display. You can have either speed or [A]ltitude/[D]istance on the large primary display. + +## App Settings + +Select the desired display units. Speed can be as per the default locale, kph, knots, mph or m/s. Distance can be km, miles or nautical miles. Altitude can be feet or metres. Select one of three colour schemes. Default (three colours), high contrast (all white on black) or night ( all red on black ). + +## Kalman Filter + +This filter smooths the altitude and the speed values and reduces these values 'jumping around' from one GPS fix to the next. The down side of this is that if these values change rapidly ( eg. a quick change in altitude ) then it can take a few GPS fixes for the values to move to the new vlaues. Disabling the Kalman filter in the settings will cause the raw values to be displayed from each GPS fix as they are found. + +## Loss of fix + +When the GPS obtains a fix the number of satellites is displayed as 'Sats:nn'. When unable to obtain a fix then the last known fix is used and the age of that fix in seconds is displayed as 'Age:nn'. Seeing 'Sats' or 'Age' indicates whether the GPS has a current fix or not. + +## Screens + +Speed and Altitude:
+![](screen1.png)

+Left tap swaps displays:
+![](screen2.png)

+Distance to waypoint DeltaW:
+![](screen5.png)

+MAX Values instead:
+![](screen3.png)

+Settings:
+![](screen4.png)

+ +## Power Saving + +The The GPS Adv Sport app obeys the watch screen off timeouts as a power saving measure. Restore the screen as per any of the colck/watch apps. Use BTN2 to lock the screen on but doing this will use more battery. + +This app will work quite happily on its own but will use the [GPS Setup App](https://banglejs.com/apps/#gps%20setup) if it is installed. You may choose to use the GPS Setup App to gain significantly longer battery life while the GPS is on. Please read the Low Power GPS Setup App Readme to understand what this does. + +When using the GPS Setup App this app switches the GPS to SuperE (default) mode while the display is lit and showing fix information. This ensures that that fixes are updated every second or so. 10 seconds after the display is blanked by the watch this app will switch the GPS to PSMOO mode and will only attempt to get a fix every two minutes. This improves power saving while the display is off and the delay gives an opportunity to restore the display before the GPS power mode is switched. + +The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode. + +## Waypoints + +Waypoints are used in [D]istance mode. Create a file waypoints.json and write to storage on the Bangle.js using the IDE. The first 6 characters of the name are displayed in Speed+[D]istance mode. + +The [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app in the App Loader has a really nice waypoints file editor. (Must be connected to your Bangle.JS and then click on the Download icon.) + +Sample waypoints.json (My sailing waypoints) + +

+[
+  {
+  "name":"NONE"
+  },
+  {
+  "name":"Omori",
+  "lat":-38.9058670,
+  "lon":175.7613350
+  },
+  {
+  "name":"DeltaW",
+  "lat":-38.9438550,
+  "lon":175.7676930
+  },
+  {
+  "name":"DeltaE",
+  "lat":-38.9395240,
+  "lon":175.7814420
+  },
+  {
+  "name":"BtClub",
+  "lat":-38.9446020,
+  "lon":175.8475720
+  },
+  {
+  "name":"Hapua",
+  "lat":-38.8177750,
+  "lon":175.8088720
+  },
+  {
+  "name":"Nook",
+  "lat":-38.7848090,
+  "lon":175.7839440
+  },
+  {
+  "name":"ChryBy",
+  "lat":-38.7975050,
+  "lon":175.7551960
+  },
+  {
+  "name":"Waiha",
+  "lat":-38.7219630,
+  "lon":175.7481520
+  },
+  {
+  "name":"KwaKwa",
+  "lat":-38.6632310,
+  "lon":175.8670320
+  },
+  {
+  "name":"Hatepe",
+  "lat":-38.8547420,
+  "lon":176.0089124
+  },
+  {
+  "name":"Kinloc",
+  "lat":-38.6614442,
+  "lon":175.9161607
+  }
+]
+
+ +## Comments and Feedback Developed for my use in sailing, cycling and motorcycling. If you find this software useful or have feedback drop me a line mike[at]kereru.com. Enjoy! -( Many thanks to Gordon Williams. Awesome job. ) +## Thanks + +Many thanks to Gordon Williams. Awesome job. + +Special thanks also to @jeffmer, for the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app and @hughbarney for the Low power GPS code development and Wouter Bulten for the Kalman filter code. + + diff --git a/apps/speedalt/app-icon.js b/apps/speedalt/app-icon.js index 6c03df55b..f4f24a18b 100644 --- a/apps/speedalt/app-icon.js +++ b/apps/speedalt/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("m02xH+AH4AJlgAMGWQ4lEw1Wq2BAAgHBG8QhFq+C1nXABGswQ4GGjhkBGJIAHOQI2ZDIcyGaQ3EmQ3WNAiaKABusNyoUDNCxuGGySdDNDBuGUoY0e2etAAWzGzoOCGheJxNlAA2J1o2LGprTNGRAAFbZw0LqwZI1pkGAAgJDUxdWGxTULFAmJFJGzURjaET6Q0DGZKYIrqjRT5Q0EGZwpEUZZqPaYaRMGg6LINhBJKNK40JAAI1Iq6fJGjDrIUQoVCwRqJDhA0OXYQRGwRsEAgWsNTCeIDYTwG1g1GqygJNROsq0ymVWq7TI2ZRJUQg1JUARkMAAg0FDhY1GDAxOKQwgAEKI6IKKAQ1KeAQ1IJ4VXAog1gC4Q1ImQVBAwYFBmQ1K1o1fMoQ1ORIQ1Ua5ahD1uzrqnEULo1L1gVBAAusGrDxHC4NlEY4aDAAZQGDhiHCGpZOJNodWmUyqxpIRBY1GQw7wCURAAPKIWtXhI1EwSFJNhIAMNQSgHwQ1EGwVXKBJsWDJSgENgjyKGyg0CNQ/XNQo1DwIRGbIS+HABYWLwI1GGwVWKhZtQChigGNhg2TCRhqIGwcy1g2YCBmsmQ0INgajIYgY1PdRKfCGpCjMG4ShNBxSfKGwyjIADOsGho1DbRI0YagQ1MG0Y0RGwjbLACLTDGh42FqxuY1lWGig2FmRuWwKdDGiZuHG6WBNC42JHAWCVBWswQyEGjQ3IHAJyBAAgHBCAwzbG5QAMGb44SGUgAkA")) +require("heatshrink").decompress(atob("mEwxH+AH4A/AE+sFtoABF12swItsF9QuFR4IwmFwwvnFw4vCGEYuIF4JgjFxIvkFxQvCGBfOAAQvqFwYwRFxYvDGBIvUFxgv/F6IuNF4n+0nB4TvXFxwvF4XBAALlPF7ZfBGC4uPF4rABGAYAGTQwvad4YwKFzYvIGBQvfFwgAE3Qvt4IvEFzgvCLxO7Lx7vULzIzTFwIvgGZheFRAiNRGSQvpGYouesYAGmQAKq3CE4PIC4wviq2eFwPCroveCRSGEC6Qv0DAwRLcoouWC4VdVYQXkr1eAgVdAoIABroNEB4gHHC5QvHwQSDAAOCA74vH1uICQIABxGtA74vIAEwv/F/4vXAH4A/AHY")) diff --git a/apps/speedalt/app.js b/apps/speedalt/app.js index 7ccb20935..c8735479d 100644 --- a/apps/speedalt/app.js +++ b/apps/speedalt/app.js @@ -1,145 +1,315 @@ /* Speed and Altitude [speedalt] -Ver : 0.07 Mike Bennett mike[at]kereru.com +1.00 : Use new GPS settings module +1.01 : Third mode large clock display +1.02 : add smoothing with kalman filter */ +var v = '1.02g'; + +/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */ +var KalmanFilter = (function () { + 'use strict'; + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } + + /** + * KalmanFilter + * @class + * @author Wouter Bulten + * @see {@link http://github.com/wouterbulten/kalmanjs} + * @version Version: 1.0.0-beta + * @copyright Copyright 2015-2018 Wouter Bulten + * @license MIT License + * @preserve + */ + var KalmanFilter = + /*#__PURE__*/ + function () { + /** + * Create 1-dimensional kalman filter + * @param {Number} options.R Process noise + * @param {Number} options.Q Measurement noise + * @param {Number} options.A State vector + * @param {Number} options.B Control vector + * @param {Number} options.C Measurement vector + * @return {KalmanFilter} + */ + function KalmanFilter() { + var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, + _ref$R = _ref.R, + R = _ref$R === void 0 ? 1 : _ref$R, + _ref$Q = _ref.Q, + Q = _ref$Q === void 0 ? 1 : _ref$Q, + _ref$A = _ref.A, + A = _ref$A === void 0 ? 1 : _ref$A, + _ref$B = _ref.B, + B = _ref$B === void 0 ? 0 : _ref$B, + _ref$C = _ref.C, + C = _ref$C === void 0 ? 1 : _ref$C; + + _classCallCheck(this, KalmanFilter); + + this.R = R; // noise power desirable + + this.Q = Q; // noise power estimated + + this.A = A; + this.C = C; + this.B = B; + this.cov = NaN; + this.x = NaN; // estimated signal without noise + } + /** + * Filter a new value + * @param {Number} z Measurement + * @param {Number} u Control + * @return {Number} + */ + + + _createClass(KalmanFilter, [{ + key: "filter", + value: function filter(z) { + var u = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + + if (isNaN(this.x)) { + this.x = 1 / this.C * z; + this.cov = 1 / this.C * this.Q * (1 / this.C); + } else { + // Compute prediction + var predX = this.predict(u); + var predCov = this.uncertainty(); // Kalman gain + + var K = predCov * this.C * (1 / (this.C * predCov * this.C + this.Q)); // Correction + + this.x = predX + K * (z - this.C * predX); + this.cov = predCov - K * this.C * predCov; + } + + return this.x; + } + /** + * Predict next value + * @param {Number} [u] Control + * @return {Number} + */ + + }, { + key: "predict", + value: function predict() { + var u = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + return this.A * this.x + this.B * u; + } + /** + * Return uncertainty of filter + * @return {Number} + */ + + }, { + key: "uncertainty", + value: function uncertainty() { + return this.A * this.cov * this.A + this.R; + } + /** + * Return the last filtered measurement + * @return {Number} + */ + + }, { + key: "lastMeasurement", + value: function lastMeasurement() { + return this.x; + } + /** + * Set measurement noise Q + * @param {Number} noise + */ + + }, { + key: "setMeasurementNoise", + value: function setMeasurementNoise(noise) { + this.Q = noise; + } + /** + * Set the process noise R + * @param {Number} noise + */ + + }, { + key: "setProcessNoise", + value: function setProcessNoise(noise) { + this.R = noise; + } + }]); + + return KalmanFilter; + }(); + + return KalmanFilter; + +}()); -const dbg = 0; var buf = Graphics.createArrayBuffer(240,160,2,{msb:true}); // Load fonts require("Font7x11Numeric7Seg").add(Graphics); -/* -var mainmenu = { - "" : { "title" : "-- Units --" }, - "default" : function() { setUnits(0,''); }, - "Kph (spd)" : function() { setUnits(1,'kph'); }, - "Knots (spd)" : function() { setUnits(1.852,'knots'); }, - "Mph (spd)" : function() { setUnits(1.60934,'mph'); }, - "m/s (spd)" : function() { setUnits(3.6,'m/s'); }, - "Meters (alt)" : function() { setUnitsAlt(1,'m'); }, - "Feet (alt)" : function() { setUnitsAlt(0.3048,'feet'); }, - "Exit" : function() { exitMenu(); }, // remove the menu and restore -}; -*/ - -var lastFix = {fix:0,satellites:0}; -var showSpeed = 1; // 1 = Speed in primary display. 0 = alt in primary +var lf = {fix:0,satellites:0}; var showMax = 0; // 1 = display the max values. 0 = display the cur fix -var maxPress = 0; // Time max button pressed. Used to calculate short or long press. +var pwrSav = 1; // 1 = default power saving with watch screen off and GPS to PMOO mode. 0 = screen kept on. var canDraw = 1; -var lastBuzz = 0; // What sort of buzz was last performed. 0 = no fix, 1 = fix. -var timerBuzz2 = 0; // ID of timer for fix second buzz var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time. +var tmrLP; // Timer for delay in switching to low power after screen turns off var max = {}; max.spd = 0; max.alt = 0; +max.n = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data. -var emulator = 0; -if (process.env.BOARD=="EMSCRIPTEN") emulator = 1; // 1 = running in emulator. Supplies test values; +var emulator = (process.env.BOARD=="EMSCRIPTEN")?1:0; // 1 = running in emulator. Supplies test values; + +var wp = {}; // Waypoint to use for distance from cur position. + +function nxtWp(inc){ + cfg.wp+=inc; + loadWp(); +} + +function loadWp() { + var w = require("Storage").readJSON('waypoints.json')||[{name:"NONE"}]; + if (cfg.wp>=w.length) cfg.wp=0; + if (cfg.wp<0) cfg.wp = w.length-1; + savSettings(); + wp = w[cfg.wp]; +} + +function radians(a) { + return a*Math.PI/180; +} + +function distance(a,b){ + var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); + var y = radians(b.lat-a.lat); + + // Distance in selected units + var d = Math.sqrt(x*x + y*y) * 6371000; + d = (d/parseFloat(cfg.dist)).toFixed(2); + if ( d >= 100 ) d = parseFloat(d).toFixed(1); + if ( d >= 1000 ) d = parseFloat(d).toFixed(0); + + return d; +} + +function drawFix(dat) { -function drawFix(speed,units,sats,alt,alt_units) { if (!canDraw) return; buf.clear(); - - var val = ''; + + var v = ''; var u=''; // Primary Display - val = speed.toString(); - if ( !showSpeed ) val = alt.toString(); + v = (cfg.primSpd)?dat.speed.toString():dat.alt.toString(); // Primary Units - u = settings.spd_unit; - if ( !showSpeed ) u = alt_units; + u = (cfg.primSpd)?cfg.spd_unit:dat.alt_units; - drawPrimary(val,u); + drawPrimary(v,u); // Secondary Display - val = alt.toString(); - if ( !showSpeed ) val = speed.toString(); + v = (cfg.primSpd)?dat.alt.toString():dat.speed.toString(); // Secondary Units - u = alt_units; - if ( !showSpeed ) u = settings.spd_unit; + u = (cfg.primSpd)?dat.alt_units:cfg.spd_unit; - drawSecondary(val,u); + drawSecondary(v,u); // Time drawTime(); + + // Waypoint name + drawWP(); //Sats - drawSats(sats); - + if ( dat.age > 10 ) { + if ( dat.age > 90 ) dat.age = '>90'; + drawSats('Age:'+dat.age); + } + else drawSats('Sats:'+dat.sats); + g.reset(); g.drawImage(img,0,40); -// g.flip(); - } - -function drawNoFix(sats) { +function drawClock() { if (!canDraw) return; - var u; - buf.clear(); - - buf.setFontAlign(0,0); - buf.setColor(3); - - buf.setFontVector(25); - buf.drawString("Waiting for GPS",120,56); - - // Time drawTime(); - - //Sats - drawSats(sats); - + drawWP(); g.reset(); g.drawImage(img,0,40); -// g.flip(); - - } function drawPrimary(n,u) { - + // Primary Display var s=40; // Font size - if ( n.length <= 7 ) s=48; - if ( n.length <= 6 ) s=55; - if ( n.length <= 5 ) s=68; - if ( n.length <= 4 ) s=85; - if ( n.length <= 3 ) s=110; + var l=n.length; + + if ( l <= 7 ) s=48; + if ( l <= 6 ) s=55; + if ( l <= 5 ) s=66; + if ( l <= 4 ) s=85; + if ( l <= 3 ) s=110; buf.setFontAlign(0,-1); //Centre buf.setColor(1); - buf.setFontVector(s); buf.drawString(n,110,0); - + + + // Primary Units buf.setFontAlign(1,-1,3); //right buf.setColor(2); - buf.setFontVector(25); + buf.setFontVector(35); buf.drawString(u,210,0); } function drawSecondary(n,u) { var s=180; // units X position - if ( n.length <= 5 ) s=155; - if ( n.length <= 4 ) s=125; - if ( n.length <= 3 ) s=100; - if ( n.length <= 2 ) s=65; - if ( n.length <= 1 ) s=35; + var l=n.length; + if ( l <= 5 ) s=155; + if ( l <= 4 ) s=125; + if ( l <= 3 ) s=100; + if ( l <= 2 ) s=65; + if ( l <= 1 ) s=35; buf.setFontAlign(-1,1); //left, bottom buf.setColor(1); @@ -149,17 +319,26 @@ function drawSecondary(n,u) { // Secondary Units buf.setFontAlign(-1,1); //left, bottom buf.setColor(2); - buf.setFontVector(25); + buf.setFontVector(30); buf.drawString(u,s,135); } - function drawTime() { - var x = 0; - var y = 160; + var x, y; + + if ( cfg.modeA == 2 ) { + x=120; + y=0; + buf.setFontAlign(0,-1); + buf.setFontVector(80); + } + else { + x = 0; + y = 160; + buf.setFontAlign(-1,1); + buf.setFont("7x11Numeric7Seg", 2); + } - buf.setFont("7x11Numeric7Seg", 2); - buf.setFontAlign(-1,1); //left, bottom buf.setColor(0); buf.drawString(time,x,y); @@ -168,181 +347,253 @@ function drawTime() { buf.drawString(time,x,y); } +function drawWP() { + var nm = wp.name; + if ( nm == undefined || nm == 'NONE' || cfg.modeA ==1 ) nm = ''; + buf.setColor(2); + + if ( cfg.modeA == 0 ) { // dist mode + buf.setFontAlign(-1,1); //left, bottom + buf.setFontVector(20); + buf.drawString(nm.substring(0,6),72,160); + } + + if ( cfg.modeA == 2 ) { // clock/large mode + buf.setFontAlign(0,1); //left, bottom + buf.setFontVector(55); + buf.drawString(nm.substring(0,6),120,160); + } + +} + function drawSats(sats) { - buf.setFontAlign(1,1); //right, bottom + buf.setColor(3); buf.setFont("6x8", 2); - if ( showMax ) { - buf.setColor(2); - buf.drawString("MAX",240,160); + buf.setFontAlign(1,1); //right, bottom + buf.drawString(sats,240,160); + + buf.setFontVector(30); + buf.setColor(2); + + if ( cfg.modeA == 1 ) { + buf.drawString('A',240,140); + if ( showMax ) { + buf.setFontAlign(0,1); //centre, bottom + buf.drawString('MAX',120,164); + } } - else buf.drawString("Sats:"+sats,240,160); + if ( cfg.modeA == 0 ) buf.drawString('D',240,140); } function onGPS(fix) { - lastFix = fix; + if ( emulator ) { + fix.fix = 1; + fix.speed = 10 + (Math.random()*5); + fix.alt = 354 + (Math.random()*50); + fix.lat = -38.92; + fix.lon = 175.7613350; + fix.course = 245; + fix.satellites = 12; + fix.time = new Date(); + fix.smoothed = 0; + } + var m; - if (fix.fix || emulator) { - doBuzz(1); + var sp = '---'; + var al = '---'; + var di = '---'; + var age = '---'; - //==== Speed ==== - if ( settings.spd == 0 ) { - var strSpeed = require("locale").speed(fix.speed); - m = strSpeed.match(/([0-9,\.]+)(.*)/); // regex splits numbers from units + if (fix.fix) lf = fix; - if ( emulator ) { - speed = '125'; //testing only - settings.spd_unit = 'kph'; - } - else { - speed = m[1]; - settings.spd_unit = m[2]; - } + if (lf.fix) { + + // Smooth data + if ( lf.smoothed !== 1 ) { + if ( cfg.spdFilt ) lf.speed = spdFilter.filter(lf.speed); + if ( cfg.altFilt ) lf.alt = altFilter.filter(lf.alt); + lf.smoothed = 1; + if ( max.n <= 15 ) max.n++; } - // Calculate for selected units - else { - speed = fix.speed; - if ( emulator ) speed = '100'; - speed = Math.round(parseFloat(speed)/parseFloat(settings.spd),0); + + + // Speed + if ( cfg.spd == 0 ) { + m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units + sp = parseFloat(m[1]); + cfg.spd_unit = m[2]; } + else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units - // ==== Altitude ==== - alt = fix.alt; - if ( emulator ) alt = '360'; - alt = Math.round(parseFloat(alt)/parseFloat(settings.alt),0); - - // Record max values - if (parseFloat(speed) > parseFloat(max.spd) ) max.spd = parseFloat(speed); - if (parseFloat(alt) > parseFloat(max.alt) ) max.alt = parseFloat(alt); - - if ( showMax ) drawFix(max.spd,settings.spd_unit,fix.satellites,max.alt,settings.alt_unit); - else drawFix(speed,settings.spd_unit,fix.satellites,alt,settings.alt_unit); + if ( sp < 10 ) sp = sp.toFixed(1); + else sp = Math.round(sp); + if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = parseFloat(sp); - } else { - doBuzz(0); - drawNoFix(fix.satellites); + // Altitude + al = lf.alt; + al = Math.round(parseFloat(al)/parseFloat(cfg.alt)); + if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = parseFloat(al); + + // Distance to waypoint + di = distance(lf,wp); + if (isNaN(di)) di = 0; + + // Age of last fix (secs) + age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000)); + } + + if ( cfg.modeA == 1 ) { + if ( showMax ) + drawFix({ + speed:max.spd, + sats:lf.satellites, + alt:max.alt, + alt_units:cfg.alt_unit, + age:age, + fix:lf.fix + }); // Speed and alt maximums + else + drawFix({ + speed:sp, + sats:lf.satellites, + alt:al, + alt_units:cfg.alt_unit, + age:age, + fix:lf.fix + }); // Show speed/altitude + } + if ( cfg.modeA == 0 ) { + // Show speed/distance + if ( di <= 0 ) + drawFix({ + speed:sp, + sats:lf.satellites, + alt:'', + alt_units:'', + age:age, + fix:lf.fix + }); // No WP selected + else + drawFix({ + speed:sp, + sats:lf.satellites, + alt:di, + alt_units:cfg.dist_unit, + age:age, + fix:lf.fix + }); + } + if ( cfg.modeA == 2 ) { + // Large clock + drawClock(); } } -// Vibrate watch when fix lost or gained. -function doBuzz(hasFix) { - - // nothing to do - if ( lastBuzz === hasFix || !settings.buzz ) { - return; - } - - // fix gained - double buzz - if ( !lastBuzz && hasFix ) { - if ( dbg ) print('Fix'); - lastBuzz = 1; - Bangle.buzz(); - timerBuzz2 = setInterval(doBuzz2, 600); // Trigger a second buzz - return; - } - - // fix lost - single buzz - if ( lastBuzz && !hasFix ) { - if ( dbg ) print('Fix lost'); - lastBuzz = 0; - Bangle.buzz(); - return; - } - - -} - -// Second buzz -function doBuzz2() { - if ( dbg ) print('Buzz2'); - clearInterval(timerBuzz2); - Bangle.buzz(); - } - -function toggleDisplay() { - showSpeed = !showSpeed; - onGPS(lastFix); // Back to Speed display -} - -function toggleMax() { -// if ( inMenu ) return; - showMax = !showMax; - onGPS(lastFix); // Back to Speed display -} - function setButtons(){ - - // Show launcher when middle button pressed - setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); - // Switch between fix and max display on short press or reset max values on long press - setWatch(maxPressed, BTN1,{repeat:true,edge:"rising"}); - setWatch(maxReleased, BTN1,{repeat:true,edge:"falling"}); - - // Touch screen to toggle display - setWatch(toggleDisplay, BTN4, {repeat:true,edge:"falling"}); - setWatch(toggleDisplay, BTN5, {repeat:true,edge:"falling"}); + // Spd+Dist : Select next waypoint + setWatch(function(e) { + var dur = e.time - e.lastTime; + if ( cfg.modeA == 1 ) { + // Spd+Alt mode - Switch between fix and MAX + if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display + else { max.spd = 0; max.alt = 0; } // Long press resets max values. + } + else nxtWp(1); // Spd+Dist or Clock mode - Select next waypoint + onGPS(lf); + }, BTN1, { edge:"falling",repeat:true}); + // Power saving on/off + setWatch(function(e){ + pwrSav=!pwrSav; + if ( pwrSav ) { + LED1.reset(); + var s = require('Storage').readJSON('setting.json',1)||{}; + var t = s.timeout||10; + Bangle.setLCDTimeout(t); + } + else { + Bangle.setLCDTimeout(0); + Bangle.setLCDPower(1); + LED1.set(); + } + }, BTN2, {repeat:true,edge:"falling"}); -} - -function maxPressed() { - maxPress = getTime(); -} - -function maxReleased() { - var dur = getTime()-maxPress; + // Toggle between alt or dist + setWatch(function(e){ + cfg.modeA = cfg.modeA+1; + if ( cfg.modeA > 2 ) cfg.modeA = 0; + savSettings(); + onGPS(lf); + }, BTN3, {repeat:true,edge:"falling"}); - if ( dur < 2 ) toggleMax(); // Short press toggle fix/max display - else { - max.spd = 0; // Long press resets max values. - max.alt = 0; - onGPS(lastFix); // redraw display - } + // Touch left screen to toggle display + setWatch(function(e){ + cfg.primSpd = !cfg.primSpd; + savSettings(); + onGPS(lf); // Update display + }, BTN4, {repeat:true,edge:"falling"}); + } function updateClock() { - if ( dbg ) print('Updating clock'); if (!canDraw) return; - drawTime(); g.reset(); g.drawImage(img,0,40); -// g.flip(); - - // Something different to display in the emulator - if ( emulator ) { - max.spd++; - max.alt++; - } - + if ( emulator ) {max.spd++;max.alt++;} } function startDraw(){ canDraw=true; + setLpMode('SuperE'); // off g.clear(); Bangle.drawWidgets(); - onGPS(lastFix); // draw app screen + onGPS(lf); // draw app screen } function stopDraw() { canDraw=false; + if (!tmrLP) tmrLP=setInterval(function () {if (lf.fix) setLpMode('PSMOO');}, 10000); //Drop to low power in 10 secs. Keep lp mode off until we have a first fix. } -// ===== Main Prog ===== +function savSettings() { + require("Storage").write('speedalt.json',cfg); +} + +function setLpMode(m) { + if (tmrLP) {clearInterval(tmrLP);tmrLP = false;} // Stop any scheduled drop to low power + if ( !gpssetup ) return; + gpssetup.setPowerMode({power_mode:m}); +} + +// =Main Prog // Read settings. -let settings = require('Storage').readJSON('speedalt.json',1)||{}; +let cfg = require('Storage').readJSON('speedalt.json',1)||{}; -settings.spd = settings.spd||0; // Multiplier for speed unit conversions. 0 = use the locale values for speed -settings.spd_unit = settings.spd_unit||''; // Displayed speed unit -settings.alt = settings.alt||0.3048;// Multiplier for altitude unit conversions. -settings.alt_unit = settings.alt_unit||'feet'; // Displayed altitude units -settings.colour = settings.colour||0; // Colour scheme. -settings.buzz = settings.buzz||0; // Buzz when fix lost or gained. +cfg.spd = cfg.spd||0; // Multiplier for speed unit conversions. 0 = use the locale values for speed +cfg.spd_unit = cfg.spd_unit||''; // Displayed speed unit +cfg.alt = cfg.alt||0.3048;// Multiplier for altitude unit conversions. +cfg.alt_unit = cfg.alt_unit||'feet'; // Displayed altitude units +cfg.dist = cfg.dist||1000;// Multiplier for distnce unit conversions. +cfg.dist_unit = cfg.dist_unit||'km'; // Displayed altitude units +cfg.colour = cfg.colour||0; // Colour scheme. +cfg.wp = cfg.wp||0; // Last selected waypoint for dist +cfg.modeA = cfg.modeA||0; // 0 = [D]ist, 1 = [A]ltitude, 2 = [C]lock +cfg.primSpd = cfg.primSpd||0; // 1 = Spd in primary, 0 = Spd in secondary + +cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt; +cfg.altFilt = cfg.altFilt==undefined?true:cfg.altFilt; + +if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 }); +if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 }); + +loadWp(); /* Colour Pallet Idx @@ -359,50 +610,43 @@ var img = { palette:new Uint16Array([0,0x4FE0,0xEFE0,0x07DB]) }; -if ( settings.colour == 1 ) img.palette = new Uint16Array([0,0xFFFF,0xFFFF,0xFFFF]); -if ( settings.colour == 2 ) img.palette = new Uint16Array([0,0xFF800,0xF800,0xF800]); - - -// Find speed unit if using locale speed -if ( settings.spd == 0 ) { - var strSpeed = require("locale").speed(1); - m = strSpeed.match(/([0-9,\.]+)(.*)/); // regex splits numbers from units - settings.spd_unit = m[2]; -} +if ( cfg.colour == 1 ) img.palette = new Uint16Array([0,0xFFFF,0xFFF6,0xDFFF]); +if ( cfg.colour == 2 ) img.palette = new Uint16Array([0,0xFF800,0xFAE0,0xF813]); var SCREENACCESS = { withApp:true, - request:function(){ - this.withApp=false; - stopDraw(); - clearWatch(); - }, - release:function(){ - this.withApp=true; - startDraw(); - setButtons(); - } + request:function(){this.withApp=false;stopDraw();}, + release:function(){this.withApp=true;startDraw();} }; Bangle.on('lcdPower',function(on) { if (!SCREENACCESS.withApp) return; - if (on) { - startDraw(); - } else { - stopDraw(); - } + if (on) startDraw(); + else stopDraw(); }); +var gpssetup; +try { + gpssetup = require("gpssetup"); +} catch(e) { + gpssetup = false; +} + // All set up. Lets go. g.clear(); -Bangle.setLCDBrightness(1); Bangle.loadWidgets(); Bangle.drawWidgets(); +onGPS(lf); Bangle.setGPSPower(1); -onGPS(lastFix); +if ( gpssetup ) { + gpssetup.setPowerMode({power_mode:"SuperE"}).then(function() { Bangle.setGPSPower(1); }); +} +else { + Bangle.setGPSPower(1); +} + Bangle.on('GPS', onGPS); setButtons(); -setInterval(updateClock, 30000); - +setInterval(updateClock, 10000); diff --git a/apps/speedalt/app.png b/apps/speedalt/app.png index 41849d307..93d8e57dc 100644 Binary files a/apps/speedalt/app.png and b/apps/speedalt/app.png differ diff --git a/apps/speedalt/screen1.png b/apps/speedalt/screen1.png index eac5e7e7a..fa477875d 100644 Binary files a/apps/speedalt/screen1.png and b/apps/speedalt/screen1.png differ diff --git a/apps/speedalt/screen2.png b/apps/speedalt/screen2.png index 37d7bb7cf..cf506231b 100644 Binary files a/apps/speedalt/screen2.png and b/apps/speedalt/screen2.png differ diff --git a/apps/speedalt/screen3.png b/apps/speedalt/screen3.png index 9e061958e..1c3272019 100644 Binary files a/apps/speedalt/screen3.png and b/apps/speedalt/screen3.png differ diff --git a/apps/speedalt/screen5.png b/apps/speedalt/screen5.png new file mode 100644 index 000000000..dece4daaf Binary files /dev/null and b/apps/speedalt/screen5.png differ diff --git a/apps/speedalt/settings.js b/apps/speedalt/settings.js index 21171edee..488ba3b81 100644 --- a/apps/speedalt/settings.js +++ b/apps/speedalt/settings.js @@ -19,32 +19,45 @@ writeSettings(); } + function setUnitsDist(d,u) { + settings.dist = d; + settings.dist_unit = u; + writeSettings(); + } + function setColour(c) { settings.colour = c; writeSettings(); } + const appMenu = { '': {'title': 'GPS Speed Alt'}, '< Back': back, + '< Load GPS Adv Sport': ()=>{load('speedalt.app.js');}, 'Units' : function() { E.showMenu(unitsMenu); }, 'Colours' : function() { E.showMenu(colMenu); }, + 'Kalman Filter' : function() { E.showMenu(kalMenu); }/*, 'Vibrate' : { value : settings.buzz, format : v => v?"On":"Off", onchange : () => { settings.buzz = !settings.buzz; writeSettings(); } - }}; + }*/ + }; const unitsMenu = { '': {'title': 'Units'}, '< Back': function() { E.showMenu(appMenu); }, 'default (spd)' : function() { setUnits(0,''); }, 'Kph (spd)' : function() { setUnits(1,'kph'); }, - 'Knots (spd)' : function() { setUnits(1.852,'knots'); }, + 'Knots (spd)' : function() { setUnits(1.852,'kts'); }, 'Mph (spd)' : function() { setUnits(1.60934,'mph'); }, 'm/s (spd)' : function() { setUnits(3.6,'m/s'); }, + 'Km (dist)' : function() { setUnitsDist(1000,'km'); }, + 'Miles (dist)' : function() { setUnitsDist(1609.344,'mi'); }, + 'Nm (dist)' : function() { setUnitsDist(1852.001,'nm'); }, 'Meters (alt)' : function() { setUnitsAlt(1,'m'); }, - 'Feet (alt)' : function() { setUnitsAlt(0.3048,'feet'); } + 'Feet (alt)' : function() { setUnitsAlt(0.3048,'ft'); } }; const colMenu = { @@ -55,7 +68,22 @@ 'Night' : function() { setColour(2); } }; + const kalMenu = { + '': {'title': 'Kalman Filter'}, + '< Back': function() { E.showMenu(appMenu); }, + 'Speed' : { + value : settings.spdFilt, + format : v => v?"On":"Off", + onchange : () => { settings.spdFilt = !settings.spdFilt; writeSettings(); } + }, + 'Altitude' : { + value : settings.altFilt, + format : v => v?"On":"Off", + onchange : () => { settings.altFilt = !settings.altFilt; writeSettings(); } + } + }; + E.showMenu(appMenu); -}) +}); diff --git a/apps/stepo/ChangeLog b/apps/stepo/ChangeLog new file mode 100644 index 000000000..a28c163c7 --- /dev/null +++ b/apps/stepo/ChangeLog @@ -0,0 +1,3 @@ +0.01: First version +0.02: Speeded up draw, start stop timer, long press BTN2 to switch to the launcher +0.03: Reduced buffer size, save on memory diff --git a/apps/stepo/README.md b/apps/stepo/README.md new file mode 100644 index 000000000..d26f656b7 --- /dev/null +++ b/apps/stepo/README.md @@ -0,0 +1,36 @@ +# Stepometer Clock + +A large font watch, displays step count in a doughnut guage and warns of low battery + +## Features + +- Displays the time in large font +- Display current step count in a doughnut guage +- Show step count in the middle of the doughnut guage +- The guage show percentage of steps out of a goal of 10000 steps + + +## Dependancies +- Requires one of the steps widgets to be installed + + +![](screenshot1.jpg) + +- When the battery is less than 25% the doughnut turns red + +![](screenshot2.jpg) + + +## BTN2 Long press to start the launcher + +BTN2 is confiured to respond to a 1.5 second press in order to switch +to the launcher App. Simply press and hold until you hear a buzz and +release. This avoids accidently switching out of the watch app when +clothing catches it. + +## Notes + +* Uses an arrayBuffer to prepare the doughnut guage. The arrayBuffer + is 160*160 and is larger than required. The reason for this is that + I plan to use this watch face with others in a multiclock format + and want to be able to reuse the arrayBuffer with other clocks. diff --git a/apps/stepo/app.js b/apps/stepo/app.js new file mode 100644 index 000000000..b8a1ea8c9 --- /dev/null +++ b/apps/stepo/app.js @@ -0,0 +1,147 @@ +var pal4color = new Uint16Array([0x0000,0xFFFF,0x7BEF,0xAFE5],0,2); // b,w,grey,greenyellow +var pal4red = new Uint16Array([0x0000,0xFFFF,0xF800,0xAFE5],0,2); // b,w,red,greenyellow +var buf = Graphics.createArrayBuffer(120,120,2,{msb:true}); +var intervalRefSec; + +function flip(x,y) { + g.drawImage({width:120,height:120,bpp:2,buffer:buf.buffer, palette:pal4color}, x, y); + buf.clear(); +} + +function flip_red(x,y) { + g.drawImage({width:120,height:120,bpp:2,buffer:buf.buffer, palette:pal4red}, x, y); + buf.clear(); +} + +function radians(a) { + return a*Math.PI/180; +} + +function drawSteps() { + var i = 0; + var cx = 60; + var cy = 60; + var r = 56; + var steps = getSteps(); + var percent = steps / 10000; + + if (percent > 1) percent = 1; + + var startrot = 0 - 180; + var midrot = -180 - (360 * percent); + var endrot = -360 - 180; + + buf.setColor(3); // green-yellow + + // draw guauge + for (i = startrot; i > midrot; i -= 4) { + x = cx + r * Math.sin(radians(i)); + y = cy + r * Math.cos(radians(i)); + buf.fillCircle(x,y,4); + } + + buf.setColor(2); // grey + + // draw remainder of guage in grey + for (i = midrot; i > endrot; i -= 4) { + x = cx + r * Math.sin(radians(i)); + y = cy + r * Math.cos(radians(i)); + buf.fillCircle(x,y,4); + } + + // draw steps + buf.setColor(1); // white + buf.setFont("Vector", 24); + buf.setFontAlign(0,0); + buf.drawString(steps, cx, cy); + + // change the remaining color to RED if battery is below 25% + if (E.getBattery() > 25) + flip(60,115); + else + flip_red(60,115); +} + +function draw() { + var d = new Date(); + var da = d.toString().split(" "); + var time = da[4].substr(0,5); + + g.clearRect(0, 30, 239, 99); + g.setColor(1,1,1); + g.setFontAlign(0, -1); + g.setFont("Vector", 80); + g.drawString(time, 120, 30, true); + + drawSteps(); +} + +function getSteps() { + if (stepsWidget() !== undefined) + return stepsWidget().getSteps(); + return "-"; +} + +function startTimer() { + draw(); + intervalRefSec = setInterval(draw, 15000); +} + +function stopTimer() { + if(intervalRefSec) { intervalRefSec = clearInterval(intervalRefSec); } +} + +function stepsWidget() { + if (WIDGETS.activepedom !== undefined) { + return WIDGETS.activepedom; + } else if (WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom; + } + return undefined; +} + +// handle switch display on by pressing BTN1 +Bangle.on('lcdPower', function(on) { + if (on) + startTimer(); + else + stopTimer(); +}); + +let firstPress = 0; +var pressTimer; + +// start a timer and buzz whenn held long enough +function firstPressed() { + firstPress = getTime(); + pressTimer = setInterval(longPressCheck, 1500); +} + +// if you release too soon there is no buzz as timer is cleared +function thenReleased() { + var dur = getTime() - firstPress; + if (pressTimer) { + clearInterval(pressTimer); + pressTimer = undefined; + } + if ( dur >= 1.5 ) Bangle.showLauncher(); +} + +// when you feel the buzzer you know you have done a long press +function longPressCheck() { + Bangle.buzz(); + if (pressTimer) { + clearInterval(pressTimer); + pressTimer = undefined; + } +} + +// make BTN require a long press (1.5 seconds) to switch to launcher +setWatch(firstPressed, BTN2,{repeat:true,edge:"rising"}); +setWatch(thenReleased, BTN2,{repeat:true,edge:"falling"}); + +g.reset(); +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +startTimer(); diff --git a/apps/stepo/icon.js b/apps/stepo/icon.js new file mode 100644 index 000000000..29c93f7b3 --- /dev/null +++ b/apps/stepo/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkDEkEF5nMoUhiAVSAAUikUhCyYwBDBwWGC4YYMCwxHCAAUgCxEMCw3CC4kiPg5FF4tVgEBiIXEJI4uE4oKEgMSGBQWD4BTHGBIuDFooxEDAYJErgXCqCbJgSSHFxhJFJAZGDFxQABPQYXGCxZhESIReCRhBIIMARGPJAgXFCxqRDPAMFC6JgDOwalMMAwXDOxwXZPAUQC/4X/ABcs5jXVCQPCgEFAgPMCxwSC4gcDC55CCC4tQC5tcOQgFFLxoRDGoRINOIZBCC4ZIMFA4GCVJgPHMAQwLFwZwEBAYwJLoZvGBIaSIBgYlGGAYLHBQZVHHQgYBqoIBqoJFNRYAKQhIWMfpRJFAArLMDBIWMDBIWODIb3CCqIAQA=")) diff --git a/apps/stepo/screenshot1.jpg b/apps/stepo/screenshot1.jpg new file mode 100644 index 000000000..4e5c88a62 Binary files /dev/null and b/apps/stepo/screenshot1.jpg differ diff --git a/apps/stepo/screenshot2.jpg b/apps/stepo/screenshot2.jpg new file mode 100644 index 000000000..0388bec4a Binary files /dev/null and b/apps/stepo/screenshot2.jpg differ diff --git a/apps/stepo/stepo.info.js b/apps/stepo/stepo.info.js new file mode 100644 index 000000000..0bee0cfc7 --- /dev/null +++ b/apps/stepo/stepo.info.js @@ -0,0 +1,7 @@ +require("Storage").write("stepo.info",{ + "id":"stepo", + "name":"Stepometer Clock", + "src":"stepo.app.js", + "icon":"stepo.img", + "type":"clock" +}); diff --git a/apps/stepo/stepo.png b/apps/stepo/stepo.png new file mode 100644 index 000000000..b264f3a01 Binary files /dev/null and b/apps/stepo/stepo.png differ diff --git a/apps/sweepclock/ChangeLog b/apps/sweepclock/ChangeLog new file mode 100644 index 000000000..23841b299 --- /dev/null +++ b/apps/sweepclock/ChangeLog @@ -0,0 +1,4 @@ +0.01: Initial Release +0.02: Added Colour Themes +0.03: Added Date +0.04: Memory Footprint reduction diff --git a/apps/sweepclock/README.md b/apps/sweepclock/README.md new file mode 100644 index 000000000..34be0c42c --- /dev/null +++ b/apps/sweepclock/README.md @@ -0,0 +1,43 @@ +# Sweep Clock + +The Sweep Clock provides a clock with a perfectly smooth sweep second hand with a single Numeral Display. + +![](app.png) + +## Usage + +### Button 1 + +Use Button 1 (the top right button) to change the numeral type + +| Default clock face | Roman Numeral Font | No Digits | +| ---- | ---- | ---- | +| ![](numeral-01.jpg) | ![](numeral-02.jpg) | ![](numeral-03.jpg) | + + + +### Button 3 +Button 3 (bottom right button) is used to change the colour + +| Red | Grey | Purple | +| ---- | ---- | ---- | +| ![](color-01.jpg) | ![](color-02.jpg) | ![](color-03.jpg) | + +### Button 4 +Button 4 (bottom left of the screen) is used to change the date position. Note after cycling through the date positions there is the no date option. + +| Top Right | Bottom Right | Bottom Left | Top Left | +| ---- | ---- | ---- | ---- | +| ![](date-01.jpg) | ![](date-02.jpg) | ![](date-03.jpg) | ![](date-04.jpg) | + +## Further Details + +For further details of design and working please visit [The Project Page](https://www.notion.so/adrianwkirk/Sweep-hand-clock-6aa5b6b3d1074d4e87fc947975b1e4b7) + +## 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) \ No newline at end of file diff --git a/apps/sweepclock/app.png b/apps/sweepclock/app.png new file mode 100644 index 000000000..763c00b1d Binary files /dev/null and b/apps/sweepclock/app.png differ diff --git a/apps/sweepclock/color-01.jpg b/apps/sweepclock/color-01.jpg new file mode 100644 index 000000000..fe937a7d8 Binary files /dev/null and b/apps/sweepclock/color-01.jpg differ diff --git a/apps/sweepclock/color-02.jpg b/apps/sweepclock/color-02.jpg new file mode 100644 index 000000000..ce798dc99 Binary files /dev/null and b/apps/sweepclock/color-02.jpg differ diff --git a/apps/sweepclock/color-03.jpg b/apps/sweepclock/color-03.jpg new file mode 100644 index 000000000..5bcc124ec Binary files /dev/null and b/apps/sweepclock/color-03.jpg differ diff --git a/apps/sweepclock/color-04.jpg b/apps/sweepclock/color-04.jpg new file mode 100644 index 000000000..c89f26b26 Binary files /dev/null and b/apps/sweepclock/color-04.jpg differ diff --git a/apps/sweepclock/date-01.jpg b/apps/sweepclock/date-01.jpg new file mode 100644 index 000000000..79d12e75b Binary files /dev/null and b/apps/sweepclock/date-01.jpg differ diff --git a/apps/sweepclock/date-02.jpg b/apps/sweepclock/date-02.jpg new file mode 100644 index 000000000..14a64980c Binary files /dev/null and b/apps/sweepclock/date-02.jpg differ diff --git a/apps/sweepclock/date-03.jpg b/apps/sweepclock/date-03.jpg new file mode 100644 index 000000000..e30a01932 Binary files /dev/null and b/apps/sweepclock/date-03.jpg differ diff --git a/apps/sweepclock/date-04.jpg b/apps/sweepclock/date-04.jpg new file mode 100644 index 000000000..6e65a148f Binary files /dev/null and b/apps/sweepclock/date-04.jpg differ diff --git a/apps/sweepclock/numeral-01.jpg b/apps/sweepclock/numeral-01.jpg new file mode 100644 index 000000000..423af90dc Binary files /dev/null and b/apps/sweepclock/numeral-01.jpg differ diff --git a/apps/sweepclock/numeral-02.jpg b/apps/sweepclock/numeral-02.jpg new file mode 100644 index 000000000..603b0a6ee Binary files /dev/null and b/apps/sweepclock/numeral-02.jpg differ diff --git a/apps/sweepclock/numeral-03.jpg b/apps/sweepclock/numeral-03.jpg new file mode 100644 index 000000000..98af2e407 Binary files /dev/null and b/apps/sweepclock/numeral-03.jpg differ diff --git a/apps/sweepclock/sweepclock-icon.js b/apps/sweepclock/sweepclock-icon.js new file mode 100644 index 000000000..d9bdd8c65 --- /dev/null +++ b/apps/sweepclock/sweepclock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lEowkA/4AGmYIHABHzmVCCaE0kUin4TPmUimQTQ+UzmcvJ6EjCaP/kYABCaEymYTl+Q7SMgITTmQTQPAK0RMgITm+QTS+ciPCcikQpPY4MjmYTO+czmcyHh4TCmcvJ54nCPCBjBJx4oECc8zJ6ATTn48RE4YTTHh4SDH4ImRFBwTGFBgTGFBgSGFBYmHUgITRmcyFBASFAoUjE5PzkQLBHJxiDAQP/GxAA==")) diff --git a/apps/sweepclock/sweepclock.js b/apps/sweepclock/sweepclock.js new file mode 100644 index 000000000..777bafd8b --- /dev/null +++ b/apps/sweepclock/sweepclock.js @@ -0,0 +1,862 @@ +/** + * Adrian Kirk 2021-03 + * Simple Clock showing 1 numeral for the hour + * with a smooth sweep second. + */ + +const screen_center_x = g.getWidth()/2; +const screen_center_y = 10 + g.getHeight()/2; +const TWO_PI = 2*Math.PI; + +require("FontCopasetic40x58Numeric").add(Graphics); + +const 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], + }, + { + name: "purple", + background : [1.0,0.0,1.0], + second_hand: [1.0,1.0,0.0], + }, + { + name: "blue", + background : [0.4,0.7,1.0], + second_hand: [0.5,0.5,0.5], + } +]; + +let color_scheme_index = 0; + +const WHITE = [1.0,1.0,1.0]; +function default_white(color){ + if(color == null){ + return WHITE; + } else { + return color; + } +} + +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){} +} + +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_theme){ + super(); + this.centerX = centerX; + this.centerY = centerY; + this.length = length; + this.color_theme = color_theme; + // 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; + } + // 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 = color_schemes[color_scheme_index].background; + 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 = default_white(color_schemes[color_scheme_index][this.color_theme]); + 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(); + return true; + } else { + return false; + } + } +} + +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_theme, + base_height, + thickness){ + super(); + this.centerX = centerX; + this.centerY = centerY; + this.length = length; + this.color_theme = color_theme; + this.thickness = thickness; + 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; + 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 = default_white(color_schemes[color_scheme_index][this.color_theme]); + 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; + } + } +} +// The force draw is set to true to force all objects to redraw themselves +let force_redraw = false; +// 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, + "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 +let minutes_hand_redraw = function(angle, last_draw_time){ + return force_redraw || (seconds_hand.angle > angle && + Math.abs(seconds_hand.angle - angle) 500); +}; +let minutes_hand = new ThinHand(screen_center_x, + screen_center_y, + 80, + TWO_PI/250, + minutes_hand_redraw, + "minute_hand" +); +// The hour hand is a thick hand so we have to redraw when the minute hand +// overlaps from its behind andle 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, + "hour_hand", + 5, + 4); + +function draw_clock(){ + var date = new Date(); + draw_background(); + draw_hour_digit(date); + draw_seconds(date); + draw_mins(date); + draw_hours(date); + draw_date(date); + force_redraw = false; +} + +var local = require('locale'); +var last_date = null; +var last_datestr = null; +var last_coords = null; +const date_coords = [ + { name: "topright", coords:[180,30]}, + { name: "bottomright", coords:[180,220]}, + { name: "bottomleft", coords: [5,220]}, + { name: "topleft", coords:[5,30]}, + { name: "offscreen", coords: [240,30]} +]; + +var date_coord_index = 0; + +function draw_date(date){ + if(force_redraw || last_date == null || last_date.getDate() != date.getDate()){ + //console.log("redrawing date"); + g.setFontAlign(-1,-1,0); + g.setFont("Vector",15); + if(last_coords != null && last_datestr != null) { + var background = color_schemes[color_scheme_index].background; + g.setColor(background[0], background[1], background[2]); + g.drawString(last_datestr, last_coords[0], last_coords[1]); + } + var coords = date_coords[date_coord_index].coords; + if(coords != null) { + var date_format = local.dow(date,1) + " " + date.getDate(); + var numeral_color = default_white(color_schemes[color_scheme_index].numeral); + g.setColor(numeral_color[0], numeral_color[1], numeral_color[2]); + g.drawString(date_format, coords[0], coords[1]); + last_date = date; + last_datestr = date_format; + last_coords = coords; + } + } +} + +function next_datecoords() { + date_coord_index = date_coord_index + 1; + if (date_coord_index >= date_coords.length) { + date_coord_index = 0; + } + //console.log("date coord index->" + date_coord_index); + force_redraw = true; +} + +function set_datecoords(date_name){ + console.log("setting date:" + date_name); + for (var i=0; i < date_coords.length; i++) { + if(date_coords[i].name == date_name){ + date_coord_index = i; + force_redraw = true; + console.log("date match"); + break; + } + } +} + +// 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"); + } +} + +/** + * 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 "";} +} + +class NoFont extends NumeralFont{ + constructor(){super();} + getDimensions(hour){return [0,0];} + hour_txt(hour){ return ""; } + draw(hour_txt,x,y){ return "";} + getName(){return "NoFont";} +} + +const COPASET_DIM_20x58 = [20,58]; +const COPASET_DIM_30x58 = [30,58]; +const COPASET_DIM_40x58 = [40,58]; +const COPASET_DIM_50x58 = [50,58]; + +class CopasetFont extends NumeralFont{ + constructor(){ + super(); + } + getDimensions(hour){ + switch(hour){ + case 1: return COPASET_DIM_20x58; + case 2: + case 3: + case 4: + case 5: + case 7: + return COPASET_DIM_30x58; + case 6: + case 8: + case 9: + case 11: + case 12: + return COPASET_DIM_40x58; + case 10: + return COPASET_DIM_50x58; + default: + return COPASET_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.setFontAlign(-1,-1,0); + g.setFontCopasetic40x58Numeric(); + g.drawString(hour_txt,x,y); + } + getName(){return "Copaset";} +} + +const ROMAN_DIM_10x40 = [10,40]; +const ROMAN_DIM_20x40 = [20,40]; +const ROMAN_DIM_25x40 = [25,40]; +const ROMAN_DIM_30x40 = [30,40]; +const ROMAN_DIM_40x40 = [40,40]; +const ROMAN_DIM_60x40 = [60,40]; +const ROMAN_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 1: + return ROMAN_DIM_10x40; + case 2: + return ROMAN_DIM_25x40; + case 3: + case 4: + case 6: + case 9: + case 11: + case 12: + return ROMAN_DIM_40x40; + case 5: + return ROMAN_DIM_30x40; + case 7: + return ROMAN_DIM_60x40; + case 8: + return ROMAN_DIM_70x40; + case 10: + return ROMAN_DIM_20x40; + default: + return ROMAN_DIM_40x40; + } + } + hour_txt(hour){ return this.getText(hour); } + draw(hour_txt,x,y){ + g.setFontAlign(-1,-1,0); + g.setFont("Vector",40); + g.drawString(hour_txt,x,y); + } + getName(){return "Roman";} +} + +// 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){ + this.radius = radius; + this.numeral_font = numeral_font; + this.draw_test = draw_test; + this.curr_numeral_font = numeral_font; + 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; + } + drawHour(hours){ + var changed = false; + if(this.curr_hours != hours || this.curr_numeral_font !=this.numeral_font){ + var background = color_schemes[color_scheme_index].background; + 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"); + var hours_frac = hours / 12; + var angle = TWO_PI*hours_frac; + var dimensions = this.numeral_font.getDimensions(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(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 = 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 = default_white(color_schemes[color_scheme_index].numeral); + 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"); + } + } +} + +let numeral_fonts = [new CopasetFont(), new RomanNumeralFont(), new NoFont()]; +let numeral_fonts_index = 0; +/** + * 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 > 1.5*Math.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) || + (minutes_hand.last_draw_time.getTime() > last_draw_time.getTime()); + if(redraw){ + //console.log(angle_from + "/" + angle_to + " seconds " + seconds_hand_angle); + } + return redraw; +}; +let hour_scriber = new HourScriber(70, + numeral_fonts[numeral_fonts_index], + hour_numeral_redraw +); +/** + * Called from button 1 to change the numerals that are + * displayed on the clock face + */ +function next_font(){ + numeral_fonts_index = numeral_fonts_index + 1; + if(numeral_fonts_index >= numeral_fonts.length){ + numeral_fonts_index = 0; + } + hour_scriber.setNumeralFont( + numeral_fonts[numeral_fonts_index]); + force_redraw = true; +} + +function draw_hour_digit(date){ + var hours = date.getHours() % 12; + var mins = date.getMinutes(); + if(mins > 30){ + hours = (hours +1) % 12; + } + if(hours == 0){ + hours = 12; + } + hour_scriber.drawHour(hours); +} + +function draw_background(){ + if(force_redraw){ + var 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(){ + color_scheme_index += 1; + color_scheme_index = color_scheme_index % color_schemes.length; + //console.log("color_scheme_index=" + color_scheme_index); + force_redraw = 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("color scheme 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("font match"); + hour_scriber.setNumeralFont(numeral_fonts[numeral_fonts_index]); + break; + } + } +} + +/** + * Called on startup to set the watch to the last preference settings + */ +function load_settings(){ + try{ + var settings = require("Storage").readJSON("sweepclock.settings.json"); + if(settings != null){ + console.log("loaded:" + JSON.stringify(settings)); + if(settings.color_scheme != null){ + set_colorscheme(settings.color_scheme); + } + if(settings.font != null){ + set_font(settings.font); + } + if(settings.date != null){ + set_datecoords(settings.date); + } + } else { + console.log("no settings to load"); + } + } catch(e){ + console.log("failed to load settings:" + e); + } +} + +function print_memoryusage(){ + var m = process.memory(); + var pc = Math.round(m.usage*100/m.total); + console.log("memory usage: " + pc + "%"); +} + +/** + * 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, + date: date_coords[date_coord_index].name + }; + console.log("saving:" + JSON.stringify(settings)); + require("Storage").writeJSON("sweepclock.settings.json",settings); + print_memoryusage(); +} + +// 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(); + +// Show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2,{repeat:false,edge:"falling"}); + +function button1pressed(){ + next_font(); + save_settings(); +} + +function button3pressed(){ + next_colorscheme(); + save_settings(); +} + +function button4pressed(){ + //console.log("button 4 pressed"); + next_datecoords(); + save_settings(); +} + +// Handle button 1 being pressed +setWatch(button1pressed, BTN1,{repeat:true,edge:"falling"}); + +// Handle button 3 being pressed +setWatch(button3pressed, BTN3,{repeat:true,edge:"falling"}); + +// Handle button 3 being pressed +setWatch(button4pressed, BTN4,{repeat:true,edge:"falling"}); diff --git a/apps/sweepclock/sweepclock.png b/apps/sweepclock/sweepclock.png new file mode 100644 index 000000000..70a1cd532 Binary files /dev/null and b/apps/sweepclock/sweepclock.png differ diff --git a/apps/tapelauncher/ChangeLog b/apps/tapelauncher/ChangeLog index 918e4a9ac..b30d07ad6 100644 --- a/apps/tapelauncher/ChangeLog +++ b/apps/tapelauncher/ChangeLog @@ -1,2 +1,2 @@ 0.01: Initial version - +0.02: Fixed issues with entry in apps.json diff --git a/apps/testuserinput/ChangeLog b/apps/testuserinput/ChangeLog new file mode 100644 index 000000000..3cdee327c --- /dev/null +++ b/apps/testuserinput/ChangeLog @@ -0,0 +1,6 @@ +0.01: New App! +0.02: Tweaks for app loader +0.03: Fix app icon, add change of colors +0.04: Improvements and new radio button option elements +0.05: ... +0.06: Improvements, multiple rows with radio buttons diff --git a/apps/testuserinput/README.md b/apps/testuserinput/README.md new file mode 100644 index 000000000..47c1779be --- /dev/null +++ b/apps/testuserinput/README.md @@ -0,0 +1,54 @@ +# Test User Input + +This basic app, allows to **test the bangle.js input interface** trough every types of finger interaction. +Interactrion type is displayed in text or a switch on/off image for swipe screen. + +Besides the basics, the UI also includes multiple rows with radio buttons + + +## Captures + +(Following images can be outdated) + +Launcher icon + +![](testUserInput_ss0.png) + +1st screen - Help/Intro + +![](testUserInput_ss1.png) + +2nd screen - interface and a result + +![](testUserInput_ss2.png) + +3rd screen - interface and a result + +![](testUserInput_ss3.png) + + +## Usage + +Open and see a "help" screen +Interact with buttons or touch screen to print the event or leave the app + +## Features + +Colours, font, user input, image, load widgets + + +## Controls + - Press left area - Prints Touch1 + - Press righ area - Prints Touch2 + - Press center area - Prints Touch3 + - Swipe Left - Displays Switch OFF image + - Swipe Right - Displays Switch ON image + - BTN1 - Prints Button1 + - BTN2 - Prints Button2 + - BTN3 - Quit to Launcher + + +## Support + +This app is so basic that probably the easiest is to just edit the code +Otherwise you can contact me [here](https://github.com/dapgo) \ No newline at end of file diff --git a/apps/testuserinput/app-icon.js b/apps/testuserinput/app-icon.js new file mode 100644 index 000000000..5efac11e2 --- /dev/null +++ b/apps/testuserinput/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgjgkUikAWVDCkCkUz+cykQuSkf///zGCUin4XB/8yC6ECFwM+90/GAIXS8fzC6cv//vAIQvS+YvBAIIXT93uC6UAiUzC4UyiAXQiMjC4UhC6MBiIXB8MRd6IwBC4QuRAAQXB8AWTC/4XK8gZUC4PtAQI0SCgYAEDJwXIDBwXRBIIXNBwihIC6YJDC6gKBhoXRhwKC9vdC6IwEC6YwEC6RzLC5gwDC6YwOC8JINC5IwNC5QwMC5QwMC5YwK8AXLGBQXNGBIWMDBIWOAH4AvA=")) \ No newline at end of file diff --git a/apps/testuserinput/app.js b/apps/testuserinput/app.js new file mode 100644 index 000000000..4f5f26408 --- /dev/null +++ b/apps/testuserinput/app.js @@ -0,0 +1,331 @@ +/* Test bangle.js input interface */ + var v_mode_debug=0; //1=yes, 0=no (to disable console msg) + if (v_mode_debug==1) console.log("Debug mode enabled"); + else console.log("Debug mode disabled"); + var v_model=process.env.BOARD; + if (v_mode_debug==1) console.log("device="+v_model); + + var v_str_version='v0.06'; //testing purpose + + var x_max_screen=g.getWidth();//240; + var y_max_screen=g.getHeight(); //240; + var y_wg_bottom=g.getHeight()-25; + var y_wg_top=25; + if (v_model=='BANGLEJS') { + var x_btn_area=215; + var x_max_usable_area=x_btn_area;//Pend! only for bangle.js + var y_btn2=124; //harcoded for bangle.js cuz it is not the half of + } else x_max_usable_area=240; + var x_mid_screen=x_max_screen/2; + + var colbackg='#111111';//black + var colorange='#e56e06'; //RGB format rrggbb + var v_color_lines=0xFFFF; //White hex format + var v_color_b_area=colbackg; //for banner area + var v_color_text='#FB0E01'; + //var v_font1size=16; + var v_font1size=11; //out of quotes + var v_font2size=18; + var v_font3size=14; + + var v_clicks=0; + var v_selected_row=1; //used by round option + var v_total_rows=2;//used by round option + var array_r_option=[]; + + var v_y_optionrow1=80; + var v_y_optionrow2=110; + var v_y_optionrow3=140; + + + if (v_mode_debug==1) console.log("*** Test input interface ***"); + +//the biggest usable area, button area not included +function ClearActiveArea(x1,y1,x2,y2){ + g.setColor(colbackg); + //FOR BANGLE.JS (0,y_wg_top,x_max_usable_area,y_wg_bottom); + //fill all screen except widget area + g.fillRect(x1,y1,x2,y2); + g.flip(); +} + +function PrintHelp(){ + if (v_mode_debug==1) console.log("Log: *** Print help in screen"); + ClearActiveArea(0,y_wg_top,x_max_usable_area,y_wg_bottom); + g.setColor(colorange); + /* PRINT FROM widget BOTTOM */ + g.setFontVector(v_font2size).drawString("To test the UI, try:",5,y_wg_bottom-(10*v_font3size)); + g.flip(); + g.setColor(0,1,0); //green + g.setFontVector(v_font3size); + g.drawString("Swipe right -->", 30, y_wg_bottom-(8*v_font3size)); + g.drawString("Swipe left <--", 30, y_wg_bottom-(7*v_font3size)); + g.drawString("Click Left area", 30, y_wg_bottom-(6*v_font3size)); + g.drawString("Click Right area", 30,y_wg_bottom-(5*v_font3size)); + g.drawString("Click Middle area", 30,y_wg_bottom-(4*v_font3size)); + g.drawString("Press Button1", 30,y_wg_bottom-(3*v_font3size)); + g.drawString("Press Button2: Colour", 30,y_wg_bottom-(2*v_font3size)); + g.drawString("Press Button3: Quit", 30,y_wg_bottom-v_font3size); + g.flip(); +} + + +function ChangeColorBannerArea(v_color){ + if (v_color=='#111111') v_color='#f3f3f1'; + else if (v_color=='#f3f3f1') v_color='#51504f'; + else if (v_color=='#51504f') v_color=0x7800;// Maroon + else if (v_color==0x7800) v_color='#CC3333';//coldarkred + else if (v_color=='#CC3333') v_color='#111111'; + return (v_color); +} +//Clean fill top area with a color +function ClearBannerArea(){ + g.setColor(v_color_b_area); + g.fillRect(55,32,185,78); + g.flip(); +} + //arg input area Touch1=left Touch2=right + function DrawRoundOption(x_obj1,y_obj1,x_obj2,y_obj2,i_area){ + //draw a img from an Image object + var img_obj_check = { + width : 30, height : 30, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,28436,38872,4838,6951,34711,26355,24242,32630,36792,30517,32598,40953,26323,9129,7016]), + buffer : E.toArrayBuffer(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMwiKZmQAAAAAAAAAAAMzMIimZVVUAAAAAAAAADMzCIpmVVViAAAAAAAAAzMwiKZlVVYi7AAAAAAAMzMIimZVVWIu7oAAAAADMzCIpmVVViLu6qgAAAADMwiKZlVVYi7uqoQAAAAzMIimZVVWIu3eqERAAAAzCIpmVVViLtzNxERAAAAwiKZlVVYi7czRxEWAAAAIimZe1WIu3M0cRFmAAAAIpmePqiLtzNHERZmAAAAKZleM+q7czRxEWZtAAAAmZVV4z4XQ0cRFmbdAAAAmVVVjjPjNHERZm3XAAAAlVVYi+MzRxEWZt13AAAAlVWIu640cRFmbdd3AAAABViLu6p3ERZm3XdwAAAABYi7uqoREWZt13dwAAAAAIu7qqERFmbdd3cAAAAAAAu6qhERZm3Xd3AAAAAAAACqoREWZt13dwAAAAAAAAAKERFmbdd3cAAAAAAAAAAAARZm3XdwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) + }; + var img_obj_uncheck = { + width : 30, height : 30, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,63422,9532,13789,59197,57084,34266,28220,63390,65503,61310,61277,57116,55003,61309,40604]), + buffer : E.toArrayBuffer(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzMzMzMwAAAAAAAAAAADMzf//3MzMAAAAAAAAAAzPxmZkRrzMwAAAAAAAAM3mZmZmRiKczAAAAAAADP5mZmZmRiKpjMAAAAAAzeZmZmZkRiKq3MwAAAAAzGZmZmZkRiKq8MwAAAAM/mZmZmZkYiKtE8iAAAAMxmZmZmZEYiqtEUiAAAAN5mZmZmRGIiqtExyAAAAPxmZmZkRiIqrRMViAAAAPxEREREYiKq7RMViAAAAP4ERERiIiqq0TFViAAAAP4iIiIiIqqtETFViAAAAN6iIiIiqq7RExV1yAAAAM0qqqqqru0RMVd0iAAAAM/uqqru7RETFXdYiAAAAAzS7u7RERMxV3dIgAAAAAzdEREREzFVd3XIgAAAAADNkREzMVVXd1iIAAAAAAAM3VVVVVd3dciAAAAAAAAAzNtVd3d1iIgAAAAAAAAADMidmZnIiIAAAAAAAAAAAAiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) + }; + if (i_area=='none') { + g.drawImage(img_obj_uncheck,x_obj1,y_obj1); + g.drawImage(img_obj_uncheck,x_obj2,y_obj2); + } + else if (i_area=='Touch 1') { + g.drawImage(img_obj_check,x_obj1,y_obj1); + g.drawImage(img_obj_uncheck,x_obj2,y_obj2); + if (v_mode_debug==1) console.log("Draw option check left"); + } + else if (i_area=='Touch 2') { + g.drawImage(img_obj_uncheck,x_obj1,y_obj1); + g.drawImage(img_obj_check,x_obj2,y_obj2); + if (v_mode_debug==1) console.log("Draw option check right"); + } + +} + +function DrawSwitch(swipedir){ +if (swipedir==' <---') { + if (v_mode_debug==1) console.log("Draw switch <--"); + var img_off = { + width : 48, height : 48, bpp : 2, + transparent : 0, + palette : new Uint16Array([65535,63968,40283,50781]), + buffer : E.toArrayBuffer(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAA///9VVVVVVVQAAAD/qq/1VVVVVVVAAAP6qqv9VVVVVVVUAA/qqqv/VVVVVVVVAD+qqq//1VVVVVVVQP6qqq//1VVVVVVVQPqqqr//9VVVVVVVUvqqqr//9VVVVVVVU+qqqv/+uVVVVVVVV+qqqv+quVVVVVVVV+qqq+qqvVVVVVVVV+qqvqqqvVVVVVVVV+qv+qqquVVVVVVVV+r/+qqquVVVVVVVVv//6qqq9VVVVVVVUP//6qqq9VVVVVVVUP//qqqr1VVVVVVVQD//qqqv1VVVVVVVQA/+qqq/VVVVVVVVAAP+qqr9VVVVVVVUAAD/qq/1VVVVVVVAAAA///9VVVVVVVQAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) + }; + g.drawImage(img_off,99,33); + } + else if (swipedir==' --->') { + if (v_mode_debug==1) console.log("Draw switch -->"); + var img_on = { + width : 48, height : 48, bpp : 2, + transparent : 0, + palette : new Uint16Array([65535,36361,27879,40283]), + buffer : E.toArrayBuffer(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAABVVVVVqqf///AAAAVVVVVWqn////wAAFVVVVVWqf////8AAVVVVVVap//////ABVVVVVVqr//////wBVVVVVVqn//////8FVVVVVVqv//////8FVVVVVWqf///////VVVVVVWq////////VVVVVVWq////////VVVVVVWq////////VVVVVVWq////////VVVVVVWq////////VVVVVVWq////////FVVVVVWqf///////FVVVVVVqv//////8BVVVVVVqn//////8BVVVVVVar//////wAVVVVVVap//////AAFVVVVVWqf////8AAAVVVVVWqn////wAAABVVVVVqqf///AAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) + }; + g.drawImage(img_on,99,33); + } +} + + +function PrintUserInput(boton){ + if (v_mode_debug==1) console.log("Pressed touch/BTN",boton); + if (v_clicks==0) { + PrintAreas(); + v_clicks++; + } + ClearBannerArea(); + + if (boton==' <---') DrawSwitch(boton); + else if (boton==' --->') DrawSwitch(boton); + //all input but not swipe + else { + g.setColor(colorange); + //Call info banner + g.setFontVector(30).drawString(boton, 63, 55); + if ((boton=='Touch 1')||(boton=='Touch 2')){ + if (v_selected_row==1) v_y_opt=v_y_optionrow1; + else if (v_selected_row==2) v_y_opt=v_y_optionrow2; + DrawRoundOption(20,v_y_opt,190,v_y_opt,boton); + //set the option value in an array + array_r_option[v_selected_row]=boton; + if (v_mode_debug==1) console.log("array["+v_selected_row+"]="+array_r_option[v_selected_row]); + } + } + + g.flip(); +} + +function Btn1Clkd(boton){ + if (v_mode_debug==1) console.log("Pressed BTN1"); + if (v_clicks==0){ + PrintAreas(); //only 1st time + //v_selected_row=1; + v_clicks++; + } + + + else if ((v_clicks>0)&&(v_selected_row!=v_total_rows)){ + v_selected_row++; + //Params: row_arrow, row_clear_area + if (v_mode_debug==1) console.log("row :"+v_selected_row); + DrawRowSelArrow(v_selected_row,v_selected_row-1); + v_clicks++; + } + else if ((v_clicks>0)&&(v_selected_row==v_total_rows)){ + + DrawRowSelArrow(1,v_selected_row); + if (v_mode_debug==1) console.log("last row :"+v_selected_row); + v_selected_row=1; + v_clicks++; + } + PrintUserInput("Button1"); +} + +function Btn2Clkd(boton){ + if (v_mode_debug==1) console.log("Pressed BTN2"); + v_color_b_area=ChangeColorBannerArea(v_color_b_area); + if (v_clicks==0){ + PrintAreas();//only 1st time + v_clicks++; + } + PrintUserInput("Button2"); +} + +function DrawBangleButtons(){ + + /*Button name */ + g.setColor(v_color_text); //green + g.setFontVector(v_font3size); + g.drawString("B1", x_max_screen-g.stringWidth("B1"),y_wg_top); + g.drawString("B2", x_max_screen-g.stringWidth("B2"),y_btn2); + //y y_wg_bottom-v_font3size ? + g.drawString("B3",x_max_screen-g.stringWidth("B3"),y_wg_bottom); + + /*Button area description */ + g.setFontVector(v_font1size); + g.setColor(v_color_lines); + //y_wg_bottom-(2*v_font1size) + g.drawString("Quit", x_max_screen-g.stringWidth("Quit"),y_wg_bottom-v_font1size-2); + + + //Print version + if (v_mode_debug==1){ + g.setColor(0,1,0); //green + //y_wg_bottom-(2*v_font1size) + g.drawString(v_str_version, x_max_screen-g.stringWidth(v_str_version),y_wg_bottom-(v_font1size*3)); + } + + //under btn2, left top 90grades + g.setFontAlign(-1,-1,1); + g.drawString("Color", x_max_screen-v_font1size,y_btn2+v_font3size); + //g.drawString("Color", x_max_screen-g.stringWidth("Color"),y_btn2+v_font1size); + + + g.setColor(0,1,0); //green + g.drawString("Up", x_max_screen-v_font1size,y_wg_top+v_font3size); + g.setColor(v_color_lines); + g.drawString("Down", x_max_screen-2*v_font1size,y_wg_top+v_font3size); + g.flip(); + //back to standard /horizontal + g.setFontAlign(-1,-1,0); +} + +function DrawRowSelArrow(v_drawRow, v_clearRow){ + //Params: row_arrow, row_clear_area + //for clear previous draw arrow + if (v_clearRow!== undefined) { + g.setColor(colbackg); + if (v_clearRow==1) v_y_arrow=v_y_optionrow1+14; + else if (v_clearRow==2) v_y_arrow=v_y_optionrow2+14; + else if (v_clearRow==3) v_y_arrow=v_y_optionrow3+14; + g.fillRect(5,v_y_arrow-5,13,v_y_arrow+5); + g.flip(); + } + //draw an arrow to select a row + if (v_drawRow!== undefined) { + if (v_drawRow==1) v_y_arrow=v_y_optionrow1+14; + else if (v_drawRow==2) v_y_arrow=v_y_optionrow2+14; + else if (v_drawRow==3) v_y_arrow=v_y_optionrow3+14; + + g.setColor(v_color_lines); + g.drawLine(5, v_y_arrow, 13, v_y_arrow);//horizontal + g.drawLine(13, v_y_arrow, 10, v_y_arrow-5);//over diag + g.drawLine(13, v_y_arrow, 10, v_y_arrow+5);//under diag + g.flip(); + } + else console.log("Error: Param row nbr missing"); +} + +function PrintAreas(){ + if (v_mode_debug==1) console.log("Log: *** Print Areas in screen"); + ClearActiveArea(0,y_wg_top,x_max_usable_area,y_wg_bottom); + g.setColor(v_color_lines); + + DrawRowSelArrow(1); + DrawRoundOption(20,v_y_optionrow1,190,v_y_optionrow1,'none'); + DrawRoundOption(20,v_y_optionrow2,190,v_y_optionrow2,'none'); + + g.drawLine(x_max_screen-1, 50, x_max_screen-1, 65);//vlide right border + g.drawLine(x_mid_screen, 80, x_mid_screen, 105);//vline middle separation part1 up + g.drawLine(x_mid_screen, 140, x_mid_screen, 180);//vline middle separation part2 down + + + g.setFontVector(v_font3size); + g.drawString("Middle area", 80,155); + g.drawString("Left area", 15, 185); + g.drawString("Right area", 140,185); + + if (v_model=='BANGLEJS') DrawBangleButtons(); +} + +function UserInput(){ + Bangle.on('touch', function(button){ + switch(button){ + case 1: + PrintUserInput("Touch 1");//left + break; + case 2: + PrintUserInput("Touch 2");//right + break; + case 3: + PrintUserInput("Touch 3");//center 1+2 + break; + } + }); + //only the name of the function + setWatch(Btn1Clkd, BTN1, { repeat: true }); + setWatch(Btn2Clkd, BTN2, { repeat: true }); + setWatch(Bangle.showLauncher, BTN3, { repeat: true }); + Bangle.on('swipe', dir => { + if(dir == 1) PrintUserInput(" --->"); + else PrintUserInput(" <---"); + }); + if (v_mode_debug==1) console.log("Log: Input conditions loaded"); +} //end of UserInput + +//Main code + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + g.setColor(v_color_lines); + //optional line below widgets area + //g.drawLine(60, 30, 180, 30); + //g.flip(); + //end optional + PrintHelp(); + + UserInput(); \ No newline at end of file diff --git a/apps/testuserinput/app.png b/apps/testuserinput/app.png new file mode 100644 index 000000000..c7ad0f43f Binary files /dev/null and b/apps/testuserinput/app.png differ diff --git a/apps/testuserinput/testUserInput_ss0.png b/apps/testuserinput/testUserInput_ss0.png new file mode 100644 index 000000000..f0c1a2cec Binary files /dev/null and b/apps/testuserinput/testUserInput_ss0.png differ diff --git a/apps/testuserinput/testUserInput_ss1.png b/apps/testuserinput/testUserInput_ss1.png new file mode 100644 index 000000000..45f634b47 Binary files /dev/null and b/apps/testuserinput/testUserInput_ss1.png differ diff --git a/apps/testuserinput/testUserInput_ss2.png b/apps/testuserinput/testUserInput_ss2.png new file mode 100644 index 000000000..2a380380a Binary files /dev/null and b/apps/testuserinput/testUserInput_ss2.png differ diff --git a/apps/testuserinput/testUserInput_ss3.png b/apps/testuserinput/testUserInput_ss3.png new file mode 100644 index 000000000..0e0a83773 Binary files /dev/null and b/apps/testuserinput/testUserInput_ss3.png differ diff --git a/apps/vibrclock/ChangeLog b/apps/vibrclock/ChangeLog index b4d1ae593..82be3ac70 100644 --- a/apps/vibrclock/ChangeLog +++ b/apps/vibrclock/ChangeLog @@ -1 +1,2 @@ 0.01: First commit +0.02: Made Date more visible diff --git a/apps/vibrclock/app.js b/apps/vibrclock/app.js index 188470cdc..9995eea12 100644 --- a/apps/vibrclock/app.js +++ b/apps/vibrclock/app.js @@ -5,7 +5,7 @@ require("Font7x11Numeric7Seg").add(Graphics); var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; // position on screen const X = 160, Y = 140; - + function draw() { // work out how to display the current time var d = new Date(); @@ -23,15 +23,15 @@ function draw() { g.drawString(time, X, Y, true /*clear background*/); // draw the seconds (2x size 7 segment) g.setFont("7x11Numeric7Seg",2); - g.drawString(("0"+d.getSeconds()).substr(-2), X+30, Y, true /*clear background*/); + g.drawString(("0"+d.getSeconds()).substr(-2), X+35, Y, true /*clear background*/); // draw the date, in a normal font - g.setFont("6x8"); + g.setFont("6x8", 3); g.setFontAlign(0,1); // align center bottom // pad the date - this clears the background if the date were to change length var dateStr = " "+require("locale").date(d)+" "; - g.drawString(dateStr, g.getWidth()/2, Y+15, true /*clear background*/); + g.drawString(dateStr, g.getWidth()/2, Y+35, true /*clear background*/); } - + // Clear the screen once, at startup g.clear(); // draw immediately at first @@ -51,7 +51,7 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); // Show launcher when middle button pressed setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); - + // ====================================== Vibration // vibrate 0..9 function vibrateDigit(num) { @@ -74,24 +74,24 @@ function vibrateNumber(num) { }); }); } - + var vibrateBusy; function vibrateTime() { if (vibrateBusy) return; vibrateBusy = true; - + var d = new Date(); var hours = d.getHours(), minutes = d.getMinutes(); if (is12Hour) { if (hours == 0) hours = 12; else if (hours>12) hours -= 12; } - + vibrateNumber(hours.toString()). then(() => new Promise(resolve=>setTimeout(resolve,500))). then(() => vibrateNumber(minutes.toString())). then(() => vibrateBusy=false); } - + // when BTN1 pressed, vibrate setWatch(vibrateTime, BTN1, {repeat:true,edge:"rising"}); diff --git a/apps/walkersclock/ChangeLog b/apps/walkersclock/ChangeLog new file mode 100644 index 000000000..57404ac41 --- /dev/null +++ b/apps/walkersclock/ChangeLog @@ -0,0 +1,4 @@ +0.01: First version of the Walkers Clock +0.02: Fixed screen flicker +0.03: Added display of GPS fix lat/lon and course +0.04: Don't buzz for GPS fix in Quiet Mode diff --git a/apps/walkersclock/README.md b/apps/walkersclock/README.md new file mode 100644 index 000000000..7ff45a06a --- /dev/null +++ b/apps/walkersclock/README.md @@ -0,0 +1,61 @@ +# Walkers Clock + +A larg font watch, displays steps, can switch GPS on/off, displays grid reference + +The watch works well with GPSsetup, the Activepedom or Widpedom +wdigets. A tiny GPS power widget is waiting in the wings for when +the v2.09 firware is released. + +## Features + +- Displays the time in large font +- Uses BTN1 to select modeline display (eg battery status or switch between setting when in a function mode +- Uses BTN3 to select the function mode (eg turn on/off GPS, or change GPS display) + - two function menus at present + GPS Power = On/Off + GPS Display = Grid | Speed Alt + when the modeline in CYAN use button BTN1 to switch between options +- Display the current steps if one of the steps widgets is installed +- Ensures that BTN2 requires a 1.5 second press in order to switch to the launcher + this is so you dont accidently switch out of the GPS/watch display with you coat sleeve +- Displays the timestamp of the last GPS fix when the GPS is on +- Buzzes when the GPS aquires a positional satellite fix +- Displays the current OS map grid reference in a large font +- Displays the age of the current GPS position fix in seconds +- Works in tandem with the GPS setup app so that you can reduce the power consumption of the GPS + +## BTN1 cycles the information line + +- By default the modeline is switched off +- Click BTN1 once and display your steps (if one of the step widgets is installed) +- Click BTN1 again and it will show battery % charge +- Click BTN1 again and it will switch the modeline off + +## BTN2 Long press to start the launcher + +BTN2 is confiured to respond to a 1.5 second press in order to switch +to the launcher App. Simply press and hold until you hear a buzz and +release. This avoids accidently switching out of the watch app when +clothing catches it. + +## BTN3 cycles the function mode + +- Click BTN3 once and the GPS ON / OFF menu is displayed +- If the GPS is OFF then pressing BTN1 will turn it ON +- If the GPS is ON then Clicking BTN1 will turn it OFF + +When the GPS is ON a second function menu can be displayed by +pressing BTN3 again. This will show options to change the GPS display +on the second line of the watch. + +- Grid - will display the GPS position converted to an OS Grid Ref +- Speed - will display the GPS speed inforation supplied in the last GPS fix +- Alt - will display the altitude information + +![](gps_osref.jpg) +![](gps_speed.jpg) +![](gps_alt.jpg) + +## Future Enhancements +* Ability to turn on the heart rate monitor and display the rate on the info line +* Maybe a simple stopwatch capability diff --git a/apps/walkersclock/app.js b/apps/walkersclock/app.js new file mode 100644 index 000000000..8a5e826c4 --- /dev/null +++ b/apps/walkersclock/app.js @@ -0,0 +1,576 @@ +/* + * Walkers clock, hugh barney AT googlemail DOT com + * + * A clock that has the following features + * - displays the time in large font + * - uses BTN1 to select modeline display (eg battery status or switch between setting when in a function mode + * - uses BTN3 to select the function mode (eg turn on/off GPS, or change GPS display) + * - two function menus at present + * GPS Power = On/Off + * GPS Display = Grid | Speed Alt + * when the modeline in CYAN use button BTN1 to switch between options + * - display the current steps if one of the steps widgets is installed + * - ensures that BTN2 requires a 1.5 second press in order to switch to the launcher + * this is so you dont accidently switch out of the GPS/watch display with you coat sleeve + * - displays the timestamp of the last GPS fix when the GPS is on + * - buzzes when the GPS aquires a positional satellite fix + * - displays the current OS map grid reference in a large font + * - displays the age of the current GPS position fix in seconds + * - works in tandem with the GPS setup app so that you can reduce the power consumption of the GPS + * + */ + +const INFO_NONE = "none"; +const INFO_BATT = "batt"; +const INFO_STEPS = "step"; + +const FN_MODE_OFF = "fn_mode_off"; +const FN_MODE_GPS = "fn_mode_gps"; +const FN_MODE_GDISP = "fn_mode_gdisp"; + +const GPS_OFF = "gps_off"; +const GPS_TIME = "gps_time"; +const GPS_SATS = "gps_sats"; +const GPS_RUNNING = "gps_running"; + +const GDISP_OS = "g_osref"; +const GDISP_LATLN = "g_latln"; +const GDISP_SPEED = "g_speed"; +const GDISP_ALT = "g_alt"; +const GDISP_COURSE = "g_course"; + +const Y_TIME = 40; +const Y_ACTIVITY = 120; +const Y_MODELINE = 200; + +let gpsState = GPS_OFF; +let gpsPowerState = false; +let infoMode = INFO_NONE; +let functionMode = FN_MODE_OFF; +let gpsDisplay = GDISP_OS; +let prevInfoStr = "clear"; +let prevActivityStr = "clear"; +let prevSteps = "clear"; +let clearActivityArea = true; + +let last_steps = undefined; +let firstPress = 0; + +let last_fix = { + fix: 0, + alt: 0, + lat: 0, + lon: 0, + speed: 0, + time: 0, + satellites: 0 +}; + +function drawTime() { + var d = new Date(); + var da = d.toString().split(" "); + var time = da[4].substr(0,5); + + g.reset(); + g.clearRect(0,Y_TIME, 239, Y_ACTIVITY - 1); + + g.setColor(1,1,1); // white + g.setFontAlign(0, -1); + + if (gpsState == GPS_SATS || gpsState == GPS_RUNNING) { + time = last_fix.time.toUTCString().split(" "); + time = time[4]; + g.setFont("Vector", 56); + } else { + g.setFont("Vector", 80); + } + + g.drawString(time, g.getWidth()/2, Y_TIME); +} + +function drawActivity() { + var steps = getSteps(); + if (!gpsPowerState && steps != prevSteps) + clearActivityArea = true; + + prevSteps = steps; + + if (clearActivityArea) { + g.clearRect(0, Y_ACTIVITY, 239, Y_MODELINE - 1); + clearActivityArea = false; + } + + if (!gpsPowerState) { + g.setColor(0,255,0); // green + g.setFont("Vector", 60); + g.drawString(getSteps(), g.getWidth()/2, Y_ACTIVITY); + return; + } + + g.setFont("6x8", 3); + g.setColor(1,1,1); + g.setFontAlign(0, -1); + + if (gpsState == GPS_TIME) { + g.drawString("Waiting for", g.getWidth()/2, Y_ACTIVITY); + g.drawString("GPS", g.getWidth()/2, Y_ACTIVITY + 30); + return; + } + + if (gpsState == GPS_SATS) { + g.drawString("Satellites", g.getWidth()/2, Y_ACTIVITY); + g.drawString(last_fix.satellites, g.getWidth()/2, Y_ACTIVITY + 30); + return; + } + + if (gpsState == GPS_RUNNING) { + //console.log("Draw GPS Running"); + let time = formatTime(last_fix.time); + let age = timeSince(time); + let os = OsGridRef.latLongToOsGrid(last_fix); + let ref = to_map_ref(6, os.easting, os.northing); + let speed; + let activityStr = ""; + + if (age < 0) age = 0; + g.setFontVector(40); + g.setColor(0xFFC0); + + switch(gpsDisplay) { + case GDISP_OS: + activityStr = ref; + break; + case GDISP_LATLN: + g.setFontVector(26); + activityStr = last_fix.lat.toFixed(4) + ", " + last_fix.lon.toFixed(4); + break; + case GDISP_SPEED: + speed = last_fix.speed; + speed = speed.toFixed(1); + activityStr = speed + "kph"; + break; + case GDISP_ALT: + activityStr = last_fix.alt + "m"; + break; + case GDISP_COURSE: + activityStr = last_fix.course; + break; + } + + 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); + } +} + +function onTick() { + if (!Bangle.isLCDOn()) + return; + + if (gpsPowerState) { + drawAll(); + return; + } + + if (last_steps != getSteps()) { + last_steps = getSteps(); + drawAll(); + return; + } + + var t = new Date(); + + if (t.getSeconds() === 0 && !gpsPowerState) { + drawAll(); + } +} + +function drawAll(){ + drawTime(); + drawActivity(); // steps, hrt or gps + drawInfo(); +} + +function drawInfo() { + let val; + let str = ""; + let col = 0x07E0; // green + + switch(functionMode) { + case FN_MODE_OFF: + break; + case FN_MODE_GPS: + col = 0x07FF; // cyan + str = "GPS: " + (gpsPowerState ? "ON" : "OFF"); + drawModeLine(str,col); + return; + case FN_MODE_GDISP: + col = 0x07FF; // cyan + switch(gpsDisplay) { + case GDISP_OS: + str = "GPS: Grid"; + break; + case GDISP_LATLN: + str = "GPS: Lat,Lon"; + break; + case GDISP_SPEED: + str = "GPS: Speed"; + break; + case GDISP_ALT: + str = "GPS: Alt"; + break; + case GDISP_COURSE: + str = "GPS: Course"; + break; + } + drawModeLine(str,col); + return; + } + + switch(infoMode) { + case INFO_NONE: + col = 0x0000; + str = ""; + break; + case INFO_STEPS: + str = "Steps: " + getSteps(); + break; + case INFO_BATT: + default: + str = "Battery: " + E.getBattery() + "%"; + } + + drawModeLine(str,col); +} + +function drawModeLine(str,col) { + // check if we need to draw, avoid flicker + if (str == prevInfoStr) + return; + + prevInfoStr = str; + drawModeLine(str,col); + g.setFont("6x8", 3); + g.setColor(col); + g.fillRect(0, Y_MODELINE - 3, 239, Y_MODELINE + 25); + g.setColor(0,0,0); + g.setFontAlign(0, -1); + g.drawString(str, g.getWidth()/2, Y_MODELINE); +} + +function changeInfoMode() { + switch(functionMode) { + case FN_MODE_OFF: + break; + case FN_MODE_GPS: + gpsPowerState = !gpsPowerState; + Bangle.buzz(); + Bangle.setGPSPower(gpsPowerState ? 1 : 0); + if (gpsPowerState) { + gpsState = GPS_TIME; // waiting first response so we can display time + Bangle.on('GPS', processFix); + } else { + Bangle.removeListener("GPS", processFix); + gpsState = GPS_OFF; + } + resetLastFix(); + + // poke the gps widget indicator to change + if (WIDGETS.gps !== undefined) { + WIDGETS.gps.draw(); + } + functionMode = FN_MODE_OFF; + infoMode = INFO_NONE; + clearActivityArea = true; + return; + + case FN_MODE_GDISP: + switch (gpsDisplay) { + case GDISP_OS: + gpsDisplay = GDISP_SPEED; + break; + case GDISP_SPEED: + gpsDisplay = GDISP_ALT; + break; + case GDISP_ALT: + gpsDisplay = GDISP_COURSE; + break; + case GDISP_COURSE: + gpsDisplay = GDISP_LATLN; + break; + case GDISP_LATLN: + default: + gpsDisplay = GDISP_OS; + break; + } + } + + switch(infoMode) { + case INFO_NONE: + if (stepsWidget() !== undefined) + infoMode = INFO_STEPS; + else + infoMode = INFO_BATT; + break; + case INFO_STEPS: + infoMode = INFO_BATT; + break; + case INFO_BATT: + default: + infoMode = INFO_NONE; + } + + clearActivityArea = true; +} + +function changeFunctionMode() { + //console.log("changeFunctionMode()"); + + if (gpsState != GPS_RUNNING) { + switch(functionMode) { + case FN_MODE_OFF: + functionMode = FN_MODE_GPS; + break; + case FN_MODE_GPS: + default: + functionMode = FN_MODE_OFF; + break; + } + } else { + // if GPS is RUNNING then we want the GPS display options first + switch(functionMode) { + case FN_MODE_OFF: + functionMode = FN_MODE_GDISP; + break; + case FN_MODE_GDISP: + functionMode = FN_MODE_GPS; + break; + case FN_MODE_GPS: + default: + functionMode = FN_MODE_OFF; + break; + } + } + + infoMode = INFO_NONE; // function mode overrides info mode +} + +function resetLastFix() { + last_fix = { + fix: 0, + alt: 0, + lat: 0, + lon: 0, + speed: 0, + time: 0, + satellites: 0 + }; +} + +function processFix(fix) { + last_fix.time = fix.time; + + if (gpsState == GPS_TIME) { + gpsState = GPS_SATS; + clearActivityArea = true; + } + + if (fix.fix) { + if (!last_fix.fix) { + if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.buzz(); // buzz on first position + } + clearActivityArea = true; + } + gpsState = GPS_RUNNING; + last_fix = fix; + } +} + +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; +} + + +/************* GPS / OSREF Code **************************/ + +function formatTime(now) { + var fd = now.toUTCString().split(" "); + return fd[4]; +} + +function timeSince(t) { + var hms = t.split(":"); + var now = new Date(); + + var sn = 3600*(now.getHours()) + 60*(now.getMinutes()) + 1*(now.getSeconds()); + var st = 3600*(hms[0]) + 60*(hms[1]) + 1*(hms[2]); + + return (sn - st); +} + +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}`; +} + +// start a timer and buzz whenn held long enough +function firstPressed() { + firstPress = getTime(); + pressTimer = setInterval(longPressCheck, 1500); +} + +// if you release too soon there is no buzz as timer is cleared +function thenReleased() { + var dur = getTime() - firstPress; + if (pressTimer) { + clearInterval(pressTimer); + pressTimer = undefined; + } + if ( dur >= 1.5 ) Bangle.showLauncher(); +} + +// when you feel the buzzer you know you have done a long press +function longPressCheck() { + Bangle.buzz(); + if (pressTimer) { + clearInterval(pressTimer); + pressTimer = undefined; + } +} + +var pressTimer; + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +drawAll(); + +Bangle.on('lcdPower',function(on) { + functionMode = FN_MODE_OFF; + infoMode = INFO_NONE; + if (on) { + prevInfoStr = "on"; // forces are redraw + drawAll(); + } else { + prevInfoStr = "off"; // forces are redraw + drawInfo(); + } +}); + +var click = setInterval(onTick, 5000); + +setWatch(() => { changeInfoMode(); drawAll(); }, BTN1, {repeat: true}); +setWatch(() => { changeFunctionMode(); drawAll(); }, BTN3, {repeat: true}); + +// make BTN require a long press (1.5 seconds) to switch to launcher +setWatch(firstPressed, BTN2,{repeat:true,edge:"rising"}); +setWatch(thenReleased, BTN2,{repeat:true,edge:"falling"}); + diff --git a/apps/walkersclock/gps_alt.jpg b/apps/walkersclock/gps_alt.jpg new file mode 100644 index 000000000..d407998b6 Binary files /dev/null and b/apps/walkersclock/gps_alt.jpg differ diff --git a/apps/walkersclock/gps_osref.jpg b/apps/walkersclock/gps_osref.jpg new file mode 100644 index 000000000..8d3f9796c Binary files /dev/null and b/apps/walkersclock/gps_osref.jpg differ diff --git a/apps/walkersclock/gps_speed.jpg b/apps/walkersclock/gps_speed.jpg new file mode 100644 index 000000000..5c021b128 Binary files /dev/null and b/apps/walkersclock/gps_speed.jpg differ diff --git a/apps/walkersclock/icon.js b/apps/walkersclock/icon.js new file mode 100644 index 000000000..cc1b8b8da --- /dev/null +++ b/apps/walkersclock/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AEcsAAYuuGFIqEF9gFJF/62SACAvvGCgYEwIASqwxVF4mmACQvEGCJcCF65iDF/4vbqwvVd4YeBJgIECAAulq4KDmS/XAAVXJwgvHOAIKCOoQuTGQ1WvYvCvYAGBQdWLwYtUMQwAQGAQuYXgJPCmQEBAAwKDCQKNZlmsAAKECAoQAEX4QQESKh4EDwVXegIvPSKgbBDwMs64ABAoOBAoQAE1gvGTAQwQCINXF4olBFw4KCAAYvCDIQvZABgv/F/4vqCoYvpGAMyF7ICBF6QADEIeBABQuCF4QACFyAwFDwRlCABNWHoUyFyoxEwOIxGBPoIAJMYYvCFyrzEGAIALFwS7TYRYACmQAGSxAtcACaNWwGArizBAwIAININWrgTCF6+GAAYvCA4gACwAKGCQIv/F+qqBAAQvCA4gACXAIKFF67cBAAQvCAwYKFmQGFF6wAZF6YxaFyo1HIBYpcAH4A/AAoA==")) diff --git a/apps/walkersclock/walkersclock48.png b/apps/walkersclock/walkersclock48.png new file mode 100644 index 000000000..492af0c61 Binary files /dev/null and b/apps/walkersclock/walkersclock48.png differ diff --git a/apps/waypointer/README.md b/apps/waypointer/README.md new file mode 100644 index 000000000..3f0f529b8 --- /dev/null +++ b/apps/waypointer/README.md @@ -0,0 +1,176 @@ +# Waypointer - navigate to waypoints + +The app is aimed at navigation whilst walking. Please note that it +would be foolish in the extreme to rely on this as your only +navigation aid! + +Please refer to the section on calibration of the compass. This +should be done each time the app is going to be used. + +The main part of the display is a compass arrow that points in the +direction you need to walk in. Once you have selected a waypoint a +bearing from your current position (received from a GPS fix) is +calculated and the compass is set to point in that direction. If the +arrow is pointing to the left, turning left should straighten the arrow +up so that it is pointing straight ahead. + + +![](waypointer_screenshot.jpg) + +The large digits are the bearing from the current position. On the +left is the distance to the waypoint in local units. The top of the +display is a circular compass which displays the direction you will +need to travel in to reach the selected waypoint. The blue text is +the name of the current waypoint. NONE means that there is no +waypoint set and so bearing and distance will remain at 0. To select +a waypoint, press BTN2 (middle) and wait for the blue text to turn +white. Then use BTN1 and BTN3 to select a waypoint. The waypoint +choice is fixed by pressing BTN2 again. In the screen shot below a +waypoint giving the location of Stone Henge has been selected. + +The screenshot above shows that Stone Henge is 259.9 miles from the +current location. To travel towards Stone Henge I need to turn +slightly left until the arrow is pointing straight ahead. As you +continue to walk in the pointed direction you should see the distance +to the waypoint reduce. The frequency of updates will depend on +which settings you have used in the GPS. + +At the top of the screen you can see two widgets. These are the [GPS +Power +Widget](https://github.com/espruino/BangleApps/tree/master/apps/widgps) +and the [Compass Power Indicator Widget]. These can be installed +seperately and provide you a indication of when the GPS and Compass +are switched on and drawing power. + + +## Marking Waypoints + +The app lets you mark your current location as follows. There are +vacant slots in the waypoint file which can be allocated a +location. In the distributed waypoint file these are labelled WP0 to +WP4. Select one of these - WP2 is shown below. + +![](wp2_screenshot.jpg) + +Bearing and distance are both zero as WP2 has currently no GPS +location associated with it. To mark the location, press BTN2. + +![](wp2_saved.jpg) + +The app indicates that WP2 is now marked by adding the prefix @ to +it's name. The distance should be small as shown in the screen shot +as you have just marked your current location. + +## Waypoint JSON file + +When the app is loaded from the app loader, a file named +`waypoints.json` is loaded along with the javascript etc. The file +has the following contents: + + +``` +[ + { + "name":"NONE" + }, + { + "name":"No10", + "lat":51.5032, + "lon":-0.1269 + }, + { + "name":"Stone", + "lat":51.1788, + "lon":-1.8260 + }, + { "name":"WP0" }, + { "name":"WP1" }, + { "name":"WP2" }, + { "name":"WP3" }, + { "name":"WP4" } +] +``` + +The file contains the initial NONE waypoint which is useful if you +just want to display course and speed. The next two entries are +waypoints to No 10 Downing Street and to Stone Henge - obtained from +Google Maps. The last five entries are entries which can be *marked*. + +You add and delete entries using the Web IDE to load and then save +the file from and to watch storage. The app itself does not limit the +number of entries although it does load the entire file into RAM +which will obviously limit this. + + +## Waypoint Editor + +Clicking on the download icon of gpsnav in the app loader invokes the +waypoint editor. The editor downloads and displays the current +`waypoints.json` file. Clicking the `Edit` button beside an entry +causes the entry to be deleted from the list and displayed in the +edit boxes. It can be restored - by clicking the `Add waypoint` +button. A new markable entry is created by using the `Add name` +button. The edited `waypoints.json` file is uploaded to the Bangle by +clicking the `Upload` button. + + +## Calibration of the Compass + +The Compass should be calibrated before using the App to navigate to +a waypoint (or a series of waypoints). To do this use either the +Arrow Compass or the [Navigation +Compass](https://github.com/espruino/BangleApps/tree/master/apps/magnav). +Open the compass app and clicking on BTN3. The calibration process +takes 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`. + + +## Advantages and Disadvantages + +This approach has some advantages and disadvantages. First following +the arrow is fairly easy to do and once the bearing has been +established it does not matter if there is not another GPS fix for a +while as the compass will continue to point in the general direction. +Second the GPS will only supply a course to the waypoint (a bearing) +once you are travelling above 8m/s or 28kph. This is not a practical +walking speed. 5kmph is considered a marching pace. + +One disadvantage is that the compass is not very accurate. I have +observed it being 20-30 degrees off when compared to a hiking +compass. Sometime its is necessary to walk in the opposite direction +for a bit to establish the correct direction to go in. The accuracy +of the compass is impacted by the magnetic clamps on the charging +cable, so it is particularly important that you recalibtrate the +compass after the watch has been charged. That said I have found I +am successfully able to follow a chain of waypoints as a route. + + +## Possible Future Enhancements + +- Buzz when the GPS establishes its first fix. + +- Add a small LED to show the status of the GPS during the phase of + establishing a first fix. + +- Add an option to calibrate the Compass without having to use the + Arrow Compass or the Navigation Compass. + +- Investigate the accuracy of the Compass and how it changes + throughout the day after the watch battery has been fully charged. + +- Investigate the possibility of setting the GPS in low speed mode so + that a current course value can be obtained. + +- Buzz when you arrive within 20m of a waypoint to signify arrival + + +## Acknowledgements + +The majority of the code in this application is a merge of +[jeffmer's](https://github.com/jeffmer/JeffsBangleAppsDev) GPS +Navigation and Compass Navigation Applications. + diff --git a/apps/waypointer/app.js b/apps/waypointer/app.js new file mode 100644 index 000000000..d3aab7c50 --- /dev/null +++ b/apps/waypointer/app.js @@ -0,0 +1,283 @@ +var pal_by = new Uint16Array([0x0000,0xFFC0],0,1); // black, yellow +var pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white +var pal_bb = new Uint16Array([0x0000,0x07ff],0,1); // black, blue + +// having 3 2 color pallette keeps the memory requirement lower +var buf1 = Graphics.createArrayBuffer(160,160,1, {msb:true}); +var buf2 = Graphics.createArrayBuffer(80,40,1, {msb:true}); +var arrow_img = require("heatshrink").decompress(atob("lEowIPMjAEDngEDvwED/4DCgP/wAEBgf/4AEBg//8AEBh//+AEBj///AEBn///gEBv///wmCAAImCAAIoBFggE/AkaaEABo=")); + +function flip1(x,y) { + g.drawImage({width:160,height:160,bpp:1,buffer:buf1.buffer, palette:pal_by},x,y); + buf1.clear(); +} + +function flip2_bw(x,y) { + g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bw},x,y); + buf2.clear(); +} + +function flip2_bb(x,y) { + g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bb},x,y); + buf2.clear(); +} + +var candraw = true; +var wp_bearing = 0; +var direction = 0; +var wpindex=0; +var loc = require("locale"); +var selected = false; + +var previous = { + bs: '', + dst: '', + wp_name: '', + course: 0, + selected: false, +}; + +// clear the attributes that control the display refresh +function clear_previous() { + previous.bs = '-'; + previous.dst = '-'; + previous.wp_name = '-'; + previous.course = -999; +} + +function drawCompass(course) { + if(!candraw) return; + if (Math.abs(previous.course - course) < 9) return; // reduce number of draws due to compass jitter + previous.course = course; + + buf1.setColor(1); + buf1.fillCircle(80,80,79,79); + buf1.setColor(0); + buf1.fillCircle(80,80,69,69); + buf1.setColor(1); + buf1.drawImage(arrow_img, 80, 80, {scale:3, rotate:radians(course)} ); + flip1(40, 30); +} + +/***** COMPASS CODE ***********/ + +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 CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; + +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; +} + +// Note actual mag is 360-m, error in firmware +function read_compass() { + var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale); + heading = newHeading(d,heading); + direction = wp_bearing - heading; + if (direction < 0) direction += 360; + if (direction > 360) direction -= 360; + drawCompass(direction); +} + + +/***** END Compass ***********/ + +var speed = 0; +var satellites = 0; +var wp; +var dist=0; + +function radians(a) { + return a*Math.PI/180; +} + +function degrees(a) { + var d = a*180/Math.PI; + return (d+360)%360; +} + +function bearing(a,b){ + var delta = radians(b.lon-a.lon); + var alat = radians(a.lat); + var blat = radians(b.lat); + var y = Math.sin(delta) * Math.cos(blat); + var x = Math.cos(alat)*Math.sin(blat) - + Math.sin(alat)*Math.cos(blat)*Math.cos(delta); + return Math.round(degrees(Math.atan2(y, x))); +} + +function distance(a,b){ + var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); + var y = radians(b.lat-a.lat); + return Math.round(Math.sqrt(x*x + y*y) * 6371000); +} + + +function drawN(){ + buf2.setFont("Vector",24); + var bs = wp_bearing.toString(); + bs = wp_bearing<10?"00"+bs : wp_bearing<100 ?"0"+bs : bs; + var dst = loc.distance(dist); + + // -1=left (default), 0=center, 1=right + + // show distance on the left + if (previous.dst !== dst) { + previous.dst = dst + buf2.setColor(1); + buf2.setFontAlign(-1,-1); + buf2.setFont("Vector", 20); + buf2.drawString(dst,0,0); + flip2_bw(0, 200); + } + + // bearing, place in middle at bottom of compass + if (previous.bs !== bs) { + previous.bs = bs; + buf2.setColor(1); + buf2.setFontAlign(0, -1); + buf2.setFont("Vector",38); + buf2.drawString(bs,40,0); + flip2_bw(80, 200); + } + + // waypoint name on right + if (previous.wp_name !== wp.name || previous.selected !== selected) { + previous.selected = selected; + buf2.setColor(1); + buf2.setFontAlign(1,-1); // right, bottom + buf2.setFont("Vector", 20); + buf2.drawString(wp.name, 80, 0); + + if (selected) + flip2_bw(160, 200); + else + flip2_bb(160, 200); + } +} + +var savedfix; + +function onGPS(fix) { + savedfix = fix; + if (fix!==undefined){ + satellites = fix.satellites; + } + + if (candraw) { + if (fix!==undefined && fix.fix==1){ + dist = distance(fix,wp); + if (isNaN(dist)) dist = 0; + wp_bearing = bearing(fix,wp); + if (isNaN(wp_bearing)) wp_bearing = 0; + drawN(); + } + } +} + +var intervalRef; + +function stopdraw() { + candraw=false; + prev_course = -1; + if(intervalRef) {clearInterval(intervalRef);} +} + +function startTimers() { + candraw=true; + intervalRefSec = setInterval(function() { + read_compass(); + }, 500); +} + +function drawAll(){ + g.setColor(1,1,1); + drawN(); + drawCompass(direction); +} + +function startdraw(){ + g.clear(); + Bangle.drawWidgets(); + startTimers(); + candraw=true; + drawAll(); +} + +function setButtons(){ + setWatch(nextwp.bind(null,-1), BTN1, {repeat:true,edge:"falling"}); + setWatch(doselect, BTN2, {repeat:true,edge:"falling"}); + setWatch(nextwp.bind(null,1), BTN3, {repeat:true,edge:"falling"}); +} + +Bangle.on('lcdPower',function(on) { + if (on) { + clear_previous(); + startdraw(); + } else { + stopdraw(); + } +}); + +var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; +wp=waypoints[0]; + +function nextwp(inc){ + if (!selected) return; + wpindex+=inc; + if (wpindex>=waypoints.length) wpindex=0; + if (wpindex<0) wpindex = waypoints.length-1; + wp = waypoints[wpindex]; + drawN(); +} + +function doselect(){ + if (selected && wpindex!=0 && waypoints[wpindex].lat===undefined && savedfix.fix) { + waypoints[wpindex] ={name:"@"+wp.name, lat:savedfix.lat, lon:savedfix.lon}; + wp = waypoints[wpindex]; + require("Storage").writeJSON("waypoints.json", waypoints); + } + selected=!selected; + drawN(); +} + +Bangle.on('kill',()=>{ + Bangle.setCompassPower(0); + Bangle.setGPSPower(0); +}); + +g.clear(); +Bangle.setLCDBrightness(1); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +// load widgets can turn off GPS +Bangle.setGPSPower(1); +Bangle.setCompassPower(1); +drawAll(); +startTimers(); +Bangle.on('GPS', onGPS); +setButtons(); diff --git a/apps/waypointer/icon.js b/apps/waypointer/icon.js new file mode 100644 index 000000000..a5be96818 --- /dev/null +++ b/apps/waypointer/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AFcBiAWViMRDCkBiUhC68RC64AFGxsRC4UiAAY2HOAQAEC4MSn//AAXzGAwWGC4czC4f/mIwEFwIlEBoIXDBQnyGAkRiYWE/8yLAIXBGAhgEFw5WBC4R0BkYaBmRfFF44XCNI6OGGAQlBAAIXIX4yPJaBq/JC5oeHC/4X/C/4X/C/4X/C/4X/C88RiIXUDAIWVAH4AVA=")) diff --git a/apps/waypointer/waypointer.png b/apps/waypointer/waypointer.png new file mode 100644 index 000000000..b72f9313c Binary files /dev/null and b/apps/waypointer/waypointer.png differ diff --git a/apps/waypointer/waypointer_screenshot.jpg b/apps/waypointer/waypointer_screenshot.jpg new file mode 100644 index 000000000..ba7d9f492 Binary files /dev/null and b/apps/waypointer/waypointer_screenshot.jpg differ diff --git a/apps/waypointer/waypoints.html b/apps/waypointer/waypoints.html new file mode 100644 index 000000000..d02260732 --- /dev/null +++ b/apps/waypointer/waypoints.html @@ -0,0 +1,170 @@ + + + + + + + +

List of waypoints

+ + + + + + + + + + + + +
NameLat.Long.Actions
+
+

Add a new waypoint

+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + + + + + + diff --git a/apps/waypointer/waypoints.json b/apps/waypointer/waypoints.json new file mode 100644 index 000000000..98a670c0d --- /dev/null +++ b/apps/waypointer/waypoints.json @@ -0,0 +1,20 @@ +[ + { + "name":"NONE" + }, + { + "name":"No10", + "lat":51.5032, + "lon":-0.1269 + }, + { + "name":"Stone", + "lat":51.1788, + "lon":-1.8260 + }, + { "name":"WP0" }, + { "name":"WP1" }, + { "name":"WP2" }, + { "name":"WP3" }, + { "name":"WP4" } +] \ No newline at end of file diff --git a/apps/waypointer/wp2_saved.jpg b/apps/waypointer/wp2_saved.jpg new file mode 100644 index 000000000..abec34b41 Binary files /dev/null and b/apps/waypointer/wp2_saved.jpg differ diff --git a/apps/waypointer/wp2_screenshot.jpg b/apps/waypointer/wp2_screenshot.jpg new file mode 100644 index 000000000..a6df13c93 Binary files /dev/null and b/apps/waypointer/wp2_screenshot.jpg differ diff --git a/apps/welcome/ChangeLog b/apps/welcome/ChangeLog index 9545dbbfa..ce9194c5d 100644 --- a/apps/welcome/ChangeLog +++ b/apps/welcome/ChangeLog @@ -11,3 +11,4 @@ 0.09: Allow welcome to run after a fresh install More useful app menu BTN2 now goes to menu on release +0.10: Tweaks to reduce memory usage diff --git a/apps/welcome/app.js b/apps/welcome/app.js index 8cbdc2efa..565e87d5d 100644 --- a/apps/welcome/app.js +++ b/apps/welcome/app.js @@ -11,7 +11,7 @@ function animate(seq,period) { // Fade in to FG color with angled lines function fade(callback) { var n = 0; - function f() { + function f() {"ram" for (var i=n;i<240;i+=10) { g.drawLine(i,0,0,i); g.drawLine(i,240,240,i); @@ -24,16 +24,17 @@ function fade(callback) { f(); } - -var scenes = [ - function() { +var SCENE_COUNT=11; +function getScene(n) { + if (n==0) 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); @@ -41,7 +42,8 @@ var scenes = [ setTimeout(()=>g.drawString("Smart Watch",34,168), 1500); } },50); - },function() { + }; + if (n==1) 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.setColor("#6633ff"); @@ -73,7 +75,8 @@ var scenes = [ },20); },3500); - },function() { + }; + if (n==2) return function() { g.reset(); g.setBgColor("#ffa800");g.clear(); g.setFont("6x8",2); @@ -88,8 +91,8 @@ var scenes = [ ()=>g.drawString("2",200,120), ()=>g.drawString("3",200,200) ],200); - }, - function() { + }; + if (n==3) return function() { g.reset(); g.setBgColor("#00a8ff");g.clear(); g.setFontAlign(0,0); @@ -98,8 +101,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==4) return function() { g.reset(); g.setBgColor("#00a8ff");g.clear(); g.setFontAlign(0,0); @@ -108,8 +111,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==5) return function() { g.reset(); g.setBgColor("#00a8ff");g.clear(); g.setFontAlign(0,0); @@ -118,8 +121,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==6) return function() { g.reset(); g.setBgColor("#ff3300");g.clear(); g.setFontAlign(0,0); @@ -129,8 +132,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==7) return function() { g.reset(); g.setBgColor("#00a8ff");g.clear(); g.setFont("6x8",2); @@ -147,8 +150,8 @@ var scenes = [ g.drawString("work too. Try now",x,y+=h); g.drawString("to change page.",x,y+=h);} ],300); - }, - function() { + }; + if (n==8) return function() { g.reset(); g.setBgColor("#339900");g.clear(); g.setFont("6x8",2); @@ -165,8 +168,8 @@ var scenes = [ g.drawString("with a Bluetooth",x,y+=h); g.drawString("capable device",x,y+=h);}, ],400); - }, - function() { + }; + if (n==9) return function() { g.reset(); g.setBgColor("#990066");g.clear(); g.setFont("6x8",2); @@ -227,8 +230,8 @@ var scenes = [ } setInterval(draw,50); - }, - function() { + }; + if (n==10) return function() { g.reset(); g.setBgColor("#660099");g.clear(); g.setFontAlign(0,0); @@ -245,18 +248,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(); - scenes[sceneNumber](); + getScene(sceneNumber)(); if (sceneNumber>1) { - var l = scenes.length; + var l = SCENE_COUNT; for (var i=0;imove(1), BTN3, {repeat:true}); setWatch(()=>{ // If we're on the last page - if (sceneNumber == scenes.length-1) { + if (sceneNumber == SCENE_COUNT-1) { load(); } }, BTN2, {repeat:true,edge:"falling"}); diff --git a/apps/whereworld/ChangeLog b/apps/whereworld/ChangeLog new file mode 100644 index 000000000..863a2e9a2 --- /dev/null +++ b/apps/whereworld/ChangeLog @@ -0,0 +1 @@ +0.01: Created app \ No newline at end of file diff --git a/apps/whereworld/app-icon.js b/apps/whereworld/app-icon.js new file mode 100644 index 000000000..00ff3a14b --- /dev/null +++ b/apps/whereworld/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+oIA/AANAFNFjtIrBoFoxmMGMopDtFpAgQvBGEgpDAYYwDHwYECG4tjHyonCsYuFtAkDxlpF49osiMSshbDLwwfDsdoKpCoBMCASBOgIsBAgIvFsaOGRYwcCCAYvMJwIrCCQRfIAAouDIgYGCtFpGJRbCAAIWBDgruEOwwQBOYqSBsZFEDQJZBsh7BCgQXDDwLGBF49AAwZfBUIoAFE4IoBCBBeCBYI6BJQYuFIQVpFpYAEtLiGc4oBBEwIuBLIg+CACZHEMA1AtJaCtLsFCwJILKJKiCWgIzFaQTxDf4qaCFRx3DeIp8IAwRACLq7sCJoyUJGIJfGRaRYDAA9pT5I/EZIL+ECw4aEF5Z+GCQ69CsbXBUAi9IRhCAJT5gwERhNpJZAhHZ4NoZx4ABSI6MKGA1otJRBIgwvHFgNpoNkNAJnBXYK8LG49osloSIpfIOAQLBAYJJBAoYAReoZIEoFkGQo+DCAILBAIRgSoKqLdgb/HsdkVQQNBBw5fMF45gDQwKqHAALCCAYIwUTZS/CGAzMEGQQvQtASGEIQKBDxDNGYh6kBRozAEDpQMBDQSOQIwVkCaCSIPIagIF4xCRVhQCCSBqzBUB4TCBjBfDcJIA/aq4utaBw+EAYIBEJKdAsYNMf4OMtFAsgEBAAoKBJSFoxgpHsYKBAIIADFxAxDJxBtCDINjEgQNFEZQANDwpJDJopAFsYuXZwhNKUIhdZLwhMLF4TVBFzJfGRQoPENgRvLAB1pVqFkoABBLzLdSRra/FF5oAdsi9BMYJSdeBzzDGFY=")) \ No newline at end of file diff --git a/apps/whereworld/app.js b/apps/whereworld/app.js new file mode 100644 index 000000000..48d0f7b0c --- /dev/null +++ b/apps/whereworld/app.js @@ -0,0 +1,69 @@ +const landColor = 0x8FAB, seaColor= 0x365D, markerColor = 0xF800; +let lastSuccess = true; + +const mapImg = { + width : 240, + height : 240, + palette: new Uint16Array([landColor, seaColor]), + buffer: require("Storage").read("whereworld.worldmap") +}; + +function getMarkerImg(x, y) { + const b = Graphics.createArrayBuffer(240, 240, 2, {msb:true}); + b.setColor(1); + b.drawLine(x, 0, x, b.getHeight()); + b.drawLine(0, y, b.getWidth(), y); + const pal = new Uint16Array([0, markerColor]); + return {width: 240, height: 240, bpp: 2, palette: pal, buffer: b.buffer, transparent: 0}; +} + +function degreesToRadians(deg) { + return (deg / 180) * Math.PI; +} + +function coordsToScreenLocation(lat, lon) { + const maxMapHeight = g.getHeight() - 1, maxMapWidth = g.getWidth() - 1; + const maxLong = 180; + const x = ((lon + maxLong) / (maxLong * 2)) * maxMapWidth; + const mercN = Math.log(Math.tan((Math.PI / 4) + (degreesToRadians(lat) / 2))); + const y = (maxMapHeight / 2) - (maxMapWidth * mercN / (2 * Math.PI)); + return {x: x, y: y}; +} + +function drawLocation(lat, lon) { + const location = coordsToScreenLocation(lat, lon); + g.drawImages([ + {image: mapImg}, + {image: getMarkerImg(location.x, location.y)} + ]); +} + +function drawNoFixMessage() { + const b = Graphics.createArrayBuffer(240, 216, 1, {msb:true}); + const throbber = ".".repeat(new Date().getSeconds() % 4); + b.setColor(1); + b.setFont("6x8", 2); + b.drawString("Finding GPS Fix" + throbber, 15, 94); + g.drawImage({ + width: b.getWidth(), + height: b.getHeight(), + palette: new Uint16Array([0, 0xF800]), + buffer: b.buffer + }, 0, 24); +} + +Bangle.setGPSPower(1); +Bangle.loadWidgets(); +Bangle.on('GPS', function(gps) { + if (gps.fix) { + drawLocation(gps.lat, gps.lon); + lastSuccess = true; + } + else { + if (lastSuccess) { + Bangle.drawWidgets(); + lastSuccess = false; + } + drawNoFixMessage(); + } +}); \ No newline at end of file diff --git a/apps/whereworld/app.png b/apps/whereworld/app.png new file mode 100644 index 000000000..1e12db39d Binary files /dev/null and b/apps/whereworld/app.png differ diff --git a/apps/whereworld/worldmap b/apps/whereworld/worldmap new file mode 100644 index 000000000..d7e30424f Binary files /dev/null and b/apps/whereworld/worldmap differ diff --git a/apps/widancs/ChangeLog b/apps/widancs/ChangeLog index 7844830d1..471507736 100644 --- a/apps/widancs/ChangeLog +++ b/apps/widancs/ChangeLog @@ -4,5 +4,4 @@ 0.04: Works on both standard and modified firmware 0.05: Bug fixes w.r.t. reconnection 0.06: Update README - Release version - - +0.07: Respect Quiet Mode diff --git a/apps/widancs/ancs.js b/apps/widancs/ancs.js index 84a79fbf9..50720cd23 100644 --- a/apps/widancs/ancs.js +++ b/apps/widancs/ancs.js @@ -187,9 +187,11 @@ //we may already be displaying a prompt, so clear it E.showPrompt(); if (screentimeout) clearTimeout(screentimeout); - Bangle.setLCDPower(true); + if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.setLCDPower(true); + } SCREENACCESS.request(); - if (!buzzing){ + if (!buzzing && !(require('Storage').readJSON('setting.json',1)||{}).quiet){ buzzing=true; Bangle.buzz(500).then(()=>{buzzing=false;}); } diff --git a/apps/widbat/ChangeLog b/apps/widbat/ChangeLog index b9d50ab8b..128cee034 100644 --- a/apps/widbat/ChangeLog +++ b/apps/widbat/ChangeLog @@ -2,3 +2,4 @@ 0.03: Tweaks for variable size widget system 0.04: Ensure redrawing works with variable size widget system 0.05: Fix regression stopping correct widget updates +0.06: Use 'g.theme' (requires bootloader 0.23) diff --git a/apps/widbat/widget.js b/apps/widbat/widget.js index bca3ae046..95fad1b20 100644 --- a/apps/widbat/widget.js +++ b/apps/widbat/widget.js @@ -7,16 +7,16 @@ function draw() { var s = 39; var x = this.x, y = this.y; + g.reset(); if (Bangle.isCharging()) { g.setColor(CHARGING).drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); x+=16; } - g.setColor(-1); + g.setColor(g.theme.fg); g.fillRect(x,y+2,x+s-4,y+21); g.clearRect(x+2,y+4,x+s-6,y+19); g.fillRect(x+s-3,y+10,x+s,y+14); g.setColor(CHARGING).fillRect(x+4,y+6,x+4+E.getBattery()*(s-12)/100,y+17); - g.setColor(-1); } Bangle.on('charging',function(charging) { if(charging) Bangle.buzz(); diff --git a/apps/widbatwarn/ChangeLog b/apps/widbatwarn/ChangeLog index c51b06842..5420b9706 100644 --- a/apps/widbatwarn/ChangeLog +++ b/apps/widbatwarn/ChangeLog @@ -1 +1,2 @@ -0.01: New Battery Warning! \ No newline at end of file +0.01: New Battery Warning! +0.02: Respect Quiet Mode diff --git a/apps/widbatwarn/widget.js b/apps/widbatwarn/widget.js index c5c2f2bf0..3eb603b84 100644 --- a/apps/widbatwarn/widget.js +++ b/apps/widbatwarn/widget.js @@ -39,7 +39,10 @@ .setColor(0xF800).drawString(`${E.getBattery()}%`, a.x+8+100, a.y+a.h/2); }, }); - if (setting("buzz")) Bangle.buzz(); + if (setting("buzz") + && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.buzz(); + } } Bangle.on("charging", check); diff --git a/apps/widbt/widget.js b/apps/widbt/widget.js index 2236ee50d..0dac82e76 100644 --- a/apps/widbt/widget.js +++ b/apps/widbt/widget.js @@ -1,13 +1,11 @@ (function(){ - var img_bt = E.toArrayBuffer(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA==")); - function draw() { g.reset(); if (NRF.getSecurityStatus().connected) g.setColor(0,0.5,1); else g.setColor(0.3,0.3,0.3); - g.drawImage(img_bt,10+this.x,2+this.y); + g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),10+this.x,2+this.y); } function changed() { WIDGETS["bluetooth"].draw(); diff --git a/apps/widchime/ChangeLog b/apps/widchime/ChangeLog new file mode 100644 index 000000000..432ac994e --- /dev/null +++ b/apps/widchime/ChangeLog @@ -0,0 +1,2 @@ +0.01: First version +0.02: Bugfix: now it actually chimes the hour instead of crashing diff --git a/apps/widchime/icons.txt b/apps/widchime/icons.txt new file mode 100644 index 000000000..c22bc31e7 --- /dev/null +++ b/apps/widchime/icons.txt @@ -0,0 +1 @@ +widget.png: https://icons8.com/icon/15715/plus-1-hour diff --git a/apps/widchime/settings.js b/apps/widchime/settings.js new file mode 100644 index 000000000..54e94546b --- /dev/null +++ b/apps/widchime/settings.js @@ -0,0 +1,22 @@ +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + // default to buzzing + let type = (require("Storage").readJSON("widchime.json", 1) || {type: 1}).type|0 + const chimes = ["Off", "Buzz", "Beep", "Both"] + const menu = { + "": {"title": "Hour Chime"}, + "< Back": back, + "Chime Type": { + value: type, + min: 0, max: 2, // both is just silly + format: v => chimes[v], + onchange: function(v) { + type = v + require("Storage").write("widchime.json", {type: v}) + }, + }, + } + E.showMenu(menu) +}) diff --git a/apps/widchime/widget.js b/apps/widchime/widget.js new file mode 100644 index 000000000..e229bdde6 --- /dev/null +++ b/apps/widchime/widget.js @@ -0,0 +1,26 @@ +(function() { + // 0: off, 1: buzz, 2: beep, 3: both + const type = (require("Storage").readJSON("widchime.json", 1) || {type: 1}).type; + if (!type) return; + + function chime() { + if ((require("Storage").readJSON("setting.json", 1) || {}).quiet) return; + if (type&1) Bangle.buzz(100); + if (type&2) Bangle.beep(); + } + + let lastHour = (new Date()).getHours(); // don't chime when (re)loaded at a whole hour + function check() { + const now = new Date(), + h = now.getHours(), m = now.getMinutes(), + s = now.getSeconds(), ms = now.getMilliseconds(); + if (h!==lastHour && m===0) chime(); + lastHour = h; + // check again when this hour is over + const mLeft = 60-m, sLeft = (mLeft*60)-s, msLeft = (sLeft*1000)-ms; + setTimeout(check, msLeft); + } + + check(); +}) +(); diff --git a/apps/widchime/widget.png b/apps/widchime/widget.png new file mode 100644 index 000000000..7c3d7eebd Binary files /dev/null and b/apps/widchime/widget.png differ diff --git a/apps/widcom/README.md b/apps/widcom/README.md new file mode 100644 index 000000000..1e45d9090 --- /dev/null +++ b/apps/widcom/README.md @@ -0,0 +1,11 @@ +# Compass Power Status Widget + +A simple widget that shows the on/off status of the compass. + +The compass draws around 1mA when on. Whilst this is not a big draw +on the battery it is still easy to have it switched on and not be +aware. + +- Uses Bangle.isCompassOn(), requires firmware v2.08.167 or later +- Shows in grey when the compass is off +- Shows in amber when the compass is on diff --git a/apps/widcom/widget.js b/apps/widcom/widget.js new file mode 100644 index 000000000..b9c911dbf --- /dev/null +++ b/apps/widcom/widget.js @@ -0,0 +1,30 @@ +(function(){ + //var img = E.toArrayBuffer(atob("FBSBAAAAAAAAA/wAf+AP/wH/2D/zw/w8PwfD9nw+b8Pg/Dw/w8/8G/+A//AH/gA/wAAAAAAA")); + //var img = E.toArrayBuffer(atob("GBiBAAB+AAP/wAeB4A4AcBgAGDAADHAADmABhmAHhsAfA8A/A8BmA8BmA8D8A8D4A2HgBmGABnAADjAADBgAGA4AcAeB4AP/wAB+AA==")); + var img = E.toArrayBuffer(atob("FBSBAAH4AH/gHAODgBwwAMYABkAMLAPDwPg8CYPBkDwfA8PANDACYABjAAw4AcHAOAf+AB+A")); + + function draw() { + g.reset(); + if (Bangle.isCompassOn()) { + g.setColor(1,0.8,0); // on = amber + } else { + g.setColor(0.3,0.3,0.3); // off = grey + } + g.drawImage(img, 10+this.x, 2+this.var); + } + + var timerInterval; + Bangle.on('lcdPower', function(on) { + if (on) { + WIDGETS.compass.draw(); + if (!timerInterval) timerInterval = setInterval(()=>WIDGETS.compass.draw(), 2000); + } else { + if (timerInterval) { + clearInterval(timerInterval); + timerInterval = undefined; + } + } + }); + + WIDGETS.compass={area:"tr",width:24,draw:draw}; +})(); diff --git a/apps/widcom/widget.png b/apps/widcom/widget.png new file mode 100644 index 000000000..6c50374a3 Binary files /dev/null and b/apps/widcom/widget.png differ diff --git a/apps/widgps/ChangeLog b/apps/widgps/ChangeLog new file mode 100644 index 000000000..d80e09912 --- /dev/null +++ b/apps/widgps/ChangeLog @@ -0,0 +1,2 @@ +0.01: First version +0.02: Don't break if running on 2v08 firmware (just don't display anything) diff --git a/apps/widgps/README.md b/apps/widgps/README.md new file mode 100644 index 000000000..d4a27c130 --- /dev/null +++ b/apps/widgps/README.md @@ -0,0 +1,10 @@ +# GPS Power Status Widget + +A simple widget that shows the on/off status of the GPS. + +The GPS can quickly run the battery down if it is on all the time so +it is useful to know if it has been switched on or not. + +- Uses Bangle.isGPSOn(), requires firmware v2.08.167 or later +- Shows in grey when the GPS is off +- Shows in amber when the GPS is on diff --git a/apps/widgps/widget.js b/apps/widgps/widget.js new file mode 100644 index 000000000..19be2abaf --- /dev/null +++ b/apps/widgps/widget.js @@ -0,0 +1,28 @@ +(function(){ + if (!Bangle.isGPSOn) return; // old firmware + + function draw() { + g.reset(); + if (Bangle.isGPSOn()) { + g.setColor(1,0.8,0); // on = amber + } else { + g.setColor(0.3,0.3,0.3); // off = grey + } + g.drawImage(atob("GBiBAAAAAAAAAAAAAA//8B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+A//8AAAAAAAAAAAAA=="), 10+this.x, 2+this.y); + } + + var timerInterval; + Bangle.on('lcdPower', function(on) { + if (on) { + WIDGETS.gps.draw(); + if (!timerInterval) timerInterval = setInterval(()=>WIDGETS["gps"].draw(), 2000); + } else { + if (timerInterval) { + clearInterval(timerInterval); + timerInterval = undefined; + } + } + }); + + WIDGETS.gps={area:"tr",width:24,draw:draw}; +})(); diff --git a/apps/widgps/widget.png b/apps/widgps/widget.png new file mode 100644 index 000000000..3677154aa Binary files /dev/null and b/apps/widgps/widget.png differ diff --git a/apps/widhrt/ChangeLog b/apps/widhrt/ChangeLog new file mode 100644 index 000000000..fdb495797 --- /dev/null +++ b/apps/widhrt/ChangeLog @@ -0,0 +1,3 @@ +0.01: First version +0.02: Don't break if running on 2v08 firmware (just don't display anything) + diff --git a/apps/widhrt/README.md b/apps/widhrt/README.md new file mode 100644 index 000000000..db16d3d35 --- /dev/null +++ b/apps/widhrt/README.md @@ -0,0 +1,8 @@ +# Heart Rate Power Monitor Widget + +A simple widget that shows the on/off status of the Heart Rate +Monitor. + +- Uses Bangle.isHRTOn(). Requires firmware v2.08.167 or later. +- Shows in grey when the HRT is off +- Shows in red when the HRT is on diff --git a/apps/widhrt/widget.js b/apps/widhrt/widget.js new file mode 100644 index 000000000..8ac76def8 --- /dev/null +++ b/apps/widhrt/widget.js @@ -0,0 +1,28 @@ +(function(){ + if (!Bangle.isHRMOn) return; // old firmware + + function draw() { + g.reset(); + if (Bangle.isHRMOn()) { + g.setColor(1,0,0); // on = red + } else { + g.setColor(0.3,0.3,0.3); // off = grey + } + g.drawImage(atob("FhaBAAAAAAAAAAAAAcDgD8/AYeGDAwMMDAwwADDAAMOABwYAGAwAwBgGADAwAGGAAMwAAeAAAwAAAAAAAAAAAAA="), 10+this.x, 2+this.y); + } + + var timerInterval; + Bangle.on('lcdPower', function(on) { + if (on) { + WIDGETS.widhrt.draw(); + if (!timerInterval) timerInterval = setInterval(()=>WIDGETS["widhrt"].draw(), 2000); + } else { + if (timerInterval) { + clearInterval(timerInterval); + timerInterval = undefined; + } + } + }); + + WIDGETS.widhrt={area:"tr",width:24,draw:draw}; +})(); diff --git a/apps/widhrt/widget.png b/apps/widhrt/widget.png new file mode 100644 index 000000000..4d8f5b730 Binary files /dev/null and b/apps/widhrt/widget.png differ diff --git a/apps/widhwt/widget.js b/apps/widhwt/widget.js index 6affdea52..d178a5b5d 100644 --- a/apps/widhwt/widget.js +++ b/apps/widhwt/widget.js @@ -1,10 +1,9 @@ /* jshint esversion: 6 */ (() => { - var icon = require("heatshrink").decompress(atob("jEYwIKHgwCBhwCBh4CEggPCkACBmAXDBwVZ+EB+F4gEsjl8EgMP+EChk/gEMh+ehkA+YIBxwxBnF/4HggH/wEAj0AA==")); var color = 0x4A69; function draw() { - g.reset().setColor(color).drawImage(icon, this.x + 1, 0); + g.reset().setColor(color).drawImage(require("heatshrink").decompress(atob("jEYwIKHgwCBhwCBh4CEggPCkACBmAXDBwVZ+EB+F4gEsjl8EgMP+EChk/gEMh+ehkA+YIBxwxBnF/4HggH/wEAj0AA==")), this.x + 1, 0); } WIDGETS["widhwt"] = { area: "tr", width: 26, draw: draw }; @@ -20,4 +19,4 @@ }, 35E3); }); -})(); \ No newline at end of file +})(); diff --git a/apps/widid/widget.js b/apps/widid/widget.js index e97eecb65..68917c65a 100644 --- a/apps/widid/widget.js +++ b/apps/widid/widget.js @@ -1,9 +1,7 @@ /* jshint esversion: 6 */ (() => { - var id = NRF.getAddress().substr().substr(12).split(":"); - - // draw your widget at xpos function draw() { + var id = NRF.getAddress().substr().substr(12).split(":"); g.reset().setColor(0, 0.5, 1).setFont("6x8", 1); g.drawString(id[0], this.x+2, this.y+4, true); g.drawString(id[1], this.x+2, this.y+14, true); diff --git a/apps/widlock/ChangeLog b/apps/widlock/ChangeLog new file mode 100644 index 000000000..b4d1ae593 --- /dev/null +++ b/apps/widlock/ChangeLog @@ -0,0 +1 @@ +0.01: First commit diff --git a/apps/widlock/widget.js b/apps/widlock/widget.js new file mode 100644 index 000000000..b710de8c6 --- /dev/null +++ b/apps/widlock/widget.js @@ -0,0 +1,10 @@ +(function(){ + Bangle.on('lcdPower', function(on) { + WIDGETS["lock"].width = Bangle.isLCDOn()?0:16; + Bangle.drawWidgets(); + }); + WIDGETS["lock"]={area:"tl",width:Bangle.isLCDOn()?0:16,draw:function(w) { + if (!Bangle.isLCDOn()) + g.reset().drawImage(atob("DhABH+D/wwMMDDAwwMf/v//4f+H/h/8//P/z///f/g=="), w.x, w.y); + }}; +})() diff --git a/apps/widlock/widget.png b/apps/widlock/widget.png new file mode 100644 index 000000000..e0eaa4aa9 Binary files /dev/null and b/apps/widlock/widget.png differ diff --git a/apps/widpedom/ChangeLog b/apps/widpedom/ChangeLog index 3c62f3a09..ba198f889 100644 --- a/apps/widpedom/ChangeLog +++ b/apps/widpedom/ChangeLog @@ -8,3 +8,5 @@ 0.09: Add daily goal 0.10: Fix daily goal, don't store settings in separate file 0.11: added getSteps() method for apps to retrieve step count +0.12: Respect Quiet Mode +0.13: Now use system color theme diff --git a/apps/widpedom/widget.js b/apps/widpedom/widget.js index 58853265c..e73475526 100644 --- a/apps/widpedom/widget.js +++ b/apps/widpedom/widget.js @@ -4,11 +4,6 @@ 'goal': 10000, 'progress': false, } - const COLORS = { - 'white': -1, - 'progress': 0x001F, // Blue - 'done': 0x03E0, // DarkGreen - } const TAU = Math.PI*2; let lastUpdate = new Date(); let stp_today = 0; @@ -27,7 +22,7 @@ function drawProgress(stps) { const width = 24, half = width/2; const goal = setting('goal'), left = Math.max(goal-stps,0); - const c = left ? COLORS.progress : COLORS.done; + const c = left ? "#00f" : "#090"; // blue or dark green g.setColor(c).fillCircle(this.x + half, this.y + half, half); if (left) { const f = left/goal; // fraction to blank out @@ -47,7 +42,7 @@ p[i - 2] += this.x; p[i - 1] += this.y; } - g.setColor(0).fillPoly(p); + g.setColor(g.theme.bg).fillPoly(p); } } @@ -58,10 +53,9 @@ stp_today = stp_today % 100000; // cap to five digits + comma = 6 characters } let stps = stp_today.toString(); - g.reset(); - g.clearRect(this.x, this.y, this.x + width, this.y + 23); // erase background + g.reset().clearRect(this.x, this.y, this.x + width, this.y + 23); // erase background if (setting('progress')){ drawProgress.call(this, stps); } - g.setColor(COLORS.white); + g.setColor(g.theme.fg); if (stps.length > 3){ stps = stps.slice(0,-3) + "," + stps.slice(-3); g.setFont("4x6", 1); // if big, shrink text to fix @@ -86,7 +80,8 @@ // TODO: could save this to PEDOMFILE for lastUpdate's day? stp_today = 1; } - if (stp_today === setting('goal')) { + if (stp_today === setting('goal') + && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { let b = 3, buzz = () => { if (b--) Bangle.buzz().then(() => setTimeout(buzz, 100)) } diff --git a/apps/widtbat/widget.js b/apps/widtbat/widget.js index 8cc4b0c83..6d5aded8b 100644 --- a/apps/widtbat/widget.js +++ b/apps/widtbat/widget.js @@ -1,11 +1,10 @@ /* jshint esversion: 6 */ (() => { const CBS = 0x41f, CBC = 0x07E0; - var batS = require("heatshrink").decompress(atob("j0TwIHEv///kD////EfAYPwuEAgPB4EAg/HCgMfzgDBvwOC/IOC84ONDoUcFgc/AYOAHYRDE")); var xo = 6, xl = 22, yo = 9, h = 17; function draw() { - g.reset().setColor(CBS).drawImage(batS, this.x + 1, this.y + 4); + g.reset().setColor(CBS).drawImage(require("heatshrink").decompress(atob("j0TwIHEv///kD////EfAYPwuEAgPB4EAg/HCgMfzgDBvwOC/IOC84ONDoUcFgc/AYOAHYRDE")), this.x + 1, this.y + 4); g.setColor(0).fillRect(this.x + xo, this.y + yo, this.x + xl, this.y + h); var cbc = (Bangle.isCharging()) ? CBC : CBS; g.setColor(cbc).fillRect(this.x + xo, this.y + yo, this.x + (xl - xo) / 100 * E.getBattery() + xo, this.y + h); diff --git a/apps/widviz/widget.js b/apps/widviz/widget.js index 4282d4c96..241dabf61 100644 --- a/apps/widviz/widget.js +++ b/apps/widviz/widget.js @@ -1,34 +1,33 @@ (() => { var saved = null; - + function hide(){ if (!Bangle.isLCDOn() || saved) return; saved = []; for (var wd of WIDGETS) { - saved.push(wd.draw); + saved.push(wd.draw); wd.draw=()=>{}; } g.setColor(0,0,0); g.fillRect(0,0,239,23); } - + function reveal(){ if (!Bangle.isLCDOn() || !saved) return; for (var wd of WIDGETS) wd.draw = saved.shift(); - Bangle.drawWidgets(); + Bangle.drawWidgets(); saved=null; } - + function draw(){ - var img = E.toArrayBuffer(atob("GBgBAAAAAAAAAAAAAAAAAH4AAf+AB4HgDgBwHDw4OH4cMOcMYMMGYMMGMOcMOH4cHDw4DgBwB4HgAf+AAH4AAAAAAAAAAAAAAAAA")); g.setColor(0x07ff); - g.drawImage(img,this.x,this.y); + g.drawImage(atob("GBgBAAAAAAAAAAAAAAAAAH4AAf+AB4HgDgBwHDw4OH4cMOcMYMMGYMMGMOcMOH4cHDw4DgBwB4HgAf+AAH4AAAAAAAAAAAAAAAAA"),this.x,this.y); } - + WIDGETS["viz"] ={area:"tl", width:24,draw:draw}; Bangle.on('swipe',(dir)=>{ if (dir<0) hide(); else reveal(); - }); + }); })(); diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index 4bdad1a9a..8cc78ea2a 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -54,8 +54,8 @@ const APP_KEYS = [ 'sortorder', 'readme', 'custom', 'interface', 'storage', 'data', 'allow_emulator', 'dependencies' ]; -const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate']; -const DATA_KEYS = ['name', 'wildcard', 'storageFile']; +const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite']; +const DATA_KEYS = ['name', 'wildcard', 'storageFile', 'url', 'content', 'evaluate']; const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info const VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ]; @@ -229,7 +229,7 @@ while(fileA=allFiles.pop()) { if (globA.test(nameB)||globB.test(nameA)) { if (isGlob(nameA)||isGlob(nameB)) ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`) - else ERROR(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`) + else WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`) } }) } diff --git a/core b/core index 1b1293a5e..3f2ff467f 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 1b1293a5eb9b8bb9e4f743c4599f0587f597d368 +Subproject commit 3f2ff467f22b746da94160e59ff89b621601b261