diff --git a/apps.json b/apps.json index 9d8ce0930..9849ae958 100644 --- a/apps.json +++ b/apps.json @@ -167,7 +167,7 @@ { "id": "setting", "name": "Settings", - "version": "0.38", + "version": "0.39", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", @@ -4697,6 +4697,8 @@ "tags": "tool,timer", "readme":"README.md", "supports":["BANGLEJS2"], + "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}], + "allow_emulator": true, "storage": [ {"name":"a_speech_timer.app.js","url":"app.js"}, {"name":"a_speech_timer.img","url":"app-icon.js","evaluate":true} @@ -4945,10 +4947,12 @@ "id":"awairmonitor", "name":"Awair Monitor", "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], "allow_emulator": true, - "version":"0.01", + "version":"0.03", "description": "Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awair device.", - "tags": "tool,health", + "type": "clock", + "tags": "clock,tool,health", "readme":"README.md", "supports":["BANGLEJS2"], "storage": [ @@ -5094,6 +5098,21 @@ {"name":"ltherm.img","url":"icon.js","evaluate":true} ] }, + { + "id": "widviztime", + "name": "Widget Autohide Widget", + "shortName": "Viz Time Widget", + "version": "0.01", + "description": "The widgets will be shown for four seconds after the device is unlocked.", + "icon": "eye.png", + "type": "widget", + "tags": "widget", + "readme":"README.md", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widviztime.wid.js","url":"widget.js"} + ] + }, { "id": "supf", "name": "Simple Clock with Date", diff --git a/apps/awairmonitor/ChangeLog b/apps/awairmonitor/ChangeLog index 0cc9a42b0..71d6399c4 100644 --- a/apps/awairmonitor/ChangeLog +++ b/apps/awairmonitor/ChangeLog @@ -1 +1,3 @@ 0.01: Beta version for Bangle 2 paired with Chrome (2021/12/11) +0.02: The app is now a clock, the data is greyed after the connection is lost (2021/12/22) +0.03: Set the Awair's IP directly on the webpage (2021/12/27) diff --git a/apps/awairmonitor/README.md b/apps/awairmonitor/README.md index 69894fea2..f4c7c42c4 100644 --- a/apps/awairmonitor/README.md +++ b/apps/awairmonitor/README.md @@ -5,11 +5,10 @@ Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awai * What you need: * A BangleJS 2 * An Awair device [with local API enabled](https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Local-API-Feature) - * The web app [awair_to_bangle.html](awair_to_bangle.html) that will retrive the data from your Awair device and sent it to your BangleJS 2 through Chrome's Bluetooth LE connection + * The web app [awair_to_bangle.html](awair_to_bangle.html) that will retrieve the data from your Awair device and sent it to your BangleJS 2 through Chrome's Bluetooth LE connection * How to get started - * Open awair_to_bangle.html with a text/code editor and input the IP address of your Awair on top (const awair_ip_1 = "192.168.xx.xx") * Launch the Awair Monitor app on your BangleJS - * Open awair_to_bangle.html on Chrome and click "Connect BangleJS" - it connects to your watch the same way as the Bangle app store + * Open awair_to_bangle.html on Chrome (desktop or Android), input the IP address of your Awair device, and click "Connect BangleJS" - it connects to your watch the same way as the Bangle app store * Once connected to the watch with the app running, the watch app is updated once per second ![](screenshot.png) diff --git a/apps/awairmonitor/app.js b/apps/awairmonitor/app.js index a5a1d1a72..9123a9c2c 100644 --- a/apps/awairmonitor/app.js +++ b/apps/awairmonitor/app.js @@ -30,6 +30,8 @@ var bt_temp_history = new Array(10).fill(0); var internal_last_update = -1; +var display_frozen = false; + function draw() { g.reset().clearRect(0,24,g.getWidth(),g.getHeight()); @@ -47,14 +49,8 @@ function draw() { g.drawString("Humi", 125, 100); g.drawString("Temp", 160, 100); - g.setFont("HaxorNarrow7x17"); - g.drawString(""+bt_current_co2, 18, 110); - g.drawString(""+bt_current_voc, 53, 110); - g.drawString(""+bt_current_pm25, 88, 110); - g.drawString(""+bt_current_humi, 123, 110); - g.drawString(""+bt_current_temp, 158, 110); - if (last_update != bt_last_update) { + display_frozen = false; last_update = bt_last_update; internal_last_update = last_update; if (last_update % 10 == 0) { @@ -65,16 +61,29 @@ function draw() { bt_temp_history.shift(); bt_temp_history.push(bt_current_temp); } } - + if (internal_last_update == -1) { g.drawString("Waiting for connection", 88, 164); - } else if (internal_last_update > last_update + 5) { + } else if ((internal_last_update > last_update + 5) && (internal_last_update < last_update + 60)) { g.drawString("Trying to reconnect since " + (internal_last_update - last_update), 88, 164); + } else if (internal_last_update > last_update + 5) { + display_frozen = true; + g.drawString("Waiting for connection", 88, 164); } + if (display_frozen) { g.setColor("#888"); } + + g.setFont("HaxorNarrow7x17"); + g.drawString(""+bt_current_co2, 18, 110); + g.drawString(""+bt_current_voc, 53, 110); + g.drawString(""+bt_current_pm25, 88, 110); + g.drawString(""+bt_current_humi, 123, 110); + g.drawString(""+bt_current_temp, 158, 110); for (i = 0; i < 10; i++) { - // max height = 32 + if (display_frozen) { g.setColor("#888"); } + + // max height = 32 g.drawLine(10+i*2, 150-(Math.min(Math.max(bt_co2_history[i],400), 1200)-400)/25, 10+i*2, 150); g.drawLine(45+i*2, 150-(Math.min(Math.max(bt_voc_history[i],0), 1440)-0)/45, 45+i*2, 150); g.drawLine(80+i*2, 150-(Math.min(Math.max(bt_pm25_history[i],0), 32)-0)/1, 80+i*2, 150); @@ -91,6 +100,7 @@ function draw() { } // init +Bangle.setUI("clock"); require("FontHaxorNarrow7x17").add(Graphics); g.clear(); Bangle.loadWidgets(); diff --git a/apps/awairmonitor/awair_to_bangle.html b/apps/awairmonitor/awair_to_bangle.html index 2926cca9e..69c52499f 100644 --- a/apps/awairmonitor/awair_to_bangle.html +++ b/apps/awairmonitor/awair_to_bangle.html @@ -7,15 +7,15 @@ // Don't forget to enable the Local API on your Awair before using this // https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Local-API-Feature -const awair_ip_1 = "192.168.2.2"; // <- INPUT YOUR AWAIR IP ADDRESS HERE const awair_name_1 = "Awair"; var bt_connection; var is_connected = false; var reconnect_counter = 5; var reconnect_attempt_counter = 1; +var is_chart_started = false; -window.onload = function() { +function initChart() { var chart_co2; var chart_voc; var chart_pm; @@ -23,6 +23,8 @@ window.onload = function() { var chart_humidity; var dataPoints_1 = []; var posx = 0; + + var awair_ip_1 = document.getElementById('inputawairip').value; $.getJSON("http://"+awair_ip_1+"/air-data/latest", function(data) { $.each(data, function(key, value){ @@ -105,11 +107,12 @@ window.onload = function() { let current_humi = dataPoints_1['humid'][dataPoints_1['humid'].length-1].y; let current_temp = dataPoints_1['temp'][dataPoints_1['temp'].length-1].y; let last_update = dataPoints_1['temp'].length-1; - if (is_connected && bt_connection.isOpen) { + + if (is_connected && bt_connection && bt_connection.isOpen) { bt_connection.write('\x10bt_current_co2='+current_co2+';bt_current_voc='+current_voc+';bt_current_pm25='+current_pm25+';bt_current_humi='+current_humi+';bt_current_temp='+current_temp+';bt_last_update='+last_update+';\n'); console.log("Sent data through Bluetooth"); - } else if (is_connected && !bt_connection.isOpen) { + } else if (is_connected && bt_connection && !bt_connection.isOpen) { console.log("Disconnected - Next attempt to reconnect in " + reconnect_counter); reconnect_counter--; @@ -131,7 +134,6 @@ window.onload = function() { } } - setTimeout(function(){updateChart()}, 1000); }); } @@ -148,10 +150,16 @@ function connectBT() { bt_connection = c; is_connected = true; reconnect_attempt_counter = 1; + if (!is_chart_started) { + initChart(); + is_chart_started = true; + } }); } function disconnectBT() { + console.log("Disconnect Bluetooth button pressed. bt_connection value below.") + console.log(bt_connection); if (is_connected && bt_connection) { bt_connection.close(); is_connected = false; @@ -167,23 +175,21 @@ function disconnectBT() {

How to use -

+

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

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

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

-Step 4: Click "Connect BangleJS" -

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

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

+Step 3: Input your Awair IP address and click the Connect button: + + +

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

+Step 5: Once you are done, click the Disconnect button to properly close the Blutooth connection +

-
- - -
-

@@ -192,4 +198,476 @@ Step 5: Optionally, open the web inspector's console (Right click > Inspector >
- +(buf)); + } + + function str2ab(str) { + var buf = new ArrayBuffer(str.length); + var bufView = new Uint8Array(buf); + for (var i=0, strLen=str.length; i Device UUIDs: ' + device.uuids.join('\n' + ' '.repeat(21))); + device.addEventListener('gattserverdisconnected', function() { + log(1, "Disconnected (gattserverdisconnected)"); + connection.close(); + }); + connection.device = device; + connection.reconnect(callback); + }).catch(function(error) { + log(1, 'ERROR: ' + error); + connection.close(); + }); + + connection.reconnect = function(callback) { + connection.device.gatt.connect().then(function(server) { + log(1, "Connected"); + btServer = server; + return server.getPrimaryService(NORDIC_SERVICE); + }).then(function(service) { + log(2, "Got service"); + btService = service; + return btService.getCharacteristic(NORDIC_RX); + }).then(function (characteristic) { + rxCharacteristic = characteristic; + log(2, "RX characteristic:"+JSON.stringify(rxCharacteristic)); + rxCharacteristic.addEventListener('characteristicvaluechanged', function(event) { + var dataview = event.target.value; + var data = ab2str(dataview.buffer); + if (data.length > chunkSize) { + log(2, "Received packet of length "+data.length+", increasing chunk size"); + chunkSize = data.length; + } + if (puck.flowControl) { + for (var i=0;i pause upload"); + flowControlXOFF = true; + } else if (ch==17) {// XON + log(2,"XON received => resume upload"); + flowControlXOFF = false; + } else + remove = false; + if (remove) { // remove character + data = data.substr(0,i-1)+data.substr(i+1); + i--; + } + } + } + log(3, "Received "+JSON.stringify(data)); + connection.emit('data', data); + }); + return rxCharacteristic.startNotifications(); + }).then(function() { + return btService.getCharacteristic(NORDIC_TX); + }).then(function (characteristic) { + txCharacteristic = characteristic; + log(2, "TX characteristic:"+JSON.stringify(txCharacteristic)); + }).then(function() { + connection.txInProgress = false; + connection.isOpen = true; + connection.isOpening = false; + isBusy = false; + queue = []; + callback(connection); + connection.emit('open'); + // if we had any writes queued, do them now + connection.write(); + }).catch(function(error) { + log(1, 'ERROR: ' + error); + connection.close(); + }); + }; + + return connection; + }; + + // ---------------------------------------------------------- + var connection; + /* convenience function... Write data, call the callback with data: + callbackNewline = false => if no new data received for ~0.2 sec + callbackNewline = true => after a newline */ + function write(data, callback, callbackNewline) { + if (!checkIfSupported()) return; + + let result; + /// If there wasn't a callback function, then promisify + if (typeof callback !== 'function') { + callbackNewline = callback; + + result = new Promise((resolve, reject) => callback = (value, err) => { + if (err) reject(err); + else resolve(value); + }); + } + + if (isBusy) { + log(3, "Busy - adding Puck.write to queue"); + queue.push({type:"write", data:data, callback:callback, callbackNewline:callbackNewline}); + return result; + } + + var cbTimeout; + function onWritten() { + if (callbackNewline) { + connection.cb = function(d) { + var newLineIdx = connection.received.indexOf("\n"); + if (newLineIdx>=0) { + var l = connection.received.substr(0,newLineIdx); + connection.received = connection.received.substr(newLineIdx+1); + connection.cb = undefined; + if (cbTimeout) clearTimeout(cbTimeout); + cbTimeout = undefined; + if (callback) + callback(l); + isBusy = false; + handleQueue(); + } + }; + } + // wait for any received data if we have a callback... + var maxTime = 300; // 30 sec - Max time we wait in total, even if getting data + var dataWaitTime = callbackNewline ? 100/*10 sec if waiting for newline*/ : 3/*300ms*/; + var maxDataTime = dataWaitTime; // max time we wait after having received data + cbTimeout = setTimeout(function timeout() { + cbTimeout = undefined; + if (maxTime) maxTime--; + if (maxDataTime) maxDataTime--; + if (connection.hadData) maxDataTime=dataWaitTime; + if (maxDataTime && maxTime) { + cbTimeout = setTimeout(timeout, 100); + } else { + connection.cb = undefined; + if (callback) + callback(connection.received); + isBusy = false; + handleQueue(); + connection.received = ""; + } + connection.hadData = false; + }, 100); + } + + if (connection && (connection.isOpen || connection.isOpening)) { + if (!connection.txInProgress) connection.received = ""; + isBusy = true; + connection.write(data, onWritten); + return result + } + + connection = connect(function(puck) { + if (!puck) { + connection = undefined; + if (callback) callback(null); + return; + } + connection.received = ""; + connection.on('data', function(d) { + connection.received += d; + connection.hadData = true; + if (connection.cb) connection.cb(d); + }); + connection.on('close', function(d) { + connection = undefined; + }); + isBusy = true; + connection.write(data, onWritten); + }); + + return result + } + + // ---------------------------------------------------------- + + var puck = { + /// Are we writing debug information? 0 is no, 1 is some, 2 is more, 3 is all. + debug : 1, + /// Should we use flow control? Default is true + flowControl : true, + /// Used internally to write log information - you can replace this with your own function + log : function(level, s) { if (level <= this.debug) console.log(" "+s)}, + /// Called with the current send progress or undefined when done - you can replace this with your own function + writeProgress : function (charsSent, charsTotal) { + //console.log(charsSent + "/" + charsTotal); + }, + /** Connect to a new device - this creates a separate + connection to the one `write` and `eval` use. */ + connect : connect, + /// Write to Puck.js and call back when the data is written. Creates a connection if it doesn't exist + write : write, + /// Evaluate an expression and call cb with the result. Creates a connection if it doesn't exist + eval : function(expr, cb) { + + const response = write('\x10Bluetooth.println(JSON.stringify(' + expr + '))\n', true) + .then(function (d) { + try { + return JSON.parse(d); + } catch (e) { + log(1, "Unable to decode " + JSON.stringify(d) + ", got " + e.toString()); + return Promise.reject(d); + } + }); + + + if (cb) { + return void response.then(cb, (err) => cb(null, err)); + } else { + return response; + } + + }, + /// Write the current time to the Puck + setTime : function(cb) { + var d = new Date(); + var cmd = 'setTime('+(d.getTime()/1000)+');'; + // in 1v93 we have timezones too + cmd += 'if (E.setTimeZone) E.setTimeZone('+d.getTimezoneOffset()/-60+');\n'; + write(cmd, cb); + }, + /// Did `write` and `eval` manage to create a connection? + isConnected : function() { + return connection!==undefined; + }, + /// get the connection used by `write` and `eval` + getConnection : function() { + return connection; + }, + /// Close the connection used by `write` and `eval` + close : function() { + if (connection) + connection.close(); + }, + /** Utility function to fade out everything on the webpage and display + a window saying 'Click to continue'. When clicked it'll disappear and + 'callback' will be called. This is useful because you can't initialise + Web Bluetooth unless you're doing so in response to a user input.*/ + modal : function(callback) { + var e = document.createElement('div'); + e.style = 'position:absolute;top:0px;left:0px;right:0px;bottom:0px;opacity:0.5;z-index:100;background:black;'; + e.innerHTML = '
Click to Continue...
'; + e.onclick = function(evt) { + callback(); + evt.preventDefault(); + document.body.removeChild(e); + }; + document.body.appendChild(e); + } + }; + return puck; +})); + + + +