Skip to content

Commit 7fa5b68

Browse files
authored
Merge pull request #159 from kevincar/101-sending-periodic-advertisings-with-custom-service-data
BlessAdvertisementData for starting a server
2 parents 6a97c55 + 3c41486 commit 7fa5b68

File tree

6 files changed

+160
-18
lines changed

6 files changed

+160
-18
lines changed

bless/backends/advertisement.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import platform
2+
import warnings
3+
from dataclasses import dataclass
4+
from typing import Dict, List, Optional
5+
6+
7+
@dataclass
8+
class BlessAdvertisementData:
9+
"""
10+
Generic advertisement data for BLE backends.
11+
"""
12+
13+
local_name: Optional[str] = None
14+
service_uuids: Optional[List[str]] = None
15+
manufacturer_data: Optional[Dict[int, bytes]] = None
16+
service_data: Optional[Dict[str, bytes]] = None
17+
is_connectable: Optional[bool] = None
18+
is_discoverable: Optional[bool] = None
19+
tx_power: Optional[int] = None
20+
21+
def __post_init__(self) -> None:
22+
"""
23+
Warn when fields are provided that are not used by the current OS backend.
24+
"""
25+
system = platform.system()
26+
if system == "Darwin":
27+
unused = self._unused_fields({"local_name", "service_uuids"})
28+
elif system == "Windows":
29+
unused = self._unused_fields(
30+
{"local_name", "is_connectable", "is_discoverable"}
31+
)
32+
elif system == "Linux":
33+
unused = self._unused_fields(
34+
{
35+
"local_name",
36+
"service_uuids",
37+
"manufacturer_data",
38+
"service_data",
39+
"tx_power",
40+
}
41+
)
42+
else:
43+
unused = self._unused_fields(set())
44+
45+
if unused:
46+
unused_list = ", ".join(sorted(unused))
47+
warnings.warn(
48+
f"Advertisement fields not used on {system}: {unused_list}",
49+
RuntimeWarning,
50+
)
51+
52+
def _unused_fields(self, used: set) -> set:
53+
"""
54+
Return the provided field names that are not in the OS-used set.
55+
"""
56+
provided = {
57+
"local_name": self.local_name is not None,
58+
"service_uuids": self.service_uuids is not None,
59+
"manufacturer_data": self.manufacturer_data is not None,
60+
"service_data": self.service_data is not None,
61+
"is_connectable": self.is_connectable is not None,
62+
"is_discoverable": self.is_discoverable is not None,
63+
"tx_power": self.tx_power is not None,
64+
}
65+
return {
66+
name for name, present in provided.items() if present and name not in used
67+
}

bless/backends/bluezdbus/dbus/application.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from dbus_next.service import ServiceInterface # type: ignore
99
from dbus_next.signature import Variant # type: ignore
1010

11+
from bless.backends.advertisement import BlessAdvertisementData
1112
from bless.backends.bluezdbus.dbus.advertisement import ( # type: ignore
1213
Type,
1314
BlueZLEAdvertisement,
@@ -150,24 +151,44 @@ async def unregister(self, adapter: ProxyObject):
150151
iface: ProxyInterface = adapter.get_interface(defs.GATT_MANAGER_INTERFACE)
151152
await iface.call_unregister_application(self.path) # type: ignore
152153

153-
async def start_advertising(self, adapter: ProxyObject):
154+
async def start_advertising(
155+
self,
156+
adapter: ProxyObject,
157+
advertisement_data: Optional[BlessAdvertisementData] = None,
158+
):
154159
"""
155160
Start Advertising the application
156161
157162
Parameters
158163
----------
159164
adapter : ProxyObject
160165
The adapter object to start advertising on
166+
advertisement_data : Optional[BlessAdvertisementData]
167+
Optional advertisement payload to populate BlueZ advertisement data
161168
"""
162-
await self.set_name(adapter, self.app_name)
169+
local_name: str = self.app_name
170+
if advertisement_data and advertisement_data.local_name is not None:
171+
local_name = advertisement_data.local_name
172+
await self.set_name(adapter, local_name)
163173

164174
advertisement: BlueZLEAdvertisement = BlueZLEAdvertisement(
165175
Type.PERIPHERAL, len(self.advertisements) + 1, self
166176
)
167177
self.advertisements.append(advertisement)
168178

169-
# Only add the first UUID
170-
advertisement._service_uuids.append(self.services[0].UUID)
179+
if advertisement_data and advertisement_data.local_name is not None:
180+
advertisement._local_name = advertisement_data.local_name
181+
if advertisement_data and advertisement_data.service_uuids is not None:
182+
advertisement._service_uuids.extend(advertisement_data.service_uuids)
183+
elif len(self.services) > 0:
184+
# Only add the first UUID
185+
advertisement._service_uuids.append(self.services[0].UUID)
186+
if advertisement_data and advertisement_data.manufacturer_data is not None:
187+
advertisement._manufacturer_data = advertisement_data.manufacturer_data
188+
if advertisement_data and advertisement_data.service_data is not None:
189+
advertisement._service_data = advertisement_data.service_data
190+
if advertisement_data and advertisement_data.tx_power is not None:
191+
advertisement._tx_power = advertisement_data.tx_power
171192

172193
self.bus.export(advertisement.path, advertisement)
173194

bless/backends/bluezdbus/server.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from dbus_next.constants import BusType # type: ignore
1111

1212
from bless.backends.server import BaseBlessServer # type: ignore
13+
from bless.backends.advertisement import BlessAdvertisementData
1314
from bless.backends.bluezdbus.characteristic import BlessGATTCharacteristicBlueZDBus
1415
from bless.backends.bluezdbus.descriptor import BlessGATTDescriptorBlueZDBus
1516
from bless.backends.bluezdbus.dbus.application import ( # type: ignore
@@ -79,10 +80,17 @@ async def setup(self: "BlessServerBlueZDBus"):
7980
raise Exception("Could not locate bluetooth adapter")
8081
self.adapter: ProxyObject = cast(ProxyObject, potential_adapter)
8182

82-
async def start(self, **kwargs) -> bool:
83+
async def start(
84+
self, advertisement_data: Optional[BlessAdvertisementData] = None, **kwargs
85+
) -> bool:
8386
"""
8487
Start the server
8588
89+
Parameters
90+
----------
91+
advertisement_data : Optional[BlessAdvertisementData]
92+
Optional advertisement payload to customize BlueZ advertising data
93+
8694
Returns
8795
-------
8896
bool
@@ -97,7 +105,9 @@ async def start(self, **kwargs) -> bool:
97105
await self.app.register(self.adapter)
98106

99107
# advertise
100-
await self.app.start_advertising(self.adapter)
108+
await self.app.start_advertising(
109+
self.adapter, advertisement_data=advertisement_data
110+
)
101111

102112
return True
103113

bless/backends/corebluetooth/server.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,20 @@
1313
CBMutableDescriptor,
1414
CBAdvertisementDataLocalNameKey,
1515
CBAdvertisementDataServiceUUIDsKey,
16+
CBUUID,
1617
)
1718

1819
from bleak.backends.service import BleakGATTService # type: ignore
1920

2021
from .peripheral_manager_delegate import PeripheralManagerDelegate # type: ignore
2122
from bless.backends.server import BaseBlessServer # type: ignore
23+
from bless.backends.advertisement import BlessAdvertisementData
2224
from bless.backends.corebluetooth.service import BlessGATTServiceCoreBluetooth
2325
from bless.backends.corebluetooth.characteristic import ( # type: ignore
2426
BlessGATTCharacteristicCoreBluetooth,
2527
)
2628
from bless.backends.corebluetooth.descriptor import ( # type: ignore
27-
BlessGATTDescriptorCoreBluetooth
29+
BlessGATTDescriptorCoreBluetooth,
2830
)
2931

3032
from bless.backends.descriptor import ( # type: ignore
@@ -71,7 +73,11 @@ def __init__(self, name: str, loop: Optional[AbstractEventLoop] = None, **kwargs
7173
self.peripheral_manager_delegate.write_request_func = self.write_request
7274

7375
async def start(
74-
self, timeout: float = 10, prioritize_local_name: bool = True, **kwargs
76+
self,
77+
advertisement_data: Optional[BlessAdvertisementData] = None,
78+
timeout: float = 10,
79+
prioritize_local_name: bool = True,
80+
**kwargs,
7581
):
7682
"""
7783
Start the server
@@ -87,28 +93,42 @@ async def start(
8793
names associated with BLE applications. When true, the name of the
8894
server is prioritized over service UUIDs, and will automatrically
8995
be truncated if longer than 28 bytes.
96+
advertisement_data : Optional[BlessAdvertisementData]
97+
Optional advertisement payload to customize the local name and
98+
service UUIDs advertised
9099
"""
91100
for service_uuid in self.services:
92101
bleak_service: BleakGATTService = self.services[service_uuid]
93102
service_obj: CBService = bleak_service.obj
94103
logger.debug("Adding service: {}".format(bleak_service.uuid))
95104
await self.peripheral_manager_delegate.add_service(service_obj)
96105

106+
local_name: str = self.name
107+
if advertisement_data and advertisement_data.local_name is not None:
108+
local_name = advertisement_data.local_name
109+
97110
advertisement_uuids: List
98-
if (prioritize_local_name) and len(self.name) > 10:
111+
if advertisement_data and advertisement_data.service_uuids is not None:
112+
advertisement_uuids = [
113+
CBUUID.alloc().initWithString_(uuid)
114+
for uuid in advertisement_data.service_uuids
115+
]
116+
elif (prioritize_local_name) and len(local_name) > 10:
99117
advertisement_uuids = []
100118
else:
101119
advertisement_uuids = list(
102120
map(lambda x: self.services[x].obj.UUID(), self.services)
103121
)
104122

105-
advertisement_data = {
106-
CBAdvertisementDataLocalNameKey: self.name,
123+
advertisement_payload = {
124+
CBAdvertisementDataLocalNameKey: local_name,
107125
CBAdvertisementDataServiceUUIDsKey: advertisement_uuids,
108126
}
109-
logger.debug("Advertisement Data: {}".format(advertisement_data))
127+
logger.debug("Advertisement Data: {}".format(advertisement_payload))
110128
try:
111-
await self.peripheral_manager_delegate.start_advertising(advertisement_data)
129+
await self.peripheral_manager_delegate.start_advertising(
130+
advertisement_payload
131+
)
112132
except TimeoutError:
113133
# If advertising fails as a result of bluetooth module power
114134
# cycling or advertisement failure, attempt to start again

bless/backends/server.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Any, Optional, Dict, Callable, List
88

99
from bless.backends.service import BlessGATTService
10+
from bless.backends.advertisement import BlessAdvertisementData
1011
from bless.backends.attribute import ( # type: ignore
1112
GATTAttributePermissions
1213
)
@@ -52,10 +53,17 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
5253
# Abstract Methods
5354

5455
@abc.abstractmethod
55-
async def start(self, **kwargs) -> bool:
56+
async def start(
57+
self, advertisement_data: Optional[BlessAdvertisementData] = None, **kwargs
58+
) -> bool:
5659
"""
5760
Start the server
5861
62+
Parameters
63+
----------
64+
advertisement_data : Optional[BlessAdvertisementData]
65+
Optional advertisement payload to customize backend advertising
66+
5967
Returns
6068
-------
6169
bool

bless/backends/winrt/server.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Optional, List, Any, cast
99

1010
from bless.backends.server import BaseBlessServer # type: ignore
11+
from bless.backends.advertisement import BlessAdvertisementData
1112
from bless.backends.attribute import ( # type: ignore
1213
GATTAttributePermissions,
1314
)
@@ -122,7 +123,11 @@ def __init__(
122123
self._adapter: BLEAdapter = BLEAdapter()
123124
self._name_overwrite: bool = name_overwrite
124125

125-
async def start(self: "BlessServerWinRT", **kwargs):
126+
async def start(
127+
self: "BlessServerWinRT",
128+
advertisement_data: Optional[BlessAdvertisementData] = None,
129+
**kwargs,
130+
):
126131
"""
127132
Start the server
128133
@@ -131,16 +136,27 @@ async def start(self: "BlessServerWinRT", **kwargs):
131136
timeout : float
132137
Floating point decimal in seconds for how long to wait for the
133138
on-board bluetooth module to power on
139+
advertisement_data : Optional[BlessAdvertisementData]
140+
Optional advertisement payload to customize local name and
141+
connectable/discoverable settings
134142
"""
135143

136-
if self._name_overwrite:
144+
if advertisement_data and advertisement_data.local_name is not None:
145+
self._adapter.set_local_name(advertisement_data.local_name)
146+
elif self._name_overwrite:
137147
self._adapter.set_local_name(self.name)
138148

139149
adv_parameters: GattServiceProviderAdvertisingParameters = (
140150
GattServiceProviderAdvertisingParameters()
141151
)
142-
adv_parameters.is_discoverable = True
143-
adv_parameters.is_connectable = True
152+
if advertisement_data and advertisement_data.is_discoverable is not None:
153+
adv_parameters.is_discoverable = advertisement_data.is_discoverable
154+
else:
155+
adv_parameters.is_discoverable = True
156+
if advertisement_data and advertisement_data.is_connectable is not None:
157+
adv_parameters.is_connectable = advertisement_data.is_connectable
158+
else:
159+
adv_parameters.is_connectable = True
144160

145161
for uuid, service in self.services.items():
146162
winrt_service: BlessGATTServiceWinRT = cast(BlessGATTServiceWinRT, service)

0 commit comments

Comments
 (0)