Skip to content

Commit f38c77c

Browse files
authored
Merge pull request #15 from dhalbert/python-advertisement-data
Python advertisement data
2 parents 04c84be + 960688c commit f38c77c

File tree

10 files changed

+754
-145
lines changed

10 files changed

+754
-145
lines changed

adafruit_ble/advertising.py

Lines changed: 132 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131

3232
import struct
3333

34-
class AdvertisingData:
35-
"""Build up a BLE advertising data packet."""
34+
class AdvertisingPacket:
35+
"""Build up a BLE advertising data or scan response packet."""
3636
# BR/EDR flags not included here, since we don't support BR/EDR.
3737
FLAG_LIMITED_DISCOVERY = 0x01
3838
"""Discoverable only for a limited time period."""
@@ -53,7 +53,7 @@ class AdvertisingData:
5353
"""Complete list of 128 bit service UUIDs."""
5454
SHORT_LOCAL_NAME = 0x08
5555
"""Short local device name (shortened to fit)."""
56-
COMPLETE_LOCALNAME = 0x09
56+
COMPLETE_LOCAL_NAME = 0x09
5757
"""Complete local device name."""
5858
TX_POWER = 0x0A
5959
"""Transmit power level"""
@@ -81,32 +81,151 @@ class AdvertisingData:
8181
MAX_DATA_SIZE = 31
8282
"""Data size in a regular BLE packet."""
8383

84-
def __init__(self, flags=(FLAG_GENERAL_DISCOVERY | FLAG_LE_ONLY), max_length=MAX_DATA_SIZE):
85-
"""Initalize an advertising packet, with the given flags, no larger than max_length."""
86-
self.data = bytearray((2, self.FLAGS, flags))
84+
def __init__(self, data=None, *, max_length=MAX_DATA_SIZE):
85+
"""Create an advertising packet, no larger than max_length.
86+
87+
:param buf data: if not supplied (None), create an empty packet
88+
if supplied, create a packet with supplied data. This is usually used
89+
to parse an existing packet.
90+
:param int max_length: maximum length of packet
91+
"""
92+
self._packet_bytes = bytearray(data) if data else bytearray()
8793
self._max_length = max_length
8894
self._check_length()
8995

96+
@property
97+
def packet_bytes(self):
98+
"""The raw packet bytes."""
99+
return self._packet_bytes
100+
101+
@packet_bytes.setter
102+
def packet_bytes(self, value):
103+
self._packet_bytes = value
104+
105+
def __getitem__(self, element_type):
106+
"""Return the bytes stored in the advertising packet for the given element type.
107+
108+
:param int element_type: An integer designating an advertising element type.
109+
A number of types are defined in `AdvertisingPacket`,
110+
such as `AdvertisingPacket.TX_POWER`.
111+
:returns: bytes that are the value for the given element type.
112+
If the element type is not present in the packet, raise KeyError.
113+
"""
114+
i = 0
115+
adv_bytes = self.packet_bytes
116+
while i < len(adv_bytes):
117+
item_length = adv_bytes[i]
118+
if element_type != adv_bytes[i+1]:
119+
# Type doesn't match: skip to next item.
120+
i += item_length + 1
121+
else:
122+
return adv_bytes[i + 2:i + 1 + item_length]
123+
raise KeyError
124+
125+
def get(self, element_type, default=None):
126+
"""Return the bytes stored in the advertising packet for the given element type,
127+
returning the default value if not found.
128+
"""
129+
try:
130+
return self.__getitem__(element_type)
131+
except KeyError:
132+
return default
133+
134+
@property
135+
def bytes_remaining(self):
136+
"""Number of bytes still available for use in the packet."""
137+
return self._max_length - len(self._packet_bytes)
138+
90139
def _check_length(self):
91-
if len(self.data) > self._max_length:
92-
raise IndexError("Advertising data exceeds max_length")
140+
if len(self._packet_bytes) > self._max_length:
141+
raise IndexError("Advertising data too long")
93142

94143
def add_field(self, field_type, field_data):
95144
"""Append an advertising data field to the current packet, of the given type.
96145
The length field is calculated from the length of field_data."""
97-
self.data.append(1 + len(field_data))
98-
self.data.append(field_type)
99-
self.data.extend(field_data)
146+
self._packet_bytes.append(1 + len(field_data))
147+
self._packet_bytes.append(field_type)
148+
self._packet_bytes.extend(field_data)
100149
self._check_length()
101150

151+
def add_flags(self, flags=(FLAG_GENERAL_DISCOVERY | FLAG_LE_ONLY)):
152+
"""Add default or custom advertising flags."""
153+
self.add_field(self.FLAGS, struct.pack("<B", flags))
154+
102155
def add_16_bit_uuids(self, uuids):
103156
"""Add a complete list of 16 bit service UUIDs."""
104-
self.add_field(self.ALL_16_BIT_SERVICE_UUIDS, bytes(uuid.uuid16 for uuid in uuids))
157+
for uuid in uuids:
158+
self.add_field(self.ALL_16_BIT_SERVICE_UUIDS, struct.pack("<H", uuid.uuid16))
105159

106160
def add_128_bit_uuids(self, uuids):
107161
"""Add a complete list of 128 bit service UUIDs."""
108-
self.add_field(self.ALL_128_BIT_SERVICE_UUIDS, bytes(uuid.uuid128 for uuid in uuids))
162+
for uuid in uuids:
163+
self.add_field(self.ALL_128_BIT_SERVICE_UUIDS, uuid.uuid128)
109164

110165
def add_mfr_specific_data(self, mfr_id, data):
111166
"""Add manufacturer-specific data bytes."""
112167
self.add_field(self.MANUFACTURER_SPECIFIC_DATA, struct.pack('<H', mfr_id) + data)
168+
169+
170+
class ServerAdvertisement:
171+
"""
172+
Data to advertise a peripheral's services.
173+
174+
The advertisement consists of an advertising data packet and an optional scan response packet,
175+
The scan response packet is created only if there is not room in the
176+
advertising data packet for the complete peripheral name.
177+
178+
:param peripheral Peripheral the Peripheral to advertise. Use its services and name
179+
:param int tx_power: transmit power in dBm at 0 meters (8 bit signed value). Default 0 dBm
180+
"""
181+
182+
def __init__(self, peripheral, *, tx_power=0):
183+
self._peripheral = peripheral
184+
185+
packet = AdvertisingPacket()
186+
packet.add_flags()
187+
self._scan_response_packet = None
188+
189+
# Need to check service.secondary
190+
uuids_16_bits = [service.uuid for service in peripheral.services
191+
if service.uuid.size == 16 and not service.secondary]
192+
if uuids_16_bits:
193+
packet.add_16_bit_uuids(uuids_16_bits)
194+
195+
uuids_128_bits = [service.uuid for service in peripheral.services
196+
if service.uuid.size == 128 and not service.secondary]
197+
if uuids_128_bits:
198+
packet.add_128_bit_uuids(uuids_128_bits)
199+
200+
packet.add_field(AdvertisingPacket.TX_POWER, struct.pack("<b", tx_power))
201+
202+
# 2 bytes needed for field length and type.
203+
bytes_available = packet.bytes_remaining - 2
204+
if bytes_available <= 0:
205+
raise IndexError("No room for name")
206+
207+
name_bytes = bytes(peripheral.name, 'utf-8')
208+
if bytes_available >= len(name_bytes):
209+
packet.add_field(AdvertisingPacket.COMPLETE_LOCAL_NAME, name_bytes)
210+
else:
211+
packet.add_field(AdvertisingPacket.SHORT_LOCAL_NAME, name_bytes[:bytes_available])
212+
self._scan_response_packet = AdvertisingPacket()
213+
try:
214+
self._scan_response_packet.add_field(AdvertisingPacket.COMPLETE_LOCAL_NAME,
215+
name_bytes)
216+
except IndexError:
217+
raise IndexError("Name too long")
218+
219+
self._advertising_data_packet = packet
220+
221+
@property
222+
def advertising_data_bytes(self):
223+
"""The raw bytes for the initial advertising data packet."""
224+
return self._advertising_data_packet.packet_bytes
225+
226+
@property
227+
def scan_response_bytes(self):
228+
"""The raw bytes for the scan response packet. None if there is no response packet."""
229+
if self._scan_response_packet:
230+
return self._scan_response_packet.packet_bytes
231+
return None

adafruit_ble/beacon.py

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,29 @@
3232
import struct
3333
import bleio
3434

35-
from .advertising import AdvertisingData
35+
from .advertising import AdvertisingPacket
3636

3737
class Beacon:
3838
"""Base class for Beacon advertisers."""
39-
def __init__(self, advertising_data, interval=1.0):
40-
"""Set up a beacon with the given AdvertisingData.
39+
def __init__(self, advertising_packet):
40+
"""Set up a beacon with the given AdvertisingPacket.
4141
42-
:param AdvertisingData advertising_data: The advertising packet
43-
:param float interval: Advertising interval in seconds
42+
:param AdvertisingPacket advertising_packet
4443
"""
45-
self.broadcaster = bleio.Broadcaster(interval)
46-
self.advertising_data = advertising_data
44+
self._broadcaster = bleio.Peripheral(name=None)
45+
self._advertising_packet = advertising_packet
46+
47+
def start(self, interval=1.0):
48+
"""Turn on beacon.
4749
48-
def start(self):
49-
"""Turn on beacon."""
50-
self.broadcaster.start_advertising(self.advertising_data.data)
50+
:param float interval: Advertising interval in seconds
51+
"""
52+
self._broadcaster.start_advertising(self._advertising_packet.packet_bytes,
53+
interval=interval)
5154

5255
def stop(self):
5356
"""Turn off beacon."""
54-
self.broadcaster.stop_advertising()
57+
self._broadcaster.stop_advertising()
5558

5659

5760

@@ -60,7 +63,7 @@ class LocationBeacon(Beacon):
6063
Used for Apple iBeacon, Nordic nRF Beacon, etc.
6164
"""
6265
# pylint: disable=too-many-arguments
63-
def __init__(self, company_id, uuid, major, minor, rssi, interval=1.0):
66+
def __init__(self, company_id, uuid, major, minor, rssi):
6467
"""Create a beacon with the given values.
6568
6669
:param int company_id: 16-bit company id designating beacon specification owner
@@ -69,7 +72,6 @@ def __init__(self, company_id, uuid, major, minor, rssi, interval=1.0):
6972
:param int major: 16-bit major number, such as a store number
7073
:param int minor: 16-bit minor number, such as a location within a store
7174
:param int rssi: Signal strength in dBm at 1m (signed 8-bit value)
72-
:param float interval: Advertising interval in seconds
7375
7476
Example::
7577
@@ -81,8 +83,9 @@ def __init__(self, company_id, uuid, major, minor, rssi, interval=1.0):
8183
b.start()
8284
"""
8385

84-
adv_data = AdvertisingData()
85-
adv_data.add_mfr_specific_data(
86+
adv = AdvertisingPacket()
87+
adv.add_flags()
88+
adv.add_mfr_specific_data(
8689
company_id,
8790
b''.join((
8891
# 0x02 means a beacon. 0x15 (=21) is length (16 + 2 + 2 + 1)
@@ -91,8 +94,8 @@ def __init__(self, company_id, uuid, major, minor, rssi, interval=1.0):
9194
# iBeacon and similar expect big-endian UUIDS. Usually they are little-endian.
9295
bytes(reversed(uuid.uuid128)),
9396
# major and minor are big-endian.
94-
struct.pack(">HHB", major, minor, rssi))))
95-
super().__init__(adv_data, interval=interval)
97+
struct.pack(">HHb", major, minor, rssi))))
98+
super().__init__(adv)
9699

97100

98101
class EddystoneURLBeacon(Beacon):
@@ -126,16 +129,16 @@ class EddystoneURLBeacon(Beacon):
126129
'.gov',
127130
)
128131

129-
def __init__(self, url, tx_power=0, interval=1.0):
132+
def __init__(self, url, tx_power=0):
130133
"""Create a URL beacon with an encoded version of the url and a transmit power.
131134
132135
:param url URL to encode. Must be short enough to fit after encoding.
133136
:param int tx_power: transmit power in dBm at 0 meters (8 bit signed value)
134-
:param float interval: Advertising interval in seconds
135137
"""
136138

137-
adv_data = AdvertisingData()
138-
adv_data.add_field(AdvertisingData.ALL_16_BIT_SERVICE_UUIDS, self._EDDYSTONE_ID)
139+
adv = AdvertisingPacket()
140+
adv.add_flags()
141+
adv.add_field(AdvertisingPacket.ALL_16_BIT_SERVICE_UUIDS, self._EDDYSTONE_ID)
139142
short_url = None
140143
for idx, prefix in enumerate(self._URL_SCHEMES):
141144
if url.startswith(prefix):
@@ -148,9 +151,9 @@ def __init__(self, url, tx_power=0, interval=1.0):
148151
short_url = short_url.replace(subst + '/', chr(code))
149152
for code, subst in enumerate(self._SUBSTITUTIONS, 7):
150153
short_url = short_url.replace(subst, chr(code))
151-
adv_data.add_field(AdvertisingData.SERVICE_DATA_16_BIT_UUID,
152-
b''.join((self._EDDYSTONE_ID,
153-
b'\x10',
154-
struct.pack("<BB", tx_power, url_scheme_num),
155-
bytes(short_url, 'ascii'))))
156-
super().__init__(adv_data, interval)
154+
adv.add_field(AdvertisingPacket.SERVICE_DATA_16_BIT_UUID,
155+
b''.join((self._EDDYSTONE_ID,
156+
b'\x10',
157+
struct.pack("<bB", tx_power, url_scheme_num),
158+
bytes(short_url, 'ascii'))))
159+
super().__init__(adv)

0 commit comments

Comments
 (0)