Mercurial > code > home > repos > homeauto
changeset 291:299ddd7e2070
start bt beacon tools
Ignore-this: a19bb907ede601562ef44c27ae706dca
author | drewp@bigasterisk.com |
---|---|
date | Wed, 20 Jul 2016 23:52:03 -0700 |
parents | 7b5cff542078 |
children | 105969d248d6 |
files | service/beacon/beacon-map.html service/beacon/beaconmap.html service/beacon/beaconmap.py service/beacon/house-model.html service/beacon/rssiscan.py |
diffstat | 5 files changed, 1177 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/beacon/beacon-map.html Wed Jul 20 23:52:03 2016 -0700 @@ -0,0 +1,114 @@ +<link rel="import" href="/lib/polymer/1.0.9/polymer/polymer.html"> +<link rel="import" href="/lib/polymer/1.0.9/iron-ajax/iron-ajax.html"> + +<dom-module id="beacon-map"> + <template> + <style> + #map { } + canvas { border: 1px solid gray; } + </style> + <iron-ajax on-response="onPoints" needs="redo" + url="points" + params="{{pointsParams}}" + ></iron-ajax> + <canvas id="map" width="500" height="800"></canvas> + </template> + <script src="dat.gui.js"></script> + + <script> + HTMLImports.whenReady(function () { + Polymer({ + is: "beacon-map", + properties: { + show: { type: Object, notify: true }, + pointsParams: { computed: '_pointsParams(show)' } + }, + ready: function() { + this.scaleSum = 1; + //var gui = new dat.GUI(); + //gui.add(this, 'scaleSum', .001, 3); + //gui.listen({updateDisplay: this.redraw.bind(this)}); + + }, + _pointsParams: function(show) { + if (show) { + return { addr: show.addr }; + } + }, + onPoints: function(ev) { + this.points = ev.detail.response.points; + this.redraw(); + }, + redraw: function() { + if (!this.points || this.points.length < 1) { + return; + } + var ctx = this.$.map.getContext('2d'); + var w = this.$.map.width, h = this.$.map.height; + + ctx.clearRect(0, 0, w, h); + + ctx.font = "12px serif"; + + var pos = [ + [.2, .2], + [.2, .7], + [.8, .2], + [.9, .6], + [.8, .8], + [.4, .5], + [.7, .6], + ]; + + + ctx.fillText("changing", pos[0][0] * 400, pos[0][1] * 400); + ctx.fillText("garage", pos[1][0] * 400, pos[1][1] * 400); + ctx.fillText("bed", pos[2][0] * 400, pos[2][1] * 400); + ctx.fillText("dash", pos[3][0] * 400, pos[3][1] * 400); + ctx.fillText("bang", pos[4][0] * 400, pos[4][1] * 400); + ctx.fillText("living", pos[5][0] * 400, pos[5][1] * 400); + ctx.fillText("kitchen", pos[6][0] * 400, pos[6][1] * 400); + + ctx.beginPath(); + var first = true; + this.points.forEach(function(p) { + if (!p.map) { + return; + } + var weak = -95, strong = -60; + var w = p.map(function(rssi) { + return (rssi - weak) / (strong - weak); + }); + + var sum = w[0] + w[1] + w[2] + w[3] + w[4] + w[5] + w[6]; + sum *= this.scaleSum; + w[0] /= sum; + w[1] /= sum; + w[2] /= sum; + w[3] /= sum; + w[4] /= sum; + w[5] /= sum; + w[6] /= sum; + + var x=0, y=0; + for (var i=0; i<7; i++) { + x += pos[i][0] * w[i]; + y += pos[i][1] * w[i]; + } + + x *= 400; + y *= 400; + if (first) { + ctx.moveTo(x, y); + first = false; + } else { + ctx.lineTo(x, y); + + } + }); + ctx.stroke(); + } + }); + }); + </script> +</dom-module>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/beacon/beaconmap.html Wed Jul 20 23:52:03 2016 -0700 @@ -0,0 +1,358 @@ +<!doctype html> +<html> + <head> + <title>beaconmap</title> + <meta charset="utf-8" /> + <script src="/lib/polymer/1.0.9/webcomponentsjs/webcomponents-lite.min.js"></script> + <script> + window.Polymer = { + dom: 'shadow', + }; + </script> + <link rel="import" href="/lib/polymer/1.0.9/polymer/polymer.html"> + <link rel="import" href="/lib/polymer/1.0.9/iron-ajax/iron-ajax.html"> + <link rel="import" href="/lib/polymer/1.0.9/iron-list/iron-list.html"> + <link rel="import" href="/lib/polymer/1.0.9/paper-header-panel/paper-header-panel.html"> + <link rel="import" href="/lib/polymer/1.0.9/iron-flex-layout/iron-flex-layout-classes.html"> + <style is="custom-style" include="iron-flex iron-flex-alignment iron-positioning"></style> + + <script src="dat.gui.min.js"></script> + <link rel="import" href="beacon-map.html"> + <link rel="import" href="house-model.html"> + </head> + <body class="fullbleed layout vertical"> + <dom-module id="beacon-devices"> + <template> + + <style> + iron-list { height: 100%; } + .row { border: 1px outset #dadada; margin: 2px; background: #f7f7f7;} + </style> + <iron-ajax url="devices" + auto + last-response="{{response}}"></iron-ajax> + <iron-list items="[[response.devices]]" + selection-enabled="true" + selected-item="{{selected}}"> + <template> + <div> + <div class="row"> {{item.addr}} {{item.name}} ({{item.recentCount}})</div> + </div> + </template> + </iron-list> + </template> + <script> + HTMLImports.whenReady(function () { + Polymer({ + is: "beacon-devices", + properties: { + response: { type: Object, notify: true }, + selected: { type: Object, notify: true }, + } + }); + }); + </script> + </dom-module> + + <script src="/lib/rickshaw/90852d8/vendor/d3.min.js"></script> + <script> + var hashCode = function(s){ + var hash = 0; + if (s.length == 0) return hash; + for (var i = 0; i < s.length; i++) { + var character = s.charCodeAt(i); + hash = ((hash<<5)-hash)+character; + hash = hash & hash; // Convert to 32bit integer + } + return hash; + }; + var colorForAddr = function(addr) { + var hue = hashCode(addr) % 360; + return d3.hsl(hue, 1, 0.5) + ""; + }; + var colorForSensor = function(from) { + var hue = hashCode(from) % 360; + return d3.hsl(hue, .7, 0.7) + ""; + }; + </script> + + <dom-module id="beacon-sensor-graph"> + <link rel="import" type="css" href="/lib/rickshaw/90852d8/src/css/graph.css"> + <link rel="import" type="css" href="/lib/rickshaw/90852d8/src/css/detail.css"> + <link rel="import" type="css" href="/lib/rickshaw/90852d8/src/css/legend.css"> + + <template> + <iron-ajax id="get" url="sensor" params='{{params}}' auto last-response="{{response}}"></iron-ajax> + <div>{{sensor.from}}</div> + <div id="chart_container" on-click="onClick"> + <div id="chart"></div> + </div> + + </template> + <script src="/lib/jquery-2.0.3.min.js"></script> + <script src="/lib/rickshaw/90852d8/vendor/d3.min.js"></script> + <script src="/lib/rickshaw/90852d8/rickshaw.min.js"></script> + <script> + HTMLImports.whenReady(function () { + Polymer({ + is: "beacon-sensor-graph", + properties: { + sensor: {type: Object}, + response: {type: Object, notify: true}, + params: {computed: '_params(sensor.from)'} + }, + _params: function(from) { + return {from: from, secs: 60*5}; + }, + observers: [ + 'onResponse(response)' + ], + ready: function() { + this.scopeSubtree(this.$.chart_container, true); + + }, + onClick: function() { + this.$.get.generateRequest(); + }, + redraw: function() { + var serieses = []; + + for (var addr of Object.keys(this.points)) { + var pts = this.points[addr]; + var transformed = pts.map(function(p) { + return {x: p[0] + this.startTime, + y: .5 * (p[1]-(-120)) / (-60+120)}; + }.bind(this)); + serieses.push({ + name: addr, + data: transformed, + color: colorForAddr(addr), + }); + } + + this.$.chart.innerHTML = ''; + var graph = new Rickshaw.Graph( { + element: this.$.chart, + width: 400, + height: 60, + renderer: 'line', + series: serieses, + } ); + + graph.render(); + + var hoverDetail = new Rickshaw.Graph.HoverDetail( { + graph: graph + } ); + + var axes = new Rickshaw.Graph.Axis.Time( { + graph: graph + } ); + axes.render(); + + + var yAxis = new Rickshaw.Graph.Axis.Y( { + graph: graph, + tickFormat: Rickshaw.Fixtures.Number.formatKMBT, + } ); + + yAxis.render(); + + this.graph = graph; + }, + onResponse: function(response) { + this.points = response.points; + this.startTime = response.startTime; + this.redraw(); + + } + }); + }); + </script> + </dom-module> + + <dom-module id="beacon-device-graph"> + <link rel="import" type="css" href="/lib/rickshaw/90852d8/src/css/graph.css"> + <link rel="import" type="css" href="/lib/rickshaw/90852d8/src/css/detail.css"> + <link rel="import" type="css" href="/lib/rickshaw/90852d8/src/css/legend.css"> + <template> + + <div>{{addr}} (continuous update)</div> + <div id="chart_container" on-click="onClick"> + <div id="chart"></div> + </div> + <div>{{latest}}</div> + </template> + <script src="/lib/jquery-2.0.3.min.js"></script> + <script src="/lib/rickshaw/90852d8/vendor/d3.min.js"></script> + <script src="/lib/rickshaw/90852d8/rickshaw.min.js"></script> + <script> + HTMLImports.whenReady(function () { + Polymer({ + is: "beacon-device-graph", + properties: { + addr: {type: String, notify: true}, + params: {computed: '_params(addr)'} + + }, + _params: function(from) { + return {addr: this.addr}; + }, + observers: [ + 'onResponse(response)', + 'startEvents(addr)', + ], + ready: function() { + this.scopeSubtree(this.$.chart_container, true); + }, + startEvents: function(addr) { + if (this.events) { + this.events.close(); + } + if (addr) { + console.log('new es', addr); + this.events = new EventSource('points?addr=' + encodeURIComponent(addr)); + this.events.addEventListener('message', function(e) { + var body = JSON.parse(e.data); + this.points = body.points; + this.startTime = body.startTime; + this.redraw(); + }.bind(this)); + } + }, + + onClick: function() { + this.$.get.generateRequest(); + }, + redraw: function() { + var serieses = []; + var latestForSensor = {}; + for (var row of this.points) { + var transformed = row.points.map(function(p) { + return {x: p[0] + this.startTime, + y: .5 * (p[1]-(-120)) / (-60+120)}; + }.bind(this)); + serieses.push({ + name: row.from, + data: transformed, + color: colorForSensor(row.from), + }); + latestForSensor[row.from] = row.points[row.points.length - 1][1]; + } + this.latest = JSON.stringify(latestForSensor); + this.$.chart.innerHTML = ''; + var graph = new Rickshaw.Graph( { + element: this.$.chart, + width: 640, + height: 300, + renderer: 'line', + series: serieses, + } ); + + graph.render(); + + var hoverDetail = new Rickshaw.Graph.HoverDetail( { + graph: graph + } ); + + var axes = new Rickshaw.Graph.Axis.Time( { + graph: graph + } ); + axes.render(); + + var yAxis = new Rickshaw.Graph.Axis.Y( { + graph: graph, + tickFormat: Rickshaw.Fixtures.Number.formatKMBT, + } ); + + yAxis.render(); + + this.graph = graph; + }, + + }); + }); + </script> + </dom-module> + + <dom-module id="beacon-page"> + <template> + <link rel="import" href="/lib/polymer/1.0.9/iron-flex-layout/iron-flex-layout-classes.html"> + <style is="custom-style" include="iron-flex iron-flex-alignment iron-flex-factors"></style> + <style> + </style> + <paper-header-panel class="flex"> + <div class="paper-header">beacon map</div> + <div class="layout horizontal"> + + <beacon-devices style="height: 500px" + class="layout justified flex-1" + selected="{{sel}}"></beacon-devices> + <div class="layout vertical" style="flex-grow: 2"> + <house-model position-estimates="{{positionEstimates}}" beacons="{{beacons}}"></house-model> + <beacon-device-graph addr="{{sel.addr}}"></beacon-device-graph> + <div> + save white levels: + <iron-ajax url="save" + method="POST" + id="save" + verbose="true" + handle-as="text" + last-response="{{saveResponse}}"></iron-ajax> + <button on-click="onSave" style="padding: 20px">save</button> {{saveResponse}} + </div> + + <div id="sensors"> + <iron-ajax url="sensors" + auto + last-response="{{sensorsResponse}}"></iron-ajax> + <template is="dom-repeat" items="{{sensorsResponse.sensors}}"> + <beacon-sensor-graph sensor="{{item}}"></beacon-sensor-graph> + </template> + </div> + + <beacon-map class="flex-4" show="{{sel}}"></beacon-map> + </div> + </div> + </paper-header-panel> + beacon page gets this + + </template> + <script> + HTMLImports.whenReady(function () { + Polymer({ + is: "beacon-page", + properties: { + sel: { type: Object, notify: true }, + saveResponse: { type: String, notify: true}, + }, + onSave: function() { + this.$.save.generateRequest(); + }, + ready: function() { + +this.sel = {addr: '00:ea:23:23:c6:c4'}; + + var events = new EventSource('positionEstimates?addr=' + + encodeURIComponent('00:ea:23:23:c6:c4')); + events.addEventListener('message', function(e) { + var body = JSON.parse(e.data); + this.positionEstimates = body.nearest; + this.set('beacons', [{label: 'apollo', pos: body.weightedCoord}]); + }.bind(this)); + }, + }); + }); + + </script> + </dom-module> + + <style> + .paper-header { + background-color: var(--paper-light-blue-500); + } + </style> + + <beacon-page class="layout fit"></beacon-page> + </body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/beacon/beaconmap.py Wed Jul 20 23:52:03 2016 -0700 @@ -0,0 +1,293 @@ +from __future__ import division +import sys, cyclone.web, json, datetime, time +import arrow +from twisted.internet import reactor, task +from pymongo import MongoClient +from dateutil.tz import tzlocal +import math +import cyclone.sse +from locator import Locator, Measurement + +sys.path.append("/my/proj/homeauto/lib") +from cycloneerr import PrettyErrorHandler +from logsetup import log + +class Devices(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self): + devices = [] + startCount = datetime.datetime.now(tzlocal()) - datetime.timedelta(seconds=60*60*2) + filt = { + #"addr_type": "Public", + } + for addr in scan.distinct('addr', filt): + filtAddr = filt.copy() + filtAddr['addr'] = addr + row = scan.find_one(filtAddr, sort=[('t', -1)], limit=1) + filtAddrRecent = filtAddr.copy() + filtAddrRecent['t'] = {'$gt': startCount} + freq = scan.count(filtAddrRecent) + if not freq: + continue + name = None + if addr == "00:ea:23:23:c6:c4": + name = 'apollo' + if addr == "00:ea:23:21:e0:a4": + name = 'white' + if addr == "00:ea:23:24:f8:d4": + name = 'green' + if 'Eddystone-URL' in row: + name = row['Eddystone-URL'] + devices.append({ + 'addr': addr, + 'recentCount': freq, + 'lastSeen': row['t'].isoformat(), + 'name': name}) + devices.sort(key=lambda d: (d['name'] or 'zzz', + -d['recentCount'], + d['addr'])) + self.set_header("Content-Type", "application/json") + self.write(json.dumps({'devices': devices})) + + +class Poller(object): + def __init__(self): + self.listeners = [] # Points handlers + + self.lastPointTime = {} # addr : secs + self.lastValues = {} # addr : {sensor: (secs, rssi)} + task.LoopingCall(self.poll).start(1) + + def poll(self): + addrs = set(l.addr for l in self.listeners if l.addr) + seconds = 60 * 20 + now = datetime.datetime.now(tzlocal()) + startTime = (now - datetime.timedelta(seconds=seconds)) + startTimeSec = arrow.get(startTime).timestamp + for addr in addrs: + points = {} # from: [offsetSec, rssi] + for row in scan.find({'addr': addr, 't': {'$gt': startTime}, + #'addr_type': 'Public', + }, + sort=[('t', 1)]): + t = (arrow.get(row['t']) - startTime).total_seconds() + points.setdefault(row['from'], []).append([ + round(t, 2), row['rssi']]) + self.lastValues.setdefault(addr, {})[row['from']] = ( + now, row['rssi']) + + for pts in points.values(): + smooth(pts) + + if not points: + continue + + last = max(pts[-1][0] + startTimeSec for pts in points.values()) + if self.lastPointTime.get(addr, 0) == last: + continue + self.lastPointTime[addr] = last + msg = json.dumps({ + 'addr': addr, + 'startTime': startTimeSec, + 'points': [{'from': k, 'points': v} + for k,v in sorted(points.items())]}) + for lis in self.listeners: + if lis.addr == addr: + lis.sendEvent(msg) + + def lastValue(self, addr, maxSensorAgeSec=30): + """note: only considers actively polled addrs""" + out = {} # from: rssi + now = datetime.datetime.now(tzlocal()) + for sensor, (t, rssi) in self.lastValues.get(addr, {}).iteritems(): + print 'consider %s %s' % (t, now) + if (now - t).total_seconds() < maxSensorAgeSec: + out[sensor] = rssi + return out + +def smooth(pts): + # see https://filterpy.readthedocs.io/en/latest/kalman/UnscentedKalmanFilter.html + for i in range(0, len(pts)): + if i == 0: + prevT, smoothX = pts[i] + else: + t, x = pts[i] + if t - prevT < 30: + smoothX = .8 * smoothX + .2 * x + else: + smoothX = x + pts[i] = [t, round(smoothX, 1)] + prevT = t + +class Points(cyclone.sse.SSEHandler): + def __init__(self, application, request, **kw): + cyclone.sse.SSEHandler.__init__(self, application, request, **kw) + if request.headers['accept'] != 'text/event-stream': + raise ValueError('ignoring bogus request') + self.addr = request.arguments.get('addr', [None])[0] + + def bind(self): + if not self.addr: + return + poller.listeners.append(self) + def unbind(self): + if not self.addr: + return + poller.listeners.remove(self) + +class LocatorEstimatesPoller(object): + def __init__(self): + self.listeners = [] + self.lastResult = {} + self.locator = Locator() + task.LoopingCall(self.poll).start(1) + + def poll(self): + addrs = set(l.addr for l in self.listeners if l.addr) + now = datetime.datetime.now(tzlocal()) + cutoff = (now - datetime.timedelta(seconds=60)) + + for addr in addrs: + d = {} # from: [(t, rssi)] + for row in scan.find({'addr': addr, 't': {'$gt': cutoff}}, + sort=[('t', 1)]): + d.setdefault(row['from'], []).append((arrow.get(row['t']).timestamp, row['rssi'])) + + for pts in d.values(): + smooth(pts) + meas = Measurement(dict((k, v[-1][1]) for k, v in d.items())) + nearest = [ + (dist, coord) for dist, coord in self.locator.nearestPoints(meas) if dist < 25 + ] + if nearest: + floors = [row[1][2] for row in nearest] + freqs = [(floors.count(z), z) for z in floors] + freqs.sort() + bestFloor = freqs[-1][1] + sameFloorMatches = [(dist, coord) for dist, coord in nearest + if coord[2] == bestFloor] + weightedCoord = [0, 0, 0] + totalWeight = 0 + for dist, coord in sameFloorMatches: + weight = 25 / (dist + .001) + totalWeight += weight + for i in range(3): + weightedCoord[i] += weight * coord[i] + for i in range(3): + weightedCoord[i] /= totalWeight + + self.lastResult[addr] = {'nearest': nearest, 'weightedCoord': weightedCoord} + + for lis in self.listeners: + lis.sendEvent(self.lastResult[addr]) + + +class PositionEstimates(cyclone.sse.SSEHandler): + def __init__(self, application, request, **kw): + cyclone.sse.SSEHandler.__init__(self, application, request, **kw) + if request.headers['accept'] != 'text/event-stream': + raise ValueError('ignoring bogus request') + self.addr = request.arguments.get('addr', [None])[0] + + def bind(self): + if not self.addr: + return + locatorEstimatesPoller.listeners.append(self) + def unbind(self): + if not self.addr: + return + locatorEstimatesPoller.listeners.remove(self) + + + +class Sensors(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self): + t1 = datetime.datetime.now(tzlocal()) - datetime.timedelta(seconds=60*10) + out = [] + for sens in scan.distinct('from', {'t': {'$gt': t1}}): + rssiHist = {} # level: count + for row in scan.find({'from': sens, 't': {'$gt': t1}}, + {'_id': False, 'rssi': True}): + bucket = (row['rssi'] // 5) * 5 + rssiHist[bucket] = rssiHist.get(bucket, 0) + 1 + + recent = {} + for row in scan.find({'from': sens}, + {'_id': False, + 'addr': True, + 't': True, + 'rssi': True, + 'addr_type': True}, + sort=[('t', -1)], + modifiers={'$maxScan': 100000}): + addr = row['addr'] + if addr not in recent: + recent[addr] = row + recent[addr]['t'] = arrow.get(recent[addr]['t']).timestamp + + out.append({ + 'from': sens, + 'count': sum(rssiHist.values()), + 'hist': rssiHist, + 'recent': sorted(recent.values()) + }) + + self.set_header("Content-Type", "application/json") + self.write(json.dumps({'sensors': out})) + + + +class Sensor(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self): + from_ = self.get_argument('from') + if not from_: + return + seconds = int(self.get_argument('secs', default=60 * 2)) + startTime = (datetime.datetime.now(tzlocal()) - + datetime.timedelta(seconds=seconds)) + points = {} # addr : [offsetSec, rssi] + for row in scan.find({'from': from_, 't': {'$gt': startTime}, + #'addr_type': 'Public', + }, + {'_id': False, + 'addr': True, + 't': True, + 'rssi': True, + }, + sort=[('t', 1)]): + points.setdefault(row['addr'], []).append([ + round((arrow.get(row['t']) - startTime).total_seconds(), 2), + row['rssi']]) + + self.set_header("Content-Type", "application/json") + self.write(json.dumps({ + 'sensor': from_, + 'startTime': arrow.get(startTime).timestamp, + 'points': points})) + +class Save(PrettyErrorHandler, cyclone.web.RequestHandler): + def post(self): + lines = open('saved_points').readlines() + lineNum = len(lines) + 1 + row = poller.lastValue('00:ea:23:21:e0:a4') + with open('saved_points', 'a') as out: + out.write('%s %r\n' % (lineNum, row)) + self.write('wrote line %s: %r' % (lineNum, row)) + +scan = MongoClient('bang', 27017, tz_aware=True)['beacon']['scan'] +poller = Poller() +locatorEstimatesPoller = LocatorEstimatesPoller() + +reactor.listenTCP( + 9113, + cyclone.web.Application([ + (r"/(|.*\.(?:js|html|json))$", cyclone.web.StaticFileHandler, { + "path": ".", "default_filename": "beaconmap.html"}), + (r"/devices", Devices), + (r'/points', Points), + (r'/sensors', Sensors), + (r'/sensor', Sensor), + (r'/save', Save), + (r'/positionEstimates', PositionEstimates), + ])) +log.info('serving on 9113') +reactor.run()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/beacon/house-model.html Wed Jul 20 23:52:03 2016 -0700 @@ -0,0 +1,218 @@ +<link rel="import" href="/lib/polymer/1.0.9/polymer/polymer.html"> + +<dom-module id="house-model"> + <template> + <style> + #scene { + position: relative; + overflow: hidden; + } + .label { + position: absolute; + color: white; + font-family: sans-serif; + font-size: 10px; + font-weight: bold; + text-shadow: 0 0 1px black; + } + </style> + <div id="scene"></div> + </template> + <script src="/lib/threejs/r78/three.js"></script> + <script src="dat.gui.js"></script> + + <script> + + function Bar(sceneParent, domParent, pos) { + this.domParent = domParent; + this.mesh = new THREE.Mesh(Bar.geometry, Bar.material); + this.mesh.scale.set(.1, 1, .1); + this.mesh.translateX(pos.x); + this.mesh.translateZ(pos.z); + this.pos = pos; + + sceneParent.add(this.mesh); + + } + Bar.material = new THREE.MeshLambertMaterial({ + "color": 0xe314a3, + "emissive": 0x531251 + }); + Bar.geometry = new THREE.CylinderGeometry( 1, 1, 1, 32 ); + Bar.prototype.setValue = function(value, label) { + this.value = value; + this.label = label; + this.mesh.position.setY(this.pos.y + value / 2); + this.mesh.scale.setY(value); + if (this.div) { + this.div.innerText = '' + label; + } + }; + Bar.prototype.update2d = function(camera, screenSize) { + if (!this.div) { + this.div = document.createElement("div"); + this.div.classList.add("label"); + this.domParent.appendChild(this.div); + } + + var vector = this.mesh.position.clone().project(camera); + vector.x = (vector.x + 1) / 2 * screenSize.x; + vector.y = -(vector.y - 1) / 2 * screenSize.y; + + this.div.style.left = vector.x + 'px'; + this.div.style.top = vector.y + 'px'; + }; + + + + Polymer({ + is: "house-model", + properties: { + positionEstimates: {type: Object, notify: true, observer: 'onPositionEstimates'}, + beacons: {type: Array, notify: true, observer: 'onBeacons'}, + }, + addLights: function(scene) { + var addLight = function(color, pos) { + var light = new THREE.PointLight(color, 1, 0, 0); + light.position.set.apply(pos); + scene.add(light); + return light; + }; + addLight(0xffafff, [0, 6, 0]); + addLight(0xffffaf, [-6, 6, 9]); + addLight(0xffcfaf, [5, 5, 0]); + addLight(0xefffef, [6, 4, 2]); + }, + setMaterials: function(scene) { + this.wall = new THREE.MeshPhongMaterial({ + color : 0x7c7c7c, + emissive: 0x303030, + name : "wall", + opacity : 0.34, + shininess : 1.3, + side : THREE.DoubleSide, + specular : 16777215, + transparent : true, + depthTest: true, + depthWrite: false, + alphaTest: 0.01, + }); + + + scene.children.forEach(function(obj) { + obj.material = this.wall; + }.bind(this)); + }, + ready: function() { + var container = this.$.scene; + + this.renderer = new THREE.WebGLRenderer( { antialias: true } ); + this.size = new THREE.Vector2(800, 500); + this.renderer.setSize(this.size.x, this.size.y); + container.appendChild( this.renderer.domElement ); + + this.config = { + camRotSpeed: 5, + camY: 8.5, + camDist: 11, + cursorX: 0, cursorY: 0, cursorZ: 0, + }; + + var gui = new dat.GUI(); + gui.add(this.config, 'camRotSpeed', -80, 80); + gui.add(this.config, 'camY', -2, 10); + gui.add(this.config, 'camDist', 1, 25); + gui.add(this.config, 'cursorX', -7, 7); + gui.add(this.config, 'cursorY', -7, 7); + gui.add(this.config, 'cursorZ', -7, 7); + + this.bars = {}; // pos : Bar + this.scene = null; + + var loader = new THREE.ObjectLoader(); + loader.load('house.json', function(scene) { + window.scene = scene; + this.scene = scene; + this.addLights(scene); + + this.setMaterials(scene); + + gui.add(this.wall, 'depthTest'); + gui.add(this.wall, 'depthWrite'); + gui.add(this.wall, 'transparent'); + gui.add(this.wall, 'opacity', 0, 1); + gui.add(this.wall, 'shininess', 0, 3); + gui.add(this.wall, 'alphaTest', 0, 1); + gui.listen({updateDisplay: function() { this.wall.needsUpdate = true; }.bind(this)}); + + this.camera = new THREE.PerspectiveCamera( 45, this.size.x / this.size.y, .1, 100 ); + scene.add(this.camera); + + var geometry = new THREE.SphereGeometry( .3, 32, 32 ); + var material = new THREE.MeshBasicMaterial( {color: 0xffff00} ); + this.cursor = new THREE.Mesh( geometry, material ); + scene.add(this.cursor); + + this.rotY = 0; + this.startAnimLoop(); + }.bind(this)); + }, + startAnimLoop: function() { + var lastTime = Date.now(); + requestAnimationFrame(function animate(){ + var now = Date.now(); + var dt = (now - lastTime) / 1000; + lastTime = now; + this.step(dt); + requestAnimationFrame(animate.bind(this)); + }.bind(this)) + }, + step: function(dt) { + this.rotY += this.config.camRotSpeed * dt; + + var p = new THREE.Vector3(this.config.camDist, this.config.camY, 0); + p.applyAxisAngle(new THREE.Vector3(0, 1, 0), this.rotY / 360 * 6.28); + this.camera.position.copy(p); + + this.camera.lookAt(new THREE.Vector3(-1, 0, 0)); + + this.cursor.position.set(this.config.cursorX, this.config.cursorY, this.config.cursorZ); + for (var p of Object.keys(this.bars)) { + var b = this.bars[p]; + b.update2d(this.camera, this.size); + b.setValue(b.value, b.label); + } + this.renderer.render(this.scene, this.camera); + }, + onPositionEstimates: function() { + if (!this.scene) { + return; + } + + for (var p of Object.keys(this.bars)) { + var b = this.bars[p]; + b.setValue(0.001, ''); + } + this.positionEstimates.forEach(function(row) { + var posKey = (Math.round(row[1][0] * 100) + ' ' + + Math.round(row[1][1] * 100) + ' ' + + Math.round(row[1][2] * 100)); + var b = this.bars[posKey]; + if (!b) { + b = new Bar(this.scene, this.$.scene, + new THREE.Vector3(-row[1][0], row[1][2], row[1][1])); + this.bars[posKey] = b; + } + b.setValue(10 / row[0], Math.round(row[0] * 100) / 100); + }.bind(this)); + }, + onBeacons: function() { + this.beacons.forEach(function(b) { + this.config.cursorX = -b.pos[0]; + this.config.cursorY = b.pos[2]; + this.config.cursorZ = b.pos[1]; + }.bind(this)); + } + }); + </script> +</dom-module>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/beacon/rssiscan.py Wed Jul 20 23:52:03 2016 -0700 @@ -0,0 +1,194 @@ +# BLE iBeaconScanner based on https://github.com/adamf/BLE/blob/master/ble-scanner.py +# JCS 06/07/14 +# Adapted for Python3 by Michael duPont 2015-04-05 + +#DEBUG = False +# BLE scanner based on https://github.com/adamf/BLE/blob/master/ble-scanner.py +# BLE scanner, based on https://code.google.com/p/pybluez/source/browse/trunk/examples/advanced/inquiry-with-rssi.py + +# https://github.com/pauloborges/bluez/blob/master/tools/hcitool.c for lescan +# https://kernel.googlesource.com/pub/scm/bluetooth/bluez/+/5.6/lib/hci.h for opcodes +# https://github.com/pauloborges/bluez/blob/master/lib/hci.c#L2782 for functions used by lescan + +import struct +import sys +import bluetooth._bluetooth as bluez +from pymongo import MongoClient +import datetime +from dateutil.tz import tzlocal +from bson.binary import Binary + +LE_META_EVENT = 0x3e +OGF_LE_CTL=0x08 +OCF_LE_SET_SCAN_ENABLE=0x000C + +# these are actually subevents of LE_META_EVENT +EVT_LE_CONN_COMPLETE=0x01 +EVT_LE_ADVERTISING_REPORT=0x02 + +def hci_enable_le_scan(sock): + enable = 0x01 + cmd_pkt = struct.pack("<BB", enable, 0x00) + bluez.hci_send_cmd(sock, OGF_LE_CTL, OCF_LE_SET_SCAN_ENABLE, cmd_pkt) + + +# ported from android/source/external/bluetooth/hcidump/parser/hci.c + +def evttype2str(etype): + return { + 0x00: "ADV_IND", # Connectable undirected advertising + 0x01: "ADV_DIRECT_IND", # Connectable directed advertising + 0x02: "ADV_SCAN_IND", # Scannable undirected advertising + 0x03: "ADV_NONCONN_IND", # Non connectable undirected advertising + 0x04: "SCAN_RSP", # Scan Response + }.get(etype, "Reserved") + +def bdaddrtype2str(btype): + return { + 0x00: "Public", + 0x01: "Random", + }.get(btype, "Reserved") + + +# from https://github.com/google/eddystone/blob/master/eddystone-url/implementations/linux/scan-for-urls + +schemes = [ + "http://www.", + "https://www.", + "http://", + "https://", + ] + +extensions = [ + ".com/", ".org/", ".edu/", ".net/", ".info/", ".biz/", ".gov/", + ".com", ".org", ".edu", ".net", ".info", ".biz", ".gov", + ] + +def decodeUrl(encodedUrl): + """ + Decode a url encoded with the Eddystone (or UriBeacon) URL encoding scheme + """ + + decodedUrl = schemes[encodedUrl[0]] + for c in encodedUrl[1:]: + if c <= 0x20: + decodedUrl += extensions[c] + else: + decodedUrl += chr(c) + + return decodedUrl + +def decodeBeacon(data, row): + # this padding makes the offsets line up to the scan-for-urls code + padData = map(ord, '*' * 14 + data) + + # Eddystone + if len(padData) >= 20 and padData[19] == 0xaa and padData[20] == 0xfe: + serviceDataLength = padData[21] + frameType = padData[25] + + # Eddystone-URL + if frameType == 0x10: + row["Eddystone-URL"] = decodeUrl(padData[27:22 + serviceDataLength]) + elif frameType == 0x00: + row["Eddystone-UID"] = Binary(data) + elif frameType == 0x20: + row["Eddystone-TLM"] = Binary(data) + else: + row["Eddystone"] = "Unknown Eddystone frame type: %r data: %r" % (frameType, data) + + # UriBeacon + elif len(padData) >= 20 and padData[19] == 0xd8 and padData[20] == 0xfe: + serviceDataLength = padData[21] + row["UriBeacon"] = decodeUrl(padData[27:22 + serviceDataLength]) + + else: + pass # "Unknown beacon type" + +def decodeInquiryData(data, row): + # offset 19 is totally observed from data, not any spec. IDK if the preceding part is variable-length. + if len(data) > 20 and data[19] in ['\x08', '\x09']: + localName = data[20:] + if data[19] == '\x08': + row['local_name_shortened'] = Binary(localName) + else: + row['local_name_complete'] = Binary(localName) + # more at android/source/external/bluetooth/hcidump/parser/hci.c ext_inquiry_data_dump + +# from android/source/external/bluetooth/hcidump/parser/hci.c +def evt_le_advertising_report_dump(frm, now): + num_reports = ord(frm[0]) + frm = frm[1:] + + for i in range(num_reports): + fmt = 'B B 6B B' + row = {'t': now} + + evt_type, bdaddr_type, b5, b4, b3, b2, b1, b0, length = struct.unpack(fmt, frm[:struct.calcsize(fmt)]) + frm = frm[struct.calcsize(fmt):] + + row['addr'] = '%02x:%02x:%02x:%02x:%02x:%02x' % (b0, b1, b2, b3, b4, b5) + row['addr_type'] = bdaddrtype2str(bdaddr_type) + row['evt_type'] = evttype2str(evt_type) + + data = frm[:length] + frm = frm[length:] + row['data'] = Binary(data) + #row['data_hex'] = ' '.join('%02x' % ord(c) for c in data) + + decodeBeacon(data, row) + decodeInquiryData(data, row) + + row['rssi'], = struct.unpack('b', frm[-1]) + frm = frm[1:] + yield row + + + +def parse_events(sock, loop_count, source, coll): + old_filter = sock.getsockopt( bluez.SOL_HCI, bluez.HCI_FILTER, 14) + flt = bluez.hci_filter_new() + bluez.hci_filter_all_events(flt) + bluez.hci_filter_set_ptype(flt, bluez.HCI_EVENT_PKT) + sock.setsockopt( bluez.SOL_HCI, bluez.HCI_FILTER, flt ) + + for i in range(0, loop_count): + pkt = sock.recv(255) + ptype, event, plen = struct.unpack("BBB", pkt[:3]) + now = datetime.datetime.now(tzlocal()) + if event == bluez.EVT_INQUIRY_RESULT_WITH_RSSI: + print "EVT_INQUIRY_RESULT_WITH_RSSI" + elif event == bluez.EVT_NUM_COMP_PKTS: + print "EVT_NUM_COMP_PKTS" + elif event == bluez.EVT_DISCONN_COMPLETE: + print "EVT_DISCONN_COMPLETE" + elif event == LE_META_EVENT: + subevent, = struct.unpack("B", pkt[3:4]) + pkt = pkt[4:] + if subevent == EVT_LE_CONN_COMPLETE: + pass + elif subevent == EVT_LE_ADVERTISING_REPORT: + rows = list(evt_le_advertising_report_dump(pkt, now)) + for row in sorted(rows): + #print row['addr'], row['t'], row['rssi'], row + row['from'] = source + coll.insert(row) + + + sock.setsockopt( bluez.SOL_HCI, bluez.HCI_FILTER, old_filter ) + + +if __name__ == '__main__': + mongoHost, myLocation = sys.argv[1:] + + client = MongoClient(mongoHost) + coll = client['beacon']['scan'] + + dev_id = 0 + sock = bluez.hci_open_dev(dev_id) + sock.getsockopt( bluez.SOL_HCI, bluez.HCI_FILTER, 14) + + hci_enable_le_scan(sock) + + while True: + parse_events(sock, 10, source=myLocation, coll=coll)