comparison service/beacon/beaconmap.py @ 291:299ddd7e2070

start bt beacon tools Ignore-this: a19bb907ede601562ef44c27ae706dca
author drewp@bigasterisk.com
date Wed, 20 Jul 2016 23:52:03 -0700
parents
children 6ba2c88f9847
comparison
equal deleted inserted replaced
290:7b5cff542078 291:299ddd7e2070
1 from __future__ import division
2 import sys, cyclone.web, json, datetime, time
3 import arrow
4 from twisted.internet import reactor, task
5 from pymongo import MongoClient
6 from dateutil.tz import tzlocal
7 import math
8 import cyclone.sse
9 from locator import Locator, Measurement
10
11 sys.path.append("/my/proj/homeauto/lib")
12 from cycloneerr import PrettyErrorHandler
13 from logsetup import log
14
15 class Devices(PrettyErrorHandler, cyclone.web.RequestHandler):
16 def get(self):
17 devices = []
18 startCount = datetime.datetime.now(tzlocal()) - datetime.timedelta(seconds=60*60*2)
19 filt = {
20 #"addr_type": "Public",
21 }
22 for addr in scan.distinct('addr', filt):
23 filtAddr = filt.copy()
24 filtAddr['addr'] = addr
25 row = scan.find_one(filtAddr, sort=[('t', -1)], limit=1)
26 filtAddrRecent = filtAddr.copy()
27 filtAddrRecent['t'] = {'$gt': startCount}
28 freq = scan.count(filtAddrRecent)
29 if not freq:
30 continue
31 name = None
32 if addr == "00:ea:23:23:c6:c4":
33 name = 'apollo'
34 if addr == "00:ea:23:21:e0:a4":
35 name = 'white'
36 if addr == "00:ea:23:24:f8:d4":
37 name = 'green'
38 if 'Eddystone-URL' in row:
39 name = row['Eddystone-URL']
40 devices.append({
41 'addr': addr,
42 'recentCount': freq,
43 'lastSeen': row['t'].isoformat(),
44 'name': name})
45 devices.sort(key=lambda d: (d['name'] or 'zzz',
46 -d['recentCount'],
47 d['addr']))
48 self.set_header("Content-Type", "application/json")
49 self.write(json.dumps({'devices': devices}))
50
51
52 class Poller(object):
53 def __init__(self):
54 self.listeners = [] # Points handlers
55
56 self.lastPointTime = {} # addr : secs
57 self.lastValues = {} # addr : {sensor: (secs, rssi)}
58 task.LoopingCall(self.poll).start(1)
59
60 def poll(self):
61 addrs = set(l.addr for l in self.listeners if l.addr)
62 seconds = 60 * 20
63 now = datetime.datetime.now(tzlocal())
64 startTime = (now - datetime.timedelta(seconds=seconds))
65 startTimeSec = arrow.get(startTime).timestamp
66 for addr in addrs:
67 points = {} # from: [offsetSec, rssi]
68 for row in scan.find({'addr': addr, 't': {'$gt': startTime},
69 #'addr_type': 'Public',
70 },
71 sort=[('t', 1)]):
72 t = (arrow.get(row['t']) - startTime).total_seconds()
73 points.setdefault(row['from'], []).append([
74 round(t, 2), row['rssi']])
75 self.lastValues.setdefault(addr, {})[row['from']] = (
76 now, row['rssi'])
77
78 for pts in points.values():
79 smooth(pts)
80
81 if not points:
82 continue
83
84 last = max(pts[-1][0] + startTimeSec for pts in points.values())
85 if self.lastPointTime.get(addr, 0) == last:
86 continue
87 self.lastPointTime[addr] = last
88 msg = json.dumps({
89 'addr': addr,
90 'startTime': startTimeSec,
91 'points': [{'from': k, 'points': v}
92 for k,v in sorted(points.items())]})
93 for lis in self.listeners:
94 if lis.addr == addr:
95 lis.sendEvent(msg)
96
97 def lastValue(self, addr, maxSensorAgeSec=30):
98 """note: only considers actively polled addrs"""
99 out = {} # from: rssi
100 now = datetime.datetime.now(tzlocal())
101 for sensor, (t, rssi) in self.lastValues.get(addr, {}).iteritems():
102 print 'consider %s %s' % (t, now)
103 if (now - t).total_seconds() < maxSensorAgeSec:
104 out[sensor] = rssi
105 return out
106
107 def smooth(pts):
108 # see https://filterpy.readthedocs.io/en/latest/kalman/UnscentedKalmanFilter.html
109 for i in range(0, len(pts)):
110 if i == 0:
111 prevT, smoothX = pts[i]
112 else:
113 t, x = pts[i]
114 if t - prevT < 30:
115 smoothX = .8 * smoothX + .2 * x
116 else:
117 smoothX = x
118 pts[i] = [t, round(smoothX, 1)]
119 prevT = t
120
121 class Points(cyclone.sse.SSEHandler):
122 def __init__(self, application, request, **kw):
123 cyclone.sse.SSEHandler.__init__(self, application, request, **kw)
124 if request.headers['accept'] != 'text/event-stream':
125 raise ValueError('ignoring bogus request')
126 self.addr = request.arguments.get('addr', [None])[0]
127
128 def bind(self):
129 if not self.addr:
130 return
131 poller.listeners.append(self)
132 def unbind(self):
133 if not self.addr:
134 return
135 poller.listeners.remove(self)
136
137 class LocatorEstimatesPoller(object):
138 def __init__(self):
139 self.listeners = []
140 self.lastResult = {}
141 self.locator = Locator()
142 task.LoopingCall(self.poll).start(1)
143
144 def poll(self):
145 addrs = set(l.addr for l in self.listeners if l.addr)
146 now = datetime.datetime.now(tzlocal())
147 cutoff = (now - datetime.timedelta(seconds=60))
148
149 for addr in addrs:
150 d = {} # from: [(t, rssi)]
151 for row in scan.find({'addr': addr, 't': {'$gt': cutoff}},
152 sort=[('t', 1)]):
153 d.setdefault(row['from'], []).append((arrow.get(row['t']).timestamp, row['rssi']))
154
155 for pts in d.values():
156 smooth(pts)
157 meas = Measurement(dict((k, v[-1][1]) for k, v in d.items()))
158 nearest = [
159 (dist, coord) for dist, coord in self.locator.nearestPoints(meas) if dist < 25
160 ]
161 if nearest:
162 floors = [row[1][2] for row in nearest]
163 freqs = [(floors.count(z), z) for z in floors]
164 freqs.sort()
165 bestFloor = freqs[-1][1]
166 sameFloorMatches = [(dist, coord) for dist, coord in nearest
167 if coord[2] == bestFloor]
168 weightedCoord = [0, 0, 0]
169 totalWeight = 0
170 for dist, coord in sameFloorMatches:
171 weight = 25 / (dist + .001)
172 totalWeight += weight
173 for i in range(3):
174 weightedCoord[i] += weight * coord[i]
175 for i in range(3):
176 weightedCoord[i] /= totalWeight
177
178 self.lastResult[addr] = {'nearest': nearest, 'weightedCoord': weightedCoord}
179
180 for lis in self.listeners:
181 lis.sendEvent(self.lastResult[addr])
182
183
184 class PositionEstimates(cyclone.sse.SSEHandler):
185 def __init__(self, application, request, **kw):
186 cyclone.sse.SSEHandler.__init__(self, application, request, **kw)
187 if request.headers['accept'] != 'text/event-stream':
188 raise ValueError('ignoring bogus request')
189 self.addr = request.arguments.get('addr', [None])[0]
190
191 def bind(self):
192 if not self.addr:
193 return
194 locatorEstimatesPoller.listeners.append(self)
195 def unbind(self):
196 if not self.addr:
197 return
198 locatorEstimatesPoller.listeners.remove(self)
199
200
201
202 class Sensors(PrettyErrorHandler, cyclone.web.RequestHandler):
203 def get(self):
204 t1 = datetime.datetime.now(tzlocal()) - datetime.timedelta(seconds=60*10)
205 out = []
206 for sens in scan.distinct('from', {'t': {'$gt': t1}}):
207 rssiHist = {} # level: count
208 for row in scan.find({'from': sens, 't': {'$gt': t1}},
209 {'_id': False, 'rssi': True}):
210 bucket = (row['rssi'] // 5) * 5
211 rssiHist[bucket] = rssiHist.get(bucket, 0) + 1
212
213 recent = {}
214 for row in scan.find({'from': sens},
215 {'_id': False,
216 'addr': True,
217 't': True,
218 'rssi': True,
219 'addr_type': True},
220 sort=[('t', -1)],
221 modifiers={'$maxScan': 100000}):
222 addr = row['addr']
223 if addr not in recent:
224 recent[addr] = row
225 recent[addr]['t'] = arrow.get(recent[addr]['t']).timestamp
226
227 out.append({
228 'from': sens,
229 'count': sum(rssiHist.values()),
230 'hist': rssiHist,
231 'recent': sorted(recent.values())
232 })
233
234 self.set_header("Content-Type", "application/json")
235 self.write(json.dumps({'sensors': out}))
236
237
238
239 class Sensor(PrettyErrorHandler, cyclone.web.RequestHandler):
240 def get(self):
241 from_ = self.get_argument('from')
242 if not from_:
243 return
244 seconds = int(self.get_argument('secs', default=60 * 2))
245 startTime = (datetime.datetime.now(tzlocal()) -
246 datetime.timedelta(seconds=seconds))
247 points = {} # addr : [offsetSec, rssi]
248 for row in scan.find({'from': from_, 't': {'$gt': startTime},
249 #'addr_type': 'Public',
250 },
251 {'_id': False,
252 'addr': True,
253 't': True,
254 'rssi': True,
255 },
256 sort=[('t', 1)]):
257 points.setdefault(row['addr'], []).append([
258 round((arrow.get(row['t']) - startTime).total_seconds(), 2),
259 row['rssi']])
260
261 self.set_header("Content-Type", "application/json")
262 self.write(json.dumps({
263 'sensor': from_,
264 'startTime': arrow.get(startTime).timestamp,
265 'points': points}))
266
267 class Save(PrettyErrorHandler, cyclone.web.RequestHandler):
268 def post(self):
269 lines = open('saved_points').readlines()
270 lineNum = len(lines) + 1
271 row = poller.lastValue('00:ea:23:21:e0:a4')
272 with open('saved_points', 'a') as out:
273 out.write('%s %r\n' % (lineNum, row))
274 self.write('wrote line %s: %r' % (lineNum, row))
275
276 scan = MongoClient('bang', 27017, tz_aware=True)['beacon']['scan']
277 poller = Poller()
278 locatorEstimatesPoller = LocatorEstimatesPoller()
279
280 reactor.listenTCP(
281 9113,
282 cyclone.web.Application([
283 (r"/(|.*\.(?:js|html|json))$", cyclone.web.StaticFileHandler, {
284 "path": ".", "default_filename": "beaconmap.html"}),
285 (r"/devices", Devices),
286 (r'/points', Points),
287 (r'/sensors', Sensors),
288 (r'/sensor', Sensor),
289 (r'/save', Save),
290 (r'/positionEstimates', PositionEstimates),
291 ]))
292 log.info('serving on 9113')
293 reactor.run()