Skip to content
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

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open

ANCS Support #2217

wants to merge 28 commits into from

Conversation

cyberneel
Copy link

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

"ANCS is not guaranteed to always be present. As a result, the NC should look for and subscribe to the Service Changed characteristic of the GATT service in order to monitor for the potential publishing and unpublishing of the ANCS at any time."

Copy link

github-actions bot commented Jan 3, 2025

Build size and comparison to main:

Section Size Difference
text 381588B 8580B
data 948B 0B
bss 22720B 184B

Run in InfiniEmu

@liamcharger
Copy link
Contributor

liamcharger commented Jan 3, 2025

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@mark9064 mark9064 added the new feature This thread is about a new feature label Jan 16, 2025
@mark9064
Copy link
Member

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

@cyberneel
Copy link
Author

@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?

@mark9064
Copy link
Member

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

@minacode
Copy link
Contributor

minacode commented Feb 4, 2025

How long did you test it? Could it be that the service disappears after a few hours?

@liamcharger
Copy link
Contributor

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.

@minacode
Copy link
Contributor

minacode commented Feb 9, 2025

Thank you for implementing this, @cyberneel!
I just typed the secure connection code into my iPhone for the first time, am already a little hyped from that and will test this PR over the next week :)

Copy link
Member

@mark9064 mark9064 left a 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);
Copy link
Member

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);
Copy link
Member

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?

Copy link
Author

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);
Copy link
Member

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];
Copy link
Member

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) {
Copy link
Member

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 {
Copy link
Member

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

Comment on lines +119 to +128
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};
Copy link
Member

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 {
Copy link
Member

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
Copy link
Member

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)
Copy link
Member

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?

@minacode
Copy link
Contributor

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.

@minacode
Copy link
Contributor

My test over the last week was very positive. Great job!
Feature-wise I would merge it as is, but haven't looked at the code in detail yet and will hopefully do so in the next days.

Copy link
Contributor

@minacode minacode left a 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:

  1. Seeing if we can cut some needed memory.
  2. Reducing the amount of magic numbers by using constants or adding some explanations.

Comment on lines +1 to +4
#include "components/ble/AppleNotificationCenterClient.h"
#include <algorithm>
#include "components/ble/NotificationManager.h"
#include "systemtask/SystemTask.h"
Copy link
Contributor

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) {
Copy link
Contributor

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?

Comment on lines +87 to +88
} else {
if (characteristic != nullptr) {
Copy link
Contributor

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?

Comment on lines +270 to +277
if (titleSize >= maxTitleSize) {
decodedTitle.resize(maxTitleSize - 3);
decodedTitle += "...";
if (!decodedSubTitle.empty()) {
decodedTitle += " - ";
} else {
decodedTitle += "-";
}
Copy link
Contributor

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.

Comment on lines +180 to +190
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, &notificationUuid);
Copy link
Contributor

@minacode minacode Feb 15, 2025

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?

Comment on lines +219 to +221
uint8_t titleSize = maxTitleSize + 4;
uint8_t subTitleSize = maxSubtitleSize + 4;
uint8_t messageSize = maxMessageSize + 4;
Copy link
Contributor

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?

Comment on lines +303 to +311
std::string notifStr;

if (incomingCall) {
notifStr += "Incoming Call:";
notifStr += decodedTitle;
notifStr += "\n";
notifStr += decodedSubTitle;
} else {
notifStr += decodedTitle;
Copy link
Contributor

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.

Comment on lines +319 to +320
if (notifStr.size() > 100) {
notifStr.resize(97);
Copy link
Contributor

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> {};
Copy link
Contributor

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

Comment on lines +327 to +332
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
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new feature This thread is about a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants