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: 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

(buf)); } function str2ab(str) { var buf = new ArrayBuffer(str.length); var bufView = new Uint8Array(buf); for (var i=0, strLen=str.length; i<strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; } function handleQueue() { if (!queue.length) return; var q = queue.shift(); log(3,"Executing "+JSON.stringify(q)+" from queue"); if (q.type == "write") puck.write(q.data, q.callback, q.callbackNewline); else log(1,"Unknown queue item "+JSON.stringify(q)); } function connect(callback) { if (!checkIfSupported()) return; var connection = { on : function(evt,cb) { this["on"+evt]=cb; }, emit : function(evt,data) { if (this["on"+evt]) this["on"+evt](data); }, isOpen : false, isOpening : true, txInProgress : false }; var btServer = undefined; var btService; var connectionDisconnectCallback; var txCharacteristic; var rxCharacteristic; var txDataQueue = []; var flowControlXOFF = false; var chunkSize = DEFAULT_CHUNKSIZE; connection.close = function() { connection.isOpening = false; if (connection.isOpen) { connection.isOpen = false; connection.emit('close'); } else { if (callback) callback(null); } if (btServer) { btServer.disconnect(); btServer = undefined; txCharacteristic = undefined; rxCharacteristic = undefined; chunkSize = DEFAULT_CHUNKSIZE; } }; connection.write = function(data, callback) { if (data) txDataQueue.push({data:data,callback:callback,maxLength:data.length}); if (connection.isOpen && !connection.txInProgress) writeChunk(); function writeChunk() { if (flowControlXOFF) { // flow control - try again later setTimeout(writeChunk, 50); return; } var chunk; if (!txDataQueue.length) { puck.writeProgress(); return; } var txItem = txDataQueue[0]; puck.writeProgress(txItem.maxLength - txItem.data.length, txItem.maxLength); if (txItem.data.length <= chunkSize) { chunk = txItem.data; txItem.data = undefined; } else { chunk = txItem.data.substr(0,chunkSize); txItem.data = txItem.data.substr(chunkSize); } connection.txInProgress = true; log(2, "Sending "+ JSON.stringify(chunk)); txCharacteristic.writeValue(str2ab(chunk)).then(function() { log(3, "Sent"); if (!txItem.data) { txDataQueue.shift(); // remove this element if (txItem.callback) txItem.callback(); } connection.txInProgress = false; writeChunk(); }).catch(function(error) { log(1, 'SEND ERROR: ' + error); txDataQueue = []; connection.close(); }); } }; navigator.bluetooth.requestDevice({ filters:[ { namePrefix: 'Puck.js' }, { namePrefix: 'Pixl.js' }, { namePrefix: 'MDBT42Q' }, { namePrefix: 'RuuviTag' }, { namePrefix: 'iTracker' }, { namePrefix: 'Thingy' }, { namePrefix: 'Espruino' }, { services: [ NORDIC_SERVICE ] } ], optionalServices: [ NORDIC_SERVICE ]}).then(function(device) { log(1, 'Device Name: ' + device.name); log(1, 'Device ID: ' + device.id); // Was deprecated: Should use getPrimaryServices for this in future //log('BT> 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<data.length;i++) { var ch = data.charCodeAt(i); var remove = true; if (ch==19) {// XOFF log(2,"XOFF received => 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("<BLE> "+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; }));