Merge ~sylvain-pineau/checkbox-support:eddystone-scanner into checkbox-support:master
- Git
- lp:~sylvain-pineau/checkbox-support
- eddystone-scanner
- Merge into master
Proposed by
Sylvain Pineau
Status: | Merged |
---|---|
Approved by: | Sylvain Pineau |
Approved revision: | 6b02a8bfe37b28f69054617a78da4866006047d1 |
Merged at revision: | 52937e937a410bc379b56b8b5f4d5f554d381a68 |
Proposed branch: | ~sylvain-pineau/checkbox-support:eddystone-scanner |
Merge into: | checkbox-support:master |
Diff against target: |
1794 lines (+1748/-0) 7 files modified
checkbox_support/scripts/eddystone_scanner.py (+87/-0) checkbox_support/vendor/__init__.py (+0/-0) checkbox_support/vendor/aioblescan/LICENSE.txt (+20/-0) checkbox_support/vendor/aioblescan/__init__.py (+2/-0) checkbox_support/vendor/aioblescan/aioblescan.py (+1275/-0) checkbox_support/vendor/aioblescan/eddystone.py (+362/-0) setup.py (+2/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sylvain Pineau (community) | Approve | ||
Review via email: mp+356229@code.launchpad.net |
Commit message
Description of the change
New console script to scan for Eddystone URL beacon advertisements.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/checkbox_support/scripts/eddystone_scanner.py b/checkbox_support/scripts/eddystone_scanner.py |
2 | new file mode 100644 |
3 | index 0000000..3ee398a |
4 | --- /dev/null |
5 | +++ b/checkbox_support/scripts/eddystone_scanner.py |
6 | @@ -0,0 +1,87 @@ |
7 | +#!/usr/bin/env python3 |
8 | +# encoding: UTF-8 |
9 | +# Copyright (c) 2018 Canonical Ltd. |
10 | +# |
11 | +# Authors: |
12 | +# Sylvain Pineau <sylvain.pineau@canonical.com> |
13 | +# |
14 | +# This program is free software: you can redistribute it and/or modify |
15 | +# it under the terms of the GNU General Public License as published by |
16 | +# the Free Software Foundation, either version 3 of the License, or |
17 | +# (at your option) any later version. |
18 | +# |
19 | +# This program is distributed in the hope that it will be useful, |
20 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
21 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
22 | +# GNU General Public License for more details. |
23 | +# |
24 | +# You should have received a copy of the GNU General Public License |
25 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
26 | + |
27 | +import argparse |
28 | +import asyncio |
29 | + |
30 | +from checkbox_support.vendor.aioblescan import create_bt_socket |
31 | +from checkbox_support.vendor.aioblescan import BLEScanRequester |
32 | +from checkbox_support.vendor.aioblescan import HCI_Cmd_LE_Advertise |
33 | +from checkbox_support.vendor.aioblescan import HCI_Event |
34 | +from checkbox_support.vendor.aioblescan.eddystone import EddyStone |
35 | + |
36 | + |
37 | +def main(): |
38 | + parser = argparse.ArgumentParser( |
39 | + description="Track BLE advertised packets") |
40 | + parser.add_argument("-D", "--device", default='hci0', |
41 | + help="Select the hciX device to use " |
42 | + "(default hci0).") |
43 | + |
44 | + async def timeout(): |
45 | + await asyncio.sleep(10.0) |
46 | + |
47 | + def ble_process(data): |
48 | + ev = HCI_Event() |
49 | + ev.decode(data) |
50 | + advertisement = EddyStone().decode(ev) |
51 | + if advertisement: |
52 | + print("EddyStone URL: {}".format(advertisement['url'])) |
53 | + for task in asyncio.Task.all_tasks(): |
54 | + task.cancel() |
55 | + |
56 | + try: |
57 | + opts = parser.parse_args() |
58 | + except Exception as e: |
59 | + parser.error("Error: " + str(e)) |
60 | + return 1 |
61 | + event_loop = asyncio.get_event_loop() |
62 | + # First create and configure a STREAM socket |
63 | + try: |
64 | + mysocket = create_bt_socket(int(opts.device.replace('hci', ''))) |
65 | + except OSError as e: |
66 | + print(e) |
67 | + return 1 |
68 | + # Create a connection with the STREAM socket |
69 | + fac = event_loop._create_connection_transport( |
70 | + mysocket, BLEScanRequester, None, None) |
71 | + # Start it |
72 | + conn, btctrl = event_loop.run_until_complete(fac) |
73 | + # Attach processing |
74 | + btctrl.process = ble_process |
75 | + # Probe |
76 | + btctrl.send_scan_request() |
77 | + try: |
78 | + event_loop.run_until_complete(timeout()) |
79 | + return 1 |
80 | + except asyncio.CancelledError: |
81 | + return 0 |
82 | + except KeyboardInterrupt: |
83 | + return 1 |
84 | + finally: |
85 | + btctrl.stop_scan_request() |
86 | + command = HCI_Cmd_LE_Advertise(enable=False) |
87 | + btctrl.send_command(command) |
88 | + conn.close() |
89 | + event_loop.close() |
90 | + |
91 | + |
92 | +if __name__ == '__main__': |
93 | + raise SystemExit(main()) |
94 | diff --git a/checkbox_support/vendor/__init__.py b/checkbox_support/vendor/__init__.py |
95 | new file mode 100644 |
96 | index 0000000..e69de29 |
97 | --- /dev/null |
98 | +++ b/checkbox_support/vendor/__init__.py |
99 | diff --git a/checkbox_support/vendor/aioblescan/LICENSE.txt b/checkbox_support/vendor/aioblescan/LICENSE.txt |
100 | new file mode 100644 |
101 | index 0000000..cb44740 |
102 | --- /dev/null |
103 | +++ b/checkbox_support/vendor/aioblescan/LICENSE.txt |
104 | @@ -0,0 +1,20 @@ |
105 | +The MIT License (MIT) |
106 | + |
107 | +Copyright © 2017 François Wautier |
108 | + |
109 | +Permission is hereby granted, free of charge, to any person obtaining a copy of this |
110 | +oftware and associated documentation files (the "Software"), to deal in the Software |
111 | +without restriction, including without limitation the rights to use, copy, modify, |
112 | +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to |
113 | +permit persons to whom the Software is furnished to do so, subject to the following |
114 | +conditions: |
115 | + |
116 | +The above copyright notice and this permission notice shall be included in all copies |
117 | +or substantial portions of the Software. |
118 | + |
119 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
120 | +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR |
121 | +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE |
122 | +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT ORxi |
123 | +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
124 | +DEALINGS IN THE SOFTWARE. |
125 | diff --git a/checkbox_support/vendor/aioblescan/__init__.py b/checkbox_support/vendor/aioblescan/__init__.py |
126 | new file mode 100644 |
127 | index 0000000..5fdf345 |
128 | --- /dev/null |
129 | +++ b/checkbox_support/vendor/aioblescan/__init__.py |
130 | @@ -0,0 +1,2 @@ |
131 | +from .aioblescan import * |
132 | +__version__ = '0.2.1' |
133 | diff --git a/checkbox_support/vendor/aioblescan/aioblescan.py b/checkbox_support/vendor/aioblescan/aioblescan.py |
134 | new file mode 100644 |
135 | index 0000000..b0d9d0d |
136 | --- /dev/null |
137 | +++ b/checkbox_support/vendor/aioblescan/aioblescan.py |
138 | @@ -0,0 +1,1275 @@ |
139 | +#!/usr/bin/env python3 |
140 | +# -*- coding:utf-8 -*- |
141 | +# |
142 | +# This application is simply a python only Bluetooth LE Scan command with |
143 | +# decoding of advertised packets |
144 | +# |
145 | +# Copyright (c) 2017 François Wautier |
146 | +# |
147 | +# Note large part of this code was taken from scapy and other opensource software |
148 | +# |
149 | +# Permission is hereby granted, free of charge, to any person obtaining a copy |
150 | +# of this software and associated documentation files (the "Software"), to deal |
151 | +# in the Software without restriction, including without limitation the rights |
152 | +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
153 | +# of the Software, and to permit persons to whom the Software is furnished to do so, |
154 | +# subject to the following conditions: |
155 | +# |
156 | +# The above copyright notice and this permission notice shall be included in all copies |
157 | +# or substantial portions of the Software. |
158 | +# |
159 | +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
160 | +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
161 | +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
162 | +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
163 | +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR |
164 | +# IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE |
165 | + |
166 | +import socket, asyncio, sys |
167 | +from struct import pack, unpack, calcsize |
168 | + |
169 | + |
170 | +#A little bit of HCI |
171 | +HCI_COMMAND = 0x01 |
172 | +HCI_ACL_DATA = 0x02 |
173 | +HCI_SCO_DATA = 0x03 |
174 | +HCI_EVENT = 0x04 |
175 | +HCI_VENDOR = 0x05 |
176 | + |
177 | +PRINT_INDENT=" " |
178 | + |
179 | +CMD_SCAN_REQUEST = 0x200c #mixing the OGF in with that HCI shift |
180 | + |
181 | +# |
182 | +EDDY_UUID=b"\xfe\xaa" #Google UUID |
183 | + |
184 | +#Generated from https://www.uuidgenerator.net/ 906ed6ab-6785-4eab-9847-bf9889c098ae alternative is 668997f8-4acd-48ea-b35b-749e54215860 |
185 | +MY_UUID = b'\x90\x6e\xd6\xab\x67\x85\x4e\xab\x98\x47\xbf\x98\x89\xc0\x98\xae' |
186 | +#MY_UUID = b'\x66\x89\x97\xf8\x4a\xcd\x48\xea\xb3\x5b\x74\x9e\x54\x21\x58\x60' |
187 | +# |
188 | +# Let's define some useful types |
189 | +# |
190 | +class MACAddr: |
191 | + """Class representing a MAC address. |
192 | + |
193 | + :param name: The name of the instance |
194 | + :type name: str |
195 | + :param mac: the mac address. |
196 | + :type mac: str |
197 | + :returns: MACAddr instance. |
198 | + :rtype: MACAddr |
199 | + |
200 | + """ |
201 | + def __init__(self,name,mac="00:00:00:00:00:00"): |
202 | + self.name = name |
203 | + self.val=mac.lower() |
204 | + |
205 | + def encode (self): |
206 | + """Encode the MAC address to a byte array. |
207 | + |
208 | + :returns: The encoded version of the MAC address |
209 | + :rtype: bytes |
210 | + """ |
211 | + return int(self.val.replace(":",""),16).to_bytes(6,"little") |
212 | + |
213 | + def decode(self,data): |
214 | + """Decode the MAC address from a byte array. |
215 | + |
216 | + This will take the first 6 bytes from data and transform them into a MAC address |
217 | + string representation. This will be assigned to the attribute "val". It then returns |
218 | + the data stream minus the bytes consumed |
219 | + |
220 | + :param data: The data stream containing the value to decode at its head |
221 | + :type data: bytes |
222 | + :returns: The datastream minus the bytes consumed |
223 | + :rtype: bytes |
224 | + """ |
225 | + self.val=':'.join("%02x" % x for x in reversed(data[:6])) |
226 | + return data[6:] |
227 | + |
228 | + def __len__(self): |
229 | + return 6 |
230 | + |
231 | + def show(self,depth=0): |
232 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
233 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.val)) |
234 | + |
235 | +class Bool: |
236 | + """Class representing a boolean value. |
237 | + |
238 | + :param name: The name of the instance |
239 | + :type name: str |
240 | + :param val: the boolean value. |
241 | + :type mac: bool |
242 | + :returns: Bool instance. |
243 | + :rtype: Bool |
244 | + |
245 | + """ |
246 | + def __init__(self,name,val=True): |
247 | + self.name=name |
248 | + self.val=val |
249 | + |
250 | + def encode (self): |
251 | + val=(self.val and b'\x01') or b'\x00' |
252 | + return val |
253 | + |
254 | + def decode(self,data): |
255 | + self.val= data[:1]==b"\x01" |
256 | + return data[1:] |
257 | + |
258 | + def __len__(self): |
259 | + return 1 |
260 | + |
261 | + def show(self,depth=0): |
262 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
263 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.val)) |
264 | + |
265 | +class Byte: |
266 | + """Class representing a single byte value. |
267 | + |
268 | + :param name: The name of the instance |
269 | + :type name: str |
270 | + :param val: the single byte value. |
271 | + :type val: byte |
272 | + :returns: Byte instance. |
273 | + :rtype: Byte |
274 | + |
275 | + """ |
276 | + def __init__(self,name,val=0): |
277 | + self.name=name |
278 | + self.val=val |
279 | + |
280 | + def encode (self): |
281 | + val=pack("<c",self.val) |
282 | + return val |
283 | + |
284 | + def decode(self,data): |
285 | + self.val= unpack("<c",data[:1])[0] |
286 | + return data[1:] |
287 | + |
288 | + def __len__(self): |
289 | + return 1 |
290 | + |
291 | + def show(self,depth=0): |
292 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
293 | + print("{}{}".format(PRINT_INDENT*(depth+1),":".join(map(lambda b: format(b, "02x"), self.val)))) |
294 | + |
295 | +class EnumByte: |
296 | + """Class representing a single byte value from a limited set of value |
297 | + |
298 | + :param name: The name of the instance |
299 | + :type name: str |
300 | + :param val: the single byte value. |
301 | + :type val: byte |
302 | + :param loval: the list of possible values. |
303 | + :type loval: dict |
304 | + :returns: EnumByte instance. |
305 | + :rtype: EnumByte |
306 | + |
307 | + """ |
308 | + def __init__(self,name,val=0,loval={0:"Undef"}): |
309 | + self.name=name |
310 | + self.val=val |
311 | + self.loval=loval |
312 | + |
313 | + def encode (self): |
314 | + val=pack(">B",self.val) |
315 | + return val |
316 | + |
317 | + def decode(self,data): |
318 | + self.val= unpack(">B",data[:1])[0] |
319 | + return data[1:] |
320 | + |
321 | + @property |
322 | + def strval(self): |
323 | + if self.val in self.loval: |
324 | + return self.loval[self.val] |
325 | + else: |
326 | + return str(self.val) |
327 | + |
328 | + def __len__(self): |
329 | + return 1 |
330 | + |
331 | + def show(self,depth=0): |
332 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
333 | + if self.val in self.loval: |
334 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.loval[self.val])) |
335 | + else: |
336 | + print("{}Undef".format(PRINT_INDENT*(depth+1))) |
337 | + |
338 | +class BitFieldByte: |
339 | + """Class representing a single byte value as a bit field. |
340 | + |
341 | + :param name: The name of the instance |
342 | + :type name: str |
343 | + :param val: the single byte value. |
344 | + :type val: byte |
345 | + :param loval: the list defining the name of the property represented by each bit. |
346 | + :type loval: list |
347 | + :returns: BitFieldByte instance. |
348 | + :rtype: BitFieldByte |
349 | + |
350 | + """ |
351 | + def __init__(self,name,val=0,loval=["Undef"]*8): |
352 | + self.name=name |
353 | + self._val=val |
354 | + self.loval=loval |
355 | + |
356 | + def encode (self): |
357 | + val=pack(">B",self._val) |
358 | + return val |
359 | + |
360 | + def decode(self,data): |
361 | + self._val= unpack(">B",data[:1])[0] |
362 | + return data[1:] |
363 | + |
364 | + def __len__(self): |
365 | + return 1 |
366 | + |
367 | + @property |
368 | + def val(self): |
369 | + resu={} |
370 | + for x in self.loval: |
371 | + if x not in ["Undef","Reserv"]: |
372 | + resu[x]=(self._val & mybit)>0 |
373 | + return resu |
374 | + |
375 | + def show(self,depth=0): |
376 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
377 | + mybit=0x80 |
378 | + for x in self.loval: |
379 | + if x not in ["Undef","Reserv"]: |
380 | + print("{}{}: {}".format(PRINT_INDENT*(depth+1),x, ((self._val & mybit) and "True") or False)) |
381 | + mybit = mybit >>1 |
382 | + |
383 | +class IntByte: |
384 | + """Class representing a single byte as a signed integer. |
385 | + |
386 | + :param name: The name of the instance |
387 | + :type name: str |
388 | + :param val: the integer value. |
389 | + :type val: int |
390 | + :returns: IntByte instance. |
391 | + :rtype: IntByte |
392 | + |
393 | + """ |
394 | + def __init__(self,name,val=0): |
395 | + self.name=name |
396 | + self.val=val |
397 | + |
398 | + def encode (self): |
399 | + val=pack(">b",self.val) |
400 | + return val |
401 | + |
402 | + def decode(self,data): |
403 | + self.val= unpack(">b",data[:1])[0] |
404 | + return data[1:] |
405 | + |
406 | + def __len__(self): |
407 | + return 1 |
408 | + |
409 | + def show(self,depth=0): |
410 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
411 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.val)) |
412 | + |
413 | +class UIntByte: |
414 | + """Class representing a single byte as an unsigned integer. |
415 | + |
416 | + :param name: The name of the instance |
417 | + :type name: str |
418 | + :param val: the integer value. |
419 | + :type val: int |
420 | + :returns: UIntByte instance. |
421 | + :rtype: UIntByte |
422 | + |
423 | + """ |
424 | + def __init__(self,name,val=0): |
425 | + self.name=name |
426 | + self.val=val |
427 | + |
428 | + def encode (self): |
429 | + val=pack(">B",self.val) |
430 | + return val |
431 | + |
432 | + def decode(self,data): |
433 | + self.val= unpack(">B",data[:1])[0] |
434 | + return data[1:] |
435 | + |
436 | + def __len__(self): |
437 | + return 1 |
438 | + |
439 | + def show(self,depth=0): |
440 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
441 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.val)) |
442 | + |
443 | +class ShortInt: |
444 | + """Class representing 2 bytes as a signed integer. |
445 | + |
446 | + :param name: The name of the instance |
447 | + :type name: str |
448 | + :param val: the integer value. |
449 | + :type val: int |
450 | + :param endian: Endianess of the bytes. "big" or no "big" (i.e. "little") |
451 | + :type endian: str |
452 | + :returns: ShortInt instance. |
453 | + :rtype: ShortInt |
454 | + |
455 | + """ |
456 | + def __init__(self,name,val=0,endian="big"): |
457 | + self.name=name |
458 | + self.val=val |
459 | + self.endian = endian |
460 | + |
461 | + def encode (self): |
462 | + if self.endian == "big": |
463 | + val=pack(">h",self.val) |
464 | + else: |
465 | + val=pack("<h",self.val) |
466 | + return val |
467 | + |
468 | + def decode(self,data): |
469 | + if self.endian == "big": |
470 | + self.val= unpack(">h",data[:2])[0] |
471 | + else: |
472 | + self.val= unpack("<h",data[:2])[0] |
473 | + return data[2:] |
474 | + |
475 | + def __len__(self): |
476 | + return 2 |
477 | + |
478 | + def show(self,depth=0): |
479 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
480 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.val)) |
481 | + |
482 | +class UShortInt: |
483 | + """Class representing 2 bytes as an unsigned integer. |
484 | + |
485 | + :param name: The name of the instance |
486 | + :type name: str |
487 | + :param val: the integer value. |
488 | + :type val: int |
489 | + :param endian: Endianess of the bytes. "big" or no "big" (i.e. "little") |
490 | + :type endian: str |
491 | + :returns: UShortInt instance. |
492 | + :rtype: UShortInt |
493 | + |
494 | + """ |
495 | + def __init__(self,name,val=0,endian="big"): |
496 | + self.name=name |
497 | + self.val=val |
498 | + self.endian = endian |
499 | + |
500 | + def encode (self): |
501 | + if self.endian == "big": |
502 | + val=pack(">H",self.val) |
503 | + else: |
504 | + val=pack("<H",self.val) |
505 | + return val |
506 | + |
507 | + def decode(self,data): |
508 | + if self.endian == "big": |
509 | + self.val= unpack(">H",data[:2])[0] |
510 | + else: |
511 | + self.val= unpack("<H",data[:2])[0] |
512 | + return data[2:] |
513 | + |
514 | + def __len__(self): |
515 | + return 2 |
516 | + |
517 | + def show(self,depth=0): |
518 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
519 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.val)) |
520 | + |
521 | +class LongInt: |
522 | + """Class representing 4 bytes as a signed integer. |
523 | + |
524 | + :param name: The name of the instance |
525 | + :type name: str |
526 | + :param val: the integer value. |
527 | + :type val: int |
528 | + :param endian: Endianess of the bytes. "big" or no "big" (i.e. "little") |
529 | + :type endian: str |
530 | + :returns: LongInt instance. |
531 | + :rtype: LongInt |
532 | + |
533 | + """ |
534 | + def __init__(self,name,val=0,endian="big"): |
535 | + self.name=name |
536 | + self.val=val |
537 | + self.endian = endian |
538 | + |
539 | + def encode (self): |
540 | + if self.endian == "big": |
541 | + val=pack(">l",self.val) |
542 | + else: |
543 | + val=pack("<l",self.val) |
544 | + return val |
545 | + |
546 | + def decode(self,data): |
547 | + if self.endian == "big": |
548 | + self.val= unpack(">l",data[:4])[0] |
549 | + else: |
550 | + self.val= unpack("<l",data[:4])[0] |
551 | + return data[4:] |
552 | + |
553 | + def __len__(self): |
554 | + return 4 |
555 | + |
556 | + def show(self,depth=0): |
557 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
558 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.val)) |
559 | + |
560 | +class ULongInt: |
561 | + """Class representing 4 bytes as an unsigned integer. |
562 | + |
563 | + :param name: The name of the instance |
564 | + :type name: str |
565 | + :param val: the integer value. |
566 | + :type val: int |
567 | + :param endian: Endianess of the bytes. "big" or no "big" (i.e. "little") |
568 | + :type endian: str |
569 | + :returns: ULongInt instance. |
570 | + :rtype: ULongInt |
571 | + |
572 | + """ |
573 | + def __init__(self,name,val=0,endian="big"): |
574 | + self.name=name |
575 | + self.val=val |
576 | + self.endian = endian |
577 | + |
578 | + def encode (self): |
579 | + if self.endian == "big": |
580 | + val=pack(">L",self.val) |
581 | + else: |
582 | + val=pack("<L",self.val) |
583 | + return val |
584 | + |
585 | + def decode(self,data): |
586 | + if self.endian == "big": |
587 | + self.val= unpack(">L",data[:4])[0] |
588 | + else: |
589 | + self.val= unpack("<L",data[:4])[0] |
590 | + return data[4:] |
591 | + |
592 | + def __len__(self): |
593 | + return 4 |
594 | + |
595 | + def show(self,depth=0): |
596 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
597 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.val)) |
598 | + |
599 | +class OgfOcf: |
600 | + """Class representing the 2 bytes that specify the command in an HCI command packet. |
601 | + |
602 | + :param name: The name of the instance |
603 | + :type name: str |
604 | + :param ogf: the Op-code Group (6 bits). |
605 | + :type ogf: bytes |
606 | + :param ocf: the Op-code Command (10 bits). |
607 | + :type ocf: bytes |
608 | + :returns: OgfOcf instance. |
609 | + :rtype: OgfOcf |
610 | + |
611 | + """ |
612 | + def __init__(self,name,ogf=b"\x00",ocf=b"\x00"): |
613 | + self.name=name |
614 | + self.ogf= ogf |
615 | + self.ocf= ocf |
616 | + |
617 | + def encode (self): |
618 | + val=pack("<H",(ord(self.ogf) << 10) | ord(self.ocf)) |
619 | + return val |
620 | + |
621 | + def decode(self,data): |
622 | + val = unpack("<H",data[:len(self)])[0] |
623 | + self.ogf =val>>10 |
624 | + self.ocf = int(val - (self.ogf<<10)).to_bytes(1,"big") |
625 | + self.ogf = int(self.ogf).to_bytes(1,"big") |
626 | + return data[len(self):] |
627 | + |
628 | + def __len__(self): |
629 | + return calcsize("<H") |
630 | + |
631 | + def show(self,depth=0): |
632 | + print("{}Cmd Group:".format(PRINT_INDENT*depth)) |
633 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.ogf)) |
634 | + print("{}Cmd Code:".format(PRINT_INDENT*depth)) |
635 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.ocf)) |
636 | + |
637 | +class Itself: |
638 | + """Class representing a byte array that need no manipulation. |
639 | + |
640 | + :param name: The name of the instance |
641 | + :type name: str |
642 | + :returns: Itself instance. |
643 | + :rtype: Itself |
644 | + |
645 | + """ |
646 | + def __init__(self,name): |
647 | + self.name=name |
648 | + self.val=b"" |
649 | + |
650 | + def encode(self): |
651 | + val=pack(">%ds"%len(self.val),self.val) |
652 | + return val |
653 | + |
654 | + def decode(self,data): |
655 | + self.val=unpack(">%ds"%len(data),data)[0] |
656 | + return b"" |
657 | + |
658 | + def __len__(self): |
659 | + return len(self.val) |
660 | + |
661 | + def show(self,depth=0): |
662 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
663 | + print("{}{}".format(PRINT_INDENT*(depth+1),":".join(map(lambda b: format(b, "02x"), self.val)))) |
664 | + |
665 | +class String: |
666 | + """Class representing a string. |
667 | + |
668 | + :param name: The name of the instance |
669 | + :type name: str |
670 | + :returns: String instance. |
671 | + :rtype: String |
672 | + |
673 | + """ |
674 | + def __init__(self,name): |
675 | + self.name=name |
676 | + self.val="" |
677 | + |
678 | + def encode(self): |
679 | + if isinstance(self.val,str): |
680 | + self.val = self.val.encode() |
681 | + val=pack(">%ds"%len(self.val),self.val) |
682 | + return val |
683 | + |
684 | + def decode(self,data): |
685 | + self.val=data |
686 | + return b"" |
687 | + |
688 | + def __len__(self): |
689 | + return len(self.val) |
690 | + |
691 | + def show(self,depth=0): |
692 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
693 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.val)) |
694 | + |
695 | + |
696 | +class NBytes: |
697 | + """Class representing a byte string. |
698 | + |
699 | + :param name: The name of the instance |
700 | + :type name: str |
701 | + :param length: The length |
702 | + :type length: int |
703 | + :returns: NBytes instance. |
704 | + :rtype: NBytes |
705 | + |
706 | + """ |
707 | + def __init__(self,name,length=2): |
708 | + self.name=name |
709 | + self.length=length |
710 | + self.val=b"" |
711 | + |
712 | + def encode(self): |
713 | + val=pack(">%ds"%len(self.length),self.val) |
714 | + return val |
715 | + |
716 | + def decode(self,data): |
717 | + self.val=unpack(">%ds"%self.length,data[:self.length])[0][::-1] |
718 | + return data[self.length:] |
719 | + |
720 | + def __len__(self): |
721 | + return self.length |
722 | + |
723 | + def show(self,depth=0): |
724 | + if self.name: |
725 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
726 | + print("{}{}".format(PRINT_INDENT*(depth+1),":".join(map(lambda b: format(b, "02x"), self.val)))) |
727 | + |
728 | + def __eq__(self,b): |
729 | + return self.val==b |
730 | + |
731 | +class NBytes_List: |
732 | + """Class representing a list of bytes string. |
733 | + |
734 | + :param name: The name of the instance |
735 | + :type name: str |
736 | + :param bytes: Length of the bytes strings (2, 4 or 16) |
737 | + :type bytes: int |
738 | + :returns: NBytes_List instance. |
739 | + :rtype: NBytes_List |
740 | + |
741 | + """ |
742 | + def __init__(self,name,bytes=2): |
743 | + #Bytes should be one of 2, 4 or 16 |
744 | + self.name=name |
745 | + self.length=bytes |
746 | + self.lonbytes = [] |
747 | + |
748 | + def decode(self,data): |
749 | + while data: |
750 | + mynbyte=NBytes("",self.length) |
751 | + data=mynbyte.decode(data) |
752 | + self.lonbytes.append(mynbyte) |
753 | + return data |
754 | + |
755 | + def show(self,depth=0): |
756 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
757 | + for x in self.lonbytes: |
758 | + x.show(depth+1) |
759 | + |
760 | + def __len__(self): |
761 | + return len(self.lonbytes)+self.length |
762 | + |
763 | + def __contains__(self,b): |
764 | + for x in self.lonbytes: |
765 | + if b == x: |
766 | + return True |
767 | + |
768 | + return False |
769 | + |
770 | +class Float88: |
771 | + """Class representing a 8.8 fixed point quantity. |
772 | + |
773 | + :param name: The name of the instance |
774 | + :type name: str |
775 | + :returns: Float88 instance. |
776 | + :rtype: Float88 |
777 | + |
778 | + """ |
779 | + def __init__(self,name): |
780 | + self.name=name |
781 | + self.val=0.0 |
782 | + |
783 | + def encode (self): |
784 | + val=pack(">h",int(self.val*256)) |
785 | + return val |
786 | + |
787 | + def decode(self,data): |
788 | + self.val= unpack(">h",data)[0]/256.0 |
789 | + return data[2:] |
790 | + def __len__(self): |
791 | + return 2 |
792 | + |
793 | + def show(self,depth=0): |
794 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
795 | + print("{}{}".format(PRINT_INDENT*(depth+1),self.val)) |
796 | + |
797 | + |
798 | + |
799 | + |
800 | +class EmptyPayload: |
801 | + def __init__(self): |
802 | + pass |
803 | + |
804 | + def encode(self): |
805 | + return b"" |
806 | + |
807 | + def decode(self,data): |
808 | + return data |
809 | + |
810 | + def __len__(self): |
811 | + return 0 |
812 | + |
813 | + def show(self,depth=0): |
814 | + return |
815 | + |
816 | +# |
817 | +# Bluetooth starts here |
818 | +# |
819 | + |
820 | +class Packet: |
821 | + """Class representing a generic HCI packet. |
822 | + |
823 | + :param header: The packet header. |
824 | + :type header: bytes |
825 | + :returns: Packet instance. |
826 | + :rtype: Packet |
827 | + |
828 | + """ |
829 | + """A generic packet that will be build fromparts""" |
830 | + def __init__(self, header="\x00", fmt=">B"): |
831 | + self.header = header |
832 | + self.fmt = fmt |
833 | + self.payload=[] |
834 | + self.raw_data=None |
835 | + |
836 | + def encode (self) : |
837 | + return pack(self.fmt, self.header) |
838 | + |
839 | + def decode (self, data): |
840 | + try: |
841 | + if unpack(self.fmt,data[:calcsize(self.fmt)])[0] == self.header: |
842 | + self.raw_data=data |
843 | + return data[calcsize(self.fmt):] |
844 | + except: |
845 | + pass |
846 | + return None |
847 | + |
848 | + def retrieve(self,aclass): |
849 | + """Look for a specifc class/name in the packet""" |
850 | + resu=[] |
851 | + for x in self.payload: |
852 | + try: |
853 | + if isinstance(aclass,str): |
854 | + if x.name == aclass: |
855 | + resu.append(x) |
856 | + else: |
857 | + if isinstance(x,aclass): |
858 | + resu.append(x) |
859 | + |
860 | + resu+=x.retrieve(aclass) |
861 | + except: |
862 | + pass |
863 | + return resu |
864 | +# |
865 | +# Commands |
866 | +# |
867 | + |
868 | +class HCI_Command(Packet): |
869 | + """Class representing a command HCI packet. |
870 | + |
871 | + :param ogf: the Op-code Group (6 bits). |
872 | + :type ogf: bytes |
873 | + :param ocf: the Op-code Command (10 bits). |
874 | + :type ocf: bytes |
875 | + :returns: HCI_Command instance. |
876 | + :rtype: HCI_Command |
877 | + |
878 | + """ |
879 | + |
880 | + def __init__(self,ogf,ocf): |
881 | + super().__init__(HCI_COMMAND) |
882 | + self.cmd = OgfOcf("command",ogf,ocf) |
883 | + self.payload = [] |
884 | + |
885 | + def encode(self): |
886 | + pld=b"" |
887 | + for x in self.payload: |
888 | + pld+=x.encode() |
889 | + plen=len(pld) |
890 | + pld=b"".join([super().encode(),self.cmd.encode(),pack(">B",plen),pld]) |
891 | + return pld |
892 | + |
893 | + def show(self,depth=0): |
894 | + self.cmd.show(depth) |
895 | + for x in self.payload: |
896 | + x.show(depth+1) |
897 | + |
898 | +class HCI_Cmd_LE_Scan_Enable(HCI_Command): |
899 | + """Class representing a command HCI command to enable/disable BLE scanning. |
900 | + |
901 | + :param enable: enable/disable scanning. |
902 | + :type enable: bool |
903 | + :param filter_dups: filter duplicates. |
904 | + :type filter_dups: bool |
905 | + :returns: HCI_Cmd_LE_Scan_Enable instance. |
906 | + :rtype: HCI_Cmd_LE_Scan_Enable |
907 | + |
908 | + """ |
909 | + |
910 | + def __init__(self,enable=True,filter_dups=True): |
911 | + super(self.__class__, self).__init__(b"\x08",b"\x0c") |
912 | + self.payload.append(Bool("enable",enable)) |
913 | + self.payload.append(Bool("filter",filter_dups)) |
914 | + |
915 | +class HCI_Cmd_LE_Set_Scan_Params(HCI_Command): |
916 | + """Class representing an HCI command to set the scanning parameters. |
917 | + |
918 | + This will set a number of parameters related to the scanning functions. For the |
919 | + interval and window, it will always silently enforce the Specs that says it should be >= 2.5 ms |
920 | + and <= 10.24s. It will also silently enforce window <= interval |
921 | + |
922 | + :param scan_type: Type of scanning. 0 => Passive (default) |
923 | + 1 => Active |
924 | + :type scan_type: int |
925 | + :param interval: Time in ms between the start of a scan and the next scan start. Default 10 |
926 | + :type interval: int/float |
927 | + :param window: maximum advertising interval in ms. Default 10 |
928 | + :type window: int.float |
929 | + :param oaddr_type: Type of own address Value 0 => public (default) |
930 | + 1 => Random |
931 | + 2 => Private with public fallback |
932 | + 3 => Private with random fallback |
933 | + :type oaddr_type: int |
934 | + :param filter: How white list filter is applied. 0 => No filter (Default) |
935 | + 1 => sender must be in white list |
936 | + 2 => Similar to 0. Some directed advertising may be received. |
937 | + 3 => Similar to 1. Some directed advertising may be received. |
938 | + :type filter: int |
939 | + :returns: HCI_Cmd_LE_Scan_Params instance. |
940 | + :rtype: HCI_Cmd_LE_Scan_Params |
941 | + |
942 | + """ |
943 | + |
944 | + def __init__(self,scan_type=0x0,interval=10, window=750, oaddr_type=0,filter=0): |
945 | + |
946 | + super(self.__class__, self).__init__(b"\x08",b"\x0b") |
947 | + self.payload.append(EnumByte("scan type",scan_type, |
948 | + {0: "Passive", |
949 | + 1: "Active"})) |
950 | + self.payload.append(UShortInt("Interval",int(round(min(10240,max(2.5,interval))/0.625)),endian="little")) |
951 | + self.payload.append(UShortInt("Window",int(round(min(10240,max(2.5,min(interval,window)))/0.625)),endian="little")) |
952 | + self.payload.append(EnumByte("own addresss type",oaddr_type, |
953 | + {0: "Public", |
954 | + 1: "Random", |
955 | + 2: "Private IRK or Public", |
956 | + 3: "Private IRK or Random"})) |
957 | + self.payload.append(EnumByte("filter policy",filter, |
958 | + {0: "None", |
959 | + 1: "Sender In White List", |
960 | + 2: "Almost None", |
961 | + 3: "SIWL and some"})) |
962 | + |
963 | + |
964 | +class HCI_Cmd_LE_Advertise(HCI_Command): |
965 | + """Class representing a command HCI command to enable/disable BLE advertising. |
966 | + |
967 | + :param enable: enable/disable advertising. |
968 | + :type enable: bool |
969 | + :returns: HCI_Cmd_LE_Scan_Enable instance. |
970 | + :rtype: HCI_Cmd_LE_Scan_Enable |
971 | + |
972 | + """ |
973 | + |
974 | + def __init__(self,enable=True): |
975 | + super(self.__class__, self).__init__(b"\x08",b"\x0a") |
976 | + self.payload.append(Bool("enable",enable)) |
977 | + |
978 | +class HCI_Cmd_LE_Set_Advertised_Msg(HCI_Command): |
979 | + """Class representing an HCI command to set the advertised content. |
980 | + |
981 | + :param enable: enable/disable advertising. |
982 | + :type enable: bool |
983 | + :returns: HCI_Cmd_LE_Scan_Enable instance. |
984 | + :rtype: HCI_Cmd_LE_Scan_Enable |
985 | + |
986 | + """ |
987 | + |
988 | + def __init__(self,msg=EmptyPayload()): |
989 | + super(self.__class__, self).__init__(b"\x08",b"\x08") |
990 | + self.payload.append(msg) |
991 | + |
992 | +class HCI_Cmd_LE_Set_Advertised_Params(HCI_Command): |
993 | + """Class representing an HCI command to set the advertised parameters. |
994 | + |
995 | + This will set a number of parameters relted to the advertising functions. For the |
996 | + min and max intervals, it will always silently enforce the Specs that says it should be >= 20ms |
997 | + and <= 10.24s. It will also silently enforce interval_max >= interval_min |
998 | + |
999 | + :param interval_min: minimum advertising interval in ms. Default 500 |
1000 | + :type interval_min: int/float |
1001 | + :param interval_max: maximum advertising interval in ms. Default 750 |
1002 | + :type interval_max: int/float |
1003 | + :param adv_type: Type of advertising. Value 0 +> Connectable, Scannable advertising |
1004 | + 1 => Connectable directed advertising (High duty cycle) |
1005 | + 2 => Scannable Undirected advertising |
1006 | + 3 => Non connectable undirected advertising (default) |
1007 | + :type adv_type: int |
1008 | + :param oaddr_type: Type of own address Value 0 => public (default) |
1009 | + 1 => Random |
1010 | + 2 => Private with public fallback |
1011 | + 3 => Private with random fallback |
1012 | + :type oaddr_type: int |
1013 | + :param paddr_type: Type of peer address Value 0 => public (default) |
1014 | + 1 => Random |
1015 | + :type paddr_type: int |
1016 | + :param peer_addr: Peer MAC address Default 00:00:00:00:00:00 |
1017 | + :type peer_addr: str |
1018 | + :param cmap: Channel map. A bit field dfined as "Channel 37","Channel 38","Channel 39","RFU","RFU","RFU","RFU","RFU" |
1019 | + Default value is 0x7. The value 0x0 is RFU. |
1020 | + :type cmap: int |
1021 | + :param filter: How white list filter is applied. 0 => No filter (Default) |
1022 | + 1 => scan are filtered |
1023 | + 2 => Connection are filtered |
1024 | + 3 => scan and connection are filtered |
1025 | + :type filter: int |
1026 | + :returns: HCI_Cmd_LE_Scan_Params instance. |
1027 | + :rtype: HCI_Cmd_LE_Scan_Params |
1028 | + |
1029 | + """ |
1030 | + |
1031 | + def __init__(self,interval_min=500, interval_max=750, |
1032 | + adv_type=0x3, oaddr_type=0, paddr_type=0, |
1033 | + peer_addr="00:00:00:00:00:00", cmap=0x7, filter=0): |
1034 | + |
1035 | + super(self.__class__, self).__init__(b"\x08",b"\x06") |
1036 | + self.payload.append(UShortInt("Adv minimum",int(round(min(10240,max(20,interval_min))/0.625)),endian="little")) |
1037 | + self.payload.append(UShortInt("Adv maximum",int(round(min(10240,max(20,max(interval_min,interval_max)))/0.625)),endian="little")) |
1038 | + self.payload.append(EnumByte("adv type",adv_type, |
1039 | + {0: "ADV_IND", |
1040 | + 1: "ADV_DIRECT_IND high", |
1041 | + 2: "ADV_SCAN_IND", |
1042 | + 3: "ADV_NONCONN_IND", |
1043 | + 4: "ADV_DIRECT_IND low"})) |
1044 | + self.payload.append(EnumByte("own addresss type",paddr_type, |
1045 | + {0: "Public", |
1046 | + 1: "Random", |
1047 | + 2: "Private IRK or Public", |
1048 | + 3: "Private IRK or Random"})) |
1049 | + self.payload.append(EnumByte("peer addresss type",oaddr_type, |
1050 | + {0: "Public", |
1051 | + 1: "Random"})) |
1052 | + self.payload.append(MACAddr("peer",mac=peer_addr)) |
1053 | + self.payload.append(BitFieldByte("Channels",cmap,["Channel 37","Channel 38","Channel 39","RFU","RFU","RFU","RFU", "RFU"])) |
1054 | + |
1055 | + self.payload.append(EnumByte("filter policy",filter, |
1056 | + {0: "None", |
1057 | + 1: "Scan", |
1058 | + 2: "Connection", |
1059 | + 3: "Scan and Connection"})) |
1060 | + |
1061 | +class HCI_Cmd_Reset(HCI_Command): |
1062 | + """Class representing an HCI command to reset the adapater. |
1063 | + |
1064 | + |
1065 | + :returns: HCI_Cmd_Reset instance. |
1066 | + :rtype: HCI_Cmd_Reset |
1067 | + |
1068 | + """ |
1069 | + |
1070 | + def __init__(self): |
1071 | + super(self.__class__, self).__init__(b"\x03",b"\x03") |
1072 | + |
1073 | + |
1074 | +#### |
1075 | +# HCI EVents |
1076 | +#### |
1077 | + |
1078 | +class HCI_Event(Packet): |
1079 | + |
1080 | + def __init__(self,code=0,payload=[]): |
1081 | + super().__init__(HCI_EVENT) |
1082 | + self.payload.append(Byte("code")) |
1083 | + self.payload.append(UIntByte("length")) |
1084 | + |
1085 | + def decode(self,data): |
1086 | + data=super().decode(data) |
1087 | + if data is None: |
1088 | + return None |
1089 | + |
1090 | + for x in self.payload: |
1091 | + x.decode(data[:len(x)]) |
1092 | + data=data[len(x):] |
1093 | + code=self.payload[0] |
1094 | + length=self.payload[1].val |
1095 | + if code.val==b"\x0e": |
1096 | + ev = HCI_CC_Event() |
1097 | + data=ev.decode(data) |
1098 | + self.payload.append(ev) |
1099 | + elif code.val==b"\x3e": |
1100 | + ev = HCI_LE_Meta_Event() |
1101 | + data=ev.decode(data) |
1102 | + self.payload.append(ev) |
1103 | + else: |
1104 | + ev=Itself("Payload") |
1105 | + data=ev.decode(data) |
1106 | + self.payload.append(ev) |
1107 | + return data |
1108 | + |
1109 | + def show(self,depth=0): |
1110 | + print("{}HCI Event:".format(PRINT_INDENT*depth)) |
1111 | + for x in self.payload: |
1112 | + x.show(depth+1) |
1113 | + |
1114 | + |
1115 | +class HCI_CC_Event(Packet): |
1116 | + """Command Complete event""" |
1117 | + def __init__(self): |
1118 | + self.name="Command Completed" |
1119 | + self.payload=[UIntByte("allow pkt"),OgfOcf("cmd"),Itself("resp code")] |
1120 | + |
1121 | + |
1122 | + def decode(self,data): |
1123 | + for x in self.payload: |
1124 | + data=x.decode(data) |
1125 | + return data |
1126 | + |
1127 | + def show(self,depth=0): |
1128 | + for x in self.payload: |
1129 | + x.show(depth+1) |
1130 | + |
1131 | +class HCI_LE_Meta_Event(Packet): |
1132 | + def __init__(self): |
1133 | + self.name="LE Meta" |
1134 | + self.payload=[Byte("code")] |
1135 | + |
1136 | + def decode(self,data): |
1137 | + for x in self.payload: |
1138 | + data=x.decode(data) |
1139 | + code=self.payload[0] |
1140 | + if code.val==b"\x02": |
1141 | + ev=HCI_LEM_Adv_Report() |
1142 | + data=ev.decode(data) |
1143 | + self.payload.append(ev) |
1144 | + else: |
1145 | + ev=Itself("Payload") |
1146 | + data=ev.decode(data) |
1147 | + self.payload.append(ev) |
1148 | + return data |
1149 | + |
1150 | + def show(self,depth=0): |
1151 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
1152 | + for x in self.payload: |
1153 | + x.show(depth+1) |
1154 | + |
1155 | + |
1156 | +class HCI_LEM_Adv_Report(Packet): |
1157 | + def __init__(self): |
1158 | + self.name="Adv Report" |
1159 | + self.payload=[UIntByte("num reports"), |
1160 | + EnumByte("ev type",0,{0:"generic adv", 3:"no connection adv", 4:"scan rsp"}), |
1161 | + EnumByte("addr type",0,{0:"public", 1:"random"}), |
1162 | + MACAddr("peer"),UIntByte("length")] |
1163 | + |
1164 | + |
1165 | + def decode(self,data): |
1166 | + |
1167 | + for x in self.payload: |
1168 | + data=x.decode(data) |
1169 | + #Now we have a sequence of len, type data with possibly a RSSI byte at the end |
1170 | + while len(data) > 1: |
1171 | + length=UIntByte("sublen") |
1172 | + data=length.decode(data) |
1173 | + code=EIR_Hdr() |
1174 | + data=code.decode(data) |
1175 | + |
1176 | + if code.val == 0x01: |
1177 | + #Flag |
1178 | + myinfo=BitFieldByte("flags",0,["Undef","Undef","Simul LE - BR/EDR (Host)","Simul LE - BR/EDR (Control.)","BR/EDR Not Supported", |
1179 | + "LE General Disc.","LE Limited Disc."]) |
1180 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1181 | + self.payload.append(myinfo) |
1182 | + elif code.val == 0x02: |
1183 | + myinfo=NBytes_List("Incomplete uuids",2) |
1184 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1185 | + self.payload.append(myinfo) |
1186 | + elif code.val == 0x03: |
1187 | + myinfo=NBytes_List("Complete uuids",2) |
1188 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1189 | + self.payload.append(myinfo) |
1190 | + elif code.val == 0x04: |
1191 | + myinfo=NBytes_List("Incomplete uuids",4) |
1192 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1193 | + self.payload.append(myinfo) |
1194 | + elif code.val == 0x05: |
1195 | + myinfo=NBytes_List("Complete uuids",4) |
1196 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1197 | + self.payload.append(myinfo) |
1198 | + elif code.val == 0x06: |
1199 | + myinfo=NBytes_List("Incomplete uuids",16) |
1200 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1201 | + self.payload.append(myinfo) |
1202 | + elif code.val == 0x07: |
1203 | + myinfo=NBytes_List("Complete uuids",16) |
1204 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1205 | + self.payload.append(myinfo) |
1206 | + elif code.val == 0x14: |
1207 | + myinfo=NBytes_List("Service Solicitation uuid",2) |
1208 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1209 | + self.payload.append(myinfo) |
1210 | + elif code.val == 0x16: |
1211 | + myinfo=Adv_Data("Advertised Data",2) |
1212 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1213 | + self.payload.append(myinfo) |
1214 | + elif code.val == 0x1f: |
1215 | + myinfo=NBytes_List("Service Solicitation uuid",4) |
1216 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1217 | + self.payload.append(myinfo) |
1218 | + elif code.val == 0x20: |
1219 | + myinfo=Adv_Data("Advertised Data",4) |
1220 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1221 | + self.payload.append(myinfo) |
1222 | + elif code.val == 0x15: |
1223 | + myinfo=NBytes_List("Service Solicitation uuid",16) |
1224 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1225 | + self.payload.append(myinfo) |
1226 | + elif code.val == 0x21: |
1227 | + myinfo=Adv_Data("Advertised Data",16) |
1228 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1229 | + self.payload.append(myinfo) |
1230 | + elif code.val == 0x08: |
1231 | + myinfo=String("Short Name") |
1232 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1233 | + self.payload.append(myinfo) |
1234 | + elif code.val == 0x09: |
1235 | + myinfo=String("Complete Name") |
1236 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1237 | + self.payload.append(myinfo) |
1238 | + else: |
1239 | + myinfo=Itself("Payload for %s"%code.strval) |
1240 | + xx=myinfo.decode(data[:length.val-len(code)]) |
1241 | + self.payload.append(myinfo) |
1242 | + |
1243 | + data=data[length.val-len(code):] |
1244 | + if data: |
1245 | + myinfo=IntByte("rssi") |
1246 | + data=myinfo.decode(data) |
1247 | + self.payload.append(myinfo) |
1248 | + return data |
1249 | + |
1250 | + def show(self,depth=0): |
1251 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
1252 | + for x in self.payload: |
1253 | + x.show(depth+1) |
1254 | + |
1255 | +class EIR_Hdr(Packet): |
1256 | + def __init__(self): |
1257 | + self.type= EnumByte("type", 0, { |
1258 | + 0x01: "flags", |
1259 | + 0x02: "incomplete_list_16_bit_svc_uuids", |
1260 | + 0x03: "complete_list_16_bit_svc_uuids", |
1261 | + 0x04: "incomplete_list_32_bit_svc_uuids", |
1262 | + 0x05: "complete_list_32_bit_svc_uuids", |
1263 | + 0x06: "incomplete_list_128_bit_svc_uuids", |
1264 | + 0x07: "complete_list_128_bit_svc_uuids", |
1265 | + 0x08: "shortened_local_name", |
1266 | + 0x09: "complete_local_name", |
1267 | + 0x0a: "tx_power_level", |
1268 | + 0x0d: "class_of_device", |
1269 | + 0x0e: "simple_pairing_hash", |
1270 | + 0x0f: "simple_pairing_rand", |
1271 | + 0x10: "sec_mgr_tk", |
1272 | + 0x11: "sec_mgr_oob_flags", |
1273 | + 0x12: "slave_conn_intvl_range", |
1274 | + 0x17: "pub_target_addr", |
1275 | + 0x18: "rand_target_addr", |
1276 | + 0x19: "appearance", |
1277 | + 0x1a: "adv_intvl", |
1278 | + 0x1b: "le_addr", |
1279 | + 0x1c: "le_role", |
1280 | + 0x14: "list_16_bit_svc_sollication_uuids", |
1281 | + 0x1f: "list_32_bit_svc_sollication_uuids", |
1282 | + 0x15: "list_128_bit_svc_sollication_uuids", |
1283 | + 0x16: "svc_data_16_bit_uuid", |
1284 | + 0x20: "svc_data_32_bit_uuid", |
1285 | + 0x21: "svc_data_128_bit_uuid", |
1286 | + 0x22: "sec_conn_confirm", |
1287 | + 0x23: "sec_conn_rand", |
1288 | + 0x24: "uri", |
1289 | + 0xff: "mfg_specific_data", |
1290 | + }) |
1291 | + |
1292 | + def decode(self,data): |
1293 | + return self.type.decode(data) |
1294 | + |
1295 | + def show(self): |
1296 | + return self.type.show() |
1297 | + |
1298 | + @property |
1299 | + def val(self): |
1300 | + return self.type.val |
1301 | + |
1302 | + @property |
1303 | + def strval(self): |
1304 | + return self.type.strval |
1305 | + |
1306 | + def __len__(self): |
1307 | + return len(self.type) |
1308 | + |
1309 | +class Adv_Data(Packet): |
1310 | + def __init__(self,name,length): |
1311 | + self.name=name |
1312 | + self.length=length |
1313 | + self.payload=[] |
1314 | + |
1315 | + def decode(self,data): |
1316 | + myinfo=NBytes("Service Data uuid",self.length) |
1317 | + data=myinfo.decode(data) |
1318 | + self.payload.append(myinfo) |
1319 | + if data: |
1320 | + myinfo=Itself("Adv Payload") |
1321 | + data=myinfo.decode(data) |
1322 | + self.payload.append(myinfo) |
1323 | + return data |
1324 | + |
1325 | + def show(self,depth=0): |
1326 | + print("{}{}:".format(PRINT_INDENT*depth,self.name)) |
1327 | + for x in self.payload: |
1328 | + x.show(depth+1) |
1329 | + |
1330 | + def __len__(self): |
1331 | + resu=0 |
1332 | + for x in self.payload: |
1333 | + resu+=len(x) |
1334 | + return resu |
1335 | + |
1336 | + |
1337 | + |
1338 | +# |
1339 | +# The defs are over. Now the realstuffs |
1340 | +# |
1341 | + |
1342 | +def create_bt_socket(interface=0): |
1343 | + exceptions = [] |
1344 | + sock = None |
1345 | + try: |
1346 | + sock = socket.socket(family=socket.AF_BLUETOOTH, |
1347 | + type=socket.SOCK_RAW, |
1348 | + proto=socket.BTPROTO_HCI) |
1349 | + sock.setblocking(False) |
1350 | + sock.setsockopt(socket.SOL_HCI, socket.HCI_FILTER, pack("IIIh2x", 0xffffffff,0xffffffff,0xffffffff,0)) #type mask, event mask, event mask, opcode |
1351 | + try: |
1352 | + sock.bind((interface,)) |
1353 | + except OSError as exc: |
1354 | + exc = OSError( |
1355 | + exc.errno, 'error while attempting to bind on ' |
1356 | + 'interface {!r}: {}'.format( |
1357 | + interface, exc.strerror)) |
1358 | + exceptions.append(exc) |
1359 | + except OSError as exc: |
1360 | + if sock is not None: |
1361 | + sock.close() |
1362 | + exceptions.append(exc) |
1363 | + except: |
1364 | + if sock is not None: |
1365 | + sock.close() |
1366 | + raise |
1367 | + if len(exceptions) == 1: |
1368 | + raise exceptions[0] |
1369 | + elif len(exceptions) > 1: |
1370 | + model = str(exceptions[0]) |
1371 | + if all(str(exc) == model for exc in exceptions): |
1372 | + raise exceptions[0] |
1373 | + raise OSError('Multiple exceptions: {}'.format( |
1374 | + ', '.join(str(exc) for exc in exceptions))) |
1375 | + return sock |
1376 | + |
1377 | +########### |
1378 | + |
1379 | +class BLEScanRequester(asyncio.Protocol): |
1380 | + '''Protocol handling the requests''' |
1381 | + def __init__(self): |
1382 | + self.transport = None |
1383 | + self.smac = None |
1384 | + self.sip = None |
1385 | + self.process = self.default_process |
1386 | + |
1387 | + def connection_made(self, transport): |
1388 | + self.transport = transport |
1389 | + command=HCI_Cmd_LE_Set_Scan_Params() |
1390 | + self.transport.write(command.encode()) |
1391 | + |
1392 | + def connection_lost(self, exc): |
1393 | + super().connection_lost(exc) |
1394 | + |
1395 | + def send_scan_request(self): |
1396 | + '''Sending LE scan request''' |
1397 | + command=HCI_Cmd_LE_Scan_Enable(True,False) |
1398 | + self.transport.write(command.encode()) |
1399 | + |
1400 | + def stop_scan_request(self): |
1401 | + '''Sending LE scan request''' |
1402 | + command=HCI_Cmd_LE_Scan_Enable(False,False) |
1403 | + self.transport.write(command.encode()) |
1404 | + |
1405 | + def send_command(self,command): |
1406 | + '''Sending an arbitrary command''' |
1407 | + self.transport.write(command.encode()) |
1408 | + |
1409 | + def data_received(self, packet): |
1410 | + self.process(packet) |
1411 | + |
1412 | + def default_process(self,data): |
1413 | + pass |
1414 | diff --git a/checkbox_support/vendor/aioblescan/eddystone.py b/checkbox_support/vendor/aioblescan/eddystone.py |
1415 | new file mode 100644 |
1416 | index 0000000..3cc0d2c |
1417 | --- /dev/null |
1418 | +++ b/checkbox_support/vendor/aioblescan/eddystone.py |
1419 | @@ -0,0 +1,362 @@ |
1420 | +#!/usr/bin/env python3 |
1421 | +# -*- coding:utf-8 -*- |
1422 | +# |
1423 | +# This file deal with EddyStone formated message |
1424 | +# |
1425 | +# Copyright (c) 2017 François Wautier |
1426 | +# |
1427 | +# Note part of this code was adapted from PyBeacon (https://github.com/nirmankarta/PyBeacon) |
1428 | +# |
1429 | +# Permission is hereby granted, free of charge, to any person obtaining a copy |
1430 | +# of this software and associated documentation files (the "Software"), to deal |
1431 | +# in the Software without restriction, including without limitation the rights |
1432 | +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
1433 | +# of the Software, and to permit persons to whom the Software is furnished to do so, |
1434 | +# subject to the following conditions: |
1435 | +# |
1436 | +# The above copyright notice and this permission notice shall be included in all copies |
1437 | +# or substantial portions of the Software. |
1438 | +# |
1439 | +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
1440 | +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
1441 | +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
1442 | +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
1443 | +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR |
1444 | +# IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE |
1445 | + |
1446 | +import checkbox_support.vendor.aioblescan as aios |
1447 | +from urllib.parse import urlparse |
1448 | +from enum import Enum |
1449 | + |
1450 | +# |
1451 | +EDDY_UUID=b"\xfe\xaa" #Google UUID |
1452 | + |
1453 | +class ESType(Enum): |
1454 | + """Enumerator for Eddystone types.""" |
1455 | + uid = 0x00 |
1456 | + url = 0x10 |
1457 | + tlm = 0x20 |
1458 | + eid = 0x30 |
1459 | + |
1460 | +url_schemes = [ |
1461 | + ("http",True), |
1462 | + ("https",True), |
1463 | + ("http",False), |
1464 | + ("https",False), |
1465 | + ] |
1466 | + |
1467 | +url_domain = [ |
1468 | + "com","org","edu","net","info","biz","gov"] |
1469 | + |
1470 | +class EddyStone(object): |
1471 | + """Class defining the content of an EddyStone advertisement. |
1472 | + |
1473 | + Here the param type will depend on the type. |
1474 | + |
1475 | + For URL it should be a string with a compatible URL. |
1476 | + |
1477 | + For UID it is a dictionary with 2 keys, "namespace" and "instance", values are bytes . |
1478 | + |
1479 | + For TLM it shall be an dictionary with 4 keys: "battery","temperature", "count" and "uptime". |
1480 | + Any missing key shall be replaced by its default value. |
1481 | + |
1482 | + For EID it should me a bytes string of length 8 |
1483 | + |
1484 | + :param type: The type of EddyStone advertisement. From ESType |
1485 | + :type type: ESType |
1486 | + :oaram param: The payload corresponding to the type |
1487 | + |
1488 | + """ |
1489 | + |
1490 | + def __init__(self, type=ESType.url, param="https://goo.gl/m9UiEA"): |
1491 | + self.power = 0 |
1492 | + self.payload = [] #As defined in https://github.com/google/eddystone/blob/master/protocol-specification.md |
1493 | + self.payload.append(aios.Byte("Flag Length",b'\x02')) |
1494 | + self.payload.append(aios.Byte("Flag Data Type",b'\x01')) |
1495 | + self.payload.append(aios.Byte("Flag Data",b'\x1a')) |
1496 | + self.payload.append(aios.Byte("Length UUID services",b'\x03')) |
1497 | + self.payload.append(aios.Byte("Complete List UUID Service",b'\x03')) |
1498 | + self.payload.append(aios.Byte("Eddystone UUID",b'\xaa')) |
1499 | + self.payload.append(aios.Byte("...",b'\xfe')) |
1500 | + self.service_data_length = aios.IntByte("Service Data length",4) |
1501 | + self.payload.append(self.service_data_length) |
1502 | + self.payload.append(aios.Byte("Service Data data type value",b'\x16')) |
1503 | + self.payload.append(aios.Byte("Eddystone UUID",b'\xaa')) |
1504 | + self.payload.append(aios.Byte("...",b'\xfe')) |
1505 | + self.type = aios.EnumByte("type",type.value,{ESType.uid.value:"Eddystone-UID", |
1506 | + ESType.url.value:"Eddystone-URL", |
1507 | + ESType.tlm.value:"Eddystone-TLM", |
1508 | + ESType.eid.value:"Eddystone-EID"}) |
1509 | + self.payload.append(self.type) |
1510 | + self.parsed_payload = b'' |
1511 | + self.type_payload=param |
1512 | + |
1513 | + def change_type(self, type, param): |
1514 | + self.type.val=type.value |
1515 | + self.type_payload=param |
1516 | + self.service_data_length.val=4 |
1517 | + self.parsed_payload = b'' |
1518 | + |
1519 | + def change_type_payload(self, param): |
1520 | + self.type_payload=param |
1521 | + self.service_data_length.val=4 |
1522 | + self.parsed_payload = b'' |
1523 | + |
1524 | + def url_encoder(self): |
1525 | + encodedurl = [] |
1526 | + encodedurl.append(aios.IntByte("Tx Power",self.power)) |
1527 | + asisurl="" |
1528 | + myurl = urlparse(self.type_payload) |
1529 | + myhostname = myurl.hostname |
1530 | + mypath = myurl.path |
1531 | + if (myurl.scheme,myhostname.startswith("www.")) in url_schemes: |
1532 | + encodedurl.append(aios.IntByte("URL Scheme", |
1533 | + url_schemes.index((myurl.scheme,myhostname.startswith("www."))))) |
1534 | + if myhostname.startswith("www."): |
1535 | + myhostname = myhostname[4:] |
1536 | + |
1537 | + extval=None |
1538 | + if myhostname.split(".")[-1] in url_domain: |
1539 | + extval = url_domain.index(myhostname.split(".")[-1]) |
1540 | + myhostname = ".".join(myhostname.split(".")[:-1]) |
1541 | + if extval and not mypath.startswith("/"): |
1542 | + extval+=7 |
1543 | + else: |
1544 | + if myurl.port is None: |
1545 | + if extval: |
1546 | + mypath = mypath[1:] |
1547 | + else: |
1548 | + extval += 7 |
1549 | + encodedurl.append(aios.String("URL string")) |
1550 | + encodedurl[-1].val = myhostname |
1551 | + if extval: |
1552 | + encodedurl.append(aios.IntByte("URL Extention",extval)) |
1553 | + |
1554 | + if myurl.port: |
1555 | + asisurl += ":"+str(myurl.port)+mypath |
1556 | + asisurl += mypath |
1557 | + if myurl.params: |
1558 | + asisurl += ";"+myurl.params |
1559 | + if myurl.query: |
1560 | + asisurl += "?"+myurl.query |
1561 | + if myurl.fragment: |
1562 | + asisurl += "#"+myurl.fragment |
1563 | + encodedurl.append(aios.String("Rest of URL")) |
1564 | + encodedurl[-1].val = asisurl |
1565 | + tlength=0 |
1566 | + for x in encodedurl: #Check the payload length |
1567 | + tlength += len(x) |
1568 | + if tlength > 19: #Actually 18 but we have tx power |
1569 | + raise Exception("Encoded url too long (max 18 bytes)") |
1570 | + self.service_data_length.val += tlength #Update the payload length |
1571 | + return encodedurl |
1572 | + |
1573 | + |
1574 | + def uid_encoder(self): |
1575 | + encodedurl = [] |
1576 | + encodedurl.append(aios.IntByte("Tx Power",self.power)) |
1577 | + encodedurl.append(aios.NBytes("Namespace",10)) |
1578 | + encodedurl[-1].val = self.type_payload["namespace"] |
1579 | + encodedurl.append(aios.NBytes("Instance",6)) |
1580 | + encodedurl[-1].val = self.type_payload["instance"] |
1581 | + encodedurl.append(aios.NBytes("RFU",2)) |
1582 | + encodedurl[-1].val = b'\x00\x00' |
1583 | + self.service_data_length.val = 23 #Update the payload length/ways the same for uid |
1584 | + return encodedurl |
1585 | + |
1586 | + def tlm_encoder(self): |
1587 | + encodedurl = [] |
1588 | + encodedurl.append(aios.NBytes("VBATT",2)) |
1589 | + if "battery" in self.type_payload: |
1590 | + encodedurl[-1].val = self.type_payload["battery"] |
1591 | + else: |
1592 | + encodedurl[-1].val = -128 |
1593 | + encodedurl.append(aios.Float88("Temperature")) |
1594 | + if "temperature" in self.type_payload: |
1595 | + encodedurl[-1].val = self.type_payload["temperature"] |
1596 | + else: |
1597 | + encodedurl[-1].val = -128 |
1598 | + |
1599 | + encodedurl.append(aios.ULongInt("Count")) |
1600 | + if "count" in self.type_payload: |
1601 | + encodedurl[-1].val = self.type_payload["count"] |
1602 | + else: |
1603 | + encodedurl[-1].val = 0 |
1604 | + |
1605 | + encodedurl.append(aios.ULongInt("Uptime")) |
1606 | + if "uptime" in self.type_payload: |
1607 | + encodedurl[-1].val = self.type_payload["uptime"] |
1608 | + else: |
1609 | + encodedurl[-1].val = 0 |
1610 | + return encodedurl |
1611 | + |
1612 | + |
1613 | + def eid_encoder(self): |
1614 | + encodedurl = [] |
1615 | + encodedurl.append(aios.IntByte("Tx Power",self.power)) |
1616 | + encodedurl.append(aios.NBytes("Namespace",8)) |
1617 | + encodedurl[-1].val = self.type_payload |
1618 | + self.service_data_length.val = 13 |
1619 | + return encodedurl |
1620 | + |
1621 | + |
1622 | + def encode(self): |
1623 | + #Generate the payload |
1624 | + if self.type.val == ESType.uid.value: |
1625 | + espayload = self.uid_encoder() |
1626 | + elif self.type.val == ESType.url.value: |
1627 | + espayload = self.url_encoder() |
1628 | + elif self.type.val == ESType.tlm.value: |
1629 | + espayload = self.tlm_encoder() |
1630 | + elif self.type.val == ESType.eid.value: |
1631 | + espayload = self.eid_encoder() |
1632 | + encmsg=b'' |
1633 | + for x in self.payload+espayload: |
1634 | + encmsg += x.encode() |
1635 | + mylen=aios.IntByte("Length",len(encmsg)) |
1636 | + encmsg = mylen.encode()+encmsg |
1637 | + for x in range(32-len(encmsg)): |
1638 | + encmsg+=b'\x00' |
1639 | + return encmsg |
1640 | + |
1641 | + def decode(self, packet): |
1642 | + """Check a parsed packet and figure out if it is an Eddystone Beacon. |
1643 | + If it is , return the relevant data as a dictionary. |
1644 | + |
1645 | + Return None, it is not an Eddystone Beacon advertising packet""" |
1646 | + |
1647 | + ssu=packet.retrieve("Complete uuids") |
1648 | + found=False |
1649 | + for x in ssu: |
1650 | + if EDDY_UUID in x: |
1651 | + found=True |
1652 | + break |
1653 | + if not found: |
1654 | + return None |
1655 | + |
1656 | + found=False |
1657 | + adv=packet.retrieve("Advertised Data") |
1658 | + for x in adv: |
1659 | + luuid=x.retrieve("Service Data uuid") |
1660 | + for uuid in luuid: |
1661 | + if EDDY_UUID == uuid: |
1662 | + found=x |
1663 | + break |
1664 | + if found: |
1665 | + break |
1666 | + |
1667 | + |
1668 | + if not found: |
1669 | + return None |
1670 | + |
1671 | + try: |
1672 | + top=found.retrieve("Adv Payload")[0] |
1673 | + except: |
1674 | + return None |
1675 | + #Rebuild that part of the structure |
1676 | + found.payload.remove(top) |
1677 | + #Now decode |
1678 | + result={} |
1679 | + data=top.val |
1680 | + etype = aios.EnumByte("type",self.type.val,{ESType.uid.value:"Eddystone-UID", |
1681 | + ESType.url.value:"Eddystone-URL", |
1682 | + ESType.tlm.value:"Eddystone-TLM", |
1683 | + ESType.eid.value:"Eddystone-EID"}) |
1684 | + data=etype.decode(data) |
1685 | + found.payload.append(etype) |
1686 | + if etype.val== ESType.uid.value: |
1687 | + power=aios.IntByte("tx_power") |
1688 | + data=power.decode(data) |
1689 | + found.payload.append(power) |
1690 | + result["tx_power"]=power.val |
1691 | + |
1692 | + nspace=aios.Itself("namespace") |
1693 | + xx=nspace.decode(data[:10]) #According to https://github.com/google/eddystone/tree/master/eddystone-uid |
1694 | + data=data[10:] |
1695 | + found.payload.append(nspace) |
1696 | + result["name space"]=nspace.val |
1697 | + |
1698 | + nspace=aios.Itself("instance") |
1699 | + xx=nspace.decode(data[:6]) #According to https://github.com/google/eddystone/tree/master/eddystone-uid |
1700 | + data=data[6:] |
1701 | + found.payload.append(nspace) |
1702 | + result["instance"]=nspace.val |
1703 | + |
1704 | + elif etype.val== ESType.url.value: |
1705 | + power=aios.IntByte("tx_power") |
1706 | + data=power.decode(data) |
1707 | + found.payload.append(power) |
1708 | + result["tx_power"]=power.val |
1709 | + |
1710 | + url=aios.EnumByte("type",0,{0x00:"http://www.",0x01:"https://www.",0x02:"http://",0x03:"https://"}) |
1711 | + data=url.decode(data) |
1712 | + result["url"]=url.strval |
1713 | + for x in data: |
1714 | + if bytes([x]) == b"\x00": |
1715 | + result["url"]+=".com/" |
1716 | + elif bytes([x]) == b"\x01": |
1717 | + result["url"]+=".org/" |
1718 | + elif bytes([x]) == b"\x02": |
1719 | + result["url"]+=".edu/" |
1720 | + elif bytes([x]) == b"\x03": |
1721 | + result["url"]+=".net/" |
1722 | + elif bytes([x]) == b"\x04": |
1723 | + result["url"]+=".info/" |
1724 | + elif bytes([x]) == b"\x05": |
1725 | + result["url"]+=".biz/" |
1726 | + elif bytes([x]) == b"\x06": |
1727 | + result["url"]+=".gov/" |
1728 | + elif bytes([x]) == b"\x07": |
1729 | + result["url"]+=".com" |
1730 | + elif bytes([x]) == b"\x08": |
1731 | + result["url"]+=".org" |
1732 | + elif bytes([x]) == b"\x09": |
1733 | + result["url"]+=".edu" |
1734 | + elif bytes([x]) == b"\x10": |
1735 | + result["url"]+=".net" |
1736 | + elif bytes([x]) == b"\x11": |
1737 | + result["url"]+=".info" |
1738 | + elif bytes([x]) == b"\x12": |
1739 | + result["url"]+=".biz" |
1740 | + elif bytes([x]) == b"\x13": |
1741 | + result["url"]+=".gov" |
1742 | + else: |
1743 | + result["url"]+=chr(x) #x.decode("ascii") #Yep ASCII only |
1744 | + url=aios.String("url") |
1745 | + url.decode(result["url"]) |
1746 | + found.payload.append(url) |
1747 | + elif etype.val== ESType.tlm.value: |
1748 | + myinfo=aios.IntByte("version") |
1749 | + data=myinfo.decode(data) |
1750 | + found.payload.append(myinfo) |
1751 | + myinfo=aios.ShortInt("battery") |
1752 | + data=myinfo.decode(data) |
1753 | + result["battery"]=myinfo.val |
1754 | + found.payload.append(myinfo) |
1755 | + myinfo=aios.Float88("temperature") |
1756 | + data=myinfo.decode(data) |
1757 | + found.payload.append(myinfo) |
1758 | + result["temperature"]=myinfo.val |
1759 | + myinfo=aios.LongInt("pdu count") |
1760 | + data=myinfo.decode(data) |
1761 | + found.payload.append(myinfo) |
1762 | + result["pdu count"]=myinfo.val |
1763 | + myinfo=aios.LongInt("uptime") |
1764 | + data=myinfo.decode(data) |
1765 | + found.payload.append(myinfo) |
1766 | + result["uptime"]=myinfo.val*100 #in msecs |
1767 | + return result |
1768 | + #elif etype.val== ESType.tlm.eid: |
1769 | + else: |
1770 | + result["data"]=data |
1771 | + xx=Itself("data") |
1772 | + xx.decode(data) |
1773 | + found.payload.append(xx) |
1774 | + |
1775 | + rssi=packet.retrieve("rssi") |
1776 | + if rssi: |
1777 | + result["rssi"]=rssi[-1].val |
1778 | + mac=packet.retrieve("peer") |
1779 | + if mac: |
1780 | + result["mac address"]=mac[-1].val |
1781 | + return result |
1782 | diff --git a/setup.py b/setup.py |
1783 | index c2517fd..153e8e0 100755 |
1784 | --- a/setup.py |
1785 | +++ b/setup.py |
1786 | @@ -90,6 +90,8 @@ setup( |
1787 | "checkbox_support.scripts.nmea_test:main"), |
1788 | ("checkbox-support-snap_connect=" |
1789 | "checkbox_support.scripts.snap_connect:main"), |
1790 | + ("checkbox-support-eddystone_scanner=" |
1791 | + "checkbox_support.scripts.eddystone_scanner:main"), |
1792 | ], |
1793 | }, |
1794 | ) |
Tested last week on a large set of devices used to run SRU tests in cert lab and devices running UC.
Self-approved