/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* global mui, bt_company_ids, ble, LocalFileSystem, capabilityManager, MANUFACTUREDECODER */ var app; app = { mode: 0, stop: false, log: {}, activeServices: {}, list: {}, foundDevices: {}, manufactureDecoder: new MANUFACTUREDECODER(), // Application Constructor initialize: function() { this.bindEvents(); }, arrayBufferToIntArray: function(buffer) { var result; if (buffer) { var typedArray = new Uint8Array(buffer); result = []; for (var i = 0; i < typedArray.length; i++) { result[i] = typedArray[i]; } } return result; }, parseAdvertisingData: function(bytes) { var length; var type; var data; var i = 0; var advertisementData = {}; while (length !== 0) { length = bytes[i] & 0xFF; i++; type = bytes[i] & 0xFF; i++; data = bytes.slice(i, i + length - 1); // Length includes type byte, but not length byte i += length - 2; // Move to end of data i++; advertisementData[type] = data; } return advertisementData; }, handle255: function(buffer) { 'use strict'; var company; var cid; var manID; var bin = buffer; var decoded = {}; console.log('Block255', bin); manID = app.manufactureDecoder.getManID(bin); console.log('ManID:', manID); cid = '0x' + manID; company = bt_company_ids.find(cid); switch (manID) { case '004C': { decoded = app.manufactureDecoder.decodeIbeacon(bin); decoded.company = company; break; } case '1235': { decoded = app.manufactureDecoder.decodeSiliconLabsSensorPuck(bin); decoded.company = company; break; } case '0060': { decoded = app.manufactureDecoder.decodeSansible(bin); decoded.company = company; break; } default: { console.log('Unknown manID: ', manID); decoded = {company: company}; } } return decoded; }, makeHexBuffer: function(buffer) { 'use strict'; return buffer.map(function(i) { return ('00' + i.toString(16)).slice(-2) + ','; }); }, makeChars: function(buffer) { 'use strict'; return buffer.map(function(i) { return String.fromCharCode(i); }); }, calculateDistance: function(txPower, rssi) { // If there is 0 txPower then default it to -12dBm which appears to be a general default. var _txPower = (txPower !== 0) ? txPower : -12; var ratio_db = _txPower - rssi; var ratio_linear = Math.pow(10, ratio_db / 10); return Math.sqrt(ratio_linear); }, /** * * @param device * @param device.rssi * @param device.otherData * @param device.id * @param device.name * @param device.rssiBuffer * @returns {jQuery|HTMLElement|*} */ buildNewDeviceResultPanel: function(device) { 'use strict'; var dString; var accuracy; var avg; var sum; var newPanel, newRow; var otherData = device.otherData; var newId = 'd-' + device.id.replace(/:/g, '').split('-')[0]; var title = device.hasOwnProperty('name') ? device.name : '*** Unknown'; newPanel = $('
', {id: newId, class: 'mui-panel deviceRow', style: 'min-height:75px;'}); newRow = $('
', {class: 'mui-row'}); newRow.append($('
', {class: 'mui-col-xs-12 mui--text-title', text: device.id})); newPanel.append(newRow); newRow = $('
', {class: 'mui-row'}); newRow.append($('
', {class: 'mui-col-xs-3', text: 'Name:'})); newRow.append($('
', {class: 'mui-col-xs-3', text: title})); if (typeof otherData !== 'undefined' && otherData !== null && otherData.hasOwnProperty( 'txpower')) { if (device.hasOwnProperty('rssiBuffer') && (device.rssiBuffer.length > 0)) { sum = device.rssiBuffer.reduce(function(a, b) { return a + b; }); avg = sum / device.rssiBuffer.length; accuracy = app.calculateDistance(otherData.txpower, avg); } else { accuracy = app.calculateDistance(otherData.txpower, device.rssi); } dString = (accuracy <= 30.00) ? accuracy.toFixed(2) + ' m' : 'Far'; newRow.append($('
', {class: 'mui-col-xs-3', text: 'Distance:'})); newRow.append($('
', {class: 'mui-col-xs-3', text: dString})); } else { newRow.append($('
', {class: 'mui-col-xs-3', text: 'RSSI:'})); newRow.append($('
', {class: 'mui-col-xs-3', text: device.rssi + ' dB'})); } newPanel.append(newRow); if (typeof otherData !== 'undefined' && otherData !== null) { if (otherData.hasOwnProperty('msg')) { newRow = $('
', {class: 'mui-row'}); newRow.append($('
', {class: 'mui-col-xs-3', text: 'Details:'})); newRow.append($('
', {class: 'mui-col-xs-8', text: otherData.msg})); newPanel.append(newRow); } } return newPanel; }, extractPData: function(prev) { 'use strict'; if (typeof prev === 'undefined' || prev === null) { return {}; } return prev.pData; }, extractRSSIBuffer: function(prev) { 'use strict'; if (typeof prev === 'undefined' || prev === null) { return []; } return prev.rssiBuffer; }, processPData: function(newData, oldData) { 'use strict'; var output = {}; var wa = []; if (newData === null || newData.data === null) { return {}; } for (var key in newData.data) { if (newData.data.hasOwnProperty(key)) { if (Object.keys(oldData).indexOf(key) !== -1) { wa = oldData[key]; } if (wa.length === 99) { wa = wa.slice(1); } wa.push(newData.data[key]); output[key] = wa; } } return output; }, processRSSIData: function(rssi, oldBuffer) { 'use strict'; if (typeof oldBuffer === 'undefined' || oldBuffer === null) { return []; } var wa = oldBuffer; if (wa.length === 10) { wa = wa.slice(1); } wa.push(rssi); return wa; }, doScan: function(mode) { 'use strict'; app.mode = mode; $('#ripple').show(); if (mode !== 2) { $('#tbody').empty(); } ble.startScan([], app.foundDevice.bind(this), function(e) { console.error(e); }); var _t = [5000, 60000, 200][mode]; setTimeout(ble.stopScan, _t, app.scanComplete, function() { console.log('stopScan failed'); $('#ripple').hide(); }); }, populateObject: function(source, dest) { var rObj = dest; for (var item in source) { if (source.hasOwnProperty(item)) { rObj[item] = source[item]; } } return rObj; }, /** * * @param device * @param device.advertising * @param device.rssi * @param device.id */ foundDevice: function(device) { var rssiBuffer; var oldRSSIBuffer; var newPData; var oldPdata; var parsed; var hexBuffer; var advertBuffer; var newTR; var newId = 'd-' + device.id.replace(/:/g, '').split('-')[0]; var _device = app.foundDevices[newId] || {}; var $newID; var otherData; _device = app.populateObject(device, _device); // _device.pData = {}; otherData = null; this.list[newId] = _device.id; if (_device.hasOwnProperty('advertising')) { advertBuffer = app.arrayBufferToIntArray(_device.advertising); hexBuffer = app.makeHexBuffer(advertBuffer); parsed = app.parseAdvertisingData(advertBuffer); if (parsed.hasOwnProperty('9')) { var name = app.makeChars(parsed['9']); _device.name = name.join(''); console.log('Name: ', name.join('')); } if (parsed.hasOwnProperty('255')) { otherData = app.handle255(parsed['255']); console.log(otherData); _device.otherData = otherData; } _device.advertBuffer = advertBuffer; _device.hexBuffer = hexBuffer; _device.parsed = parsed; } // OldPdata = app.extractPData(app.log[newId]); oldPdata = app.extractPData(_device); newPData = app.processPData(otherData, oldPdata); // OldRSSIBuffer = app.extractRSSIBuffer(app.log[newId]); oldRSSIBuffer = app.extractRSSIBuffer(_device); rssiBuffer = app.processRSSIData(_device.rssi, oldRSSIBuffer); _device.pData = newPData; _device.rssiBuffer = rssiBuffer; newTR = app.buildNewDeviceResultPanel(_device); $newID = $('div#' + newId); if ($newID.length > 0) { $newID.replaceWith(newTR); } else { $('#scanResults').append(newTR); } app.log[newId] = _device; app.foundDevices[newId] = _device; console.log(JSON.stringify(_device)); }, scanComplete: function() { console.log('Scan complete'); if (app.mode === 1) { app.saveLog(); $('#ripple').hide(); } if (app.mode === 2) { if (!app.stop) { setTimeout(function() { app.doScan(2); }.bind(this), 200); } else { app.saveLog(); $('#ripple').hide(); } } }, writeFile: function(fileEntry, dataObj) { // Create a FileWriter object for our FileEntry (log.txt). fileEntry.createWriter(function(fileWriter) { fileWriter.onwriteend = function() { console.log('Successful file write...'); // ReadFile(fileEntry); }; fileWriter.onerror = function(e) { console.error('Failed file write: ' + e.toString()); }; // If data object is not passed in, // create a new Blob instead. if (!dataObj) { dataObj = new Blob(['some file data'], {type: 'text/plain'}); } fileWriter.write(dataObj); }); }, saveLog: function() { 'use strict'; var dt = new Date().toISOString().replace(/:|-/g, '').replace(/(\.\w+)/g, ''); var payload = JSON.stringify(app.log); var filename = 'sensortoy-' + dt + '.json'; // Var dataObj = new Blob(payload, { type: 'text/plain' }); window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) { console.log('file system open: ' + fs.name); fs.root.getFile(filename, {create: true, exclusive: false}, function(fileEntry) { console.log('fileEntry is file?' + fileEntry.isFile.toString()); // FileEntry.name == 'someFile.txt' // fileEntry.fullPath == '/someFile.txt' console.log('Path: ', fileEntry.fullPath); app.writeFile(fileEntry, payload); app.log = []; }, app.onError); }, app.onError); }, forceStop: function() { 'use strict'; app.stop = true; $('#scan').show(); $('#stop').hide(); }, // Bind Event Listeners // // Bind any events that are required on startup. Common events are: // 'load', 'deviceready', 'offline', and 'online'. bindEvents: function() { var self = this; document.addEventListener('deviceready', this.onDeviceReady, false); $('#scan').on('click', function() { 'use strict'; this.stop = false; this.doScan(2); $('#scan').hide(); $('#stop').show(); }.bind(this)); $('#stop').on('click', function() { 'use strict'; app.forceStop(); }.bind(this)); $('#longScan').on('click', function() { 'use strict'; this.doScan(1); }.bind(this)); $('#scanResults').on('click', 'div.mui-panel.deviceRow', function() { 'use strict'; var tID = $(this).context.id; var id = self.list[tID]; console.log(tID, id); app.forceStop(); self.connect(id); }); }, addTab: function(tID) { var appTabs = $('#app-tabs'); var panes = $('#tab-panes'); var paneID = 'pane-' + tID; var _device = app.foundDevices[tID]; var _name = _device.name || _device.id; console.log('Found:', _device); $('
', {class: 'mui-tabs__pane', id: paneID}).appendTo(panes); var li = $('
  • ').append($('', {'data-mui-toggle': 'tab', 'data-mui-controls': paneID, text: _name})); appTabs.append(li); return paneID; }, // Deviceready Event Handler // // The scope of 'this' is the event. In order to call the 'receivedEvent' // function, we must explicitly call 'app.receivedEvent(...);' onDeviceReady: function() { }, doAnimate: function() { for (var item in app.activeServices) { if (app.activeServices.hasOwnProperty(item)) { var activeService = app.activeServices[item]; for (var t = 0; t < activeService.length; t++) { activeService[t].animateGraph(); } } } window.requestAnimFrame(app.doAnimate.bind(this)); }, connect: function(deviceId) { $('#results').slideUp(); console.log('Connect to ', deviceId); var tID = 'd-' + deviceId.replace(/:/g, '').split('-')[0]; /** * * @param a * @param a.services */ var onConnect = function(a) { var services = []; services = a.services; console.log('Searching services for ', tID); var usedServices = []; var target = app.addTab(tID); var _params = { deviceID: deviceId, target: target }; for (var t = 0; t < services.length; t++) { var ident = services[t].toUpperCase(); var SERVICE = capabilityManager.discover(ident); if (SERVICE !== null) { var newService = new SERVICE(_params); newService.startService(); usedServices.push(newService); } else { console.error('Unknown service: ', ident); } } app.activeServices[tID] = usedServices; mui.tabs.activate(target); window.requestAnimFrame(app.doAnimate.bind(this)); }; if (!app.activeServices.hasOwnProperty(tID)) { ble.connect(deviceId, onConnect, function(e) { 'use strict'; console.log(e); console.error(e); }); } }, onError: function(reason) { console.error('ERROR: ' + reason); // Real apps should use notification.alert } }; window.requestAnimFrame = (function() { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60); }; })(); app.initialize();