Mercurial > code > home > repos > homeauto
annotate service/beacon/rssiscan.py @ 1462:2b29f14eb6bd
try new graph+view widget
Ignore-this: d5f9c5dc52f04324368716ba2f604fdb
darcs-hash:44e85a5c075ef73c34a58deaa3a3c1e8390dae52
author | drewp <drewp@bigasterisk.com> |
---|---|
date | Sun, 24 Nov 2019 00:01:00 -0800 |
parents | 1ddd1f057749 |
children |
rev | line source |
---|---|
1096 | 1 # BLE iBeaconScanner based on https://github.com/adamf/BLE/blob/master/ble-scanner.py |
2 # JCS 06/07/14 | |
3 # Adapted for Python3 by Michael duPont 2015-04-05 | |
4 | |
5 #DEBUG = False | |
6 # BLE scanner based on https://github.com/adamf/BLE/blob/master/ble-scanner.py | |
7 # BLE scanner, based on https://code.google.com/p/pybluez/source/browse/trunk/examples/advanced/inquiry-with-rssi.py | |
8 | |
9 # https://github.com/pauloborges/bluez/blob/master/tools/hcitool.c for lescan | |
10 # https://kernel.googlesource.com/pub/scm/bluetooth/bluez/+/5.6/lib/hci.h for opcodes | |
11 # https://github.com/pauloborges/bluez/blob/master/lib/hci.c#L2782 for functions used by lescan | |
12 | |
13 import struct | |
14 import sys | |
1113 | 15 import time |
1096 | 16 import bluetooth._bluetooth as bluez |
17 import datetime | |
18 from dateutil.tz import tzlocal | |
19 from bson.binary import Binary | |
1113 | 20 from influxdb import InfluxDBClient |
21 from pymongo import MongoClient | |
22 from repoze.lru import LRUCache | |
1096 | 23 |
24 LE_META_EVENT = 0x3e | |
25 OGF_LE_CTL=0x08 | |
26 OCF_LE_SET_SCAN_ENABLE=0x000C | |
27 | |
28 # these are actually subevents of LE_META_EVENT | |
29 EVT_LE_CONN_COMPLETE=0x01 | |
30 EVT_LE_ADVERTISING_REPORT=0x02 | |
31 | |
32 def hci_enable_le_scan(sock): | |
33 enable = 0x01 | |
34 cmd_pkt = struct.pack("<BB", enable, 0x00) | |
35 bluez.hci_send_cmd(sock, OGF_LE_CTL, OCF_LE_SET_SCAN_ENABLE, cmd_pkt) | |
36 | |
37 | |
38 # ported from android/source/external/bluetooth/hcidump/parser/hci.c | |
39 | |
40 def evttype2str(etype): | |
41 return { | |
42 0x00: "ADV_IND", # Connectable undirected advertising | |
43 0x01: "ADV_DIRECT_IND", # Connectable directed advertising | |
44 0x02: "ADV_SCAN_IND", # Scannable undirected advertising | |
45 0x03: "ADV_NONCONN_IND", # Non connectable undirected advertising | |
46 0x04: "SCAN_RSP", # Scan Response | |
47 }.get(etype, "Reserved") | |
48 | |
49 def bdaddrtype2str(btype): | |
50 return { | |
51 0x00: "Public", | |
52 0x01: "Random", | |
53 }.get(btype, "Reserved") | |
54 | |
55 | |
56 # from https://github.com/google/eddystone/blob/master/eddystone-url/implementations/linux/scan-for-urls | |
57 | |
58 schemes = [ | |
59 "http://www.", | |
60 "https://www.", | |
61 "http://", | |
62 "https://", | |
63 ] | |
64 | |
65 extensions = [ | |
66 ".com/", ".org/", ".edu/", ".net/", ".info/", ".biz/", ".gov/", | |
67 ".com", ".org", ".edu", ".net", ".info", ".biz", ".gov", | |
68 ] | |
69 | |
70 def decodeUrl(encodedUrl): | |
71 """ | |
72 Decode a url encoded with the Eddystone (or UriBeacon) URL encoding scheme | |
73 """ | |
74 | |
75 decodedUrl = schemes[encodedUrl[0]] | |
76 for c in encodedUrl[1:]: | |
77 if c <= 0x20: | |
78 decodedUrl += extensions[c] | |
79 else: | |
80 decodedUrl += chr(c) | |
81 | |
82 return decodedUrl | |
83 | |
84 def decodeBeacon(data, row): | |
85 # this padding makes the offsets line up to the scan-for-urls code | |
86 padData = map(ord, '*' * 14 + data) | |
87 | |
88 # Eddystone | |
89 if len(padData) >= 20 and padData[19] == 0xaa and padData[20] == 0xfe: | |
90 serviceDataLength = padData[21] | |
91 frameType = padData[25] | |
92 | |
93 # Eddystone-URL | |
94 if frameType == 0x10: | |
95 row["Eddystone-URL"] = decodeUrl(padData[27:22 + serviceDataLength]) | |
96 elif frameType == 0x00: | |
97 row["Eddystone-UID"] = Binary(data) | |
98 elif frameType == 0x20: | |
99 row["Eddystone-TLM"] = Binary(data) | |
100 else: | |
101 row["Eddystone"] = "Unknown Eddystone frame type: %r data: %r" % (frameType, data) | |
102 | |
103 # UriBeacon | |
104 elif len(padData) >= 20 and padData[19] == 0xd8 and padData[20] == 0xfe: | |
105 serviceDataLength = padData[21] | |
106 row["UriBeacon"] = decodeUrl(padData[27:22 + serviceDataLength]) | |
107 | |
108 else: | |
109 pass # "Unknown beacon type" | |
110 | |
111 def decodeInquiryData(data, row): | |
112 # offset 19 is totally observed from data, not any spec. IDK if the preceding part is variable-length. | |
113 if len(data) > 20 and data[19] in ['\x08', '\x09']: | |
114 localName = data[20:] | |
115 if data[19] == '\x08': | |
116 row['local_name_shortened'] = Binary(localName) | |
117 else: | |
118 row['local_name_complete'] = Binary(localName) | |
119 # more at android/source/external/bluetooth/hcidump/parser/hci.c ext_inquiry_data_dump | |
120 | |
121 # from android/source/external/bluetooth/hcidump/parser/hci.c | |
1113 | 122 def evt_le_advertising_report_dump(frm): |
1096 | 123 num_reports = ord(frm[0]) |
124 frm = frm[1:] | |
125 | |
126 for i in range(num_reports): | |
127 fmt = 'B B 6B B' | |
1113 | 128 row = {} |
1096 | 129 |
130 evt_type, bdaddr_type, b5, b4, b3, b2, b1, b0, length = struct.unpack(fmt, frm[:struct.calcsize(fmt)]) | |
131 frm = frm[struct.calcsize(fmt):] | |
132 | |
133 row['addr'] = '%02x:%02x:%02x:%02x:%02x:%02x' % (b0, b1, b2, b3, b4, b5) | |
134 row['addr_type'] = bdaddrtype2str(bdaddr_type) | |
135 row['evt_type'] = evttype2str(evt_type) | |
136 | |
137 data = frm[:length] | |
138 frm = frm[length:] | |
139 row['data'] = Binary(data) | |
140 #row['data_hex'] = ' '.join('%02x' % ord(c) for c in data) | |
141 | |
142 decodeBeacon(data, row) | |
143 decodeInquiryData(data, row) | |
144 | |
145 row['rssi'], = struct.unpack('b', frm[-1]) | |
146 frm = frm[1:] | |
147 yield row | |
148 | |
149 | |
150 | |
1113 | 151 def parse_events(sock, loop_count, source, influx, coll, lastDoc): |
1096 | 152 old_filter = sock.getsockopt( bluez.SOL_HCI, bluez.HCI_FILTER, 14) |
153 flt = bluez.hci_filter_new() | |
154 bluez.hci_filter_all_events(flt) | |
155 bluez.hci_filter_set_ptype(flt, bluez.HCI_EVENT_PKT) | |
156 sock.setsockopt( bluez.SOL_HCI, bluez.HCI_FILTER, flt ) | |
157 | |
1113 | 158 points = [] |
1096 | 159 for i in range(0, loop_count): |
160 pkt = sock.recv(255) | |
161 ptype, event, plen = struct.unpack("BBB", pkt[:3]) | |
162 now = datetime.datetime.now(tzlocal()) | |
1113 | 163 nowMs = int(time.time() * 1000) |
1096 | 164 if event == bluez.EVT_INQUIRY_RESULT_WITH_RSSI: |
165 print "EVT_INQUIRY_RESULT_WITH_RSSI" | |
166 elif event == bluez.EVT_NUM_COMP_PKTS: | |
167 print "EVT_NUM_COMP_PKTS" | |
168 elif event == bluez.EVT_DISCONN_COMPLETE: | |
169 print "EVT_DISCONN_COMPLETE" | |
170 elif event == LE_META_EVENT: | |
171 subevent, = struct.unpack("B", pkt[3:4]) | |
172 pkt = pkt[4:] | |
173 if subevent == EVT_LE_CONN_COMPLETE: | |
174 pass | |
175 elif subevent == EVT_LE_ADVERTISING_REPORT: | |
1113 | 176 rows = list(evt_le_advertising_report_dump(pkt)) |
1096 | 177 for row in sorted(rows): |
1113 | 178 rssi = row.pop('rssi') |
1265
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
179 if row['addr_type'] == 'Public': # or, someday, if it's a device we know |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
180 points.append(dict( |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
181 measurement='rssi', |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
182 tags={'from': source, 'toAddr': row['addr']}, |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
183 fields={'value': rssi}, |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
184 time=nowMs, |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
185 )) |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
186 key = (row['addr'], row['evt_type']) |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
187 if lastDoc.get(key) != row: |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
188 # should check mongodb here- maybe another |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
189 # node already wrote this row |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
190 lastDoc.put(key, row) |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
191 row = row.copy() |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
192 row['t'] = now |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
193 coll.insert(row) |
1ddd1f057749
some old Measurement and Locator edits
drewp <drewp@bigasterisk.com>
parents:
1113
diff
changeset
|
194 |
1113 | 195 influx.write_points(points, time_precision='ms') |
1096 | 196 sock.setsockopt( bluez.SOL_HCI, bluez.HCI_FILTER, old_filter ) |
197 | |
198 | |
199 if __name__ == '__main__': | |
200 mongoHost, myLocation = sys.argv[1:] | |
201 | |
1113 | 202 influx = InfluxDBClient(mongoHost, 9060, 'root', 'root', 'beacon') |
1096 | 203 client = MongoClient(mongoHost) |
1113 | 204 coll = client['beacon']['data'] |
1096 | 205 |
206 dev_id = 0 | |
207 sock = bluez.hci_open_dev(dev_id) | |
208 sock.getsockopt( bluez.SOL_HCI, bluez.HCI_FILTER, 14) | |
209 | |
210 hci_enable_le_scan(sock) | |
211 | |
1113 | 212 lastDoc = LRUCache(1000) # (addr, evt_type) : data row |
1096 | 213 while True: |
1113 | 214 parse_events(sock, 10, source=myLocation, coll=coll, influx=influx, lastDoc=lastDoc) |