27
27
28
28
"""
29
29
30
+ import struct
31
+ import time
32
+
30
33
from . import Service
31
34
from ..uuid import VendorUUID
35
+ from ..characteristics .stream import StreamIn , StreamOut
32
36
33
37
__version__ = "0.0.0-auto.0"
34
38
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git"
@@ -41,10 +45,197 @@ class UnknownApple1Service(Service):
41
45
"""Unknown service. Unimplemented."""
42
46
uuid = VendorUUID ("9fa480e0-4967-4542-9390-d343dc5d04ae" )
43
47
48
+ class _NotificationAttribute :
49
+ def __init__ (self , attribute_id , * , max_length = False ):
50
+ self ._id = attribute_id
51
+ self ._max_length = max_length
52
+
53
+ def __get__ (self , notification , cls ):
54
+ if self ._id in notification ._attribute_cache :
55
+ return notification ._attribute_cache [self ._id ]
56
+
57
+ if self ._max_length :
58
+ command = struct .pack ("<BIBH" , 0 , notification .id , self ._id , 255 )
59
+ else :
60
+ command = struct .pack ("<BIB" , 0 , notification .id , self ._id )
61
+ notification .control_point .write (command )
62
+ while notification .data_source .in_waiting == 0 :
63
+ pass
64
+
65
+ _ , _ = struct .unpack ("<BI" , notification .data_source .read (5 ))
66
+ attribute_id , attribute_length = struct .unpack ("<BH" , notification .data_source .read (3 ))
67
+ if attribute_id != self ._id :
68
+ raise RuntimeError ("Data for other attribute" )
69
+ value = notification .data_source .read (attribute_length )
70
+ value = value .decode ("utf-8" )
71
+ notification ._attribute_cache [self ._id ] = value
72
+ return value
73
+
74
+ NOTIFICATION_CATEGORIES = (
75
+ "Other" ,
76
+ "IncomingCall" ,
77
+ "MissedCall" ,
78
+ "Voicemail" ,
79
+ "Social" ,
80
+ "Schedule" ,
81
+ "Email" ,
82
+ "News" ,
83
+ "HealthAndFitness" ,
84
+ "BusinessAndFinance" ,
85
+ "Location" ,
86
+ "Entertainment"
87
+ )
88
+
89
+ class Notification :
90
+ """One notification that appears in the iOS notification center."""
91
+ # pylint: disable=too-many-instance-attributes
92
+
93
+ app_id = _NotificationAttribute (0 )
94
+ """String id of the app that generated the notification. It is not the name of the app. For
95
+ example, Slack is "com.tinyspeck.chatlyio" and Twitter is "com.atebits.Tweetie2"."""
96
+
97
+ title = _NotificationAttribute (1 , max_length = True )
98
+ """Title of the notification. Varies per app."""
99
+
100
+ subtitle = _NotificationAttribute (2 , max_length = True )
101
+ """Subtitle of the notification. Varies per app."""
102
+
103
+ message = _NotificationAttribute (3 , max_length = True )
104
+ """Message body of the notification. Varies per app."""
105
+
106
+ message_size = _NotificationAttribute (4 )
107
+ """Total length of the message string."""
108
+
109
+ _raw_date = _NotificationAttribute (5 )
110
+ positive_action_label = _NotificationAttribute (6 )
111
+ """Human readable label of the positive action."""
112
+
113
+ negative_action_label = _NotificationAttribute (7 )
114
+ """Human readable label of the negative action."""
115
+
116
+ def __init__ (self , notification_id , event_flags , category_id , category_count , * , control_point ,
117
+ data_source ):
118
+ self .id = notification_id # pylint: disable=invalid-name
119
+ """Integer id of the notification."""
120
+
121
+ self .removed = False
122
+ """True when the notification has been cleared on the iOS device."""
123
+
124
+
125
+ self .silent = False
126
+ self .important = False
127
+ self .preexisting = False
128
+ """True if the notification existed before we connected to the iOS device."""
129
+
130
+ self .positive_action = False
131
+ """True if the notification has a positive action to respond with. For example, this could
132
+ be answering a phone call."""
133
+
134
+ self .negative_action = False
135
+ """True if the notification has a negative action to respond with. For example, this could
136
+ be declining a phone call."""
137
+
138
+ self .category_count = 0
139
+ """Number of other notifications with the same category."""
140
+
141
+ self .update (event_flags , category_id , category_count )
142
+
143
+ self ._attribute_cache = {}
144
+
145
+ self .control_point = control_point
146
+ self .data_source = data_source
147
+
148
+ def update (self , event_flags , category_id , category_count ):
149
+ """Update the notification and clear the attribute cache."""
150
+ self .category_id = category_id
151
+
152
+ self .category_count = category_count
153
+
154
+ self .silent = (event_flags & (1 << 0 )) != 0
155
+ self .important = (event_flags & (1 << 1 )) != 0
156
+ self .preexisting = (event_flags & (1 << 2 )) != 0
157
+ self .positive_action = (event_flags & (1 << 3 )) != 0
158
+ self .negative_action = (event_flags & (1 << 4 )) != 0
159
+
160
+ self ._attribute_cache = {}
161
+
162
+ def __str__ (self ):
163
+ # pylint: disable=too-many-branches
164
+ flags = []
165
+ category = None
166
+ if self .category_id < len (NOTIFICATION_CATEGORIES ):
167
+ category = NOTIFICATION_CATEGORIES [self .category_id ]
168
+
169
+ if self .silent :
170
+ flags .append ("silent" )
171
+ if self .important :
172
+ flags .append ("important" )
173
+ if self .preexisting :
174
+ flags .append ("preexisting" )
175
+ if self .positive_action :
176
+ flags .append ("positive_action" )
177
+ if self .negative_action :
178
+ flags .append ("negative_action" )
179
+ return (category + " " +
180
+ " " .join (flags ) + " " +
181
+ self .app_id + " " +
182
+ str (self .title ) + " " +
183
+ str (self .subtitle ) + " " +
184
+ str (self .message ))
185
+
44
186
class AppleNotificationService (Service ):
45
- """Notification service. Unimplemented. """
187
+ """Notification service."""
46
188
uuid = VendorUUID ("7905F431-B5CE-4E99-A40F-4B1E122D00D0" )
47
189
190
+ control_point = StreamIn (uuid = VendorUUID ("69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9" ))
191
+ data_source = StreamOut (uuid = VendorUUID ("22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB" ),
192
+ buffer_size = 1024 )
193
+ notification_source = StreamOut (uuid = VendorUUID ("9FBF120D-6301-42D9-8C58-25E699A21DBD" ),
194
+ buffer_size = 8 * 100 )
195
+
196
+ def __init__ (self , service = None ):
197
+ super ().__init__ (service = service )
198
+ self ._active_notifications = {}
199
+
200
+ def _update (self ):
201
+ # Pylint is incorrectly inferring the type of self.notification_source so disable no-member.
202
+ while self .notification_source .in_waiting > 7 : # pylint: disable=no-member
203
+ buffer = self .notification_source .read (8 ) # pylint: disable=no-member
204
+ event_id , event_flags , category_id , category_count , nid = struct .unpack ("<BBBBI" ,
205
+ buffer )
206
+ if event_id == 0 :
207
+ self ._active_notifications [nid ] = Notification (nid , event_flags , category_id ,
208
+ category_count ,
209
+ control_point = self .control_point ,
210
+ data_source = self .data_source )
211
+ yield self ._active_notifications [nid ]
212
+ elif event_id == 1 :
213
+ self ._active_notifications [nid ].update (event_flags , category_id , category_count )
214
+ yield None
215
+ elif event_id == 2 :
216
+ self ._active_notifications [nid ].removed = True
217
+ del self ._active_notifications [nid ]
218
+ yield None
219
+
220
+ def wait_for_new_notifications (self , timeout = None ):
221
+ """Waits for new notifications and yields them. Returns on timeout, update, disconnect or
222
+ clear."""
223
+ start_time = time .monotonic ()
224
+ while timeout is None or timeout > time .monotonic () - start_time :
225
+ try :
226
+ new_notification = next (self ._update ())
227
+ except StopIteration :
228
+ return
229
+ if new_notification :
230
+ yield new_notification
231
+
232
+ @property
233
+ def active_notifications (self ):
234
+ """A dictionary of active notifications keyed by id."""
235
+ for _ in self ._update ():
236
+ pass
237
+ return self ._active_notifications
238
+
48
239
class AppleMediaService (Service ):
49
240
"""View and control currently playing media. Unimplemented."""
50
241
uuid = VendorUUID ("89D3502B-0F36-433A-8EF4-C502AD55F8DC" )
0 commit comments