291
|
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 dateutil.tz import tzlocal
|
|
6 import math
|
|
7 import cyclone.sse
|
|
8 from locator import Locator, Measurement
|
|
9
|
|
10 sys.path.append("/my/proj/homeauto/lib")
|
|
11 from cycloneerr import PrettyErrorHandler
|
|
12 from logsetup import log
|
|
13
|
310
|
14 from db import Db
|
|
15
|
291
|
16 class Devices(PrettyErrorHandler, cyclone.web.RequestHandler):
|
|
17 def get(self):
|
|
18 devices = []
|
310
|
19 startCount = datetime.datetime.now(tzlocal()) - datetime.timedelta(seconds=60*20)
|
|
20 for addr in db.addrs(startCount):
|
|
21 # limit to "addr_type": "Public"
|
291
|
22 name = None
|
|
23 if addr == "00:ea:23:23:c6:c4":
|
|
24 name = 'apollo'
|
|
25 if addr == "00:ea:23:21:e0:a4":
|
|
26 name = 'white'
|
|
27 if addr == "00:ea:23:24:f8:d4":
|
|
28 name = 'green'
|
310
|
29 row = db.latestDetail(addr)
|
291
|
30 if 'Eddystone-URL' in row:
|
|
31 name = row['Eddystone-URL']
|
|
32 devices.append({
|
|
33 'addr': addr,
|
|
34 'name': name})
|
|
35 devices.sort(key=lambda d: (d['name'] or 'zzz',
|
|
36 d['addr']))
|
|
37 self.set_header("Content-Type", "application/json")
|
|
38 self.write(json.dumps({'devices': devices}))
|
|
39
|
|
40
|
|
41 class Poller(object):
|
|
42 def __init__(self):
|
|
43 self.listeners = [] # Points handlers
|
|
44
|
|
45 self.lastPointTime = {} # addr : secs
|
|
46 self.lastValues = {} # addr : {sensor: (secs, rssi)}
|
310
|
47 task.LoopingCall(self.poll).start(2)
|
291
|
48
|
|
49 def poll(self):
|
|
50 addrs = set(l.addr for l in self.listeners if l.addr)
|
|
51 seconds = 60 * 20
|
|
52 now = datetime.datetime.now(tzlocal())
|
|
53 startTime = (now - datetime.timedelta(seconds=seconds))
|
|
54 startTimeSec = arrow.get(startTime).timestamp
|
|
55 for addr in addrs:
|
|
56 points = {} # from: [offsetSec, rssi]
|
310
|
57 for row in db.recentRssi(startTime, addr):
|
|
58 t = (row['time'] - startTime).total_seconds()
|
291
|
59 points.setdefault(row['from'], []).append([
|
|
60 round(t, 2), row['rssi']])
|
|
61 self.lastValues.setdefault(addr, {})[row['from']] = (
|
|
62 now, row['rssi'])
|
|
63
|
|
64 for pts in points.values():
|
|
65 smooth(pts)
|
|
66
|
|
67 if not points:
|
|
68 continue
|
|
69
|
|
70 last = max(pts[-1][0] + startTimeSec for pts in points.values())
|
|
71 if self.lastPointTime.get(addr, 0) == last:
|
|
72 continue
|
|
73 self.lastPointTime[addr] = last
|
|
74 msg = json.dumps({
|
|
75 'addr': addr,
|
|
76 'startTime': startTimeSec,
|
|
77 'points': [{'from': k, 'points': v}
|
|
78 for k,v in sorted(points.items())]})
|
|
79 for lis in self.listeners:
|
|
80 if lis.addr == addr:
|
|
81 lis.sendEvent(msg)
|
|
82
|
|
83 def lastValue(self, addr, maxSensorAgeSec=30):
|
|
84 """note: only considers actively polled addrs"""
|
|
85 out = {} # from: rssi
|
|
86 now = datetime.datetime.now(tzlocal())
|
|
87 for sensor, (t, rssi) in self.lastValues.get(addr, {}).iteritems():
|
|
88 print 'consider %s %s' % (t, now)
|
|
89 if (now - t).total_seconds() < maxSensorAgeSec:
|
|
90 out[sensor] = rssi
|
|
91 return out
|
|
92
|
|
93 def smooth(pts):
|
|
94 # see https://filterpy.readthedocs.io/en/latest/kalman/UnscentedKalmanFilter.html
|
|
95 for i in range(0, len(pts)):
|
|
96 if i == 0:
|
|
97 prevT, smoothX = pts[i]
|
|
98 else:
|
|
99 t, x = pts[i]
|
|
100 if t - prevT < 30:
|
|
101 smoothX = .8 * smoothX + .2 * x
|
|
102 else:
|
|
103 smoothX = x
|
|
104 pts[i] = [t, round(smoothX, 1)]
|
|
105 prevT = t
|
|
106
|
|
107 class Points(cyclone.sse.SSEHandler):
|
|
108 def __init__(self, application, request, **kw):
|
|
109 cyclone.sse.SSEHandler.__init__(self, application, request, **kw)
|
|
110 if request.headers['accept'] != 'text/event-stream':
|
|
111 raise ValueError('ignoring bogus request')
|
|
112 self.addr = request.arguments.get('addr', [None])[0]
|
|
113
|
|
114 def bind(self):
|
|
115 if not self.addr:
|
|
116 return
|
|
117 poller.listeners.append(self)
|
|
118 def unbind(self):
|
|
119 if not self.addr:
|
|
120 return
|
|
121 poller.listeners.remove(self)
|
|
122
|
|
123 class LocatorEstimatesPoller(object):
|
|
124 def __init__(self):
|
|
125 self.listeners = []
|
|
126 self.lastResult = {}
|
|
127 self.locator = Locator()
|
|
128 task.LoopingCall(self.poll).start(1)
|
|
129
|
|
130 def poll(self):
|
|
131 addrs = set(l.addr for l in self.listeners if l.addr)
|
|
132 now = datetime.datetime.now(tzlocal())
|
|
133 cutoff = (now - datetime.timedelta(seconds=60))
|
310
|
134
|
291
|
135 for addr in addrs:
|
|
136 d = {} # from: [(t, rssi)]
|
310
|
137 for row in db.recentRssi(cutoff, addr):
|
|
138 d.setdefault(row['from'], []).append((row['time'].timestamp, row['rssi']))
|
291
|
139
|
|
140 for pts in d.values():
|
310
|
141 pts.sort()
|
291
|
142 smooth(pts)
|
310
|
143
|
291
|
144 meas = Measurement(dict((k, v[-1][1]) for k, v in d.items()))
|
|
145 nearest = [
|
|
146 (dist, coord) for dist, coord in self.locator.nearestPoints(meas) if dist < 25
|
|
147 ]
|
|
148 if nearest:
|
310
|
149 weightedCoord = self.locator.estimatePosition(nearest)
|
|
150 else:
|
|
151 weightedCoord = [-999, -999, -999]
|
291
|
152 self.lastResult[addr] = {'nearest': nearest, 'weightedCoord': weightedCoord}
|
|
153
|
|
154 for lis in self.listeners:
|
|
155 lis.sendEvent(self.lastResult[addr])
|
|
156
|
|
157 class PositionEstimates(cyclone.sse.SSEHandler):
|
|
158 def __init__(self, application, request, **kw):
|
|
159 cyclone.sse.SSEHandler.__init__(self, application, request, **kw)
|
|
160 if request.headers['accept'] != 'text/event-stream':
|
|
161 raise ValueError('ignoring bogus request')
|
|
162 self.addr = request.arguments.get('addr', [None])[0]
|
|
163
|
|
164 def bind(self):
|
|
165 if not self.addr:
|
|
166 return
|
|
167 locatorEstimatesPoller.listeners.append(self)
|
|
168 def unbind(self):
|
|
169 if not self.addr:
|
|
170 return
|
|
171 locatorEstimatesPoller.listeners.remove(self)
|
|
172
|
|
173 class Sensors(PrettyErrorHandler, cyclone.web.RequestHandler):
|
|
174 def get(self):
|
310
|
175 sensors = db.sensors()
|
|
176
|
291
|
177 t1 = datetime.datetime.now(tzlocal()) - datetime.timedelta(seconds=60*10)
|
|
178 out = []
|
310
|
179
|
|
180 allRows = list(db.recentRssi(t1))
|
|
181 allRows.sort(key=lambda r: r['time'], reverse=True)
|
291
|
182
|
310
|
183 for sens in sensors:
|
|
184 rssiHist = {} # level: count
|
|
185 for row in allRows:
|
|
186 if row['from'] == sens:
|
|
187 bucket = (row['rssi'] // 5) * 5
|
|
188 rssiHist[bucket] = rssiHist.get(bucket, 0) + 1
|
291
|
189
|
|
190 out.append({
|
|
191 'from': sens,
|
|
192 'count': sum(rssiHist.values()),
|
|
193 'hist': rssiHist,
|
|
194 })
|
|
195
|
|
196 self.set_header("Content-Type", "application/json")
|
|
197 self.write(json.dumps({'sensors': out}))
|
|
198
|
|
199 class Save(PrettyErrorHandler, cyclone.web.RequestHandler):
|
|
200 def post(self):
|
|
201 lines = open('saved_points').readlines()
|
|
202 lineNum = len(lines) + 1
|
|
203 row = poller.lastValue('00:ea:23:21:e0:a4')
|
|
204 with open('saved_points', 'a') as out:
|
|
205 out.write('%s %r\n' % (lineNum, row))
|
|
206 self.write('wrote line %s: %r' % (lineNum, row))
|
310
|
207
|
|
208 db = Db()
|
|
209
|
291
|
210 poller = Poller()
|
|
211 locatorEstimatesPoller = LocatorEstimatesPoller()
|
|
212
|
|
213 reactor.listenTCP(
|
|
214 9113,
|
|
215 cyclone.web.Application([
|
|
216 (r"/(|.*\.(?:js|html|json))$", cyclone.web.StaticFileHandler, {
|
|
217 "path": ".", "default_filename": "beaconmap.html"}),
|
|
218 (r"/devices", Devices),
|
|
219 (r'/points', Points),
|
|
220 (r'/sensors', Sensors),
|
|
221 (r'/save', Save),
|
|
222 (r'/positionEstimates', PositionEstimates),
|
|
223 ]))
|
|
224 log.info('serving on 9113')
|
|
225 reactor.run()
|