Thu, 09 Mar 2023 11:13:35 +0100
BluetoothAdvertisement
- changed the 'name' property to prefer the complete name
9857 | 1 | # -*- coding: utf-8 -*- |
2 | ||
3 | # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> | |
4 | # | |
5 | ||
6 | """ | |
7 | Module implementing a class to parse and store the Bluetooth device advertisement data. | |
8 | """ | |
9 | ||
10 | import struct | |
11 | import uuid | |
12 | ||
13 | ADV_IND = 0 | |
9859 | 14 | ADV_DIRECT_IND = 1 |
9857 | 15 | ADV_SCAN_IND = 2 |
16 | ADV_NONCONN_IND = 3 | |
17 | SCAN_RSP = 4 | |
18 | ||
19 | ADV_TYPE_UUID16_INCOMPLETE = 0x02 | |
20 | ADV_TYPE_UUID16_COMPLETE = 0x03 | |
21 | ADV_TYPE_UUID32_INCOMPLETE = 0x04 | |
22 | ADV_TYPE_UUID32_COMPLETE = 0x05 | |
23 | ADV_TYPE_UUID128_INCOMPLETE = 0x06 | |
24 | ADV_TYPE_UUID128_COMPLETE = 0x07 | |
25 | ADV_TYPE_SHORT_NAME = 0x08 | |
26 | ADV_TYPE_COMPLETE_NAME = 0x09 | |
27 | ADV_TYPE_TX_POWER_LEVEL = 0x0A | |
28 | ADV_TYPE_SVC_DATA = 0x16 | |
29 | ADV_TYPE_MANUFACTURER = 0xFF | |
30 | ||
31 | ManufacturerId = { | |
32 | 0x4C: "Apple, Inc.", | |
33 | 0xE0: "Google", | |
34 | 0x75: "Samsung Electronics Co. Ltd.", | |
35 | 0x87: "Garmin International Inc.", | |
9863
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
36 | 0x822: "adafruit industries", |
9857 | 37 | } |
38 | ||
39 | ||
40 | class BluetoothAdvertisement: | |
41 | """ | |
42 | Class to parse and store the Bluetooth device advertisement data. | |
43 | """ | |
44 | ||
45 | def __init__(self, address): | |
46 | """ | |
47 | Constructor | |
48 | ||
49 | @param address address of the device advertisement | |
50 | @type str | |
51 | """ | |
52 | self.__address = address | |
9863
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
53 | ##self.__name = "" |
9857 | 54 | self.__rssi = 0 |
55 | self.__connectable = False | |
56 | ||
57 | self.__advData = None | |
58 | self.__respData = None | |
59 | ||
60 | def update(self, advType, rssi, advData): | |
61 | """ | |
62 | Public method to update the advertisement data. | |
63 | ||
64 | @param advType type of advertisement data | |
65 | @type int | |
66 | @param rssi RSSI value in dBm | |
67 | @type int | |
68 | @param advData advertisement data | |
69 | @type bytes | |
70 | """ | |
71 | if rssi != self.__rssi: | |
72 | self.__rssi = rssi | |
73 | ||
74 | if advType in (ADV_IND, ADV_NONCONN_IND): | |
75 | if advData != self.__advData: | |
76 | self.__advData = advData | |
77 | self.__connectable = advType == ADV_IND | |
78 | elif advType == ADV_SCAN_IND: | |
79 | self.__advData = advData | |
80 | elif advType == SCAN_RSP and advData and advData != self.__respData: | |
81 | self.__respData = advData | |
82 | ||
83 | def __str__(self): | |
84 | """ | |
85 | Special method to generate a string representation. | |
86 | ||
87 | @return string representation | |
88 | @rtype str | |
89 | """ | |
90 | return "Scan result: {0} {1}".format(self.__address, self.__rssi) | |
91 | ||
92 | def __decodeField(self, *advType): | |
93 | """ | |
94 | Private method to get all fields of the specified types. | |
95 | ||
96 | @param *advType type of fields to be extracted | |
97 | @type int | |
98 | @yield requested fields | |
99 | @ytype bytes | |
100 | """ | |
101 | # Advertising payloads are repeated packets of the following form: | |
102 | # 1 byte data length (N + 1) | |
103 | # 1 byte type (see constants below) | |
104 | # N bytes type-specific data | |
105 | for payload in (self.__advData, self.__respData): | |
106 | if not payload: | |
107 | continue | |
108 | ||
109 | i = 0 | |
110 | while i + 1 < len(payload): | |
111 | if payload[i + 1] in advType: | |
112 | yield payload[i + 2 : i + payload[i] + 1] | |
113 | i += 1 + payload[i] | |
114 | ||
115 | def __splitBytes(self, data, chunkSize): | |
116 | """ | |
117 | Private method to split some data into chunks of given size. | |
118 | ||
119 | @param data data to be chunked | |
120 | @type bytes, bytearray, str | |
121 | @param chunkSize size for each chunk | |
122 | @type int | |
123 | @return list of chunks | |
124 | @rtype list of bytes, bytearray, str | |
125 | """ | |
126 | start = 0 | |
127 | dataChunks = [] | |
128 | while start < len(data): | |
129 | end = start + chunkSize | |
130 | dataChunks.append(data[start:end]) | |
131 | start = end | |
132 | return dataChunks | |
133 | ||
134 | @property | |
9863
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
135 | def completeName(self): |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
136 | """ |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
137 | Public method to get the complete advertised name, if available. |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
138 | |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
139 | @return advertised name |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
140 | @rtype str |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
141 | """ |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
142 | for n in self.__decodeField(ADV_TYPE_COMPLETE_NAME): |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
143 | return str(n, "utf-8").replace("\x00", "") if n else "" |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
144 | |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
145 | return "" |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
146 | |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
147 | @property |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
148 | def shortName(self): |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
149 | """ |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
150 | Public method to get the shortened advertised name, if available. |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
151 | |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
152 | @return advertised name |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
153 | @rtype str |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
154 | """ |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
155 | for n in self.__decodeField(ADV_TYPE_SHORT_NAME): |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
156 | return str(n, "utf-8").replace("\x00", "") if n else "" |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
157 | |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
158 | return "" |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
159 | |
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
160 | @property |
9857 | 161 | def name(self): |
162 | """ | |
163 | Public method to get the complete or shortened advertised name, if available. | |
164 | ||
165 | @return advertised name | |
166 | @rtype str | |
167 | """ | |
9863
5f2377b32716
BluetoothAdvertisement
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9859
diff
changeset
|
168 | return self.completeName or self.shortName |
9857 | 169 | |
170 | @property | |
171 | def rssi(self): | |
172 | """ | |
173 | Public method to get the RSSI value. | |
174 | ||
175 | @return RSSI value in dBm | |
176 | @rtype int | |
177 | """ | |
178 | return self.__rssi | |
179 | ||
180 | @property | |
181 | def address(self): | |
182 | """ | |
183 | Public method to get the address string. | |
184 | ||
185 | @return address of the device | |
186 | @rtype str | |
187 | """ | |
188 | return self.__address | |
189 | ||
190 | @property | |
191 | def txPower(self): | |
192 | """ | |
193 | Public method to get the advertised power level in dBm. | |
194 | ||
195 | @return transmit power of the device (in dBm) | |
196 | @rtype int | |
197 | """ | |
198 | for txLevel in self.__decodeField(ADV_TYPE_TX_POWER_LEVEL): | |
9859 | 199 | return struct.unpack("<b", txLevel)[0] |
9857 | 200 | |
201 | return 0 | |
202 | ||
203 | @property | |
204 | def services(self): | |
205 | """ | |
206 | Public method to get the service IDs. | |
207 | ||
208 | @return list of tuples containing the advertised service ID and a | |
209 | flag indicating a complete ID | |
210 | @rtype list of tuple of (str, bool) | |
211 | """ | |
212 | result = [] | |
213 | ||
214 | for u in self.__decodeField(ADV_TYPE_UUID16_INCOMPLETE): | |
215 | for v in self.__splitBytes(u, 2): | |
216 | result.append((hex(struct.unpack("<H", v)[0]), False)) | |
217 | for u in self.__decodeField(ADV_TYPE_UUID16_COMPLETE): | |
218 | for v in self.__splitBytes(u, 2): | |
219 | result.append((hex(struct.unpack("<H", v)[0]), True)) | |
220 | ||
221 | for u in self.__decodeField(ADV_TYPE_UUID32_INCOMPLETE): | |
222 | for v in self.__splitBytes(u, 4): | |
223 | result.append((hex(struct.unpack("<I", v)), False)) | |
224 | for u in self.__decodeField(ADV_TYPE_UUID32_COMPLETE): | |
225 | for v in self.__splitBytes(u, 4): | |
226 | result.append((hex(struct.unpack("<I", v)), True)) | |
227 | ||
228 | for u in self.__decodeField(ADV_TYPE_UUID128_INCOMPLETE): | |
229 | for v in self.__splitBytes(u, 16): | |
230 | uid = uuid.UUID(bytes=bytes(reversed(v))) | |
231 | result.append((str(uid), False)) | |
232 | for u in self.__decodeField(ADV_TYPE_UUID128_COMPLETE): | |
233 | for v in self.__splitBytes(u, 16): | |
234 | uid = uuid.UUID(bytes=bytes(reversed(v))) | |
235 | result.append((str(uid), True)) | |
236 | ||
237 | return result | |
238 | ||
239 | def manufacturer(self, filterId=None, withName=False): | |
240 | """ | |
241 | Public method to get the manufacturer data. | |
242 | ||
243 | @param filterId manufacturer ID to filter on (defaults to None) | |
244 | @type int (optional) | |
245 | @param withName flag indicating to report the manufacturer name as well | |
246 | (if available) (defaults to False) | |
247 | @type bool | |
248 | @return tuple containing the manufacturer ID, associated data and manufacturer | |
249 | name | |
250 | @rtype tuple of (int, bytes, str) | |
251 | """ | |
252 | result = [] | |
253 | for u in self.__decodeField(ADV_TYPE_MANUFACTURER): | |
254 | if len(u) < 2: | |
255 | continue | |
256 | ||
257 | m = struct.unpack("<H", u[0:2])[0] | |
9858
6518c336fcd3
Fixed some bugs in MicroPython Bluetooth support.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
9857
diff
changeset
|
258 | if filterId is None or m == filterId: |
9857 | 259 | name = ManufacturerId.get(m, "") if withName else None |
260 | result.append((m, u[2:], name)) | |
261 | return result |