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')
|
|
179 points.append(dict(
|
|
180 measurement='rssi',
|
|
181 tags={'from': source, 'toAddr': row['addr']},
|
|
182 fields={'value': rssi},
|
|
183 time=nowMs,
|
|
184 ))
|
|
185 key = (row['addr'], row['evt_type'])
|
|
186 if lastDoc.get(key) != row:
|
|
187 # should check mongodb here- maybe another
|
|
188 # node already wrote this row
|
|
189 lastDoc.put(key, row)
|
|
190 row = row.copy()
|
|
191 row['t'] = now
|
|
192 coll.insert(row)
|
|
193
|
|
194 influx.write_points(points, time_precision='ms')
|
1096
|
195 sock.setsockopt( bluez.SOL_HCI, bluez.HCI_FILTER, old_filter )
|
|
196
|
|
197
|
|
198 if __name__ == '__main__':
|
|
199 mongoHost, myLocation = sys.argv[1:]
|
|
200
|
1113
|
201 influx = InfluxDBClient(mongoHost, 9060, 'root', 'root', 'beacon')
|
1096
|
202 client = MongoClient(mongoHost)
|
1113
|
203 coll = client['beacon']['data']
|
1096
|
204
|
|
205 dev_id = 0
|
|
206 sock = bluez.hci_open_dev(dev_id)
|
|
207 sock.getsockopt( bluez.SOL_HCI, bluez.HCI_FILTER, 14)
|
|
208
|
|
209 hci_enable_le_scan(sock)
|
|
210
|
1113
|
211 lastDoc = LRUCache(1000) # (addr, evt_type) : data row
|
1096
|
212 while True:
|
1113
|
213 parse_events(sock, 10, source=myLocation, coll=coll, influx=influx, lastDoc=lastDoc)
|