Merge ~sylvain-pineau/checkbox-support:eddystone-scanner into checkbox-support: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)
Reviewer Review Type Date Requested Status
Sylvain Pineau (community) Approve
Review via email: mp+356229@code.launchpad.net

Description of the change

New console script to scan for Eddystone URL beacon advertisements.

To post a comment you must log in.
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

Tested last week on a large set of devices used to run SRU tests in cert lab and devices running UC.

Self-approved

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/checkbox_support/scripts/eddystone_scanner.py b/checkbox_support/scripts/eddystone_scanner.py
2new file mode 100644
3index 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())
94diff --git a/checkbox_support/vendor/__init__.py b/checkbox_support/vendor/__init__.py
95new file mode 100644
96index 0000000..e69de29
97--- /dev/null
98+++ b/checkbox_support/vendor/__init__.py
99diff --git a/checkbox_support/vendor/aioblescan/LICENSE.txt b/checkbox_support/vendor/aioblescan/LICENSE.txt
100new file mode 100644
101index 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.
125diff --git a/checkbox_support/vendor/aioblescan/__init__.py b/checkbox_support/vendor/aioblescan/__init__.py
126new file mode 100644
127index 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'
133diff --git a/checkbox_support/vendor/aioblescan/aioblescan.py b/checkbox_support/vendor/aioblescan/aioblescan.py
134new file mode 100644
135index 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
1414diff --git a/checkbox_support/vendor/aioblescan/eddystone.py b/checkbox_support/vendor/aioblescan/eddystone.py
1415new file mode 100644
1416index 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
1782diff --git a/setup.py b/setup.py
1783index 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 )

Subscribers

People subscribed via source and target branches