1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
|
/*********************************************************************
This is an example for our nRF52 based Bluefruit LE modules
Pick one up today in the adafruit shop!
Adafruit invests time and resources providing this open source code,
please support Adafruit and open-source hardware by purchasing
products from Adafruit!
MIT license, check LICENSE for more information
All text above, and the splash screen below must be included in
any redistribution
*********************************************************************/
/*
* This sketch is similar to 'ancs', but it also uses a Feather OLED
* Wing to display incoming ANCS alerts:
* https://www.adafruit.com/product/2900
*
* BUTTON A: Up or accept call
* BUTTON B: Not used since it is hard to press
* BUTTON C: Down or decline call
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <bluefruit.h>
/*------------- OLED and Buttons -------------*/
#if defined ARDUINO_NRF52_FEATHER
// Feather nRF52832
#define BUTTON_A 31
#define BUTTON_B 30
#define BUTTON_C 27
#elif defined ARDUINO_NRF52840_FEATHER
// Feather nRF52840
#define BUTTON_A 9
#define BUTTON_B 6
#define BUTTON_C 5
#else
#error board not supported
#endif
#define OLED_RESET 4 // TODO remove ?
Adafruit_SSD1306 oled(OLED_RESET);
/*------------- Notification List -------------*/
#define MAX_COUNT 20
#define BUFSIZE 64
typedef struct
{
AncsNotification_t ntf;
char title[BUFSIZE];
char message[BUFSIZE];
char app_name[BUFSIZE];
} MyNotif_t;
MyNotif_t myNotifs[MAX_COUNT] = { 0 };
// Number of notifications
int notifCount = 0;
/*------------- Display Management -------------*/
#define ONSCREEN_TIME 5000 // On-screen time for each notification
int activeIndex = 0; // Index of currently displayed notification
int displayIndex = -1; // Index of notification about to display
uint32_t drawTime = 0; // Last time oled display notification
/*------------- BLE Client Service-------------*/
BLEAncs bleancs;
void setup()
{
// Button configured
pinMode(BUTTON_A, INPUT_PULLUP);
pinMode(BUTTON_B, INPUT_PULLUP);
pinMode(BUTTON_C, INPUT_PULLUP);
// init with the I2C addr 0x3C (for the 128x32) and show splashscreen
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
oled.display();
oled.setTextSize(1);// max is 4 line, 21 chars each
oled.setTextColor(WHITE);
// Config the peripheral connection with maximum bandwidth
// more SRAM required by SoftDevice
// Note: All config***() function must be called before begin()
//Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.begin();
// Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
Bluefruit.setTxPower(4);
Bluefruit.setName("Bluefruit52");
Bluefruit.setConnectCallback(connect_callback);
Bluefruit.setDisconnectCallback(disconnect_callback);
// Configure and Start Service
bleancs.begin();
bleancs.setNotificationCallback(ancs_notification_callback);
// Set up and start advertising
startAdv();
// splash screen effect
delay(100);
oled.clearDisplay();
oled.setCursor(0, 0);
oled.println("Not connected");
oled.display();
}
void startAdv(void)
{
// Advertising packet
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
// Include ANCS 128-bit uuid
Bluefruit.Advertising.addService(bleancs);
// Secondary Scan Response packet (optional)
// Since there is no room for 'Name' in Advertising packet
Bluefruit.ScanResponse.addName();
/* Start Advertising
* - Enable auto advertising if disconnected
* - Interval: fast mode = 20 ms, slow mode = 152.5 ms
* - Timeout for fast mode is 30 seconds
* - Start(timeout) with timeout = 0 will advertise forever (until connected)
*
* For recommended advertising interval
* https://developer.apple.com/library/content/qa/qa1931/_index.html
*/
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
}
void loop()
{
// If service is not yet discovered
if ( !bleancs.discovered() ) return;
// No notifications, do nothing
if ( notifCount == 0 ) return;
// Check buttons
uint32_t presedButtons = readPressedButtons();
if ( myNotifs[activeIndex].ntf.categoryID == ANCS_CAT_INCOMING_CALL )
{
/* Incoming call event
* - Button A to accept call
* - Button C to decline call
*/
if ( presedButtons & bit(BUTTON_A) )
{
bleancs.actPositive(myNotifs[activeIndex].ntf.uid);
}
if ( presedButtons & bit(BUTTON_C) )
{
bleancs.actNegative(myNotifs[activeIndex].ntf.uid);
}
}
else
{
/* Normal events navigation (wrap around)
* - Button A to display previous notification
* - Button C to display next notification
*
* When a notification is display ONSCREEN_TIME,
* we will display the next one
*/
if ( presedButtons & bit(BUTTON_A) )
{
displayIndex = (activeIndex != 0) ? (activeIndex-1) : (notifCount-1) ;
}
if ( presedButtons & bit(BUTTON_C) )
{
displayIndex = (activeIndex != (notifCount-1)) ? (activeIndex + 1) : 0;
}
// Display requested notification
if ( displayIndex >= 0 )
{
activeIndex = displayIndex;
displayIndex = -1;
displayNotification(activeIndex);
drawTime = millis(); // Save time we draw
}
// Display next notification if time is up
else if ( drawTime + ONSCREEN_TIME < millis() )
{
activeIndex = (activeIndex+1)%notifCount;
displayNotification(activeIndex);
drawTime = millis(); // Save time we draw
}
}
}
/**
* Display notification contents to oled screen
* @param index index of notification
*/
void displayNotification(int index)
{
// safeguard
if ( index < 0 || (index >= notifCount) ) return;
// let's Turn on and off RED LED when we draw to get attention
digitalWrite(LED_RED, HIGH);
/*------------- Display to OLED -------------*/
MyNotif_t* myNtf = &myNotifs[index];
oled.clearDisplay();
oled.setCursor(0, 0);
// Incoming call event, display a bit differently
if ( myNtf->ntf.categoryID == ANCS_CAT_INCOMING_CALL )
{
oled.println(myNtf->title);
oled.println(" is calling");
oled.println(" Btn A to ACCEPT");
oled.println(" Btn C to DECLINE");
}else
{
// Text size = 1, max char is 21. Text size = 2, max char is 10
char tempbuf[22];
sprintf(tempbuf, "%-15s %02d/%02d", myNtf->app_name, index+1, notifCount);
oled.println(tempbuf);
oled.println(myNtf->title);
oled.print(" ");
oled.print(myNtf->message);
}
oled.display();
digitalWrite(LED_RED, LOW);
}
/**
* Connect Callback
* Perform ANCS discovering, request Pairing
*/
void connect_callback(uint16_t conn_handle)
{
oled.clearDisplay();
oled.setCursor(0, 0);
oled.println("Connected.");
oled.print("Discovering ... ");
oled.display();
if ( bleancs.discover( conn_handle ) )
{
oled.println("OK");
// ANCS requires pairing to work
oled.print("Paring ... ");
oled.display();
if ( Bluefruit.requestPairing() )
{
oled.println("OK");
bleancs.enableNotification();
oled.println("Receiving ...");
}else
{
oled.println("Failed");
}
}else
{
oled.println("Failed");
}
oled.display();
}
/**
* Notification callback
* @param notif Notification from iDevice
*
* Save/Modify notification into myNotifs struct to display later
*/
void ancs_notification_callback(AncsNotification_t* notif)
{
if (notif->eventID == ANCS_EVT_NOTIFICATION_ADDED )
{
myNotifs[ notifCount ].ntf = *notif;
/*------------- Retrieve Title, Message, App Name -------------*/
MyNotif_t* myNtf = &myNotifs[notifCount];
uint32_t uid = myNtf->ntf.uid;
// iDevice often include Unicode "Bidirection Text Control" in the Title.
// Mostly are U+202D as beginning and U+202C as ending. Let's remove them
if ( bleancs.getTitle (uid, myNtf->title , BUFSIZE) )
{
char u202D[3] = { 0xE2, 0x80, 0xAD }; // U+202D in UTF-8
char u202C[3] = { 0xE2, 0x80, 0xAC }; // U+202C in UTF-8
int len = strlen(myNtf->title);
if ( 0 == memcmp(&myNtf->title[len-3], u202C, 3) )
{
len -= 3;
myNtf->title[len] = 0; // chop ending U+202C
}
if ( 0 == memcmp(myNtf->title, u202D, 3) )
{
memmove(myNtf->title, myNtf->title+3, len-2); // move null-terminator as well
}
}
bleancs.getMessage(uid, myNtf->message , BUFSIZE);
bleancs.getAppName(uid, myNtf->app_name, BUFSIZE);
displayIndex = notifCount++; // display new notification
}else if (notif->eventID == ANCS_EVT_NOTIFICATION_REMOVED )
{
for(int i=0; i<notifCount; i++)
{
if ( notif->uid == myNotifs[i].ntf.uid )
{
// remove by swapping with the last one
notifCount--;
myNotifs[i] = myNotifs[notifCount];
// Invalid removed data
memset(&myNotifs[notifCount], 0, sizeof(MyNotif_t));
if (activeIndex == notifCount)
{
// If remove the last notification, adjust display index
displayIndex = notifCount-1;
}else if (activeIndex == i)
{
// Re-draw if remove currently active one
displayIndex = activeIndex;
}
break;
}
}
}else
{
// Modification
for(int i=0; i<notifCount; i++)
{
if ( notif->uid == myNotifs[i].ntf.uid )
{
// Display modification
displayIndex = i;
break;
}
}
}
}
/**
* Callback invoked when a connection is dropped
* @param conn_handle connection where this event happens
* @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h
* https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/master/cores/nRF5/nordic/softdevice/s140_nrf52_6.1.1_API/include/ble_hci.h
*/
void disconnect_callback(uint16_t conn_handle, uint8_t reason)
{
(void) conn_handle;
(void) reason;
// reset notification array
notifCount = 0;
activeIndex = 0;
displayIndex = -1;
memset(myNotifs, 0, sizeof(myNotifs));
oled.clearDisplay();
oled.setCursor(0, 0);
oled.println("Not connected");
oled.display();
}
/**
* Check if button A,B,C state are pressed, include some software
* debouncing.
*
* Note: Only set bit when Button is state change from
* idle -> pressed. Press and hold only report 1 time, release
* won't report as well
*
* @return Bitmask of pressed buttons e.g If BUTTON_A is pressed
* bit 31 will be set.
*/
uint32_t readPressedButtons(void)
{
// must be exponent of 2
enum { MAX_CHECKS = 8, SAMPLE_TIME = 10 };
/* Array that maintains bounce status/, which is sampled
* 10 ms each. Debounced state is regconized if all the values
* of a button has the same value (bit set or clear)
*/
static uint32_t lastReadTime = 0;
static uint32_t states[MAX_CHECKS] = { 0 };
static uint32_t index = 0;
// Last Debounced state, used to detect changed
static uint32_t lastDebounced = 0;
// Too soon, nothing to do
if (millis() - lastReadTime < SAMPLE_TIME ) return 0;
lastReadTime = millis();
// Take current read and masked with BUTTONs
// Note: Bitwise inverted since buttons are active (pressed) LOW
uint32_t debounced = ~(*portInputRegister( digitalPinToPort(0) ));
debounced &= (bit(BUTTON_A) | bit(BUTTON_B) | bit(BUTTON_C));
// Copy current state into array
states[ (index & (MAX_CHECKS-1)) ] = debounced;
index++;
// Bitwise And all the state in the array together to get the result
// This means pin must stay at least MAX_CHECKS time to be realized as changed
for(int i=0; i<MAX_CHECKS; i++)
{
debounced &= states[i];
}
// result is button changed and current debounced is set
// Mean button is pressed (idle previously)
uint32_t result = (debounced ^ lastDebounced) & debounced;
lastDebounced = debounced;
return result;
}
|