From e9c7e2ede72369ed0b2ebb7e9401484bbd76807b Mon Sep 17 00:00:00 2001 From: OmegaRogue Date: Wed, 13 Jan 2021 14:47:50 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=80Merge=20branch=20master=20from=20up?= =?UTF-8?q?stream=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create HRV.js * Add files via upload * Create hrv-icon.js * Delete hrv-icon.js * Create HRV-icon.js * Update apps.json * Update apps.json * Update apps.json * Rename HRV.js to app.js * Rename HRV-icon.js to app-icon.js * Update apps.json * Update apps.json * Update apps.json * Update apps.json * add hardalarm * fix app description * fix same name for storage in apps.json * Add dtlaunch * Update app.js updated with declutter and additional options * Create ChangeLog * Update app.js * Create README.md * Update README.md * Update app.js * Update app.js * Update README.md * Update app.js updated so that now it takes readings every 3 minutes in continous mode and also the sample range has been extended to 30 seconds rather than just 20 * tweak * Create app.js * Add files via upload * Delete icon.png * Add files via upload * Update apps.json * Delete app-icon.png * Create app-icon.js * Update app-icon.js * Update app-icon.js * Update app-icon.js * Update app-icon.js * Add files via upload * Update app-icon.js * Update app.js * Create README.md * Update apps.json * Update README.md * Update apps.json * Update apps.json * Update app-icon.js * Update apps.json * Update app-icon.js * Update app-icon.js * Update apps.json * Update app-icon.js * Update apps.json * Update app-icon.js * Update apps.json * Update apps.json * Update README.md * Update app.js * Update README.md * Update app.js * Improve readibility to README.md * bump version * activepedom 0.05: Fix default step/distance display if it hasn't been set up first * Add files via upload * Update apps.json * Update apps.json * Update apps.json * Update app-icon.js * Update app-icon.js * Update app-icon.js * Delete app-icon.png * Add files via upload * Delete app-icon.png * Add files via upload * Update app-icon.js * Delete app-icon.png * Add files via upload * Update apps.json * Update README.md * Update apps.json * Update app-icon.js * Update app-icon.js * Update apps.json * Create hrmexp.js * Delete app.js * Update apps.json * Update apps.json * Update app-icon.js * Update app-icon.js * Update apps.json * Update app-icon.js * Update hrmexp.js * Update hrmexp.js * Update apps.json * Create README.md * Add files via upload * Create app.js * Create app-icon.js * Update apps.json * Update apps.json * Update apps.json * Update apps.json * Update apps.json * remove remove * Update README.md * Update README.md * Update README.md * Added Interface file to allow downloading (and deletion!) of BangleRun data files * added WIDGETS.activepedom.getSteps() * Update ChangeLog 0.06: Added WIDGETS.activepedom.getSteps() * bump versions * Create interface.html * Update apps.json * Update README.md * Add files via upload * Update and rename breather_settings.txt to settings.js * Update apps.json * Update app.js * Update app.js * Add files via upload * Update README.md * Delete readme_gif.gif * Update README.md * Update apps.json * Update README.md * Update README.md * Update README.md * Update README.md * Update app.js * Update app.js * Update apps.json * Rename settings.js to settings.json * feat(app): Add "Lazy Clock" app * update multiclock * update dtlaunch app * minor tweak for faster writes (best to end with a newline anyway) Co-authored-by: Gordon Williams Co-authored-by: Ben Jabituya <74158243+jabituyaben@users.noreply.github.com> Co-authored-by: jamespsteinberg@gmail.com Co-authored-by: jeffmer Co-authored-by: krichtof Co-authored-by: hughbarney Co-authored-by: Olly Cross Co-authored-by: Jeff Magee --- apps.json | 108 ++++++++++- apps/HRV/ChangeLog | 3 + apps/HRV/README.md | 18 ++ apps/HRV/app-icon.js | 1 + apps/HRV/app.js | 303 +++++++++++++++++++++++++++++++ apps/HRV/hrv.png | Bin 0 -> 749 bytes apps/activepedom/ChangeLog | 4 +- apps/activepedom/widget.js | 9 +- apps/banglerun/interface.html | 216 ++++++++++++++++++++++ apps/banglerun/src/gps.ts | 4 + apps/banglerun/src/log.ts | 5 +- apps/breath/README.md | 12 ++ apps/breath/app-icon.js | 1 + apps/breath/app-icon.png | Bin 0 -> 1336 bytes apps/breath/app.js | 222 ++++++++++++++++++++++ apps/breath/settings.json | 1 + apps/dtlaunch/ChangeLog | 4 + apps/dtlaunch/README.md | 16 ++ apps/dtlaunch/app-icon.js | 1 + apps/dtlaunch/app.js | 73 ++++++++ apps/dtlaunch/icon.png | Bin 0 -> 305 bytes apps/dtlaunch/screenshot.jpg | Bin 0 -> 49806 bytes apps/edisonsball/README.md | 5 + apps/edisonsball/app-icon.js | 1 + apps/edisonsball/app-icon.png | Bin 0 -> 821 bytes apps/edisonsball/app.js | 149 +++++++++++++++ apps/gpsrec/ChangeLog | 2 +- apps/hardalarm/ChangeLog | 1 + apps/hardalarm/app-icon.js | 1 + apps/hardalarm/app.js | 112 ++++++++++++ apps/hardalarm/app.png | Bin 0 -> 1909 bytes apps/hardalarm/boot.js | 25 +++ apps/hardalarm/hardalarm.js | 127 +++++++++++++ apps/hardalarm/widget.js | 11 ++ apps/hrrawexp/README.md | 13 ++ apps/hrrawexp/app-icon.js | 1 + apps/hrrawexp/app-icon.png | Bin 0 -> 826 bytes apps/hrrawexp/app.js | 97 ++++++++++ apps/hrrawexp/interface.html | 54 ++++++ apps/lazyclock/ChangeLog | 1 + apps/lazyclock/README.md | 26 +++ apps/lazyclock/lazyclock-app.js | 236 ++++++++++++++++++++++++ apps/lazyclock/lazyclock-icon.js | 1 + apps/lazyclock/lazyclock.png | Bin 0 -> 1229 bytes apps/multiclock/ChangeLog | 2 + apps/multiclock/ana.min.js | 2 - apps/multiclock/clock.js | 15 +- apps/smtswch/README.md | 1 + 48 files changed, 1866 insertions(+), 18 deletions(-) create mode 100644 apps/HRV/ChangeLog create mode 100644 apps/HRV/README.md create mode 100644 apps/HRV/app-icon.js create mode 100644 apps/HRV/app.js create mode 100644 apps/HRV/hrv.png create mode 100644 apps/banglerun/interface.html create mode 100644 apps/breath/README.md create mode 100644 apps/breath/app-icon.js create mode 100644 apps/breath/app-icon.png create mode 100644 apps/breath/app.js create mode 100644 apps/breath/settings.json create mode 100644 apps/dtlaunch/ChangeLog create mode 100644 apps/dtlaunch/README.md create mode 100644 apps/dtlaunch/app-icon.js create mode 100644 apps/dtlaunch/app.js create mode 100644 apps/dtlaunch/icon.png create mode 100644 apps/dtlaunch/screenshot.jpg create mode 100644 apps/edisonsball/README.md create mode 100644 apps/edisonsball/app-icon.js create mode 100644 apps/edisonsball/app-icon.png create mode 100644 apps/edisonsball/app.js create mode 100644 apps/hardalarm/ChangeLog create mode 100644 apps/hardalarm/app-icon.js create mode 100644 apps/hardalarm/app.js create mode 100644 apps/hardalarm/app.png create mode 100644 apps/hardalarm/boot.js create mode 100644 apps/hardalarm/hardalarm.js create mode 100644 apps/hardalarm/widget.js create mode 100644 apps/hrrawexp/README.md create mode 100644 apps/hrrawexp/app-icon.js create mode 100644 apps/hrrawexp/app-icon.png create mode 100644 apps/hrrawexp/app.js create mode 100644 apps/hrrawexp/interface.html create mode 100644 apps/lazyclock/ChangeLog create mode 100644 apps/lazyclock/README.md create mode 100644 apps/lazyclock/lazyclock-app.js create mode 100644 apps/lazyclock/lazyclock-icon.js create mode 100644 apps/lazyclock/lazyclock.png delete mode 100644 apps/multiclock/ana.min.js diff --git a/apps.json b/apps.json index 0dd96d3fe..68e637644 100644 --- a/apps.json +++ b/apps.json @@ -1341,7 +1341,7 @@ "name": "Active Pedometer", "shortName":"Active Pedometer", "icon": "app.png", - "version":"0.04", + "version":"0.06", "description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.", "tags": "outdoors,widget", "readme": "README.md", @@ -1554,7 +1554,8 @@ "shortName": "BangleRun", "icon": "banglerun.png", "version": "0.05", - "description": "An app for running sessions.", + "interface": "interface.html", + "description": "An app for running sessions. Displays info and logs your run for later viewing.", "tags": "run,running,fitness,outdoors", "allow_emulator": false, "storage": [ @@ -2138,12 +2139,12 @@ { "id": "multiclock", "name": "Multi Clock", "icon": "multiclock.png", - "version":"0.07", + "version":"0.08", "description": "Clock with multiple faces - Big, Analogue, Digital, Text, Time-Date.\n Switch between faces with BTN1 & BTN3", "readme": "README.md", "tags": "clock", "type":"clock", - "allow_emulator":false, + "allow_emulator":true, "storage": [ {"name":"multiclock.app.js","url":"clock.js"}, {"name":"big.face.js","url":"big.js"}, @@ -2478,5 +2479,104 @@ {"name":"gmeter.app.js","url":"app.js"}, {"name":"gmeter.img","url":"app-icon.js","evaluate":true} ] +}, +{ "id": "dtlaunch", + "name": "Desktop Launcher", + "icon": "icon.png", + "version":"0.03", + "description": "Desktop style App Launcher with six apps per page - fast access if you have lots of apps installed.", + "readme": "README.md", + "tags": "tool,system,launcher", + "type":"launch", + "storage": [ + {"name":"dtlaunch.app.js","url":"app.js"}, + {"name":"dtlaunch.img","url":"app-icon.js","evaluate":true} + ] +}, +{ "id": "HRV", + "name": "Heart Rate Variability monitor", + "shortName":"HRV monitor", + "icon": "hrv.png", + "version":"0.03", + "description": "Heart Rate Variability monitor, see Readme for more info", + "tags": "", + "readme": "README.md", + "storage": [ + {"name":"HRV.app.js","url":"app.js"}, + {"name":"HRV.img","url":"app-icon.js","evaluate":true} + ] +}, +{ "id": "hardalarm", + "name": "Hard Alarm", + "shortName":"HardAlarm", + "icon": "app.png", + "version":"0.01", + "description": "Make sure you wake up! Count to the right number to turn off the alarm", + "tags": "tool,alarm,widget", + "storage": [ + {"name":"hardalarm.app.js","url":"app.js"}, + {"name":"hardalarm.boot.js","url":"boot.js"}, + {"name":"hardalarm.js","url":"hardalarm.js"}, + {"name":"hardalarm.img","url":"app-icon.js","evaluate":true}, + {"name":"hardalarm.wid.js","url":"widget.js"} + ], + "data": [ + {"name":"hardalarm.json"} + ] +}, +{ "id": "edisonsball", + "name": "Edison's Ball", + "shortName":"Edison's Ball", + "icon": "app-icon.png", + "version":"0.01", + "description": "Hypnagogia/Micro-Sleep alarm for experimental use in exploring sleep transition and combating drowsiness", + "tags": "", + "readme": "README.md", + "storage": [ + {"name":"edisonsball.app.js","url":"app.js"}, + {"name":"edisonsball.img","url":"app-icon.js","evaluate":true} + ] +}, +{ "id": "hrrawexp", + "name": "HRM Data Exporter", + "shortName":"HRM Data Exporter", + "icon": "app-icon.png", + "version":"0.01", + "description": "export raw hrm signal data to a csv file", + "tags": "", + "readme": "README.md", + "interface": "interface.html", + "storage": [ + {"name":"hrrawexp.app.js","url":"app.js"}, + {"name":"hrrawexp.img","url":"app-icon.js","evaluate":true} + ] +}, +{ "id": "breath", + "name": "Breathing App", + "shortName":"Breathing App", + "icon": "app-icon.png", + "version":"0.01", + "description": "app to aid relaxation and train breath syncronicity using haptics and visualisation, also displays HR", + "tags": "tools,health", + "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} + ] +}, +{ "id": "lazyclock", + "name": "Lazy Clock", + "icon": "lazyclock.png", + "version":"0.01", + "readme": "README.md", + "description": "Tells the time, roughly", + "tags": "clock", + "type":"clock", + "allow_emulator":true, + "storage": [ + {"name":"lazyclock.app.js","url":"lazyclock-app.js"}, + {"name":"lazyclock.img","url":"lazyclock-icon.js","evaluate":true} + ] } ] diff --git a/apps/HRV/ChangeLog b/apps/HRV/ChangeLog new file mode 100644 index 000000000..447062294 --- /dev/null +++ b/apps/HRV/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Added options to either run as a one-off reading, or a continuous mode to log data until the watch is reset +0.03: Add RMSSD recording diff --git a/apps/HRV/README.md b/apps/HRV/README.md new file mode 100644 index 000000000..1cfb351d3 --- /dev/null +++ b/apps/HRV/README.md @@ -0,0 +1,18 @@ +Monitor Heart Rate Variability using the Bangle.JS +=================================================== + +One-time mode: +------------- + +This will take a HRV measurement over a single approx 30 second period. It will also provide you with a HR reading based on the post-processing of the signal. + +HRV metrics displayed are currently RMSSD (Root Mean Square of the Successive Differences) and also SDNN (standard deviation of NN intervals). + +Continuous mode: +---------------- + +This will continually take measurements over 30 second periods every 3 and half minutes and log them to a CSV file on the Bangle until the watch is reset; this file can then be reviewed in Excel or other apps. The log file is reset each time you restart and select this mode to save on storage. The log file is just 1 line per each 3 minute cycle showing: timestamp, HR, SDNN, RMSSD, sample count, Temp (uncalibrated CPU temp), and movement based on the accelerometer. The additional metrics aside from the HRM data are useful in analysing sleep. + +Note that in both modes, if the watch seems unresponsive, it's processing data and if you continue to hold the reset button it will eventually restart. + +If your sample count is less than around 5 samples and/or the readings don’t look right, try repositioning the watch and try again - you can use the HR monitor app to confirm fitting. diff --git a/apps/HRV/app-icon.js b/apps/HRV/app-icon.js new file mode 100644 index 000000000..1125d437a --- /dev/null +++ b/apps/HRV/app-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("MDABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAYAA8AAAA8AA8ABgB8AA8ADwD4AA8AH4HwAA8AP8PgAA8Af+fAAA8A//+AAA8B+P8AAA8D4H4AAA8HwDwAAA8PgBgAAA8PAAAAAA8GAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA/////+AA//////AAf/////AAP////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) diff --git a/apps/HRV/app.js b/apps/HRV/app.js new file mode 100644 index 000000000..5919d1d79 --- /dev/null +++ b/apps/HRV/app.js @@ -0,0 +1,303 @@ +var option = null; + +//debugging or analysis files +var logfile = require("Storage").open("HRV_log.csv", "w"); + +logfile = require("Storage").open("HRV_log.csv", "a"); + +var csv = [ + "time", + "sample count", + "HR", + "SDNN", + "RMSSD", + "Temp", + "movement" + ]; +logfile.write(csv.join(",")+"\n"); + +var debugging = true; + +var first_signals = 0; // ignore the first several signals +var heartrate = []; +var BPM_array = []; +var raw_HR_array = new Float32Array(1536); +var alternate_array = new Float32Array(3072); +var pulse_array = []; +var pulsecount = 0; +var cutoff_threshold = 0.5; +var sample_frequency = 51.6; +var gap_threshold = 0.15; +var hr_min = 40; +var hr_max = 160; +var movement = 0; + +function storeMyData(data, file_type) { + log = raw_HR_array; + // shift elements backwards - note the 4, because a Float32 is 4 bytes + log.set(new Float32Array(log.buffer, 4 /*bytes*/)); + // add ad final element + log[log.length - 1] = data; +} + +function average(samples) { + var sum = 0; + for (var i = 0; i < samples.length; i++) { + sum += parseFloat(samples[i]); + } + var avg = sum / samples.length; + return avg; +} + +function StandardDeviation (array) { + const n = array.length; + const mean = array.reduce((a, b) => a + b) / n; + return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n); +} + +function turn_off() { + Bangle.setHRMPower(0); + + var accel = setInterval(function () { + movement = movement + Bangle.getAccel().diff; + }, 1000); + + g.clear(); + g.drawString("processing 1/5", 120, 120); + + rolling_average(raw_HR_array,5); + g.clear(); + g.drawString("processing 2/5", 120, 120); + + upscale(); + g.clear(); + g.drawString("processing 3/5", 120, 120); + + rolling_average(alternate_array,5); + g.clear(); + g.drawString("processing 4/5", 120, 120); + + apply_cutoff(); + find_peaks(); + + g.clear(); + g.drawString("processing 5/5", 120, 120); + + calculate_HRV(); +} + +function bernstein(A, B, C, D, E, t) { + s = 1 - t; + x = (A * Math.pow(s, 4)) + (B * 4 * Math.pow(s, 3) * t) + (C * 6 * s * s * t * t) + + (D * 4 * s * Math.pow(t, 3)) + (E * Math.pow(t, 4)); + return x; +} + +function upscale() { + var index = 0; + for (let i = raw_HR_array.length - 1; i > 5; i -= 5) { + p0 = raw_HR_array[i]; + p1 = raw_HR_array[i - 1]; + p2 = raw_HR_array[i - 2]; + p3 = raw_HR_array[i - 3]; + p4 = raw_HR_array[i - 4]; + for (let T = 0; T < 100; T += 10) { + x = T / 100; + D = bernstein(p0, p1, p2, p3, p4, x); + alternate_array[index] = D; + index++; + } + } +} + +function rolling_average(values, count) { + var temp_array = []; + + for (let i = 0; i < values.length; i++) { + temp_array = []; + for (let x = 0; x < count; x++) + temp_array.push(values[i + x]); + + values[i] = average(temp_array); + } +} + +function apply_cutoff() { + var x; + for (let i = 0; i < alternate_array.length; i++) { + x = alternate_array[i]; + if (x < cutoff_threshold) + x = cutoff_threshold; + alternate_array[i] = x; + } +} + +function find_peaks() { + var previous; + var previous_slope = 0; + var slope; + var gap_size = 0; + var temp_array = []; + + for (let i = 0; i < alternate_array.length; i++) { + if (previous == null) + previous = alternate_array[i]; + slope = alternate_array[i] - previous; + if (slope * previous_slope < 0) { + if (gap_size > 30) { + pulse_array.push(gap_size); + gap_size = 0; + } + } + else { + gap_size++; + } + previous_slope = slope; + previous = alternate_array[i]; + } +} + +function RMSSD(samples){ + var sum = 0; + var square = 0; + var data = []; + var value = 0; + + for (let i = 0; i < samples.length-1; i++) { + value = Math.abs(samples[i]-samples[i+1])*((1 / (sample_frequency * 2)) * 1000); + data.push(value); + } + + for (let i = 0; i < data.length; i++) { + square = data[i] * data[i]; + Math.round(square); + sum += square; + } + + var meansquare = sum/data.length; + var RMS = Math.sqrt(meansquare); + RMS = parseInt(RMS); + return RMS; +} + +function calculate_HRV() { + var gap_average = average(pulse_array); + var temp_array = []; + var gap_max = (1 + gap_threshold) * gap_average; + var gap_min = (1 - gap_threshold) * gap_average; + for (let i = 0; i < pulse_array.length; i++) { + if (pulse_array[i] > gap_min && pulse_array[i] < gap_max) + temp_array.push(pulse_array[i]); + } + gap_average = average(temp_array); + var calculatedHR = (sample_frequency*60)/(gap_average/2); + if(option == 0) + g.flip(); + g.clear(); + //var display_stdv = StandardDeviation(pulse_array).toFixed(1); + var SDNN = (StandardDeviation(temp_array) * (1 / (sample_frequency * 2) * 1000)).toFixed(0); + var RMS_SD = RMSSD(temp_array); + g.drawString("SDNN:" + SDNN + +"\nRMSSD:" + RMS_SD + + "\nHR:" + calculatedHR.toFixed(0) + +"\nSample Count:" + temp_array.length, 120, 120); + + if(option == 0){ + Bangle.buzz(500,1); + clearInterval(routine); + } + + else{ + var csv = [ + 0|getTime(), + temp_array.length, + calculatedHR.toFixed(0), + SDNN, + RMS_SD, + E.getTemperature(), + movement.toFixed(5) + ]; + logfile.write(csv.join(",")+"\n"); + + movement = 0; + // for (let i = 0; i < raw_HR_array.length; i++) { + // raw_HR_array[i] = null; + //} + } +} + +function btn1Pressed() { + if(option === null){ + clearInterval(accel); + g.clear(); + g.drawString("one-off assessment", 120, 120); + option = 0; + Bangle.setHRMPower(1); + } +} + +function btn3Pressed() { + if(option === null){ + logfile.write(""); //reset HRV log + clearInterval(accel); + g.clear(); + g.drawString("continuous mode", 120, 120); + option = 1; + Bangle.setHRMPower(1); + } +} + +var routine = setInterval(function () { + clearInterval(accel); + first_signals = 0; // ignore the first several signals + pulsecount = 0; + BPM_array = []; + heartrate = []; + pulse_array = []; + Bangle.setHRMPower(1); +}, 180000); + +var accel = setInterval(function () { + movement = movement + Bangle.getAccel().diff; +}, 1000); + +g.clear(); +g.setColor("#00ff7f"); +g.setFont("6x8", 2); +g.setFontAlign(-1,1); +g.drawString("continuous", 120, 210); +g.setFontAlign(-1,1); +g.drawString("one-time", 140, 50); + +g.setColor("#ffffff"); +g.setFontAlign(0, 0); // center font +g.drawString("check app README", 120, 120); +g.drawString("for more info", 120, 140); + +setWatch(btn1Pressed, BTN1, {repeat:true}); +setWatch(btn3Pressed, BTN3, {repeat:true}); + +Bangle.on('HRM', function (hrm) { + if(option == 0) + g.flip(); + if (first_signals < 3) { + g.clear(); + g.drawString("setting up...\nremain still " + first_signals * 20 + "%", 120, 120); + first_signals++; + } + else { + BPM_array = hrm.raw; + if(hrm.bpm > hr_min && hrm.bpm < hr_max) + heartrate.push(hrm.bpm); + if (pulsecount < 7) { + for (let i = 0; i < 256; i++) { + storeMyData(BPM_array[i], 0); + } + g.clear(); + g.drawString("logging: " + ((pulsecount/6)*100).toFixed(0) + "%", 120, 120); + } + if(pulsecount == 6) + turn_off(); + pulsecount++; + } +}); diff --git a/apps/HRV/hrv.png b/apps/HRV/hrv.png new file mode 100644 index 0000000000000000000000000000000000000000..7fdbce610555f4fb39ed1106c2b7e61e6a6c339b GIT binary patch literal 749 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf00v@9M??Vs0RI60 zpuMM)0007TNkl@9@iHZ0c8tFs1 z;DQ?>iW|7pbhW3aoa)na?{&Pl9H^}AP>2u0n)uJd0QT1c_1)o0_*??$d1EMi) zAQp=$^Z8tv&1On88inXf*sX&sHXrA*a&`_DG1@1nT321|bCl^L!<{-AD0b@p4Dna+-|qQpH8O@#h=e-lFeofUK61~NX5WByj(69SuU5bg^}%cOT1q1 zhyMx1Q=vgf$G|-I@_0OCygBSs#ad0l7_QZq15I2`(_-64`Xb{pf`0+?2VmJ)wESE|pgI8BRpVtc3`Nm;$FEj|d zVDRJpeji3|(Ag~%3I-o9dTk#J2AbmWIO_RJp+P8%!H?4dbm*++ayf(NKM4&&X$*cm z6bfnILZ{Pt)=WMm`A#ev*??$d1EP@)h(h*ex`pyAW;CyO~_P f5RGg=EQ<0DVjKyPk>G_d00000NkvXXu0mjf48luD literal 0 HcmV?d00001 diff --git a/apps/activepedom/ChangeLog b/apps/activepedom/ChangeLog index ca26a648a..9dc698827 100644 --- a/apps/activepedom/ChangeLog +++ b/apps/activepedom/ChangeLog @@ -1,4 +1,6 @@ 0.01: New Widget! 0.02: Distance calculation and display 0.03: Data logging and display -0.04: Steps are set to 0 in log on new day \ No newline at end of file +0.04: Steps are set to 0 in log on new day +0.05: Fix default step/distance display if it hasn't been set up first +0.06: Added WIDGETS.activepedom.getSteps() diff --git a/apps/activepedom/widget.js b/apps/activepedom/widget.js index ed91a4cfd..f67017014 100644 --- a/apps/activepedom/widget.js +++ b/apps/activepedom/widget.js @@ -68,6 +68,8 @@ 'stepSensitivity' : 80, 'stepGoal' : 10000, 'stepLength' : 75, + 'lineOne' : "Distance", + 'lineTwo' : "Steps", }; if (!settings) { loadSettings(); } return (key in settings) ? settings[key] : DEFAULTS[key]; @@ -160,7 +162,6 @@ if (active == 1) g.setColor(0x07E0); //green else g.setColor(0xFFFF); //white g.setFont("6x8", 2); - if (setting('lineOne') == 'Steps') { g.drawString(kFormatterSteps(stepsCounted),this.x+1,this.y); //first line, big number, steps } @@ -227,6 +228,6 @@ setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive) timerStoreData = setInterval(storeData, storeDataInterval); //store data regularly - //Add widget - WIDGETS["activepedom"]={area:"tl",width:width,draw:draw}; -})(); \ No newline at end of file + //Add widget, use: WIDGETS.activepedom.getSteps() inside another App to return todays step count + WIDGETS["activepedom"]={area:"tl",width:width,draw:draw, getSteps:()=>stepsCounted}; +})(); diff --git a/apps/banglerun/interface.html b/apps/banglerun/interface.html new file mode 100644 index 000000000..177904077 --- /dev/null +++ b/apps/banglerun/interface.html @@ -0,0 +1,216 @@ + + + + + +
+ + + + + diff --git a/apps/banglerun/src/gps.ts b/apps/banglerun/src/gps.ts index 3c0ee120d..bad6fd1c0 100644 --- a/apps/banglerun/src/gps.ts +++ b/apps/banglerun/src/gps.ts @@ -24,6 +24,10 @@ function parseNmea(state: AppState, nmea: string): void { const tokens = nmea.split(','); const sentence = tokens[0].substr(3, 3); + // FIXME: Bangle.js reports HDOP from GGA - can this be used instead + // of manually parsing all of the raw GPS data, which can cause FIFO_FULL + // errors? + switch (sentence) { case 'GGA': state.lat = parseCoordinate(tokens[2]) * (tokens[3] === 'N' ? 1 : -1); diff --git a/apps/banglerun/src/log.ts b/apps/banglerun/src/log.ts index fb0676121..2ab7b4191 100644 --- a/apps/banglerun/src/log.ts +++ b/apps/banglerun/src/log.ts @@ -17,11 +17,10 @@ function initLog(state: AppState): void { 'distance', 'heartrate', 'steps', - ].join(',')); + ].join(',') + '\n'); } function updateLog(state: AppState): void { - state.file.write('\n'); state.file.write([ Date.now().toFixed(0), state.lat.toFixed(6), @@ -31,7 +30,7 @@ function updateLog(state: AppState): void { state.distance.toFixed(2), state.hr.toFixed(0), state.steps.toFixed(0), - ].join(',')); + ].join(',') + '\n'); } export { initLog, updateLog }; diff --git a/apps/breath/README.md b/apps/breath/README.md new file mode 100644 index 000000000..9db00f734 --- /dev/null +++ b/apps/breath/README.md @@ -0,0 +1,12 @@ +Breathing App +============= +This app attempts to aid relaxation and train breath syncronicity by providing a visualisation of a circle that expands and contracts to guide breathing rate. The app also modulates the vibration motor so you don't neccessarily have to look at the screen. Your HR is displayed in the lower left and there are a few parameters you can change to tailor what works best for you. The menu is quie self-explanatory, the 'ex_in_ratio' just allows an option to make the exhale speed slightly faster if you prefer that - durations can be further altered by increasing the pause times. + +Resonance frequency breathing is a way of breathing (slow relaxed diaphragmatic breathing at around 3-7 breaths per minute) that has a regulating effect on the autonomic nervous system and other key body systems such as the circulatory system. This has many benefits, supported by numerous studies e.g.: + +Increases pulmonary function +Lowers blood pressure +Improves baroreflex gain +Improves heart rate variability +Increases the ability to handle stress +Clinical improvements in asthma diff --git a/apps/breath/app-icon.js b/apps/breath/app-icon.js new file mode 100644 index 000000000..c1373e414 --- /dev/null +++ b/apps/breath/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AFqoAFF94zoF5QxkF5gxiF/t5F92k5wvt5AvtvXOAAIvqvJeBF9i9BAAYvoFwovnvQuGF816XYYwiFw4tIF8qMHF86NJF8iOLGDqOMztV0YvOxIABF5qIB0l6RxFPp1VMBgtCGB4eDvTtH0dPL4gwHFQYwPXBjBTGBwvVYTIv/FyAviFpYuOF6bsOF8wqFFpxffACIuuF7llL99lGKKPeGKAvgGBzufF6XHAAPNFzIvUAAZqEFqC/SF44AkF/4A/AH4A/ADIA=")) \ No newline at end of file diff --git a/apps/breath/app-icon.png b/apps/breath/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..740eb698f32bcf5435923df1e82a68ff80010d4d GIT binary patch literal 1336 zcmV-81;_e{P)BKMW8~1mU4^e2C=O|P)kx;(iR)6 zv`zX#P*hBSYEY8K=!(9Oc&Pt0on0R6@2Pvv|NGAQcITWqCj};$;QtL7bu?6!cY`qngxdjJ0dN_NO8}oG z!bu3}lw$~|ylrj%)J^whVg+Cm`T&3<46*N=aJTtRofS#&G{p7*cT5t@oliUL_BF!x0Qb(4Gtt}k zn%%xe*cyNcSTxT8fVc2x)x~yt8)0hzPo9j)x6z0%(+>7}ld;DBgKYz-ZbJDI=Zyx& zJS;x@xLy86*b+bzKy9%bX-b40@n$>xV=?XkvINu=yOB3Tiq>7oM{1{!cZ^t!D?pke zu%=waf?V0Cgur@I-wO!`xaV#O8=i0@_s)10^k|}En8EO?i$(Tifx1$~Trpv8B4d~( zpCR)*m~sj6J0C*(i6Aar)gc71r7jKWYIK>m>01nIVYr+!yc?E8b_c-v z0xI?W9!TPxeh#2sTxim z0G8(Gz~zvzG=E04ZUsbd8EG}Q_AO_ z3Bcu4(t0?uwefrHk@`Z%vC%4Kdbcy!0>&Py>6rv>NdV3^TS+2(Va!KYw3<{0=mqg6v&A}#v6rGV(P|PpN?3Pew?#8gMS!l+c@Qyx-?n!S&kwOElAW%FT1fX0aW1jQX25xU zTO{Br1D`YN?>%3rd|_msf&knV+4AFG&ObG2;L_f*j6I`O%(Sy(a9WULHB7F)&H40? z8TaG|VCs%+YFkbF%Sm!AuhmR6b7l6O5+Na<}rWa9(?`xyp^#`wHY2?}n za2XR0u%_(aQ%U`1-IE_6q)(+X-^F-kYd;;FPBJwSungc=a#VGfcNvA^>5?6whe}#< ztO%YuyIgGocq_mwbm89-uRfUpG=SesM5qht z!;!yealBenyHN1*X^`ZCkkdet3X&C&6!TKg0B@w_tv{LZU){=5W_n;%#(;7tvm3+b z^}w2u`O~pDQj^;#hrpo_!l4_d01^(6q(aWfdliKH!B_|&6QC}T{st{?HU0Sbsww>B z!Tz)T>QG020nY@G0j9&k8F(q4*3=>n_MS2{aMk{5`GRbO*UO@(V0bo%LRWWJreBD! uNZDb|99}Q!0DiHexdr!u2_~4pPW}a#P==mUQ_`;h0000= 0 && angle < 90) { + if (angle == 2) { + clearInterval(); + g.setFontAlign(-1, -1); + g.drawString("<<", 220, 40); + status = 7; + timeout = setTimeout(function () { + interval = restart_interval(); + }, settings.exhale_pause * 1000); + } + direction = 0; + } + else { + if (angle == 90) + angle = -90; + if (angle == -90) { + clearInterval(); + g.setFontAlign(-1, -1); + g.drawString("<<", 220, 40); + status = 7; + timeout = setTimeout(function () { + interval = restart_interval(); + }, settings.inhale_pause * 1000); + } + direction = 1; + } + g.drawString(display_HR, 20, 200); + + g.flip(); + + if (settings.vibrate == "forward") + Bangle.buzz(50, Math.abs(origin)/1.5); + else if (settings.vibrate == "backward") + Bangle.buzz(50, (1.6 - (Math.abs(origin)))); +} + +function restart_interval() { + status = 6; + var calc = 5 - settings.period; + calc *= 15; + calc += 120; + if(direction == 1 && settings.ex_in_ratio == "5:6"){ + calc -= calc*0.2; + } + interval = setInterval(circle, calc); +} + +function update_menu() { + g.clear(); + g.setColor(settings.colour[0]); + g.setFontAlign(-1, -1); + g.drawString("+/-", 200, 200); + g.drawString("<>", 220, 40); + g.drawString("GO", 210, 120); + g.setFontAlign(-1, -1); + var cursor = 60; + + while (cursor < 180) { + var key = Object.keys(settings)[(cursor - 60) / 20]; + var value = settings[key]; + + if (status == ((cursor - 60) / 20)) { + g.setColor(colours.white[0]); + } + else + g.setColor(settings.colour[0]); + + var display_txt = selection[(cursor - 60) / 20] + ": " + value; + + if(((cursor - 60) / 20) == 3) + display_txt = selection[(cursor - 60) / 20] + ": " + value[1]; + + g.drawString(display_txt, 10, cursor); + cursor += 20; + } +} + +function btn1Pressed() { + if (status < 6) { + status += 1; + if (status == 6) + status = 0; + + update_menu(); + } + else if (status == 7) { + clearTimeout(timeout); + clearInterval(); + status = 0; + update_menu(); + } +} + +function btn2Pressed() { + if (status < 6) { + settings_file = require("Storage").open("breath.settings.json", "w"); + settings_file.write(JSON.stringify(settings)); + Bangle.setHRMPower(1); + g.setColor(settings.colour[0]); + restart_interval(); + } +} + +function btn3Pressed() { + if (status < 6) { + if (status == 0) { + settings.period += 1; + if (settings.period > 6) + settings.period = 1; + } + else if (status == 1) { + settings.exhale_pause += 1; + if (settings.exhale_pause > 4) + settings.exhale_pause = 1; + } + else if (status == 2) { + settings.inhale_pause += 1; + if (settings.inhale_pause > 4) + settings.inhale_pause = 1; + } + else if (status == 3) { + if (settings.colour[0] == colours.green[0]) { + settings.colour = colours.blue; + } + else if (settings.colour[0] == colours.blue[0]) + settings.colour = colours.red; + else if (settings.colour[0] == colours.red[0]) + settings.colour = colours.yellow; + else if (settings.colour[0] == colours.yellow[0]) + settings.colour = colours.green; + } + else if (status == 4) { + if (settings.vibrate == "forward") + settings.vibrate = "backward"; + else if (settings.vibrate == "backward") + settings.vibrate = "off"; + else if (settings.vibrate == "off") + settings.vibrate = "forward"; + } + else if(status == 5){ + if(settings.ex_in_ratio == "1:1") + settings.ex_in_ratio = "5:6"; + else + settings.ex_in_ratio = "1:1"; + } + update_menu(); + } +} + +update_menu(); + +setWatch(btn1Pressed, BTN1, { repeat: true }); +setWatch(btn2Pressed, BTN2, { repeat: true }); +setWatch(btn3Pressed, BTN3, { repeat: true }); + +Bangle.on('HRM', function (hrm) { + if (first_signal) + first_signal = false; + else{ + var signal = hrm.bpm; + if(signal > 50 && signal < 180) + display_HR = signal; + } +}); diff --git a/apps/breath/settings.json b/apps/breath/settings.json new file mode 100644 index 000000000..98e585456 --- /dev/null +++ b/apps/breath/settings.json @@ -0,0 +1 @@ +{"period":2,"exhale_pause":1,"inhale_pause":1,"colour":["#00ff7f","green"],"vibrate":"forward","ex_in_ratio":"5:6"} diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog new file mode 100644 index 000000000..3df4ab63b --- /dev/null +++ b/apps/dtlaunch/ChangeLog @@ -0,0 +1,4 @@ +0.01: Initial version +0.02: Multiple pages +0.03: cycle thru pages + diff --git a/apps/dtlaunch/README.md b/apps/dtlaunch/README.md new file mode 100644 index 000000000..70f7ff931 --- /dev/null +++ b/apps/dtlaunch/README.md @@ -0,0 +1,16 @@ +# Desktop style App Launcher + +![](screenshot.jpg) + +In the picture above, the Settings app is selected. +## Controls + +**BTN1** - move backward through app icons on a page + +**BTN2** - run the selected app + +**BTN3** - move forward through app icons + +**Swipe Left** - move to next page of app icons + +**Swipe Right** - move to previous page of app icons \ No newline at end of file diff --git a/apps/dtlaunch/app-icon.js b/apps/dtlaunch/app-icon.js new file mode 100644 index 000000000..a49bb0af4 --- /dev/null +++ b/apps/dtlaunch/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4ATxAAQC+2N7vd7AX/C/6/7a/4X/a/4X/C/4X/C/4Xfl3iC6vu9wXtI653WAH4A/ABg")) \ No newline at end of file diff --git a/apps/dtlaunch/app.js b/apps/dtlaunch/app.js new file mode 100644 index 000000000..329a96958 --- /dev/null +++ b/apps/dtlaunch/app.js @@ -0,0 +1,73 @@ +/* Desktop launcher +* +*/ + +var s = require("Storage"); +var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); +apps.sort((a,b)=>{ + var n=(0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; +}); + +var Napps = apps.length; +var Npages = Math.ceil(Napps/6); +var maxPage = Npages-1; +var selected = 0; +var oldselected = -1; +var page = 0; + +function draw_icon(p,n,selected) { + var x = (n%3)*80; + var y = n>2?130:40; + (selected?g.setColor(0.3,0.3,0.3):g.setColor(0,0,0)).fillRect(x,y,x+79,y+89); + g.drawImage(s.read(apps[p*6+n].icon),x+10,y+10,{scale:1.25}); + g.setColor(-1).setFontAlign(0,-1,0).setFont("6x8",1); + var txt = apps[p*6+n].name.split(" "); + for (var i = 0; i < txt.length; i++) { + txt[i] = txt[i].trim(); + g.drawString(txt[i],x+40,y+70+i*8); + } +} + +function drawPage(p){ + g.setColor(0,0,0).fillRect(0,0,239,239); + g.setFont("6x8",2).setFontAlign(0,-1,0).setColor(1,1,1).drawString("Bangle ("+(p+1)+"/"+Npages+")",120,12); + for (var i=0;i<6;i++) { + if (!apps[p*6+i]) return i; + draw_icon(p,i,selected==i); + } +} + +Bangle.on("swipe",(dir)=>{ + selected = 0; + oldselected=-1; + if (dir<0){ + ++page; if (page>maxPage) page=0; + drawPage(page); + } else { + --page; if (page<0) page=maxPage; + drawPage(page); + } +}); + +function nextapp(d){ + oldselected = selected; + selected+=d; + selected = selected<0?5:selected>5?0:selected; + selected = (page*6+selected)>=Napps?0:selected; + draw_icon(page,selected,true); + if (oldselected>=0) draw_icon(page,oldselected,false); +} + +function doselect(){ + load(apps[page*6+selected].src); +} + +setWatch(nextapp.bind(null,-1), BTN1, {repeat:true,edge:"falling"}); +setWatch(doselect, BTN2, {repeat:true,edge:"falling"}); +setWatch(nextapp.bind(null,1), BTN3, {repeat:true,edge:"falling"}); + +drawPage(0); diff --git a/apps/dtlaunch/icon.png b/apps/dtlaunch/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..daa22371ba24a6a290c4c52e5d8b7a17c004fd1e GIT binary patch literal 305 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCwj^(N7l!{JxM1({$v}~3o-U3d z8I5mmTJtqINU%IOc;mvPA3GSt6~az%cHDEcj(WT0InS+?=^qN2txPUX6V6yTcUl!= z*a3;UX=lPeIU7w@-Ob71;xdWn>VoYaN)iR9c|N;}uy31gmZ2fP(yxNy#cH-!OtUhc z?abUQy*y5yFRY&TGxyu@2G%7T8@9yk4iGJq7JVD;@SDlz1W?38Y{Ic-A1Q8+WX4P# zgo#2{-ds z03a_9JOKaz5kQ4t05BkW0G?S8%wL!o#JLb`02VAE-~m900Qg@R09ql$e{nD5@jo)4 z1_;})w)b*hAnd;|4v429q5$0QwlDxh>|Z#<{0|0pLTUbBL<&$A5V5nff&%7BX0|p^ z3RMY;$LuyH6sAxcsDq)CxvdTTz0AEo_qP2m**KZm0f3#Aot2-Bou8eP0=%;E@UwCP z07XFhAB$n^0y6(%w@}J|SszOG5B3ItSO7o(^}?~TbFjt2{_zCHFBa}EPK<;6mVv;) z0`U+4`L``FALC*E>i-lE``!c?TyFz#Rnq?^CjNv zzAw0a5Pyx2`(*_K(eMtgLTg{zMbP_!jmrW`B?WPha3@<@~n)er5o< zF#quHG3>u#_-a=W_hA0h~Puo4K67Fc@-#@3}Akk-wi?zv;d! zHt<9OPx${dQvZnu0F?~j5aNOpi4FjKBmf`?0|2VT;&;H;~;4|?Dmw?iL zFa&&R?#CN^I^eiLt@jx85>5d?`z!MRK>v#Y0P`<~09e0bL<7pd0C4{DkpcYQm{N%X z00{r`VF03E@?d=7UIXNR`Cv%?>InzE{Ad2beFCU{$=ush3()+P4+m)fVpxFguY4=; z_%9y)4%G}Y3 zl~oZ71|R@5fn1;-C;}>hcYq%t0my-B7y)|F2^!ETdO!-&0`vfVKp7AR_yaBgFK9X` zKnwB|Dh0{q}WBZ5aLY!Iw9>=g_*%q%bqzJ|yFT+l8;fD=5B!3kpu`vMjUI|B0*Rv*#< z4ww(v8W)@xkAP>SJ?Oao*B41OGFAtJy9dM^a_#SXZ@h(T_FD9ABH z7*Y*-tpKn#qPEU1kVoF25mbATA~6R-ny6M}7@15n@vD60ng!vnU$1MRv6 zU?97IE#Lw6@D_Z(xdTRE?6ZI%a0s7*s0fre2Tp3w1oov0TICC&hFC&i zAd_JITi`Wd1JkSVD6vJd-_xUz5`%T|IC&N@HGnu{{R6|{gsuWsiV4! zp_MaK+sWJ->S%0e1yzUI*&3T+LLLGTtj|A>F%z38h{|B+}(ZD1OOdgP)G_q{lK&0-aHO2Nj5G%Rvso1Ngf_1 zPF_(iCJ}ZKRwj0FPEHO`Y=Je0)rt9HOFND>gnR z9xgsEJ~43)F%C{vQ15TLw+!>;J%(G;0sv|`UJgDENeONqP_-xr6DPMAI};x(8!wYM z8#k*YpC|{n2)hI*Ej+mdVBA&xE&qADf;IO(!v8;gj#3yI8e38r8(M=OtP~u~e07M`acL~P>mLNa95bOy2B>6=U05|9r0FdbZErC&50C-Yyw}Px`=Hz6@&%$Ek z$ZTk0YXoIBwzXz)GqeLE!^#2(2)o&VVS_qR7(q?VZ3HO~8d@kR%uNI-HM!(jYs9ZX*FtB6bf(-yoFr2NN7S65eNR}N-d2U8X{K0ZDc zR(2M4b|z4Q$L&!H&pH5wY(Iy(tcf<6B>$=dF(+W+YL zA3-RXL;h#U`2`Xg#*X6t0@XlDCAX#QLMw~-ycm;=<%$<{&5*49dp@;+LU`q5`P~O(W{FVFvC`d_hZzR8%t+k!4qq!4Q#@f*I zUL_^PUt{?18~-u`3J&Z4)ENsnE-e2H%su=&!a@}PrT<3YzY+Lv1pXU=|3=`y5%~XK z1pbvHLT$hd!WGN_fxA7x4cyWCoehE6HY>m@DkBdE_bVM0CV}Vxu*5V{Bw+Fj1Hh2L z2m=q;*x7hkIZ0rI0L1$ogpHjYyy5xx8-YJUzje|4(uFBbJC{Yn?c%DPdeG!(yJ>T` z3d1QQCT5_dq9`fzT;kV$APlu3xXE_kxwVaxgNl?Wg_gDs1;PrrZbbmobUc8<(Ad#V zL`muSz0N=7Fa3Wn=TpDZY+#(}UROU+QUHxFraW)j!^zPd+<^YFlWC{-l<%j?CxC5f(xTz+gdOu^@My;3^D&z(an~FGcsd{#l$O zA)|l-Rp7b~0t*WR2MZ5>Ke-{^_j{ypSn$}7*hC&YR5nDQw8vrhk9&tmC0gEwt1@~> z&0*vafP{>PPe4dSLreFV{s|`+HxDl#znHj$q?ELbtg4#2h9>B)u?h5*shPQjqm#3X ztDCz=;On5^kkGL3_=LoyL>c_ob5CHbKTj2G#XaAR9;LaTc1`ZAu4&mM} z2#o8!<5+O;kJuhyizp))+CQXZ_eaDLjeA$#hD61oa)@i>Fp7*v&ACEzbnn_P&;ECg z1^mDA?5|`0@@o#AzvzXo>;;9kMQyIBAg7Tj0Bg2e)NxUMnY61ZVj-|uArqub~ zmz7&Tkjsm90O1*?hNCViIOXl+4?C*Y-J}>6H_Sad+w*P>`jqn*aYcB%HfNSG+|OSR zn_HLca}|{wTtIFaJ~Y+_%8CbI)gASbRiQCN^)G%9T*2F+2+u~Vt&Z8lzFoVJkT*qr z`DS1-BT45=z?i(GF~?0g%QJ5VUA>X?P8SbPy_(rlUXk7h+1_q35l@W-Ipn9Rluk0H-Qnrj@x6@&5AG_sksIbvVl8*(jn1FeW2P(FMRFwV5>6!2rsP;I6|K$+yG zGXR!nVJ9bJZN<`!2WD>otJ}S;Y}8Hak<%S8$514cz3F9(x!G4%H15-~E}m>lfUPJO2pAn%zs3Se~%Gx0|jZz!Y2zP<1SAy~Dd@F#1?ckw_^zrgSQO>ujV zSU=`|kN!NZ*wCooufhKIV7YGX7&fNFYIZw39zJVDQu=m%T?#q6j#+KRTLFVBDOZjY zI)f(~S8D`oC+Xq&Fy>^)`B`1mzWNk9`;OYFkUmk{&;@lhv=3OEsr-hmtSI4 z_V#o1P)I%#AXi|R_JtY%mn^G_J>GQfEtpPn)~=E%vXcjBnLpgCPxN&V1(K85eccM0 zdNr@)ta)if$i&>XG5Q{~WqjV#?-KcjuHYD%m0!bkYVh6E70$QvLkM*WauIDpaXXo- z+5%M%k~n-Qai~W7RtAX^m)iQ=MB2B^m@l_d=c>1HXWdgbko-%k_8%;vIpgnYzuTBv88(2wq42(A+_|!O2!0<02>YdK%vCX*pAJs&D=3m`>X(DSoH(ZJbgPjDDnA zIoy?XC01?BfS@QZVH>&gXQWRq{DF{2BBRciZy!fDFSa1-Vdj0*QhO1;;^t080m`R-nV!JP+*PDR@4Cv zD}lN9Y~n~G%C4B1y0(JC#J!L%hSbf>&&%-+5ZJz$ilGVEiACJE*1l=sr>C;}Z0yo4 zzxdM1F(5Gf*j;03IMRS!%(@A_bBt9jn9f<1!E4~Sr@_7r0a#75HmX_Vu&l63PT_}t zTNyw1y*N%^_j$scqF_~h5hsMoByE7rdkl#h3WV>zW-c`pB>c`kFXj26r$FkldBTNC zDZ(P$F|r>5c}FzEqchs5^KRMf5(o0bwB3vH0dsriZ00w{sOEVF1!30v6jiC-IeRJn zZv?u+&U5zKo0T=5qs@KsHM-1E)AbD*TeNUdT^KMmra7fIx3}kHmEh=BN|LZ$G-7Q- z^)q=ape~6&Kdh*m<1?*SUtJr%Go_LsIza94sH3Q$_ldVi*%y|OrH3T~&MUc%=IUHe zediZ-_aTj~9Gj2#qjOwI`ACaSgtYdJtmCP=2V(n0H1wQNF{qA81Sx-9D>en(0djTR zHOXUAj1OTX2}$)Hw83?ETE{u?&{H=1hSs_eM&Xv^k!Ejy^W)M$9w{uv|)k>$L>RO!J?oEh&hF8uPSFW}NxAz*gj!6Amgo3+tsuRpC#Khx-~{fWFMg}LWjOiKh?3qTLC|vTA-w4w#LPU9ci<>| z1Ahm^ZFa4Wepja?hzp@xtYBpN`DMt{H9jXU>#|H`Lg?YHN-u3v{#X72zqpu>TnbyQ zM4u_!(K})NMp;+ud?y;0etH{>Dc=Dcnq7pJcR)&ZpuxNDxtZz$yIfT9mJ(t0#%nUG z<>k}0u?FcIj=~wKjb}=j{24LZTE2%NJHF;T_#z}*!VF}Js|@sqAFSa^knqVU26FF! z@M1@9BDc-egRC_!|1}m&txHGArm5250evC7+QjJG$174rY^|^p)9)6|Jm$R0JKsoI zFZ;?~vYgi+odv)Ck%g)s-{?OSQkvqDIa@F~QEgp1M_I>D9nS-j zb5M~xZ6YsV_ReU_!%3NUN4c;8|qy_zk?i6vNh?HN5Z zh=_ZwL5}ziC%gBi11r=q0ZL$NdSpcg@iI7{WP{WXTGJ$R0lSlyUW z-S3r^N$B9`=4|hkTiMnAc9gu@>aofJ!Jsi0M=W-XjP7yrxP_Uo%>XQ%`f-X7#klXv zqcMzg9Q<)9vvC=My1J~#twMG{54{X(;Gp4Wu7oGK!9%7cCp$+2#cs?qLethb+zu8z zRs}SlnY9nn_Xd@<1^Q>6FI)}T+%EW*i7^<6o11m$j1_NHR#g9Ns}9x;%zL57 zlIALS_8A8n`pKM6F_pGcjlz4`ZS)Ym48A6OuH8fZtS6MV5$(K2PBU7}rLd&2>!e@F zA&LRJc!`yQbtJ*EneEW~!-w?91N-6~jTKnh?5Eo1d#o<*dh4uU{=7}~+xTsf?hSRm&ZDuYM zL#vpm+T-0{FixKCVQW6yvvp)mK0v$B($P&br8u82^fVqH77)1>z2O^O!!)b@k=!Hz zeIC7&?%nwMjJ%OU0p=)hBXYX^gADakjt=9EXE-FjpWh-UYUv&7bHhLeVsj97^qF z=iE|_1Ya!qA*_R`~7;2W--F?qXe0+%LTb9Y90YvsO&Y~%bz?g+0UPYaApdjX61K{@P0_-^Bw8X(?5=9`4;cofc<@ANYC$!K+U z{#8s!51DX;N%>BC0G&~3mrr8=hC*|OM!c8&(fimG*9fQl_w#e2D;2xfpL)gv^+sm6 za3vH!Ee1S5pXq=dc8qjB$8|wju_|X;X$&H^ipS~N8l~tGij#LN}#f%HjjIZN3QAr<2K4YgxsDWwNRm9mAeSEXaSJF>f z(HqH}GU$8)gBw-zKqO;NY&szWvN4sO9YTiZ#>{xrlex+o$5fo91euaFUBJ?46K#kv zO=!cN+9tBev{Nsc*VV^mB6Lh0M_}~oVpM>fQlD?dEh#=~T@^yO;27w9HTls?*;LMb zjTF24;93dGp5*apSrTTtExYtQeV7R`RXn?EaXbTJFFC!Hv6w`n`3o%d@!{-``a91% z6hoAREERD#XKaj8(JXI{*`ljUA+mw4$| zlZ4zKQ!ZxO4w=~x*>MU}J4_5Bm7|Sx#fpZ7mKS(q+OtEjf|KvEc*SS<)B}f8*&e7R z3@bHj%pc@0$XKrUVr)-UXV1ihPFP8+co(0Hj{XArpL3Q+fVqUGfCjEkHbyk^kk|Mxq%`b^|i|+XWP*9Tx-{S zU1pteS1Zt-6>tS*f3&&N5XJ!+2C3x9tBQ8-o&XoD`3$Bu@ z`Yatwh;uBFoKa%xEtADm!F>_d!M$_6{-uo@v3-=UpL$h=7S;40V|S3snh_#$W<;nR&%BMc ztW(1^*q5WsEjxaDMWBH!FR_$=W~^2^QYCC^i>F(1))UB*q-McvQ<68L`&vRH@(yT@ zNV*_6FfiNx;T(xkuV>=Qb)~$bh`V&z)1_IXV-qUCI%_pLfIBB0hVy`Ta_!oXoFzk@ z$o!Rr^aNHdX20$!tk}^h|9+5dVecwmuAc0hM1P9qB#Kz|pT}$EW&O21bcqawr>81Q z(?5|eZg^4)M$b7sN}4b{6I5#gzA|bl@m_HCyx2YLmNP9lTTefpJ0K`(H;RfNy8~WK z29o6q_6!e7mqq8N3{`BCy7g#y2=15K5Hqz$2XZCZ7v`Yr60Ft^ys492&VXpEDl%Xs zu4`6^F-2AJ7H*AEtXoq^?_~uUFIiQc<#MLZ zJ{I3RkJDPsmj0I@eh(&eq+w?+)4S2@CLxyLX&=p*x51EynCrdv6eLcDZGz^>21HDe zCW`rwQXli4WWpv)JR%F3u5!o8#t@=-@RYM5gtT7{#s75f5f^+}hcEF0U8a5Bd@}W( zRywUS)pb8H)nVC`#mZsK9!ujT!DQJ@i~oVk68;|D=k!%wRh%!8p+CQT|C}vu<-K;W zm>0OZnMskb$6ce$X+cnyd4is%LVzymfqd0(3vsJw@MWu95P3Mba0ieL&VRfEOg2Af z%T7}!czt z=7;Q6g&+bLa}&`v1y*9>U`hAowX!^~=yQ(@$AnEoHP|q$@iH4Jox_7DVM3|)IV2;A z_&Y#1i}01!d*tp}PW9XM;OERYm?|87dey9~4rQ-{cu)Au_|@w+2Kv1fkFL)P4wu?9 zaVz&=boIr$ru*Z*>gsdP>AI!{$=DztciQxe)|x9oOjPuI7~Q73`C}rg&6qgdu_ zxjbjSFNXm<_VZAl5uV<{VG;c=P!SE}LR<;%y@NABSA`=h_6oP`f#%7V)eW^A`oXme z)c8;GB0fFk!Lh<20MH8IgvchCst8X9WSDiVJs8}p3b;h6XHXSl{OO%-6CZC{1*77S zu_O3vsKig(?mCXkOKyAh|7f@1Cl6&=Z}PA5%zP&HK}UP88?7aN6yb`Hd*7{{HO%ix zJl~=MeQ}Bp!IUjaG52Nss@5Iw`MVwbWcB3h+qaa=)8=bhdO#ufQ#>^rcy2+lYl1m9 z72bVjqsoUCHT6MXq^V!0m}u)rIFz?l3l0&ow78qh&y+DwctH_Dtc1&N(CnSGb5LB1 z<^xhVYI}uWiE0ZNjGV9;Spf^})8 z$G~aVM~1w^6USLVj;j0p5I^>C*m&N4nT+Nrxn{M(){rF*`e82aT%UgwJ};>5D$L|H z_PgO#9VsR%Y=`xisEB)|EH*h6{yanUGepcd*p98&g$OBnj_~?4qR5v2 z#xjPwefs+>S1Nb#CUk~~YvT~Jj`#v^TQyn<^YUVn(B z2d=?A$hcdEK%Ly_mVszyk9lok%g(4~imGygGFUj}`|NAqU6|8Wt7d6(=R}t^OQj>Y z3x~y3>hPymkJ3frdxlv{`54z_zq> zGifU=b%-vT?l3qZJJrA2eK-0nX#<-2sZVgk*VbcFN-WrMYRhxEfgCYecL{e#qcC~> z$!vd*({SolF2qiiDrn2qhgYbpRhP$t7h z_e1(8Z_AE9CdxN7BM3*C<3o+(o#r<@Gl^!lllh|ykQa4#G!C~lKkF#u*j&jLcDj5b zNRxZ$pOr>796#2@w3+%=?ek$LK0A9I;e-td+^e>$&=a6XtmZ+T1GQn`5UV7D+Dd)& zQs_5dqU8F_CoVc~>hnXaR>LnB;Hl)s8T0GeDeJDShnHeaOz1{>>?@BQwNqYT5ecM3 zSsa+tL~4vJ$l0<*O4=mJcB_9T*l4AAY3B*6fS;m>Gup=?ox(yq&yd-z@jCNO--ch` z8t>2ac*__^>Xuo{%^uH>UB2EGkWDRr7)%TZAMaM=HL|OD0bRlSqNlp=NMpioOyAC< zXx}u67=0BQa{53x0gxEe1+)fExNLWMDh3HX{#Jk8RQ;jCd|K(<(q>#d&rPIyd3ngq zsgfPLqTvgD(g~u0(0x;^p&|vUwT!rqH-o z=CsjsK&TO+KttRo$qbHU*rhlFk>n$VrzlJYoSwob0*x<}Uw6_Ck!iacY?S|?dVd?d zxnve)8oWs>#r}x9Ha0TwqG#ai9*~fQZW(jb^1bGrTZOE(VrvOT2p2hPAMWeUyw?rg zqZ@T2Gug;pL=4zjbqd1fdDTSp7toE#115ik?V$z$p zF9nZ!I$p5FIav}0Clv&g%fGkaAjqkugUc(fz0&W47}23Wz>VR(MM28g;M?O0>TaVy zZH`&L1Ijqo%K2{VHa`#FHVHSqK%xVuFtZp~U@yxqe(Kgf98Y0FZ0Z-9VSwU!m{nBn=_ zfD8earDbffYk6FXJ9$3Q9S|&la-=Di-9bk2+4G$>qY(#%oaY|5-B)9qbtKFp^k+}? zWi;Ths}5be41&oYHluvge1Z8Tw_>spw``$EtVj+@V*NATII_)Q%VD;^n_6WKxtBLs zoM}BKWK{^QEagQNhDWwf(e&x5PzA+mKVQH(m-lRDy|B~|+ugeMUAvj31)Uq#!^xnx z$BR4N^132Zv(nD+0m7{AOO)a-H6QI0i3mi0?|)t0Rl3xXjj6jrQ}4#m$z9)dyV9uE zjE)lFGWQy9#UyS$%Wf2lFbXCI<|?sVz@?<*+lz;COQ>#S-WXjxI*Cd{G~3Z)&baHg zJ+a|lC8LmfM7htikquYTBjGCM>6#t#=;z*=a!v6kn&tc-l9;m{EzYY|8Q@%*IMS$y zLuF`L53*?I2$2crR*Jp^S=b~=xV7wTO^=nSBuS{esSp-#BwiuMx@>Rb%0y6ToM60s zAiZMsPQb6<$MDPYa?#5@YUIvRP(JO);9VB=f@{=7nf^CT)b0^pZ2v2p~WONTpXQx;-@c&~ArnWvp#cRmk)hV+gMlFXH^S;ta&}Z)}bRPYAe}s3cPl z;Leh1_|XJ3puInk%~ z#Svg)WUL{B9pmm6vd5i2iX=CPi`h!gcRu1-ftKV>|A_H778hx)nJ}?FZpOt@?OU%f z9*>NFwpmAmC}H_fosKRy+OdR`9sFuB-ML%$|CVB>|E7aozZP@CQ;!eu*pUWj`-nch?PUMKZfrBndI}I zt2es(@?4!NT8frzOBSOdz2VYVQCYMS=E&>zJ%g(jp5{EH4OXLjqal!JfCUq%y+O zC1wWmkDi!#kwkvnNF>W}r9Q=}j^&IZOWR_qtoiKzoY??c))%bE>(|=6$}2!mQS*XZ zX?%eNJ)>zmLBbX<#N~%?XfbtEhGh4mk@c1#WpP|ZJc<~h2CDrD&P%sl{qv$VUo35H z?Qivo?KEFXVYRlEvD3YCu&G}KFDNEHZwPY_H8+uZl>5YvFV2B6sH^KRT~ypf=Qe2Y z%@%Wrl`GcMjCB(0w%a*WM`9R0;vogguY1;SF0zd!1CY3_(^ltY8J1&Re9?uz@e&dg z>*GG<+chrL^pG;HndG{b%a763W@f5F6WvJPK@(O=%hI*N4luR)Zcm5&)-Jo9mA=o) z`RQ(^ty(Zw^1dQZKe@dEKth)9AMZ{_eQZ${a>GA$ZOiOu9y3_zjUB=YfI>?c&P>?-42WJwE*=#y*(W$Jc`Eoq*R&?_W3nZ|a~&U- zVvXb8>-1N8hS~ND32k=8GqbfamHSat!W=szLBf?jMdk%_ZmO##BR?o5&gBGcc>B_k zYM#7TXkumS71(+z&8u^ZIqFPG+%*;Re)qxb{<&xN~12)V3RmP3hgjh^-v^omIwzI=sy2h6g{ zC3`2Qoam?Rwh#&-Sop%1mHnjf5~!-2Y#`3K}B z^|s;PZH`xGjoqMKPS>XfrIY ztI!O4{&Np>Gbmto@0obbD+XqK7oC|j0{Tw6T;86r%NIBa-7k}tFXZy`Q#P)g?3S=( zYp!Y8ejZs}|F{CTTbczeol7pOI2&SccVBgMY|=AX3s=*Khd-jn_KPom)`J=($-FKZ z*B^^@%rN>B-^=_C2vRQC{^1h%UX*bk_RVz!L%9RXOV{IzRjpM_O0?nO{o$b#?k{r; z!#@{FFJlyIrkVG_q3qc8w5N`1Jkg&mo0T(6(kFR5lR8+z{GHxJZwpn3Izq;*k_EOE zr^}UQsI(wwLAK`Uqz-SEbg7Y&!b)u)n~L%EJ9`62Z&t`mcV4Bi{zznS8EMo;F#~xV zroZX{Td&LQ3a5z;Hlza;w#!w6$fm4Vyc<_MT#qJ~{!!dIOC;|TVhcP{t;j4tbjxG8 z)*oA;I#XNHlLDI7&V_OgV><0AB4z8THYawjSDz1F$R(1Z6Gq$rpuYp)t|zRjN)&Yt z7ZWV%6c@jhB<+= zGV{wW(`AED22rFwhe0*^_EdOGqWIV?On zO{sA_5yj|L1BwMkgW)hmfWWrWdB%e2+(H`Tkuv(Rk1L+pcjMnoE#v)q7;K+k@b$do z>nqOY-gWKYa<#LMnW{1|8w4CmvePQsZk|Ny`o)S5O+3t!6x{tGTfA0&nfbV>WAB1p zr;?+aIZ5@Uceu50`CjRZUL3vQ<4?q&k z><7lhGt4(XzASOVz{tyRrMGtcC`~y}D88sPycgd2u<@pjq}0)Xc9+PfVxm{tlLYw5@^iu1tOV;`8orTDpAM&69JkpscvucL@{N-x z)(=ZtNN={Sg))V*9Z`BOy6JS^Y%s>}MY5VDJN*QxXN6wz?KD%=O`pO~-+T+Mt*KAS zPugVU5TQoxdw!rHuPqwMH=>MJ#1ibRb(TB1@avri8gY>ET>MWLBp1x91KpxtJ&1nYMY-LCs5f zm8rEAbtWMypX!Wov)DCJkx14ytX9QUkV>&Hlqa~t){>7k>ryF2i?^7Sd6g7Z{X8Cr=4oeSM3DQ1pAJ*?Wkdz4p^JF-As$rGt&k$gt017#Cn$F}8~Ec0ov2 zP{X>Ax}5llf7qy@T(Yr@#tjRZaojz)DXNNBdR=(l&5)jZRVhVFhdZ?2@e->rDkofB zjGOA2cG$8yU0k4zpyy^&%(=@KCj&w+?@QMe7OWYukK6nsp(Vu!#rybPJluOYgQLAo z6G?=mt;IAZrS>nj66D}wF&3$q_aTOn+1&-WXeU_y-Iiwtt0}GIAwGQLmAkgVFW@T2 zxERO@biQ4XL&x?Tk!`GK;}$n?nRDy(rBTdCB67_JNh;~7YNIQwd}pYZKV%YLe)%?= zHd=X0*XfG=?Qqwl?b_rAG`6`WacNO^X8xHPV;m=2=TY_YcUR`NmWd)uOWmen{M0Wo zPm;!yPOIy^ey$xWB_ThfBAdj~7#}*;ZR6aVU}O^*xumuj^nG%Rd05uYGIQNS}fx-06O5t?@k zM4ueb&3DUIihfoLKjyUwC&uZX?a1?q@);(tu=T&uYZmqm_jvHmcU2z0OT}DfWtymx zV^()w;(V!E?m?u`JP&8H61&kn^J2WQFqN0jp=QWW(i`ZJf&b0pnxk!g^$C<$>A4Se zUr46l#p+c$NZA#zVl@uNr9C(gQ_iTs-pl>SSyxYRttFdV(Bo_?@4GVcq@uE}G3EP| z#)-ayiVU2R_(`t{_=5h?biFu# zj*beyS|YhDTe*?H1CVsx;>TsD%dE7j3!uw0rw6Q+A~K$CbQ3WNjH4gfVMd?gJg2L^ z)Lm96SsnR0yeS;gg384`U1iyVyo*$*c&^RNl^d6Ziud%NoG8X^8#aOc~Me0*T(uwEC_sP zZ()S9`J*_EO_;-Ab4=NwHMQ%m36hU5*4IdPcjejDFQu+|GhfiZis)P-q6=cY31q~c zy_AwlaT+Eb2piMg?L%uka*s^VmW_&hz;&LzhjQ4{gqy@jv zyIlP~!$KXsuM4dYhpOuX{0Hp`U%!4XN`oHZ`dtOs&`{vqCE| z!lW@up2oPOI?OUQVOfZ!QiCnge7Ga}+D#4i{pY-U`Y?6f2x|&Z?01suWR)4J*spMt z3z!mP2dZpzCK`05*qBh+dquF}uJZ!DzEYXLL@_lh>8Y-12rD&8(jXwsYnf(IpV=?f zMmna5%p9ZtxrxGdyOyZ*u5PYK6$M)eIwCUr;Zu{YT<=ygL8(`4JHdWhRHI6-=0%Xu zE+DC`*xv~=Gs_^PDm^@#b#WND_Tb&}n<6x-uDA}{6_i@065>HkF_ZHgC(TD`eMWOh zGKhHUbHn&_r`YG#RJ!B)PEFcW6HK;yU-U)S1)GL)uT-|<`jZ6Ek!wU**{*op4arK* zBR+0g%fv-k)Hkhnqi8Y<>AFn@Ymvd2xJQ4T4a1xF3~9v($MX<~_2QrYPz&Que2u&+ z*)DFE`)tgHS;p$)GwwB;r)&FemV)bYuwg#CqrNHm##<-fb7sp*l{C_qMn)z{4x+tC zi`-DUPE$L7+cZ*>ho!1Rgzm|QLlen{xBS{n`Qg&+M)CH%#QH8p1*v|jAI55h9WQ%2lCuQAFgwXiKB`k~?)$JV;z|@> zEH!Zb0sN^a;(Vk=GnV+|cN>GYk-Rr%Ot1Z`<*1o#mV+=JQ5VOcWF(ZQOSI_)9+}%wa1)_)t z*RlIMEDEkrGLCl1a4+LLZHwnFr`tIC@^@!duZ9R-o#5i8bFpI`e;y&a`G!O+Lw2_Q zY5Rv>P;iq#`a)Oob=X=^GMAj2pE*ZUytzv`TKqr~FK|)*?y~;4{|Dh|5m&atqI@Ub z7w?SjB{obXB-UW1wqf3vb_xvbc9G3Aj$gOTwaJTL5MNf|<(-rIv_@Fdbz@V`ix2r5 z7S|x4wPFpozPkfHyg}R;q6_uHee0H@{zlW<@U$T<>{`tTi*kUKj=fC2VVXF^1=Vx9 z+Xr@%lm3jMC8)mfxFhHC1K*N-EO%c?8Js+Yx?$#LmSk-LU!qbz%I4fl_oEQ$Bm6+8 zh6Rf;_^C%>#b?SFN=vg(#hy~oJth6}8T`6R0*C&XSfTvhcpu)rb1)e2R3aSDmJha3ws-WozB}?tAlj9(9yQ(~UGV0G^5MqT zd4$Z%FJdUqPkM0tF&jPcNeI?x2==j(zeyN0O?wF!=xYm=TQUfh8#$e^x6vh^cXQ3B zd6yT&3_iy=i$mo>c2E^L{8X5_MVgjYSF?35~LxKsII2usiSCM8>jb`pJ%* z!FIZXqma1ibKgqaphED=dBetkKQ}+2(c32g$fu z*(ljaa@{3GrRID24zd`8`o42o4yaE(vJCBvU^;ywslZXqk>S0FLN9R4&d~$4ws-e@ zyCAZ>>sd6KOK3~F9?TGQ{9cu5a_pJ0u_+0m3IU%%D(p#|KGp@*;D~4@ck#PhUxSpv z<6BkL31{sbYw@cT%=~y06By08))Qh#T8RA6$;+4N2VM*-6IG!Wnnh{Ew>nvfWL07v zWaRPg!S8%9`dx7y`ve_)$%mWR>`c^t;!)*)u;iaCu2W8Ccz6deuapZ|w6m*W6KAXz z)%W`P{Zzj&=nxJPMmuW>_IWsU=8I4LKo}T*Q>;mP43$+^#dKrM-1NRd8Wg3+yveH-Kbwf`allUYx6=s>2 zI8F)%(BJ-4x0RlDRz$ZrPu~x7XU_g;eq4NB<8n;!AwazsgE)Xt3r13kSLp2G#s|0* z5STIkBEUeN`1TXq%)6I8ux4}#Sql7+^a8q&W-Kw}{^S<~k+6w0j8`LMb*q)y&Q*zV zZ-yx+SCE9`j(o#~m@Y56HK&MDA{*rkcU{xfnw?+ICg*UC&-B$9c-N&%vI?DQERc;S zsb+la;4+BJ#^Sn^Z*E!3^f^OS6<0YhXin4Nn|+#q=82M#1Y3uS z+3r`UUV{=RBplFyPZg^N;bg)`OA)>+yhoa$Tqw8BFz%X*zB74#l&@jtb&9d(?D&qc znR*!KQsX@gW&s`D8}ZvtHXRYNFrzL?i6{VDAI|{?lHn|9hmdEgbAxf&<%+6_w}>=F z6THfqPN!@I7uWkP$k|<|;79mYFv3In{;0=%TulkSDPms=(hUpRxr)>xajL-| z8jd%Vcekx(V`(qyS!n1~=i7RF-T~x1u{%7<`ikSXPTP;um0MR9LSV-uiyC%|uESVZ zwzBqDH5RBZRAua+RxHv&Ee1C;09t-@fAFcXv zz#fv{vK2}ItWfMHDq!-KCe0(XIg=9t+GPplW2ozkaQqs4&he3JaoZ8??n%_ zCVL-ye;zdkRrLlH0UsR|iH2LZ6hV)LTQ^^iU{S9_qKjz(4WT8~`y>2tFR|4<`Kp$O z7WhP^Zx7pG7xa6fY458QnF$8STqLufpbuPb;gFn>N(vU{$m7^B+(%PG>X21$p;$SvOOJza;4QYrGWcV9n0(i9+4k)Smd=OUG! z-HuBu4Rz7gHM#P09pLU-ejt!+Jj{_$sa$IA;!>Slz5B6?`2&g^rLNEsl%4fhIulX# z(YKwtSt(SvF1ge+SP4SHN1qg_GNPsIyagG)D27=vI$f@QN?-n3RUv{NTis5q$d$u3 zKr=tsxnP*u(l2zrs05p_f8glhV5#P9dYIiK){}*b&-0mLvIIxW8;1KzedTtx0WIgj zbEwCLbeZ7g$_h%v%=;sk$USpM=H9a07`DB-SrtN(9Qw|1fn+LSo<#c~F7PiZ(7o)7 z+;}>F=tyz&SCW#>*QlxO77<+_ za7E3Bm)_?v@P1ud@w{pD&BfZ)sV~Qdz1kg6aa33(9AGeRz;Y}^p059hv8Bc)mw(N? zz$#cS-+-0MRtr)SXRLuZ5cPemwwEQ+L|))WUWf44S&^5AMOp^cd+Y>y%K_?Nsh*G? zkp{AKJOGym&d}vDOg)i;*+xx8>Z7X3&yvR_oxZ!zE%|#JsIRCI^R&VqU=S47k?S{U z1V7VQr5tB!?{$@qR3fN;m@+S86}0SCyKR8R%iGtrA5*HL5+1+fJVm4!c2)wjk$h(iOvJtoe%DACo!#p()&*|(JBG)B6K zqEu>dhqb%&@pcUTK4_PoJ=H?&pC+u|)k;?(;Vr3E#g-KFU=DtMFWxSx`!dD7~-|-O%@%prN@P7cPKv%z^4zUC` z)=|Y}zRu`s38*#B^CF3m0IeJ(1dYJ}nFd*a3VvRkkBD@yw8yT!#4&kVvMub=T9$*# zzeh4WfkR`;n3Ly6pO`Tzv9N2fyTJJukbfauFT^cCT6lTv;w5CgA^?BcKYKj?0CXDg z@v)Dxa+gHT{3Ut&MHu%y6H&U3{`!4FC=%Woq+=9OF394Aept~IebrO|0sPKQaUyTF z>k;iG77v_|aB{pV=eFiJ&U#m2aE%--upGC@fk!2qpaY8KygOkf+Xjx_8?$gsrqX+X zYv;t7P^l*6JF-h)5KcSvKOaRo%=)@Ye3lQra$O3dkOS7f1Bub2)F!-L2q0zu=CrElL^Pa`ISX&$Mi!y|9G zvb>N>Y>E!$Xifnqu>pE$zTaqa1hZ{Oh81EUnzqwkMJtk$S)D`qh`? zRf;u@MgSQ6sTU$Z+3pAb0IH;70_63o@t>V=dNAXzYnASKNuQ*j2~|hI-v@wmpATDg z>C8sIHT)0YjbF!iRyr4jG!@o0TZ=C}ZEd1~WMqXAq z9*1zphb@UCfq@HSCbfTJZwYu~PWVOt00}*%^_|YQt1vghCR1@`Z*HO}EoA8;hT3Kz zjiuxyWLLdThqR0#&29BQYwPa_u~m}0Qd(`_@IF-io4yYCd&gf8bxkkFz8aG2OVMnu z7UDJ%TC5t6n+nAn$q9{%l=BEcNGpdwDMEg+_=o!~{2_zk--wTaJR!5h!oqW;FuS-i z8`WIKr_Y;XbJ|RU-n;LJpAoe?T}o{SL7ds8n-g_(8C8sd0c8Y&Hh_L+Jmg?-E5-af z@j~^kJpB{IacYtU5=j8O`(iYG12XSFCP*NHIqzNHSD{Xup;z8|>a^c&`yO2LOnrKb z6^Hj?lJ{NOezvyf#JUf{FNi)Vu+=q<2g8>VSm?%4c`d@Nn1{+z?l#;Qo!LT=RTYZ2 z%D@`*ui2JBbzc)rZbz3UnWi}RSGb=Q`i0@|6=_~7(Z99bO|&t_;`wB6l^qu{l3N)} zXXYc3^sj>aGil&EFO2>?i{Yiz7g`Ck(yE{t^44^AT8y}ReR1#DiE*F-z zUZ^^9i*E0?+;>vW9xo4wgN3ZL`_=EK-Q;*T?HpYA-te&7{{R&10Ayn;y6+!e{MYO6 zg`7Ttps?c_MTujMdAE+gBEM*+jCkHjBqV{L!Oyb2)@$|eNwU>6KLKgl)}d$F9^{i@|O`WWMWz^rhS8Q91lOw6v2|N-HC4EOc zabGU3N`U`(@A$EIx&zcAG1b_e{UdQ*ayrwQaS^YJ79cI8E14B8h9#i-k05G z{{YDSZ^J$nuCod`t|lvyU8MKD?46qRw^Q=FwmGi%z#k6$LGd3{yV86*_L1CK+BM@F z$sROhS4hgKNaQ=R;PNq!^}zoC76pAT`!3xJFC6O8q4}lIE_o-QS?!6(=U<%gwp&Xd zn$f~nSB#yR`==0N>tOQwxSAgnR+m1N_(kx8#Bq2Xd^*|}hP6BEJB!ICyu6u|l)0G7 zfHAX?+sh0TbASgPmGz{54E!GyKk$ynXW@aZy`S13wJ=+ofLTacnlldn055RPae_N` zu6Nb%NJPT{Z1Zwqz_feEav{ zV60M0q*pMt+=KZn{cf;H_UQ_$U` zkHoOwUQK2d1QznaBm*#Q3PQ?dXJEyK@4J9;Uu%3zHn)BO@lwNxqttBm85ca1NfJUc z^at~=lm7r|i;uV4d_TJb3c6L&4l&CmqYQpVy^by6JQf>;s!p7K8C_o2Uk-`et=j5( z93h%wt7jDJPHm~VMoXhg7g~3}^{cqRi7tm0N^nL1B_!GD^4S!$%*G%NW&+q&xc9wwEqAj7VbI4^4zTQ2}v%p?f|RC za-8EJt7L*n!TQ&G;tvsPT1KL8^c@xTD?N1=Rt6EZA-K30x{#&yY`A!J{VYuCrdFn{7pVc0; zY@Vn8*5dv$YAI>qn6%4g!M&VvcxH|-n36xYlkL8N-Iej&`)TD`rL zM=NO&l0Z1x(r`Yx8Lwf9OH4o+KHmQT*1S(xFMX_PS43{ai*A93Pbo1?k=O0#-41%VIz$m9HQmP;Nf~#Ves^W z@QcFvZ=3!R=ok!eNw>~%+Pq`-O8udorc_4d)bm_GVDlxW?>v*H=KV7$of}xOe|~P5eK4g@I1OW%GgEq z8OPpfY2Q!j8U7#eCZVtVD)A47ubO>3{t}yesA7;VC5{=Q5iFn+$ZTY19A>^r_;ui$ zuZI5s6TDyG-Aek?Qn<6z^wQJWDw|(MY8~C$d)!0d>YLP*A zc`EdIq@BraxcNqS0B{EbrDcenDN3KSmF)ijQ@aO;#nGXN`ogqY(P;J4RM(QR!hXcv8vOqKp_j|?yhNUwJVPMze|bMcU$4Ik=D*YaANWD_3v8Xb0}eJ%~Xx~zs)n7ImxQ#7&;QAB%B@9{VnEsmxJf{-)RNxqfys& zadh5viF*>;!6T5UWJXA$Nd81sDhOUO2JDmBTlin%FNkhrlI&e2)R8-o_U35Jh=ii7 zhEn(~fH#C01E9`&KNH*QelO52JUOLJCWi+$pxF?u1cV1M+h72pknJ)I5tD)l&2ygx z{{UzM@ddQc0bgrpPw`E<$zea3QaJ8pxA}a?jBH1kLNI9hk`6dL`*F|k={UllD(U(8 zpC40&GYHm(Dm7x}-@W}?U%2Dp@Q>{iJa%?^{+VfSWp!&4PjXf(=?w90QzYc~h(=@o{ zw45sg6p`FS%E~j1%6S(PVIzXgX>>F z{>*6ed{vGjGaG4_9%7zD#xAV4DfVDUKGpO5*jc1e#u$&oHShlbvkl#ypT(%=zM3c@ z8b$bLmPL&+CADC?n1PeD;AfmyUj|K6&T#X8%z9i+f9%Xh_+|Ms_s3iCzkxK(VKluz z!+sf6E9NKKSs2D$N1eWnem@%SRv1iHom(6ld@~ouz`|#KTAI0}eEX|j3{h9$m&KQ=*PT0+T zbTB!`D&|x9x%|(>RPfIH%f8XH@H*yQH%!v4FBnXp@h(79$}r4@ z1MN~qNXQ^^J*)C_<7RHY7yLBVCjgtM-Zf*_EoU5q^h*6b_`&h3;h&2>7vFeaP1a+U z?Vrq!Ypa_`w}n?MfF!4}!tq~;y0^q175KBlejM=Dy)+MDphX$7jyrXRNZ8wmPWJ~c zb`m-7&sz7h9O+>AI<8lZx~)>9alXFl<-eY0q zjD7evU}K@=lU{T1w)Sm9;ogUNbG@DIj78)pb2JQ4l219~uW^H3f#JKQi1?#OTaA*< zC5EXKa&+4yO#`;$%7ShC5theV`kd?c_$XIPv-{5jEQ_OwsX_k$R%WHtvzru^wrz=( zL6J)lvyww+DoAXQIXLz;Udqc=xwc=iTePu8N@XV7gOb=IJ$d{_DoHg5n$*cVB6*vO z4Z#B(Z6!xe0pq71jbhBwLQqccZdn!8N|BUdl|2sxbgz$-K87d%*ZHT%)3&AKeOfpp zdx<2uj?&p9AhXMriZlS8tmGgZaz{1k9wYsfe`tRT!@l>y)89w8b3T=IZ+qqEoHFl@ zO~auIjm|(PsqyQ}yM@;7H8_|p!fGFBjr*jrOeKxE$jZ8c0rc-*r5^^qJbu-m6lIEC zFF??>EiB`HseNZQooYuPa%kmaGh-()M9ZG6wfgdPc~sz&Tf4h|!28e1@$Z&WSGAAK z%l!*l@YtLF5*;r807;QUVQ=i`wL6AzKYEBcJurFgUL&Vi%X_EEE0V2qb2N&;@ZqFj zGCF6BX1{yCYhM>yczgDJ@g|SsyX`48{bt1=(=_-CZ=UPUjkiS{h2`?&0JBO!3I=QN z`%Skpcxz15Ac%!F;GA#(V$GKHJoo0jJnFZ!r52Ty```SJMRC9jn(h0>5LBpZG>5U;q+n$>rQ~K+32{#~t|TUSXix$9>|d zBaytNn&-)qRsaN9B-&q-fE8OjoPRp>%g+zmY0x|qT*`0bf@`9P(7n3Al9WYfQdr9B z1K@B01Fdt)Q!NNzPY5++|8e+{{Rl{%)be|EpFetGH5n&=NZ~$fG_Eq z;rt={2#*r@eeU$n7u>5scWtM~1hz3EkW5JPJ;KNp$}J;XI!ueZ6a_xQhA#EEte0vbm2etGzi=S2!hqgprZk74Fif3Bo+xkBOEy z5k|E|N!so7^|6=WZ`mUE;b)2T`@I+9UZvTC{{Sr;Gc3R0pJGOiYp)nP19Nq1<||P&VeS0V*p*r}Y#_ zeVXFrNe#YysHM0K96Qg=<)blF+<9%FWc8}6b7crXZmpCKFlUToKkzCgM-LrhmC5?n zHg{Dd+J0Z~50QR8ODtYCvv?+C-=#(}y?B~Yo^zV-JU{zk{6z5FRuJj>Hi0YYntisL zZ8h9iGdjg<8pe!3odIkval(R673Y5+&w&h%bOVO{77`G%c%js zaLMIF*H(K#{o3wtn{9lL;9uG%F9rAy!u}A0Pt=6I9`Ou1%>Mvny@KA#@;LQq^+tA# zz-|rkRW@k;a>6a!Ex7*dcX}V~Rp5;eO|aAK{0lr1SzKSs1>`d6pKG&wx4UJ&v}vrR zU+p#5iLQ2nGnCVUm~Y#s^l zuZeVT7wMx+wAF0&4N}BiUYPG%RoL=d-bRuj5i6^JNm*AU@K1;M=k~JrwebVOI4zog##$@G;suGPbVQ{ub%+?y9!n2{i8|g(_Wv}hIHt{3NA5MU2Ly@ z{{XL%V{s&w##r)5=9?@~O1ox6k$5{D{bNEm*k&l0Ru?qy2H)Y%v(a0V4uxxYd zio=@ByCURT9_!0X3*iQYZ*J31OZ`#?w6?c)oh_x3N0Kl$T04fg023b4t^;vjcgOG_ z_Dj|8lJigan|ThGt!vAs=}B!g8HO8pZN%ScH&&MqADbtiELRfFk@+tVl#=8rG~e6b z#cO$af8q}ZX}aCb!)gLIy42-~Vz<1!H&I(`OEyPKZb7Q?{0M~Uz4`~j%lYQ7zbtshEk^mSP(%30tmqXiv1PVf8d~6-hI|uc z1*^*&*h{EtGQIR6ErbE4D+I|jz~rl$GNTKU39o=P-w^BmDb{ZMN8)`#ZC6&*86>;C zxZCGkN|8qhc{T-I+apj;GI5qH#IfSV&MsGjy8OS)yg$7cDZ`cer}=IDY1<*XV0G*1 zP$PPEsmn13K9vIC9(W|z3pQJmW4P->sC0G0w&Axp{#1%^IsAo4qw?=c#*$|;9iw$% zO0pGT!>W}%N7JvpbsCPdXL%0Eb)A1vm@<@GlNwoM5OUG^&e`02=&}TU9n#rSB%c<@E00gxu z?|dnBV=mJAb^Nd^H~P(h0~!)f+(p1qzyM%$ucST`==v{-{x4|06W3aF)BHK;yXuWVI9nJ05A?5XE_|6xXC;W0fEJR zGw|-_O*i8ofbU}mb84ajK+Y0b?PPzT{oYCKUriU!m8ia&ea!Ldu1v|xz2DxC)3f^n zd?K_G#irbNmevt7zdF6e$OVo#Hy&xn8OD0^-m+i*3H9*mb&XEF;!v=!l!5M{kcCok zH#NB=bH_XmI#*TWZxQPHC+)@Hoi;oB(P6K6ed22sxs&Xq+sSwt{Ehxr<~mc91r)clhj%I7rV+rLhS$G;wbX3vd&3DCSj z+jySZ5VGHENqX;dhma_F;o+run2BHU?~(v-o= znE}CnRvwt-*XWn+C;LYHFV%h$>;C`>{vBLjc#?fq+ACT0SzXPYy|fY}k}#g&`L{AM zt`w+cEOO0`zXkYa6}k9j;oJ2R6KiJ@zUIoRtCTDk7#w4Ed9O1#bt*zGYSw+N(RAo~ z88oDm=RZGmBxvjss|qRET@7& zBfr+E**O;*l;nS#D99W)BOvwXr8QK7J^uieTG$UcsCJy< zwBJH5jTrSkR{=%+;-V_5sJoabRp2O4`#>Fl7!?GUx6F4v)yos>Jcs$UGy+kMakm%% zb>^WXqsB1|nKfICLp1YWz=1oBG+sZLCDA>mgpzwJWy{Kq9=9jI> z;amMScs1mjD^=4b0Vg*T`C=G|$sfEkMgat_9Zqm_Uq*h^zX@%A4&HcQL9+51-%HWc zO4M})iaSW|wJ6A$r*$sP=Ui`*oU^b3FaaFryAOe@UX5C`_JZbqmrM1(rOry2s&s2p zqd$tHYCk1+{{REI@`GC1w$?WAwm2WVUZ?Oh(Q9jx76^x~BVWU{bsEou{9EDuD^J%o z?GsVbtTj0qEN8y9dqHwlJi=m0g38Djo%=Q(fr;X<6c9=4lU{n0XJomfyCXzJ8)8*a z*9roR{uBTkob){^*tC?}i>AWH(s@b=`Iv>=NF7Rn!96lS>t5sgI{XU0{h_`lTKqb) z)!@*KwilX=*R$KT*4QjeA=M&DBHbf->$h_S7&*@s6(pj$q&{VBeNPCEH;~~KoSuO} zCm)3|qaf{IRP@319jp2o_-X$D1nvEw{vR}#-XylW@$KAp@0IZgfwcbsnlPspP{$l3 zs{+XE8b!`G5CwVnkAL8s{{RHMQF9)l;QeD)@NAcI%_G~|yf?NaGc$#T3zcIECU&Ai z%8G*0QFbp6R*hvYNp0o*KfwGa2zH$3>qx3e9RC3I>-S&4AMi{sgc@C> zSN{MIJWH(js^?)31&pht>BX5y%w`PTkfiax;QBB%`H}lz_-jJ&m+dd{1H+yd)9tja zD^1iap|!KKiD0*eGo@HLRE+>t1=*P3a?DRsbG&P*NhG&2r-`LfP88kR*UNiET@-6(*^y<+3X_^6cWoM?h%;-Nb4DXcBXK-Cq21- z4p^*AF!B3O?(WZPA<1fHn6-wc?W1k^Bjeq{_LN{j8Q|m)dUWeg3Of2%(SI6#5dQ$e zJ*jJ6EAWw?^Gdb5l1+BfQy<#AJ#}@PffVYyb87pT!8imIDmr1S=Z&OcNFa>lbRx3G zVW&c)rz0%aDzBH~B|{Ll+4O#BVHwG)>5_c_t6X#JI@NXS*NWni<}v$m$jSQCz{fo) zquA7d_di+??{k{-Ue@hyBm416t}-$=BL*2A#z!N#u9klnps~Bs^xG3{XL}XjpA*hz z-E(wMiGgGre4Ut$djfKET#U@bxjpi8_}6E1pjum5eWLF|*R2>wxK9+3&uh2=^SLmc ziJreW!02;WvszMY?0yH?O%|u(9YR1ACuk!<&PY&6a4~|U@CZFY74H`Fy}yn88+N{C z>wSI+GaKyMtA{L4_b|UVu^`u){3q3g_k||1HnDHg?q!5uWxV-dBp9vRB!LyPnFLAz z`|Nx3v*L@7D^}17F6I{#NwtPS#9I!32Ct!|E=;kv{c3smrO%JIr{!bz6ZV1MrGDEV z0dMDV6c?5nyhY2ZpjElHA&EQ%{onzK1Dto~;J@2m{?EgIw9kxDJo%dawCl$LXWleYd*-S~ZKc?1 z`m|b%Z*x3X(~Emin4v{ko9CU!&cNhsjW*{QX2AlpuA9wb;u!6-oTMltiAH~gK{y|; zt$x=^7K~q1eo2g->d7J)fV#Dw+0NrMQba-Txkw+4ZhS5WQuszKqmm&eoi+RyA;#js zNYbEO43o6vbAjJAlcfmSPn_fC>Nq_2t*?WUT4|@m-Wj`%r54(PTyl1U1n3iz2+t{# z&pEFzI)&+}b~{@f@YzGs{JmJDVGNuO_eS zTAIymC6t=wjlno^W2RVnaPA-U)FgZZ&q6Cp;`N-C9yzqQk<jXS=6tt z593xFOX1!vqjVs66pq4mW@gewEPC!|g1J#TTg*&AETCVODlGOk_~1 z3uJa9G;*^R?#43UdR6@!Mb*4Xe=d!nSWBy3EKMwycFHA_$;eX7nc)uLGTa>Et1>q3 zbAy5T*R*^)_~r30;kKV1h2gzU2GS#%X_rmCvx4T^%(;*}t{9@s9gavQLW)T{4Rzoa zAKNF2zb9L7^1g>YE8D6v`m#=aF4|u^CjS7f&(QCObJ*)%2=F?5WAM+D;!gw5WQi=& z$9XNxT6!M9rZ7%wufuN{YM&B56-n_ERq%$Lp~-g_+AP-A;jUx*G=W4$Mw%E;oacLl0lq+| zIV24`e-5UhpxNrb+A>>7JgaQTIzH_36c9@eqzv)ezhlUrNl!1Wio&-}5>A|@Cwy>+?y^ZQPGD){~4Zyf8t5d1-*jaNsrNn*6T z)2?B-@-*9X=5CS(beI-rV~Ek&wuRbuAwLyeT3y)BEuHPW_Ory_yn^N#E#^YNFx?!g zj-;ssWDJ4`;EMaB_J{bD@ps{ejW0Y2Zz|~tZsL6}S+qW6xB7FY1c+u1fHSixAO=>9 z?Kr_d73+G|x8i+T<6ZF

wI~H+ww$yU7f*%jF`*o80omBC3LW0biJBxJ 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%0Ajz4<8%n#*Wf8#It6?Jd= z;@_q#&@R8!U4PeQ{$jlU0LDM^D(c_$#s2`MU2ow_(8me>^H0@&XC~xy;;WUaYe}3xHFuL z4|?LR_?dve%8`FzK7?egz{!k7-2?OE@^-bi5oln-hce(VGb{V;W;E_(=<#Je_eQAfNrT!81t9u$f$nt$@^Yve#sOnkzuh3UL zZHoXW%I#YkcieBrvZL{$YkUVrpau#C2Hu^io}WsPeewS9^r-zl zmFq2!oSy8KC06-Z0uKZqLtcmRUym5D>?~}69)o93rFrx{N7R2h^dE~K@;N{Kd0Btx zy6KPbpUCHz@mHh$4>w)`{A%5&3@W1ZKaE?p>MNCOX;a~6ML98Ow@(+y$qN-L!2}`3 zG19+Nyf1BUX=i0?YZ5~YHg@S0ai~;95ex-Uzyr`%<&EF`fUiI6i~j)V75b^+{{Z!N z{{Z#t2mXs%_|7u^4L?`;A4S7^%<2@M?DoOx+|^s2Rxo;eyj7+D0FZlsy;W{HNA(}l zzd4ciaoE(qB{XE&^5pZ5GtFsfNUNd9Q~-9bAbOh3)PLj+AJ_WUrkDQ!ASeF-b$^{_ xQ)1D7COsc)XJ{;P2LruW(_=$uq^Km5+N(eOf`8y=`qjNR{yh`wYdD0D|JisQC8z)Z literal 0 HcmV?d00001 diff --git a/apps/edisonsball/README.md b/apps/edisonsball/README.md new file mode 100644 index 000000000..b8e9ec106 --- /dev/null +++ b/apps/edisonsball/README.md @@ -0,0 +1,5 @@ +The application is based on a technique that Thomas Edison used to prevent falling asleep using a steel ball. Essentially the app starts with a display that shows the current HR value that the watch alarm is set to and this can be adjusted with buttons 1 and 3. This HR settng should be the approximate value you want the alarm to trigger and so you should ideally know both what your HR is currently and what your heartrate normally is during sleep. For your current HR according to the watch, you can simply use the HR monitor available in the Espruino app loader, and then from that you can choose a lower value as the target for the alarm and adjust as required. + +When you press the middle button on the side, the HR monitor starts, the alarm will trigger when your heart rate average drops to the limit you’ve set and has a certain level of steadiness that is determined by a assessing the variance over several readings - the sensitivity of this variance can be adjusted in a variable in the app's code under 'ADVANCED SETTINGS' if needed. The code also has a basic logging function which shows, in a CSV file, when you started the HR tracker and when the alarm was triggered. + +When the alarm triggers, the app resets and you can adjust the HR setting again to a lower value and/or restart. diff --git a/apps/edisonsball/app-icon.js b/apps/edisonsball/app-icon.js new file mode 100644 index 000000000..1125d437a --- /dev/null +++ b/apps/edisonsball/app-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("MDABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAYAA8AAAA8AA8ABgB8AA8ADwD4AA8AH4HwAA8AP8PgAA8Af+fAAA8A//+AAA8B+P8AAA8D4H4AAA8HwDwAAA8PgBgAAA8PAAAAAA8GAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA/////+AA//////AAf/////AAP////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) diff --git a/apps/edisonsball/app-icon.png b/apps/edisonsball/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a6f170aec6fba37a927fb0cff74fd86258e1bbdb GIT binary patch literal 821 zcmV-51Iqk~P)?`<2o|D(2mzJ&AYh7U<11pJNg!wfF`Ask?0ElO?rvxH{(C2SKNuM9 zX20*7z1?{oG->jGlTp;l0tbOS&;@J+Rt4Wp0q=oVz$4%;@LkcnCe{N3zzk4yEHl7$ zU~>cV{$VN52TbE0JX+>}{y?j)v;j}c1R4b{1ABo^U}aEp!Ds!z>oV6p0b1)7ek(8* z2`~h7duZ$io<%f12HL68MmYQrpoe6GK?XPzXvj{a_Dd`y{MbwYI~8Aag7%_2CF&2ohggqQ0oH*5)JIJ2S! z4bci57d~H6TvlHc!k;kPL~XLbXW?@}Q+zq*Sj39o&=e22DfGvH71{K2C zqJ2bBJH%CCvVhss*2?KX%j#ZH(5%Q}U{ILN11D}WT>DqhOX<$Oz?`K<*#Nv0?Rx8$&Vg3U34-hBGYhP1NYW}42rtSi zl?C8bgEGdl12ZGb8QPXf;2yBOVd 0){ + //Bangle.beep(500,4000); + Bangle.buzz(500,1); + alarm_length--; + } + else{ + clearInterval(alarm); + if(trigger_count > 1) + Bangle.setHRMPower(0); + } +} + +function average(nums) { + return nums.reduce((a, b) => (a + b)) / nums.length; +} + +function getStandardDeviation (array) { + const n = array.length; + const mean = array.reduce((a, b) => a + b) / n; + return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n); +} + +function checkHR() { + var bpm = currentBPM, isCurrent = true; + if (bpm===undefined) { + bpm = lastBPM; + isCurrent = false; + } + if (bpm===undefined || bpm < lower_limit_BPM || bpm > upper_limit_BPM) + bpm = "--"; + if (bpm != "--"){ + HR_samples.push(bpm); + // Terminal.println(bpm); + } + + if(HR_samples.length == 5){ + g.clear(); + average_HR = average(HR_samples).toFixed(0); + stdev_HR = getStandardDeviation (HR_samples).toFixed(1); + + g.drawString("HR: " + average_HR, 120,100); + g.drawString("STDEV: " + stdev_HR, 120,160); + HR_samples = []; + if(average_HR < target_heartrate && stdev_HR < deviation_threshold){ + + Bangle.setHRMPower(0); + alarm_length = 4; + setInterval(alarm, 2000); + + trigger_count++; + var csv = [ + 0|getTime(), + launchtime, + average_HR, + stdev_HR + ]; + file.write(csv.join(",")+"\n"); + + heartrate_set = false; + update_target_HR(); + } + } +} + +update_target_HR(); + +setWatch(btn1Pressed, BTN1, {repeat:true}); +setWatch(btn2Pressed, BTN2, {repeat:true}); +setWatch(btn3Pressed, BTN3, {repeat:true}); + +Bangle.on('HRM',function(hrm) { + + if(trigger_count < 2){ + if (firstBPM) + firstBPM=false; // ignore the first one as it's usually rubbish + else { + currentBPM = hrm.bpm; + lastBPM = currentBPM; + } + checkHR(); + } +}); diff --git a/apps/gpsrec/ChangeLog b/apps/gpsrec/ChangeLog index 8c08a4ec5..b06264159 100644 --- a/apps/gpsrec/ChangeLog +++ b/apps/gpsrec/ChangeLog @@ -17,4 +17,4 @@ Ensure default time period is 10 0.14: Now use the openstmap lib for map plotting 0.15: Add plotTrack method to allow current track to be plotted on a map (#395) - Add gpsrec app to Settings menu +0.16: Add gpsrec app to Settings menu diff --git a/apps/hardalarm/ChangeLog b/apps/hardalarm/ChangeLog new file mode 100644 index 000000000..b8b4561b8 --- /dev/null +++ b/apps/hardalarm/ChangeLog @@ -0,0 +1 @@ +0.01: Add a number to match to turn off alarm diff --git a/apps/hardalarm/app-icon.js b/apps/hardalarm/app-icon.js new file mode 100644 index 000000000..6def7b58f --- /dev/null +++ b/apps/hardalarm/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkE5gA/AF9cBZXFQYIOGBIUMC5PATgQJFqAIBgovMBwISDAYQ5HGBAAGFxQ/FgMzmcgJ5BIKgAXFIxYuBgMxCIMjmcQgECiBHLFwITBFYIvBiBLBmQwLqECCYMziICBmIeBD4IwKFwQAIGAJGJRQUhgIbDAocQJBHAgYlCOQIrDAoUwhhGIiZZBX4gFEAgIXICIMwX4gFFC45eDF5RgI4pZHAo0gC43AXoaPJC4J4GRwQAMSA4XfmKuBC6kQRYQXMO4YACXYYADO46nDIwcCiBIFU47XDBwcTkBQFa4/MH4sAkYxBBAoWGC4I/DmQ1BdwJPEC5CQEmAECGQKOKMA0gCYRgELxBICCYUyFQZgCJgIWI5lQYIxjCGYMFC5PFLAiKDFwRGJGATCFLoYuKGAYYGiAuMDAkiCoMiCx6SCAAq7IF48F4tQCoMFqAXP4AQF4B1MSAZXFMwIXPA41VC5wA/ADAA==")) diff --git a/apps/hardalarm/app.js b/apps/hardalarm/app.js new file mode 100644 index 000000000..61467b421 --- /dev/null +++ b/apps/hardalarm/app.js @@ -0,0 +1,112 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +var alarms = require("Storage").readJSON("hardalarm.json",1)||[]; +/*alarms = [ + { on : true, + hr : 6.5, // hours + minutes/60 + msg : "Eat chocolate", + last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day! + rp : true, // repeat + as : false, // auto snooze + } +];*/ + +function formatTime(t) { + var hrs = 0|t; + var mins = Math.round((t-hrs)*60); + return hrs+":"+("0"+mins).substr(-2); +} + +function getCurrentHr() { + var time = new Date(); + return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600); +} + +function showMainMenu() { + const menu = { + '': { 'title': 'Alarms' }, + 'New Alarm': ()=>editAlarm(-1) + }; + alarms.forEach((alarm,idx)=>{ + txt = (alarm.on?"on ":"off ")+formatTime(alarm.hr); + if (alarm.rp) txt += " (repeat)"; + menu[txt] = function() { + editAlarm(idx); + }; + }); + menu['< Back'] = ()=>{load();}; + return E.showMenu(menu); +} + +function editAlarm(alarmIndex) { + var newAlarm = alarmIndex<0; + var hrs = 12; + var mins = 0; + var en = true; + var repeat = true; + var as = false; + if (!newAlarm) { + var a = alarms[alarmIndex]; + hrs = 0|a.hr; + mins = Math.round((a.hr-hrs)*60); + en = a.on; + repeat = a.rp; + as = a.as; + } + const menu = { + '': { 'title': 'Alarms' }, + 'Hours': { + value: hrs, + onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this' + }, + 'Minutes': { + value: mins, + onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this' + }, + 'Enabled': { + value: en, + format: v=>v?"On":"Off", + onchange: v=>en=v + }, + 'Repeat': { + value: en, + format: v=>v?"Yes":"No", + onchange: v=>repeat=v + }, + 'Auto snooze': { + value: as, + format: v=>v?"Yes":"No", + onchange: v=>as=v + } + }; + function getAlarm() { + var hr = hrs+(mins/60); + var day = 0; + // If alarm is for tomorrow not today (eg, in the past), set day + if (hr < getCurrentHr()) + day = (new Date()).getDate(); + // Save alarm + return { + on : en, hr : hr, + last : day, rp : repeat, as: as + }; + } + menu["> Save"] = function() { + if (newAlarm) alarms.push(getAlarm()); + else alarms[alarmIndex] = getAlarm(); + require("Storage").write("hardalarm.json",JSON.stringify(alarms)); + showMainMenu(); + }; + if (!newAlarm) { + menu["> Delete"] = function() { + alarms.splice(alarmIndex,1); + require("Storage").write("hardalarm.json",JSON.stringify(alarms)); + showMainMenu(); + }; + } + menu['< Back'] = showMainMenu; + return E.showMenu(menu); +} + +showMainMenu(); diff --git a/apps/hardalarm/app.png b/apps/hardalarm/app.png new file mode 100644 index 0000000000000000000000000000000000000000..ed830c5eb89e7257dbe8fd82cbed6525455f3096 GIT binary patch literal 1909 zcmV-*2a5QKP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NI-$_J4RA>e5S#3yGXBfWy$jw?{ znYGcqwTrF$Y^}OeKd*Ab(^PF?k7{YG2-t#>7bALSF=f2OI&>-?eMlmIh z;=Ze{|385LdIg8H0(a5to50`*^qvQ>184*O0)kR<5QU#1{3XzCDLs#83GgnMK&q($*e#Xu|YIV|bU)9By8ao_`>3-C)^xvznbf#}Xtzu5xIekdWx zHF^ma_W<`DET@834DuV0s4yLQRLhY#a4 zvcTcWX2!L6GXk9fZLS4{bGqX2Jb(VYOrAVh#*Q7UkeQh&H*VZ;>^#+bfIl7V_+bSe z01qwqDEE3{VWAv8d{`DNSP(I#yNrKFI^ zZof)&P-zKo+_+J;ZQCa4>FL5PGk*MdX>Dzl$B!Q?iS_H(E1tI9*48EjDL+47Dk>^u z-n@BIR8%C%$;r~v(jr`0UIuv-@<~Y{p98oAgP&PO%gV}R<;s;-E&RyK%aer*7pnbU zP*5O?7cY)!&(6-4J9q9Vy)9d|$hdLiBJCVVR#uiYG&D#nxU+$mBdz9wuS47o8}9(z zHk|oTD1_3;)1gC$q_VP7I5fT|Ui9I@*BPjWA-(7Aoo{MtQUl_7U>oZuGRae)cIb;f z{gCGh{0=Mc0x2mea`NO!nKETc#4bCtb?eqhT^IK4+ZT7R+2%+7)~#EzYSk)rV|@jk zncAq!({lyBu@qOYULDhUvJW0S=xBC0d`=L3k-l}b9?LWiU;wfVxDpZ)WbN9uv6`sy zZg-;L^685{d2z50CNd7-GBp1I=FFKR6DLlLv%{OiiH6IkFZ!f!_Q6EP0dNLLixw>k z7i_;9Hf*qQ!qt)BYoEFgj*h@I4&bJxHG4L1=6K>8mz>2`pr9YkLH)P0Ee z9_APm8ivHAjEszM!Qq#kvWF55ViA^KdF@m8A!5K*Fs?u(p0}b(o9;8MMj}}cFUA$n zx=)@ka}7oV0|T;u|9cTT|jSdZ%pjsrzcfaRdV`tIAdVs zxqbU~KWY&f`Xq%jiU>QdVBA7X14%tSJ>i1SU%m(U*49_=nKNhP?p=P%@K4&O?t`1A zaR5^-t*cjqC6EUW9FW1mLD{@{vx-oeADlRG!mSUEeC<>B!BGfI;{aZ>v>F>5-Sp%5 zjCY!wn`Q3Yxw2%*5?Qxyosv0q>Xh8S@ANq!&V*uUpSlmuQXT`yht?S2%9SfBmUlCv z`HvnwlJfF$6$WtjNqnPnF!%1=lbV_uEoyu+Z=g^5W*du`;$KCi$3Ywi;_Yp=7Z^F>FVl|S+i!T9M~?yl#+*?Ddle91-pPlJt^d405miMI0x|S0mJ1PGst;L z%-E(+pYGUsN+l*HsF%wd@Z+7B<99f|imuS(uF zJh0sJoc|y*uMH1r0959;qH5p=RzSRI_|jq~#bku>d?X~smSJqe@SwJ~R@&R!!%Fz& zZBq*P`34kGiYg3w0JPBw@IAqUlmsw`^-C{7@KeCgRcW%BY-;ps`_2+rhUfdhWq{l3 zCct1=A616Lj4BzJWdmOTY|j9OOG6$&4?NccoP8d=RDh|!bNY{lDEbBEAA#=yZlB?h z?`zcVq!tWDH3fiI$ZOYq!(zDoL1LFT)rY(SIk3u?&08kxE^T?>^WV)rm-Zm+VIATi vj-Hk5khjpY>QGR-qkaI%c!l$wG-~NzCgLD&G8fCx00000NkvXXu0mjfrc{7t literal 0 HcmV?d00001 diff --git a/apps/hardalarm/boot.js b/apps/hardalarm/boot.js new file mode 100644 index 000000000..e327317e3 --- /dev/null +++ b/apps/hardalarm/boot.js @@ -0,0 +1,25 @@ +// check for alarms +(function() { + var alarms = require('Storage').readJSON('hardalarm.json',1)||[]; + var time = new Date(); + var active = alarms.filter(a=>a.on); + if (active.length) { + active = active.sort((a,b)=>(a.hr-b.hr)+(a.last-b.last)*24); + var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600); + if (!require('Storage').read("hardalarm.js")) { + console.log("No alarm app!"); + require('Storage').write('hardalarm.json',"[]"); + } else { + var t = 3600000*(active[0].hr-hr); + if (active[0].last == time.getDate() || t < 0) t += 86400000; + if (t<1000) t=1000; + /* execute alarm at the correct time. We avoid execing immediately + since this code will get called AGAIN when hardalarm.js is loaded. alarm.js + will then clearInterval() to get rid of this call so it can proceed + normally. */ + setTimeout(function() { + load("hardalarm.js"); + },t); + } + } +})(); diff --git a/apps/hardalarm/hardalarm.js b/apps/hardalarm/hardalarm.js new file mode 100644 index 000000000..c3623a193 --- /dev/null +++ b/apps/hardalarm/hardalarm.js @@ -0,0 +1,127 @@ +// Chances are boot0.js got run already and scheduled *another* +// 'load(hardalarm.js)' - so let's remove it first! +clearInterval(); + +function formatTime(t) { + var hrs = 0|t; + var mins = Math.round((t-hrs)*60); + return hrs+":"+("0"+mins).substr(-2); +} + +function getCurrentHr() { + var time = new Date(); + return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600); +} + +function getRandomInt(max) { + return Math.floor(Math.random() * Math.floor(max)); +} + +function getRandomFromRange(lowerRangeMin, lowerRangeMax, higherRangeMin, higherRangeMax) { + var lowerRange = lowerRangeMax - lowerRangeMin; + var higherRange = higherRangeMax - higherRangeMin; + var fullRange = lowerRange + higherRange; + var randomNum = getRandomInt(fullRange); + if(randomNum <= (lowerRangeMax - lowerRangeMin)) { + return randomNum + lowerRangeMin; + } else { + return randomNum + (higherRangeMin - lowerRangeMax); + } +} + +function showNumberPicker(currentGuess, randomNum) { + if(currentGuess == randomNum) { + E.showMessage("" + currentGuess + "\n PRESS ENTER", "Get to " + randomNum); + } else { + E.showMessage("" + currentGuess, "Get to " + randomNum); + } +} + +function showPrompt(msg, buzzCount, alarm) { + E.showPrompt(msg,{ + title:"STAY AWAKE!", + buttons : {"Sleep":0,"Stop":1} // default is sleep so it'll come back in 10 mins + }).then(function(choice) { + buzzCount = 0; + if (choice==0) { + if(alarm.ohr===undefined) alarm.ohr = alarm.hr; + alarm.hr += 10/60; // 10 minutes + require("Storage").write("hardalarm.json",JSON.stringify(alarms)); + load(); + } else if(choice==1) { + alarm.last = (new Date()).getDate(); + if (alarm.ohr!==undefined) { + alarm.hr = alarm.ohr; + delete alarm.ohr; + } + if (!alarm.rp) alarm.on = false; + require("Storage").write("hardalarm.json",JSON.stringify(alarms)); + load(); + } + }); +} + +function showAlarm(alarm) { + var msg = formatTime(alarm.hr); + var buzzCount = 20; + if (alarm.msg) + msg += "\n"+alarm.msg; + var okClicked = false; + var currentGuess = 10; + var randomNum = getRandomFromRange(0, 7, 13, 20); + showNumberPicker(currentGuess, randomNum) + setWatch(o => { + if(!okClicked && currentGuess < 20) { + currentGuess = currentGuess + 1; + showNumberPicker(currentGuess, randomNum); + } + }, BTN1, {repeat: true, edge: 'rising'}); + + setWatch(o => { + if(currentGuess == randomNum) { + okClicked = true; + showPrompt(msg, buzzCount, alarm); + } + }, BTN2, {repeat: true, edge: 'rising'}); + + setWatch(o => { + if(!okClicked && currentGuess > 0) { + currentGuess = currentGuess - 1; + showNumberPicker(currentGuess, randomNum); + } + }, BTN3, {repeat: true, edge: 'rising'}); + + function buzz() { + Bangle.buzz(500).then(()=>{ + setTimeout(()=>{ + Bangle.buzz(500).then(function() { + setTimeout(()=>{ + Bangle.buzz(2000).then(function() { + if (buzzCount--) + setTimeout(buzz, 2000); + else if(alarm.as) { // auto-snooze + buzzCount = 20; + setTimeout(buzz, 600000); // 10 minutes + } + }); + },100); + }); + },100); + }); + } + buzz(); +} + +// Check for alarms +var day = (new Date()).getDate(); +var hr = getCurrentHr()+10000; // get current time - 10s in future to ensure we alarm if we've started the app a tad early +var alarms = require("Storage").readJSON("hardalarm.json",1)||[]; +var active = alarms.filter(a=>a.on&&(a.hra.hr-b.hr); + showAlarm(active[0]); +} else { + // otherwise just go back to default app + setTimeout(load, 100); +} diff --git a/apps/hardalarm/widget.js b/apps/hardalarm/widget.js new file mode 100644 index 000000000..677266195 --- /dev/null +++ b/apps/hardalarm/widget.js @@ -0,0 +1,11 @@ +(() => { + var alarms = require('Storage').readJSON('hardalarm.json',1)||[]; + alarms = alarms.filter(alarm=>alarm.on); + if (!alarms.length) return; // no alarms, no widget! + delete alarms; + // add the widget + WIDGETS["alarm"]={area:"tl",width:24,draw:function() { + g.setColor(-1); + g.drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y); + }}; +})() diff --git a/apps/hrrawexp/README.md b/apps/hrrawexp/README.md new file mode 100644 index 000000000..abf2d3d7c --- /dev/null +++ b/apps/hrrawexp/README.md @@ -0,0 +1,13 @@ +Extract hrm raw signal data to CSV file +======================================= + +Simple app that will run the heart rate monitor for a defined period of time you set at the start. + +-The app creates a csv file (it's actually just 1 column) and you can download this via My Apps in the App Loader. + +-The max time value is 60 minutes. + +-The first item holds the data/time when the readings were taken and the file is reset each time the app is run. + +-The hrm sensor is sampled @50Hz and this app does not do any processing on it other than clip overly high/extreme values, the array is written as-is. There is an example Python script that can process this signal, smooth it and also extract a myriad of heart rate variability metrics using the hrvanalysis library: +https://github.com/jabituyaben/BangleJS-HRM-Signal-Processing diff --git a/apps/hrrawexp/app-icon.js b/apps/hrrawexp/app-icon.js new file mode 100644 index 000000000..01718675e --- /dev/null +++ b/apps/hrrawexp/app-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("MDCBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAP4AAD/8A//AAH//D//gAP//n//gAf/////AA/////+AA/////8MA/////4cB/////w+B/////B+B////+D+B////8H+B////4P+B////w/+B/+P/h/+B/+H/D/+A/+D+H/8A//h8P/8A//w4f/8Af/4A//4Af/8B//4AP/+D//wAP//H//wAH/////gAD/////AAB////+AAA////+AAAf///8AAAP///wAAAH///gAAAD///AAAAA//+AAAAAf/4AAAAAH/gAAAAAB+AAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")) diff --git a/apps/hrrawexp/app-icon.png b/apps/hrrawexp/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b120cdb425f7696078594536d3867e97c9784354 GIT binary patch literal 826 zcmV-A1I7G_P)Wqu?F5J1kx$pjY z^WH4(7k;Zb=XbupJLlf}JMU6aQBhG*f{u*ideDy*cpi`AG5n7aoWw!=f_n{eb>cN_ zsO4K)%XJn1;y0YAR*@+Y{B0dbKS-F=q)P04mUF{mW;t# z@$n0BykM@Icq^~`8@OF?!6-)2SBudSdHzUTjD|dcv6AvpT&_jY`KQsHM;^Xvl6=`X zibXN6hw)A$`S}>11M-K^74upw+*db*9Wo>9=79WuJQQ#@Q(S9Wwl2J;Z9lsMO%lwF(TZcarR`KdpWIup`0Q+g%`{=Ok|w%JD!QH*@|x=*6|qQY7xHeu zPtpo_R`%XMiqB2&|DP7FbbQRWm=_zqqz8mPDq_BHqME{&i2{3s z7P*N0unecj^#zj;;;C4k4i7%#Uy%JoRxh?Uk|#7$!vz;OFWd+9SgG+%o)%-Kx7Vqx zF0s%!)+X}3sr)-;Agg>tw1@uYHR*=EH!p35sA@hWb$ zQGQbR{iqZhmEQtIV`o|UhA+Uyw$uc635S}t(Tktump>+aYnU_ip_zaJ>%`OKE@v-ph!=L!j^y6ejMMXv6K7Sbe9D~&mDgXcg07*qoM6N<$ Eg1f_xtN;K2 literal 0 HcmV?d00001 diff --git a/apps/hrrawexp/app.js b/apps/hrrawexp/app.js new file mode 100644 index 000000000..f78d3fbf6 --- /dev/null +++ b/apps/hrrawexp/app.js @@ -0,0 +1,97 @@ +var counter = 1; +var logging_started; +var interval; +var value; + +var file = require("Storage").open("hrm_log.csv", "w"); +file.write(""); + +file = require("Storage").open("hrm_log.csv", "a"); + +function update_timer() { + g.clear(); + g.setColor("#00ff7f"); + g.setFont("6x8", 4); + g.setFontAlign(0, 0); // center font + + g.drawString(counter, 120, 120); + g.setFont("6x8", 2); + g.setFontAlign(-1, -1); + g.drawString("-", 220, 200); + g.drawString("+", 220, 40); + g.drawString("GO", 210, 120); + + g.setColor("#ffffff"); + g.setFontAlign(0, 0); // center font + g.drawString("Timer (minutes)", 120, 90); + + g.setFont("6x8", 4); // bitmap font, 8x magnified + + if (!logging_started) + g.flip(); +} + +function btn1Pressed() { + if (!logging_started) { + if (counter < 60) + counter += 1; + else + counter = 1; + update_timer(); + } +} + +function btn3Pressed() { + if (!logging_started) { + if (counter > 1) + counter -= 1; + else + counter = 60; + update_timer(); + } +} + +function btn2Pressed() { + launchtime = 0 | getTime(); + file.write(launchtime + "," + "\n"); + logging_started = true; + counter = counter * 60; + interval = setInterval(countDown, 1000); + Bangle.setHRMPower(1); +} + +function fmtMSS(e) { + var m = Math.floor(e % 3600 / 60).toString().padStart(2, '0'), + s = Math.floor(e % 60).toString().padStart(2, '0'); + return m + ':' + s; +} + +function countDown() { + g.clear(); + counter--; + if (counter == 0) { + Bangle.setHRMPower(0); + clearInterval(interval); + g.drawString("Finished", g.getWidth() / 2, g.getHeight() / 2); + Bangle.buzz(500, 1); + } + else + g.drawString(fmtMSS(counter), g.getWidth() / 2, g.getHeight() / 2); +} + +update_timer(); + +setWatch(btn1Pressed, BTN1, { repeat: true }); +setWatch(btn2Pressed, BTN2, { repeat: true }); +setWatch(btn3Pressed, BTN3, { repeat: true }); + +Bangle.on('HRM', function (hrm) { + for (let i = 0; i < hrm.raw.length; i++) { + value = hrm.raw[i]; + if (value < -2) + value = -2; + if (value > 6) + value = 6; + file.write(value + "," + "\n"); + } +}); diff --git a/apps/hrrawexp/interface.html b/apps/hrrawexp/interface.html new file mode 100644 index 000000000..703341415 --- /dev/null +++ b/apps/hrrawexp/interface.html @@ -0,0 +1,54 @@ + + + + + +

+ + + + + + + diff --git a/apps/lazyclock/ChangeLog b/apps/lazyclock/ChangeLog new file mode 100644 index 000000000..1fc732a04 --- /dev/null +++ b/apps/lazyclock/ChangeLog @@ -0,0 +1 @@ +0.01: Launch app \ No newline at end of file diff --git a/apps/lazyclock/README.md b/apps/lazyclock/README.md new file mode 100644 index 000000000..5f10b1707 --- /dev/null +++ b/apps/lazyclock/README.md @@ -0,0 +1,26 @@ +# Lazy clock + +This clock gives you the time (roughly). + +* 11:05 becomes 'About eleven' +* 15:34 becomes 'Just gone half three' +* 04:12 becomes 'Around quarter past four' + +Phrases have a 10 min 'resolution': +* 10:00 - 10:09: past the hour, +* 10:10 - 10:19: about quarter past +* 10:20 - 10:29: nearly half past +* 10:30 - 10:39: just gone half past +* 10:40 - 10:49: about quarter to +* 10:50 - 10:59: almost the hour + +Various phrases and combinations for each time chunk are provided. + +## Usage + +* Press BTN-1 to see the actual time and date +* Press BTN-3 to cycle through lazy descriptions + +## Attributions + +Icon from https://icons8.com diff --git a/apps/lazyclock/lazyclock-app.js b/apps/lazyclock/lazyclock-app.js new file mode 100644 index 000000000..cd7edf329 --- /dev/null +++ b/apps/lazyclock/lazyclock-app.js @@ -0,0 +1,236 @@ +let secondInterval; +let showRealTime = false; + +const utils = { + random: function(items) { + return items[~~(items.length * Math.random())]; + }, + + oneIn: function(chance) { + return Math.floor(Math.random() * Math.floor(chance + 1)) === chance; + }, + + hours2Word: function(hours, minutes) { + const numbers = [ + 'twelve', + 'one', + 'two', + 'three', + 'four', + 'five', + 'six', + 'seven', + 'eight', + 'nine', + 'ten', + 'eleven', + 'twelve', + ]; + + let adjustedHours = hours; + + if (minutes > 40) { + adjustedHours += 1; + } + + if (adjustedHours > 12) { + adjustedHours -= 12; + } + + return numbers[adjustedHours]; + }, + + print: function(str) { + let fontSize = 4; + const width = g.getWidth(); + const height = g.getHeight() - 48; + const lines = str.split(`\n`).length; + let totalHeight; + + do { + g.setFont("6x8", fontSize); + totalHeight = g.getFontHeight() * lines; + if (fontSize === 1 || (g.stringWidth(str) < width && totalHeight < height)) { + break; + } + fontSize--; + + } while (true); + + const x = width / 2; + + const y = (g.getHeight() / 2) - (g.getFontHeight() * ((lines - 1) / 2)); + g.drawString(str, x, y < 25 ? 24 : y); + } +}; + +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`], + half: ['Half', 'Half past'], + exactly: ['exactly', 'on the dot', 'o\' clock'], + ish: ['-ish', `\n(ish)`] +}; + +function switchMode() { + showRealTime = !showRealTime; + refreshTime(); +} + +function drawRealTime(date) { + const pad = (number) => `0${number}`.substr(-2); + const hours = pad(date.getHours()); + const minutes = pad(date.getMinutes()); + + g.setFontAlign(0,1); + g.setFont("6x8", 8); + g.drawString(`${hours}:${minutes}`, g.getWidth() / 2, g.getHeight() / 2); + + g.setFont("6x8", 3); + g.setFontAlign(0, -1); + g.drawString(date.toISOString().split('T')[0], g.getWidth() / 2, g.getHeight() / 2); + +} + +function drawDumbTime(time) { + const hours = time.getHours(); + const minutes = time.getMinutes(); + + 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; + } + } + + const hoursAsWord = utils.hours2Word(hours, minutes); + + const formatter = getFormatter(hours, minutes); + + return formatter(hoursAsWord, minutes); + } + + utils.print(formatTime(hours, minutes)); +} + +function cancelTimeout() { + if (secondInterval) { + clearTimeout(secondInterval); + } + + secondInterval = undefined; +} + +function refreshTime() { + cancelTimeout(); + + g.clearRect(0, 24, g.getWidth(), g.getHeight()-24); + g.reset(); + g.setFontAlign(0,0); + + const time = new Date(); + + const method = showRealTime ? drawRealTime : drawDumbTime; + + method(time); + + const secondsTillRefresh = 60 - time.getSeconds(); + + secondInterval = setTimeout(refreshTime, secondsTillRefresh * 1000); +} + + +function startClock() { + const secondsToRefresh = refreshTime(); +} + +function addEvents() { + Bangle.on('lcdPower', (on) => { + cancelTimeout(); + if (on) { + startClock(); + } + }); + + setWatch(switchMode, BTN1, { + repeat: true, + edge: "falling" + }); + + setWatch(Bangle.showLauncher, BTN2, { + repeat: false, + edge: "falling" + }); + + + setWatch(refreshTime, BTN3, { + repeat: true, + edge: "falling" + }); +} + +function init() { + g.clear(); + + startClock(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + addEvents(); +} + + +init(); diff --git a/apps/lazyclock/lazyclock-icon.js b/apps/lazyclock/lazyclock-icon.js new file mode 100644 index 000000000..0ba6eb144 --- /dev/null +++ b/apps/lazyclock/lazyclock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A1iIXXDCwXYF1kBIzEQXlfdC9sNC6oWB6BFVFy5dWIqoXV93u8ArThwXB9wLHhGIAAeAFwwwJCwgABC54uFC5XoHQoXQnBTFCwwkFlwXClAjFFhI7CwYWB8YOFRpYMCkfjFwQPDLpYMHABQwFC6IwETQ4TIGAwXOCQIQDIyIRFGARyRGAQXQRIwXCDoTGGXJAKBeQwA/AH4A6A")) \ No newline at end of file diff --git a/apps/lazyclock/lazyclock.png b/apps/lazyclock/lazyclock.png new file mode 100644 index 0000000000000000000000000000000000000000..4991cd2db72bab73c382acf0217c280e529db8ac GIT binary patch literal 1229 zcmV;;1Ty=HP)%TK~zH71I{1+i!}{xl>; zF_;)55vwuA7*gnknqHV#QY9#VHW(upm_QQ^R83S2K{TirlAvHqC~c_-1(eb*?R2+u zJ}(MnyPe%G?rcrWK6i7zIq!MjIdf*t4CKg>BS(%5kntMm-iKBk1d0{vZ=amEtEs8U z^+VV4LnKX>0rLcQcJ@ZU^GkF*16-p;z?Z8J5b#SVU4Hs%?>s;CvKnCVgDaj=p$?dx ztueA1z=&=Zq&TkWir-^obq!c5uK>RT4scwM)!7^`2Y@!L`9T}j92&3jd|d;&;rV$e zOMxCgbyMNn8qkL7C5ZL;@R98COLr_Xo&nwX+C*ZJw@~vcU;{s6SeLoA^vGy0r1dWp zV43|OcLOfyCi;h7m%5J~pZtwUW;0-!cdnXmL#$_{rtz>pw$dlVTS!&{pzfoxZbz#R zer}k|0tiP@RMvm=pO_!EkOFP43Sm==@H9{gbRq1bH&`z#n+C?mVrr5G(B`TT0lozm zdz25;7rZ}{5artecEmln6XhO zf__GN<^h6=JgnmGOe`c1Fnrr2Wzg9FdSe#Izw#z9^G5qAEE%Ph;{4K7VgjYp)*bB> zl`o8?&F4D3Q;}ihgrlK#2^5wL$xxbav z9w|#Xe4SdZ(g;jQ!)!cJV86=HbBP7d8JNDMH{95}drI}>eMuER?Y?tlgD}@J_7>o= zWPQq}M%KviW^WW`aFE!=3ur6`a7x6k{By&C(@70n#d8MoZF%FpzaO9U%itW(PDh;C zW=CvQU!bViwF?TcLLp?r1Q4lAYJO{^M3lFa)nk8Myk;I?rmA~rYVFZ6*U6~I=E+Uq<8QDjdxlC8j8XoB(MBTELtTr6mxcC2Hh- zQC4}GFpfj?+*z;EX;)?W`G$(Nv;&lH>)Qi7nJ6A8yqQ2@VKSYJh<0~l2KtkgyKKu| z{zv_SlU~IUw@~@^%Zq>~;@gILE@joc4h>oRh84o1gjFaKB@7e;ML-w`0hd8qRrzF~Kk{LBP05(Q8P7LVw3XBzStiEb r&hXCou*9S~?aP!SM~)mh{`dR?AMT?pdR?;}00000NkvXXu0mjf!Xiaf literal 0 HcmV?d00001 diff --git a/apps/multiclock/ChangeLog b/apps/multiclock/ChangeLog index 7b553ae42..ac53da24b 100644 --- a/apps/multiclock/ChangeLog +++ b/apps/multiclock/ChangeLog @@ -5,6 +5,8 @@ 0.05: Add README 0.06: Add txt clock 0.07: Add Time Date clock and fix font sizes +0.08: Add pinned clock face + diff --git a/apps/multiclock/ana.min.js b/apps/multiclock/ana.min.js deleted file mode 100644 index f0c671e97..000000000 --- a/apps/multiclock/ana.min.js +++ /dev/null @@ -1,2 +0,0 @@ -(function(){return function(){function e(a,d,b,c){a*=k;g.fillPoly([120+Math.sin(a)*d,134-Math.cos(a)*d,120+Math.sin(a+h)*c,134-Math.cos(a+h)*c,120+Math.sin(a)*b,134-Math.cos(a)*b,120+Math.sin(a-h)*c,134-Math.cos(a-h)*c])}function l(){g.setColor(0,0,0);e(360*f.getSeconds()/60,-5,90,3);0===f.getSeconds()&&(e(360*(d.getHours()+d.getMinutes()/60)/12,-16,60,7),e(360*d.getMinutes()/60,-16,86,7),d=new Date);g.setColor(1,1,1);e(360*(d.getHours()+d.getMinutes()/60)/12,-16,60,7);e(360*d.getMinutes()/60,-16, - 86,7);g.setColor(0,1,1);f=new Date;e(360*f.getSeconds()/60,-5,90,3);g.setColor(0,0,0);g.fillCircle(120,134,2)}var h=Math.PI/2,k=Math.PI/180,d,f;return{init:function(){f=d=new Date;g.setColor(1,1,1);for(var a=0;60>a;a++){var e=360*a/60,b=e*k,c=120+100*Math.sin(b);b=134-100*Math.cos(b);0==e%90?(g.setColor(0,1,1),g.fillRect(c-6,b-6,c+6,b+6)):0==e%30?(g.setColor(0,1,1),g.fillRect(c-4,b-4,c+4,b+4)):(g.setColor(1,1,1),g.fillRect(c-1,b-1,c+1,b+1))}l()},tick:l}}})(); \ No newline at end of file diff --git a/apps/multiclock/clock.js b/apps/multiclock/clock.js index ed2383b8d..50410f096 100644 --- a/apps/multiclock/clock.js +++ b/apps/multiclock/clock.js @@ -1,6 +1,8 @@ var FACES = []; -var iface = 0; -require("Storage").list(/\.face\.js$/).forEach(face=>FACES.push(eval(require("Storage").read(face)))); +var STOR = require("Storage"); +STOR.list(/\.face\.js$/).forEach(face=>FACES.push(eval(require("Storage").read(face)))); +var lastface = STOR.readJSON("multiclock.json")||{pinned:0}; +var iface = lastface.pinned; var face = FACES[iface](); var intervalRefSec; @@ -25,7 +27,14 @@ function setButtons(){ face = FACES[iface](); startdraw(); } - setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); + function finish(){ + if (lastface.pinned!=iface){ + lastface.pinned=iface; + STOR.write("multiclock.json",lastface); + } + Bangle.showLauncher(); + } + setWatch(finish, BTN2, {repeat:false,edge:"falling"}); setWatch(newFace.bind(null,1), BTN1, {repeat:true,edge:"rising"}); setWatch(newFace.bind(null,-1), BTN3, {repeat:true,edge:"rising"}); } diff --git a/apps/smtswch/README.md b/apps/smtswch/README.md index 3ac6658c9..fbefa3431 100644 --- a/apps/smtswch/README.md +++ b/apps/smtswch/README.md @@ -20,6 +20,7 @@ This app allows you to remotely control devices (or anything else you like!) wit First, you'll need a device that supports BLE. Install EspruinoHub following the directions at [https://github.com/espruino/EspruinoHub](https://github.com/espruino/EspruinoHub) + Install [Node-RED](https://nodered.org/docs/getting-started) ## Example Node-RED flow