Skip to content

Commit 036b65f

Browse files
committed
Add support for pairing and services that require it.
This adds HID Server, Current Time Service and Apple Notification Service Client support.
1 parent 795dd52 commit 036b65f

29 files changed

+343
-180
lines changed

adafruit_ble/__init__.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,10 @@
2121
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2222
# THE SOFTWARE.
2323
"""
24-
`adafruit_ble`
25-
====================================================
2624
2725
This module provides higher-level BLE (Bluetooth Low Energy) functionality,
2826
building on the native `_bleio` module.
2927
30-
* Author(s): Dan Halbert and Scott Shawcroft for Adafruit Industries
31-
32-
Implementation Notes
33-
--------------------
34-
35-
**Hardware:**
36-
37-
Adafruit Feather nRF52840 Express <https://www.adafruit.com/product/4062>
38-
Adafruit Circuit Playground Bluefruit <https://www.adafruit.com/product/4333>
39-
40-
**Software and Dependencies:**
41-
42-
* Adafruit CircuitPython firmware for the supported boards:
43-
https://github.com/adafruit/circuitpython/releases
44-
4528
"""
4629
#pylint: disable=wrong-import-position
4730
import sys
@@ -129,6 +112,15 @@ def connected(self):
129112
"""True if the connection to the peer is still active."""
130113
return self._bleio_connection.connected
131114

115+
@property
116+
def paired(self):
117+
"""True if the paired to the peer."""
118+
return self._bleio_connection.paired
119+
120+
def pair(self, *, bond=True):
121+
"""Pair to the peer to increase security of the connection."""
122+
return self._bleio_connection.pair(bond=bond)
123+
132124
def disconnect(self):
133125
"""Disconnect from peer."""
134126
self._bleio_connection.disconnect()

adafruit_ble/advertising/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,8 @@ class Advertisement:
209209
# RANDOM_TARGET_ADDRESS = 0x18
210210
# """Random target address (chosen randomly)."""
211211
# APPEARANCE = 0x19
212-
# # self.add_field(AdvertisingPacket.APPEARANCE, struct.pack("<H", appearance))
213-
# """Appearance."""
212+
appearance = Struct("<H", advertising_data_type=0x19)
213+
"""Appearance."""
214214
# DEVICE_ADDRESS = 0x1B
215215
# """LE Bluetooth device address."""
216216
# ROLE = 0x1C

adafruit_ble/advertising/standard.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2121
# THE SOFTWARE.
2222
"""
23-
`standard`
23+
:py:mod:`~adafruit_ble.advertising.standard`
2424
====================================================
2525
2626
This module provides BLE standard defined advertisements. The Advertisements are single purpose
@@ -84,8 +84,11 @@ def __iter__(self):
8484
def append(self, service):
8585
"""Append a service to the list."""
8686
if isinstance(service.uuid, StandardUUID) and service not in self._standard_services:
87-
self._standard_services.append(service)
87+
self._standard_services.append(service.uuid)
8888
self._update(self._standard_service_fields[0], self._standard_services)
89+
elif isinstance(service.uuid, VendorUUID) and service not in self._vendor_services:
90+
self._vendor_services.append(service.uuid)
91+
self._update(self._vendor_service_fields[0], self._vendor_services)
8992

9093
# TODO: Differentiate between complete and incomplete lists.
9194
def extend(self, services):

adafruit_ble/characteristics/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2121
# THE SOFTWARE.
2222
"""
23-
:py:mod:`~adafruit_ble.characteristics`
24-
====================================================
2523
2624
This module provides core BLE characteristic classes that are used within Services.
2725
@@ -92,7 +90,7 @@ class Characteristic:
9290

9391
def __init__(self, *, uuid=None, properties=0,
9492
read_perm=Attribute.OPEN, write_perm=Attribute.OPEN,
95-
max_length=20, fixed_length=False, initial_value=None):
93+
max_length=None, fixed_length=False, initial_value=None):
9694
self.field_name = None # Set by Service during basic binding
9795

9896
if uuid:
@@ -201,7 +199,7 @@ def __init__(self, struct_format, *, uuid=None, properties=0,
201199
self._struct_format = struct_format
202200
self._expected_size = struct.calcsize(struct_format)
203201
if initial_value:
204-
initial_value = struct.pack(self._struct_format, initial_value)
202+
initial_value = struct.pack(self._struct_format, *initial_value)
205203
super().__init__(uuid=uuid, initial_value=initial_value,
206204
max_length=self._expected_size, fixed_length=True,
207205
properties=properties, read_perm=read_perm, write_perm=write_perm)

adafruit_ble/characteristics/int.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ def __init__(self, format_string, min_value, max_value, *, uuid=None, properties
4141
self._min_value = min_value
4242
self._max_value = max_value
4343
if initial_value:
44-
initial_value = (initial_value,)
4544
if not self._min_value <= initial_value <= self._max_value:
4645
raise ValueError("initial_value out of range")
46+
initial_value = (initial_value,)
4747

4848

4949
super().__init__(format_string, uuid=uuid, properties=properties,

adafruit_ble/services/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2121
# THE SOFTWARE.
2222
"""
23-
:py:mod:`~adafruit_ble.services`
24-
====================================================
2523
2624
This module provides the top level Service definition.
2725

adafruit_ble/services/apple.py

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@
2727
2828
"""
2929

30+
import struct
31+
3032
from . import Service
3133
from ..uuid import VendorUUID
34+
from ..characteristics.stream import StreamIn, StreamOut
3235

3336
__version__ = "0.0.0-auto.0"
3437
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git"
@@ -41,10 +44,121 @@ class UnknownApple1Service(Service):
4144
"""Unknown service. Unimplemented."""
4245
uuid = VendorUUID("9fa480e0-4967-4542-9390-d343dc5d04ae")
4346

47+
class Notification:
48+
def __init__(self, id, event_flags, category_id, category_count, *, control_point, data_source):
49+
self.id = id
50+
self.category_id = category_id
51+
self.removed = False
52+
53+
self.silent = (event_flags & (1 << 0)) != 0
54+
self.important = (event_flags & (1 << 1)) != 0
55+
self.preexisting = (event_flags & (1 << 2)) != 0
56+
self.positive_action = (event_flags & (1 << 3)) != 0
57+
self.negative_action = (event_flags & (1 << 4)) != 0
58+
59+
self.subtitle = None
60+
self.message = None
61+
62+
self.control_point = control_point
63+
self.data_source = data_source
64+
65+
def update(self, event_flags, category_id, category_count):
66+
pass
67+
68+
def _fetch_all(self):
69+
self.control_point.write(struct.pack("<BIBBHBHBHBBBB", 0, self.id, 0, 1, 32, 2, 32, 3, 255, 4, 5, 6, 7))
70+
while self.data_source.in_waiting == 0:
71+
pass
72+
_, _ = struct.unpack("<BI", self.data_source.read(5))
73+
74+
for attribute in ["app_id", "title", "subtitle", "message", "message_size", "date", "positive_action_label", "negative_action_label"]:
75+
attribute_id, attribute_length = struct.unpack("<BH", self.data_source.read(3))
76+
if attribute_length == 0:
77+
continue
78+
value = self.data_source.read(attribute_length).decode("utf-8")
79+
setattr(self, attribute, value)
80+
81+
@property
82+
def app(self):
83+
self.control_point.write(struct.pack("<BIB", 0, self.id, 0))
84+
while self.data_source.in_waiting == 0:
85+
pass
86+
print(self.data_source.in_waiting)
87+
print(self.data_source.read())
88+
return ""
89+
90+
91+
def __str__(self):
92+
self._fetch_all()
93+
flags = []
94+
category = None
95+
if self.category_id == 0:
96+
category = "Other"
97+
elif self.category_id == 1:
98+
category = "IncomingCall"
99+
elif self.category_id == 2:
100+
category = "MissedCall"
101+
elif self.category_id == 3:
102+
category = "Voicemail"
103+
elif self.category_id == 4:
104+
category = "Social"
105+
elif self.category_id == 5:
106+
category = "Schedule"
107+
elif self.category_id == 6:
108+
category = "Email"
109+
elif self.category_id == 7:
110+
category = "News"
111+
elif self.category_id == 8:
112+
category = "HealthAndFitness"
113+
elif self.category_id == 9:
114+
category = "BusinessAndFinance"
115+
elif self.category_id == 10:
116+
category = "Location"
117+
elif self.category_id == 11:
118+
category = "Entertainment"
119+
120+
if self.silent:
121+
flags.append("silent")
122+
if self.important:
123+
flags.append("important")
124+
if self.preexisting:
125+
flags.append("preexisting")
126+
if self.positive_action:
127+
flags.append("positive_action")
128+
if self.negative_action:
129+
flags.append("negative_action")
130+
return category + " " + " ".join(flags) + " " + self.app_id + " " + self.title + " " + str(self.subtitle) + " " + str(self.message) + " " + self.date
131+
44132
class AppleNotificationService(Service):
45-
"""Notification service. Unimplemented."""
133+
"""Notification service."""
46134
uuid = VendorUUID("7905F431-B5CE-4E99-A40F-4B1E122D00D0")
47135

136+
control_point = StreamIn(uuid=VendorUUID("69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9"))
137+
data_source = StreamOut(uuid=VendorUUID("22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB"), buffer_size=1024)
138+
notification_source = StreamOut(uuid=VendorUUID("9FBF120D-6301-42D9-8C58-25E699A21DBD"), buffer_size=8*100)
139+
140+
def __init__(self, service=None):
141+
super().__init__(service=service)
142+
self._active_notifications = {}
143+
144+
def _update_notifications(self):
145+
while self.notification_source.in_waiting > 7:
146+
buffer = self.notification_source.read(8)
147+
event_id, event_flags, category_id, category_count, id = struct.unpack("<BBBBI", buffer)
148+
if event_id == 0:
149+
self._active_notifications[id] = Notification(id, event_flags, category_id,
150+
category_count, control_point=self.control_point, data_source=self.data_source)
151+
elif event_id == 1:
152+
self._active_notifications[id].update(event_flags, category_id, category_count)
153+
elif event_id == 2:
154+
self._active_notifications[id].removed = True
155+
del self._active_notifications[id]
156+
#print(event_id, event_flags, category_id, category_count)
157+
158+
def __iter__(self):
159+
self._update_notifications()
160+
return iter(self._active_notifications.values())
161+
48162
class AppleMediaService(Service):
49163
"""View and control currently playing media. Unimplemented."""
50164
uuid = VendorUUID("89D3502B-0F36-433A-8EF4-C502AD55F8DC")
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# The MIT License (MIT)
2+
#
3+
# Copyright (c) 2019 Scott Shawcroft for Adafruit Industries
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
# THE SOFTWARE.
22+
"""
23+
24+
This module provides Service classes for BLE defined standard services.
25+
26+
"""
27+
28+
import time
29+
30+
from .. import Service
31+
from ...uuid import StandardUUID
32+
from ...characteristics.string import StringCharacteristic
33+
from ...characteristics import StructCharacteristic
34+
from ...characteristics.int import Uint8Characteristic
35+
36+
__version__ = "0.0.0-auto.0"
37+
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git"
38+
39+
class AppearanceCharacteristic(StructCharacteristic):
40+
"""What type of device it is"""
41+
uuid = StandardUUID(0x2a01)
42+
43+
def __init__(self, **kwargs):
44+
super().__init__("<H", **kwargs)
45+
46+
class GenericAccess(Service):
47+
"""Required service that provides basic device information"""
48+
uuid = StandardUUID(0x1800)
49+
device_name = StringCharacteristic(uuid=StandardUUID(0x2a00))
50+
appearance = AppearanceCharacteristic()
51+
# privacy_flag
52+
# reconnection_address
53+
# preferred_connection_parameters
54+
55+
class GenericAttribute(Service):
56+
"""Required service that provides notifications when Services change"""
57+
uuid = StandardUUID(0x1801)
58+
# service_changed - indicate only
59+
60+
class BatteryService(Service):
61+
"""Provides battery level information"""
62+
uuid = StandardUUID(0x180f)
63+
level = Uint8Characteristic(max_value=100, uuid=StandardUUID(0x2A19))
64+
65+
class CurrentTimeService(Service):
66+
"""Provides the current time."""
67+
uuid = StandardUUID(0x1805)
68+
current_time = StructCharacteristic('<HBBBBBBBB', uuid=StandardUUID(0x2a2b))
69+
"""A tuple describing the current time:
70+
(year, month, day, hour, minute, second, weekday, subsecond, adjust_reason)"""
71+
72+
local_time_info = StructCharacteristic('<bB', uuid=StandardUUID(0x2a0f))
73+
"""A tuple of location information: (timezone, dst_offset)"""
74+
75+
@property
76+
def struct_time(self):
77+
"""The current time as a `time.struct_time`. Day of year and whether DST is in effect
78+
are always -1.
79+
"""
80+
year, month, day, hour, minute, second, weekday, _, _ = self.current_time
81+
# Bluetooth weekdays count from 1. struct_time counts from 0.
82+
return time.struct_time((year, month, day, hour, minute, second, weekday - 1, -1, -1))

adafruit_ble/services/standard/device_info.py

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,8 @@
2121
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2222
# THE SOFTWARE.
2323
"""
24-
`adafruit_ble`
25-
====================================================
26-
27-
This module provides higher-level BLE (Bluetooth Low Energy) functionality,
28-
building on the native `_bleio` module.
29-
30-
* Author(s): Dan Halbert for Adafruit Industries
31-
32-
Implementation Notes
33-
--------------------
34-
35-
**Hardware:**
36-
37-
Adafruit Feather nRF52840 Express <https://www.adafruit.com/product/4062>
38-
39-
**Software and Dependencies:**
40-
41-
* Adafruit CircuitPython firmware for the supported boards:
42-
https://github.com/adafruit/circuitpython/releases
24+
:py:mod:`~adafruit_ble.services.standard.device_info`
25+
=======================================================
4326
4427
"""
4528

0 commit comments

Comments
 (0)