-
-
Notifications
You must be signed in to change notification settings - Fork 980
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ANCS Support #2217
base: main
Are you sure you want to change the base?
ANCS Support #2217
Conversation
Currently, the notification UID, Event ID and Category are shown.
Data source is not working properly, the Characteristic and/or descriptor isn't being found correctly
Datasource seems to go off and on.
Need to iron out some things like characteristics disappearing and reappearing.
Also added constants to set lengths for the title, subtitle, and message.
…essage is in the body.
Accepting and Declining works
Build size and comparison to main:
|
For reference, closes #910 Instructions: since an encrypted connection is required before ANCS is available, you’ll need to disconnect your watch from your companion app after updating, then reconnect. You’ll get a passkey on your watch to enter into the dialog on your iPhone. After bonding, toggle off and on your iPhone’s Bluetooth, then tap InfiniTime in the list of bonded devices. Finally, you’ll get an alert asking you to allow your watch to read notifications. Click allow, and now you should start receiving notifications! Something to note: as of now, since using ANCS creates a connection through iOS directly instead of through the companion app, the app will not be able to reconnect to InfiniTime without removing the bond, which results in ANCS being disabled until the steps above are repeated. We’re working on fixing this ;) Update: this is fixed in the latest InfiniLink commit on the rebuild branch |
NegativeAction = (1 << 4) | ||
}; | ||
|
||
struct AncsNotitfication { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
Does this implement subscribing to service changes? I think this is probably required given the ANCS can come and go at any time I think the implementation of this is relatively simple, subscribe to the generic attribute service changed characteristic, and any time an indication is received service discovery should be performed again |
@mark9064 I didn't explicitly implement the service changed part, but in all my testing the watch was always able to recover from a disconnect and showed new notifications. So maybe there is something in the stack that handles this? |
I don't believe it's handled elsewhere, but it could be? The text you quote at the top, about the service being registered/unregistered, isn't talking about bluetooth disconnects but actually the service itself disappearing from the list of services the apple device exposes, and then re-appearing at some later time. Maybe in practice apple devices never really do this (even though the spec allows them to), so you don't observe any issues. But I think it would be wise to have this base covered if the standard allows it |
How long did you test it? Could it be that the service disappears after a few hours? |
I'm able to use this for multiple days at a time without missing a notification. According to the logs, the watch is (very frequently) not able to subscribe to the data source, but always recovers before the next notification is sent. |
Thank you for implementing this, @cyberneel! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an enormous achievement, well done. There's a lot of code, so I have a lot of comments - no rush to getting to them all.
There are a few overall architectural bits that I've got questions about:
- There's a lot state being introduced. Is all of it being used? If possible, I think we should aim to store the minimum state required to show notification titles and text and allow calls to be accepted/declined.
- There's quite a lot of std::string usage, and I'm worried it might create a lot of heap pressure which may conflict with memory intensive watchfaces like G7710. Would it be possible to minimise memory usage by moving bytes from the BLE stack into the allocated notification object more directly? If not that's OK, but we should carefully assess the memory pressure possible.
- There seems to be an entire private storage of ANCS notifications inside the ANCS client. It'd be really great to just have one store of notifications, with that being the central notification controller. On a similar note, the extra ANCS ID being attached to each notification doesn't feel great. Maybe a UUID for each notification would be good though - perhaps refactoring towards a generic ID also usable by the other notification clients could be good? What do you think?
This review isn't complete as there's probably more things to discuss, but hopefully it's a good starting point.
Great job with this implementing this :)
} else if (ble_uuid_cmp(&dataSourceChar.u, &characteristic->uuid.u) == 0) { | ||
char msg[55]; | ||
snprintf(msg, sizeof(msg), "ANCS Characteristic discovered: Data Source\n%d", characteristic->val_handle); | ||
NRF_LOG_INFO(msg); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry probably being a bit dense here, I don't use a devkit/logging:
Why not pass characteristic->val_handle as an argument to NRF_LOG_INFO
? I believe the current way it will always do the snprintf
even when not in debug mode
bool silent = (eventFlags & static_cast<uint8_t>(EventFlags::Silent)) != 0; | ||
// bool important = eventFlags & static_cast<uint8_t>(EventFlags::Important); | ||
bool preExisting = (eventFlags & static_cast<uint8_t>(EventFlags::PreExisting)) != 0; | ||
// bool positiveAction = eventFlags & static_cast<uint8_t>(EventFlags::PositiveAction); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this commented code for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ANCS provides tags for what type of notification it is sending, and I wasn't using those at the time.
uint8_t messageSize = maxMessageSize + 4; | ||
BYTE request[14]; | ||
request[0] = 0x00; // Command ID: Get Notification Attributes | ||
request[1] = (uint8_t) (notificationUuid & 0xFF); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer using C++ style static_cast
(this applies in other locations also)
uint8_t titleSize = maxTitleSize + 4; | ||
uint8_t subTitleSize = maxSubtitleSize + 4; | ||
uint8_t messageSize = maxMessageSize + 4; | ||
BYTE request[14]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would uint8_t
be more appropriate here?
decodedSubTitle += "..."; | ||
} | ||
|
||
// if (messageSize > maxMessageSize) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this unneeded?
const ble_uuid16_t gattServiceUuid = {BLE_UUID_TYPE_16, 0x1801}; | ||
const ble_uuid16_t serviceChangedCharUuid = {BLE_UUID_TYPE_16, 0x2A05}; | ||
|
||
enum class Categories : uint8_t { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For all of these constants above and below, is there a reference document somewhere? It would be great to have the URL linked in a comment
bool isGattDiscovered {false}; | ||
bool isGattCharacteristicDiscovered {false}; | ||
bool isGattDescriptorFound {false}; | ||
bool isDiscovered {false}; | ||
bool isCharacteristicDiscovered {false}; | ||
bool isDescriptorFound {false}; | ||
bool isControlCharacteristicDiscovered {false}; | ||
bool isControlDescriptorFound {false}; | ||
bool isDataCharacteristicDiscovered {false}; | ||
bool isDataDescriptorFound {false}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Massive networks of booleans like this scare me a bit. There are theoretically 2^10 = 1024 states here. Could this be refactored into a state enum somehow?
ble_gattc_disc_svc_by_uuid(connectionHandle, &ancsUuid.u, OnDiscoveryEventCallback, this); | ||
} | ||
|
||
void AppleNotificationCenterClient::DebugNotification(const char* msg) const { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Old debugging code?
auto isInFontDefinition = [](uint32_t codepoint) -> bool { | ||
// Check if the codepoint falls into the specified font ranges or is explicitly listed | ||
return (codepoint >= 0x20 && codepoint <= 0x7E) || // Printable ASCII | ||
(codepoint >= 0x410 && codepoint <= 0x44F) || // Cyrillic |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How come there's a Cyrillic check? I thought this wasn't supported by any of the fonts. More generally do we need some generic way to check this is within the font ranges? How does the existing notification client handle this?
} else { | ||
NRF_LOG_INFO("ANCS New alert subscribe ERROR"); | ||
} | ||
if (isDescriptorFound == isControlDescriptorFound && isDescriptorFound == isDataDescriptorFound) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is hard to read. What does it mean semantically to compare the values of isDescriptorFound
and isControlDescriptorFound
(for example)? Is the meaning the same if they're both false
compared to when they're both true
?
Yes, it is really impressive that you did it. Thank you! Yesterday I found that we have no-device support for German umlauts öäü and other such characters. Since the notifications come directly from iOS, we have to discuss how far we can and want to go on the watch. But that would be no blocking issue for this PR from my side as long as the BLE service works fine otherwise. So maybe discuss this in the linked issue instead of here. |
My test over the last week was very positive. Great job! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, thank you for your contribution! I have reviewed the first half of your code and found some smaller issues. Overall, the code is written well and one can see that you figured out some clever things to get this feature up and running.
My feedback primarily focuses on two areas:
- Seeing if we can cut some needed memory.
- Reducing the amount of magic numbers by using constants or adding some explanations.
#include "components/ble/AppleNotificationCenterClient.h" | ||
#include <algorithm> | ||
#include "components/ble/NotificationManager.h" | ||
#include "systemtask/SystemTask.h" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: you could sort the imports
if (isDataCharacteristicDiscovered) { | ||
ble_gattc_disc_all_dscs(connectionHandle, dataSourceHandle, ancsEndHandle, OnANCSDescriptorDiscoveryEventCallback, this); | ||
} | ||
if (isCharacteristicDiscovered == isControlCharacteristicDiscovered && isCharacteristicDiscovered == isDataCharacteristicDiscovered) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain those conditions, please?
} else { | ||
if (characteristic != nullptr) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could inline this to else if
, right?
if (titleSize >= maxTitleSize) { | ||
decodedTitle.resize(maxTitleSize - 3); | ||
decodedTitle += "..."; | ||
if (!decodedSubTitle.empty()) { | ||
decodedTitle += " - "; | ||
} else { | ||
decodedTitle += "-"; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if decodedTitle
is longer than maxTitleSize
, then you resize it to 3 chars less than max. Then you add "..."
, which makes it full again, and then you add " - "
or even -
. To me, it seems like decodedTitle
is again too long after that.
uint8_t eventId; | ||
uint8_t eventFlags; | ||
uint8_t category; | ||
uint8_t categoryCount; | ||
uint32_t notificationUuid; | ||
|
||
os_mbuf_copydata(event->notify_rx.om, 0, 1, &eventId); | ||
os_mbuf_copydata(event->notify_rx.om, 1, 1, &eventFlags); | ||
os_mbuf_copydata(event->notify_rx.om, 2, 1, &category); | ||
os_mbuf_copydata(event->notify_rx.om, 3, 1, &categoryCount); | ||
os_mbuf_copydata(event->notify_rx.om, 4, 4, ¬ificationUuid); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe gcc is smart enough to get this, but I will ask anyway 😄
You create those local variables only to insert them into a notification. Couldn't you cut the local vairables and os_mbuf_copydata
directly into the ancsNotif
?
uint8_t titleSize = maxTitleSize + 4; | ||
uint8_t subTitleSize = maxSubtitleSize + 4; | ||
uint8_t messageSize = maxMessageSize + 4; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment here, where that + 4
comes from?
std::string notifStr; | ||
|
||
if (incomingCall) { | ||
notifStr += "Incoming Call:"; | ||
notifStr += decodedTitle; | ||
notifStr += "\n"; | ||
notifStr += decodedSubTitle; | ||
} else { | ||
notifStr += decodedTitle; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first notifStr += ...
in the if and else can be an notifStr = ...
without +
, because notifStr
is new.
if (notifStr.size() > 100) { | ||
notifStr.resize(97); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NotificationManager
has an attribute MessageSize
which encodes this 100.
Please use this and replace the two magic numbers.
notifStr += "..."; | ||
} | ||
|
||
notif.message = std::array<char, 101> {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace magic number: 101
-> MessageSize + 1
if (incomingCall) | ||
notif.message[13] = '\0'; // Separate Title and Message | ||
else if (!decodedSubTitle.empty()) | ||
notif.message[titleSize + subTitleSize] = '\0'; // Separate Title and Message | ||
else | ||
notif.message[titleSize - 1] = '\0'; // Separate Title and Message |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use braces here, just to be safe.
This PR Adds support for the Apple Notification Center Service (ANCS). It shows the Title and Subtitles as "Title - Subtitle" and the message is shown in the body of the InfiniTime notification. Accepting and Declining calls also worked.
The only "issue" right now is that you have to ReConnect after the first pair for ANCS discovery to go well. Maybe there is a way in the NimBLE stack to trigger ANCS Discovery after pairing change? Also, sometimes ANCS goes dormant and then works again. I think this could either be iOS throttling or