diff --git a/doc/releases/release-notes-4.3.rst b/doc/releases/release-notes-4.3.rst index 2e157b25d62f9..6ffbcdfbe8bfb 100644 --- a/doc/releases/release-notes-4.3.rst +++ b/doc/releases/release-notes-4.3.rst @@ -116,6 +116,10 @@ New APIs and options * :c:struct:`bt_iso_sync_receiver_info` now contains a ``big_handle`` and a ``bis_number`` field * :c:struct:`bt_le_ext_adv_info` now contains an ``sid`` field with the Advertising Set ID. + * Services + + * Introduced Alert Notification Service (ANS) :kconfig:option:`CONFIG_BT_ANS` + * CPUFreq * Introduced experimental dynamic CPU frequency scaling subsystem diff --git a/include/zephyr/bluetooth/services/ans.h b/include/zephyr/bluetooth/services/ans.h new file mode 100644 index 0000000000000..d66ff5105cb7e --- /dev/null +++ b/include/zephyr/bluetooth/services/ans.h @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2025 Sean Kyer + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_ANS_H_ +#define ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_ANS_H_ + +/** + * @brief Alert Notification Service (ANS) + * @defgroup bt_ans Alert Notification Service (ANS) + * + * @since 4.3 + * @version 0.1.0 + * + * @ingroup bluetooth + * @{ + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Command not supported error code + */ +#define BT_ANS_ERR_CMD_NOT_SUP 0xa0 + +/** + * @brief ANS max text string size in octets + * + * This is the max string size in octets to be saved in a New Alert. Text longer than the max is + * truncated. + * + * section 3.165 of + * https://btprodspecificationrefs.blob.core.windows.net/gatt-specification-supplement/GATT_Specification_Supplement.pdf + * + */ +#define BT_ANS_MAX_TEXT_STR_SIZE 18 + +/** + * @brief ANS Category ID Enum + * + * Enumeration for whether the category is supported. + */ +enum bt_ans_cat { + BT_ANS_CAT_SIMPLE_ALERT, /**< Simple alerts (general notifications). */ + BT_ANS_CAT_EMAIL, /**< Email messages. */ + BT_ANS_CAT_NEWS, /**< News updates. */ + BT_ANS_CAT_CALL, /**< Incoming call alerts. */ + BT_ANS_CAT_MISSED_CALL, /**< Missed call alerts. */ + BT_ANS_CAT_SMS_MMS, /**< SMS/MMS text messages. */ + BT_ANS_CAT_VOICE_MAIL, /**< Voicemail notifications. */ + BT_ANS_CAT_SCHEDULE, /**< Calendar or schedule alerts. */ + BT_ANS_CAT_HIGH_PRI_ALERT, /**< High-priority alerts. */ + BT_ANS_CAT_INSTANT_MESSAGE, /**< Instant messaging alerts. */ + + BT_ANS_CAT_NUM, /**< Marker for the number of categories. */ + + /* 10–15 reserved for future use */ +}; + +/** + * @brief Set the support for a given new alert category + * + * @param mask The bitmask of supported categories + * + * @return 0 on success + * @return negative error codes on failure + */ +int bt_ans_set_new_alert_support_category(uint16_t mask); + +/** + * @brief Set the support for a given unread new alert category + * + * @param mask The bitmask of supported categories + * + * @return 0 on success + * @return negative error codes on failure + */ +int bt_ans_set_unread_support_category(uint16_t mask); + +/** + * @brief Send a new alert to remote devices + * + * The new alert is transmitted to the remote devices if notifications are enabled. Each category + * will save the latest call to this function in case an immediate replay is requested via the ANS + * control point. + * + * @note This function waits on a Mutex with @ref K_FOREVER to ensure atomic updates to notification + * structs. To avoid deadlocks, do not call this function in BT RX or System Workqueue threads. + * + * @param conn The connection object to send the alert to + * @param category The category the notification is for + * @param num_new Number of new alerts since last alert + * @param text Text brief of alert, null terminated + * + * @return 0 on success + * @return negative error codes on failure + */ +int bt_ans_notify_new_alert(struct bt_conn *conn, enum bt_ans_cat category, uint8_t num_new, + const char *text); + +/** + * @brief Set the total unread count for a given category + * + * The unread count is transmitted to the remote devices if notifications are enabled. Each category + * will save the latest call to this function in case an immediate replay is requested via the ANS + * control point. + * + * @note This function waits on a Mutex with @ref K_FOREVER to ensure atomic updates to notification + * structs. To avoid deadlocks, do not call this function in BT RX or System Workqueue threads. + * + * @param conn The connection object to send the alert to + * @param category The category the unread count is for + * @param unread Total number of unread alerts + * + * @return 0 on success + * @return negative error codes on failure + */ +int bt_ans_set_unread_count(struct bt_conn *conn, enum bt_ans_cat category, uint8_t unread); + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#endif /* ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_ANS_H_ */ diff --git a/samples/bluetooth/peripheral_ans/CMakeLists.txt b/samples/bluetooth/peripheral_ans/CMakeLists.txt new file mode 100644 index 0000000000000..7bbc26196724d --- /dev/null +++ b/samples/bluetooth/peripheral_ans/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(peripheral_ans) + +target_sources(app PRIVATE + src/main.c +) diff --git a/samples/bluetooth/peripheral_ans/README.rst b/samples/bluetooth/peripheral_ans/README.rst new file mode 100644 index 0000000000000..14289acdf59d0 --- /dev/null +++ b/samples/bluetooth/peripheral_ans/README.rst @@ -0,0 +1,26 @@ +.. zephyr:code-sample:: ble_peripheral_ans + :name: Peripheral ANS + :relevant-api: bluetooth + + Send notification using Alert Notification Service (ANS). + +Overview +******** + +This sample demonstrates the usage of ANS by acting as a peripheral periodically sending +notifications to the connected remote device. + +Requirements +************ + +* A board with Bluetooth LE support +* Smartphone with BLE app (ADI Attach, nRF Connect, etc.) or dedicated BLE sniffer + +Building and Running +******************** + +To start receiving alerts over the connection, refer to +`GATT Specification Supplement `_ +section 3.12 for byte array to enable/disable notifications and control the service. + +See :zephyr:code-sample-category:`bluetooth` samples for details. diff --git a/samples/bluetooth/peripheral_ans/prj.conf b/samples/bluetooth/peripheral_ans/prj.conf new file mode 100644 index 0000000000000..3778ea08025b7 --- /dev/null +++ b/samples/bluetooth/peripheral_ans/prj.conf @@ -0,0 +1,9 @@ +CONFIG_LOG=y +CONFIG_UTF8=y +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_HCI_ERR_TO_STR=y +CONFIG_BT_DEVICE_NAME="Zephyr Peripheral ANS Sample" +CONFIG_BT_ANS=y +CONFIG_BT_ANS_LOG_LEVEL_DBG=y +CONFIG_BT_ANS_NALRT_CAT_SIMPLE_ALERT=y diff --git a/samples/bluetooth/peripheral_ans/sample.yaml b/samples/bluetooth/peripheral_ans/sample.yaml new file mode 100644 index 0000000000000..e8ceff732c488 --- /dev/null +++ b/samples/bluetooth/peripheral_ans/sample.yaml @@ -0,0 +1,15 @@ +sample: + name: Bluetooth Peripheral ANS + description: Demonstrates the Alert Notification Service (ANS) +tests: + sample.bluetooth.peripheral_ans: + harness: bluetooth + platform_allow: + - qemu_cortex_m3 + - qemu_x86 + - nrf52840dk/nrf52840 + integration_platforms: + - qemu_cortex_m3 + - qemu_x86 + - nrf52840dk/nrf52840 + tags: bluetooth diff --git a/samples/bluetooth/peripheral_ans/src/main.c b/samples/bluetooth/peripheral_ans/src/main.c new file mode 100644 index 0000000000000..9e76ef6120721 --- /dev/null +++ b/samples/bluetooth/peripheral_ans/src/main.c @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025 Sean Kyer + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(peripheral_ans, CONFIG_LOG_DEFAULT_LEVEL); + +/* + * Sample loops forever, incrementing number of new and unread notifications. Number of new and + * unread notifications will overflow and loop back around. + */ +static uint8_t num_unread; +static uint8_t num_new; + +static const struct bt_data ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA_BYTES(BT_DATA_UUID16_ALL, BT_UUID_16_ENCODE(BT_UUID_ANS_VAL))}; + +static const struct bt_data sd[] = { + BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1), +}; + +static void connected(struct bt_conn *conn, uint8_t err) +{ + if (err != 0) { + LOG_ERR("Connection failed, err 0x%02x %s", err, bt_hci_err_to_str(err)); + return; + } + + LOG_INF("Connected"); +} + +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + LOG_INF("Disconnected, reason 0x%02x %s", reason, bt_hci_err_to_str(reason)); +} + +static void start_adv(void) +{ + int err; + + err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); + if (err != 0) { + LOG_ERR("Advertising failed to start (err %d)", err); + return; + } + + LOG_INF("Advertising successfully started"); +} + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = connected, + .disconnected = disconnected, + .recycled = start_adv, +}; + +int main(void) +{ + int ret; + + LOG_INF("Sample - Bluetooth Peripheral ANS"); + + ret = bt_enable(NULL); + if (ret != 0) { + LOG_ERR("Failed to enable bluetooth: %d", ret); + return ret; + } + + start_adv(); + + num_unread = 0; + num_new = 0; + + /* At runtime, enable support for given categories */ + uint16_t new_alert_mask = (1 << BT_ANS_CAT_SIMPLE_ALERT) | (1 << BT_ANS_CAT_HIGH_PRI_ALERT); + uint16_t unread_mask = 1 << BT_ANS_CAT_SIMPLE_ALERT; + + ret = bt_ans_set_new_alert_support_category(new_alert_mask); + if (ret != 0) { + LOG_ERR("Unable to set new alert support category mask! (err: %d)", ret); + } + + ret = bt_ans_set_unread_support_category(unread_mask); + if (ret != 0) { + LOG_ERR("Unable to set unread support category mask! (err: %d)", ret); + } + + while (true) { + static const char test_msg[] = "Test Alert!"; + static const char high_pri_msg[] = "Prio Alert!"; + + num_new++; + + ret = bt_ans_notify_new_alert(NULL, BT_ANS_CAT_SIMPLE_ALERT, num_new, test_msg); + if (ret != 0) { + LOG_ERR("Failed to push new alert! (err: %d)", ret); + } + k_sleep(K_SECONDS(1)); + + ret = bt_ans_notify_new_alert(NULL, BT_ANS_CAT_HIGH_PRI_ALERT, num_new, + high_pri_msg); + if (ret != 0) { + LOG_ERR("Failed to push new alert! (err: %d)", ret); + } + k_sleep(K_SECONDS(1)); + + ret = bt_ans_set_unread_count(NULL, BT_ANS_CAT_SIMPLE_ALERT, num_unread); + if (ret != 0) { + LOG_ERR("Failed to push new unread count! (err: %d)", ret); + } + + num_unread++; + + k_sleep(K_SECONDS(5)); + } + + return 0; +} diff --git a/subsys/bluetooth/services/CMakeLists.txt b/subsys/bluetooth/services/CMakeLists.txt index bb69916e301a4..f17f683d7186c 100644 --- a/subsys/bluetooth/services/CMakeLists.txt +++ b/subsys/bluetooth/services/CMakeLists.txt @@ -1,5 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 +zephyr_sources_ifdef(CONFIG_BT_ANS ans.c) zephyr_sources_ifdef(CONFIG_BT_DIS dis.c) diff --git a/subsys/bluetooth/services/Kconfig b/subsys/bluetooth/services/Kconfig index a57d4998f3806..cad7dc5a72589 100644 --- a/subsys/bluetooth/services/Kconfig +++ b/subsys/bluetooth/services/Kconfig @@ -6,6 +6,8 @@ menu "GATT Services" depends on BT_CONN +rsource "Kconfig.ans" + rsource "Kconfig.dis" rsource "Kconfig.cts" diff --git a/subsys/bluetooth/services/Kconfig.ans b/subsys/bluetooth/services/Kconfig.ans new file mode 100644 index 0000000000000..fdc36331952cf --- /dev/null +++ b/subsys/bluetooth/services/Kconfig.ans @@ -0,0 +1,86 @@ +# Bluetooth GATT Alert Notification Service + +# Copyright (c) 2025 Sean Kyer +# +# SPDX-License-Identifier: Apache-2.0 + +menuconfig BT_ANS + bool "GATT Alert Notification Service" + depends on UTF8 + select EXPERIMENTAL + +if BT_ANS + +module = BT_ANS +module-str = Alert Notification Service (ANS) +source "subsys/logging/Kconfig.template.log_config" + +menu "New Alert Categories" + +config BT_ANS_NALRT_CAT_SIMPLE_ALERT + bool "Support Simple Alert" + +config BT_ANS_NALRT_CAT_EMAIL + bool "Support Email" + +config BT_ANS_NALRT_CAT_NEWS + bool "Support News" + +config BT_ANS_NALRT_CAT_CALL + bool "Support Call" + +config BT_ANS_NALRT_CAT_MISSED_CALL + bool "Support Missed Call" + +config BT_ANS_NALRT_CAT_SMS_MMS + bool "Support SMS/MMS" + +config BT_ANS_NALRT_CAT_VOICE_MAIL + bool "Support Voice Mail" + +config BT_ANS_NALRT_CAT_SCHEDULE + bool "Support Schedule" + +config BT_ANS_NALRT_CAT_HIGH_PRI_ALERT + bool "Support High Priority Alert" + +config BT_ANS_NALRT_CAT_INSTANT_MESSAGE + bool "Support Instant Message" + +endmenu + +menu "Unread Alert Categories" + +config BT_ANS_UNALRT_CAT_SIMPLE_ALERT + bool "Support Simple Alert" + +config BT_ANS_UNALRT_CAT_EMAIL + bool "Support Email" + +config BT_ANS_UNALRT_CAT_NEWS + bool "Support News" + +config BT_ANS_UNALRT_CAT_CALL + bool "Support Call" + +config BT_ANS_UNALRT_CAT_MISSED_CALL + bool "Support Missed Call" + +config BT_ANS_UNALRT_CAT_SMS_MMS + bool "Support SMS/MMS" + +config BT_ANS_UNALRT_CAT_VOICE_MAIL + bool "Support Voice Mail" + +config BT_ANS_UNALRT_CAT_SCHEDULE + bool "Support Schedule" + +config BT_ANS_UNALRT_CAT_HIGH_PRI_ALERT + bool "Support High Priority Alert" + +config BT_ANS_UNALRT_CAT_INSTANT_MESSAGE + bool "Support Instant Message" + +endmenu + +endif # BT_ANS diff --git a/subsys/bluetooth/services/ans.c b/subsys/bluetooth/services/ans.c new file mode 100644 index 0000000000000..418c90bc11416 --- /dev/null +++ b/subsys/bluetooth/services/ans.c @@ -0,0 +1,489 @@ +/* + * Copyright (c) 2025 Sean Kyer + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * https://www.bluetooth.com/specifications/specs/alert-notification-service-1-0/ + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(bt_ans, CONFIG_BT_ANS_LOG_LEVEL); + +/* + * This only enforces a necessary lower bound at build time. It does + * not guarantee that notification/transmit operations will never fail at + * runtime because other subsystems/services can hold outstanding ATT + * buffers concurrently. + */ +BUILD_ASSERT(CONFIG_BT_MAX_CONN <= CONFIG_BT_ATT_TX_COUNT, + "CONFIG_BT_ATT_TX_COUNT must be >= CONFIG_BT_MAX_CONN"); + +/* Build time ANS supported category bit mask */ +#define BT_ANS_NALRT_CAT_MASK \ + ((IS_ENABLED(CONFIG_BT_ANS_NALRT_CAT_SIMPLE_ALERT) << BT_ANS_CAT_SIMPLE_ALERT) | \ + (IS_ENABLED(CONFIG_BT_ANS_NALRT_CAT_EMAIL) << BT_ANS_CAT_EMAIL) | \ + (IS_ENABLED(CONFIG_BT_ANS_NALRT_CAT_NEWS) << BT_ANS_CAT_NEWS) | \ + (IS_ENABLED(CONFIG_BT_ANS_NALRT_CAT_CALL) << BT_ANS_CAT_CALL) | \ + (IS_ENABLED(CONFIG_BT_ANS_NALRT_CAT_MISSED_CALL) << BT_ANS_CAT_MISSED_CALL) | \ + (IS_ENABLED(CONFIG_BT_ANS_NALRT_CAT_SMS_MMS) << BT_ANS_CAT_SMS_MMS) | \ + (IS_ENABLED(CONFIG_BT_ANS_NALRT_CAT_VOICE_MAIL) << BT_ANS_CAT_VOICE_MAIL) | \ + (IS_ENABLED(CONFIG_BT_ANS_NALRT_CAT_SCHEDULE) << BT_ANS_CAT_SCHEDULE) | \ + (IS_ENABLED(CONFIG_BT_ANS_NALRT_CAT_HIGH_PRI_ALERT) << BT_ANS_CAT_HIGH_PRI_ALERT) | \ + (IS_ENABLED(CONFIG_BT_ANS_NALRT_CAT_INSTANT_MESSAGE) << BT_ANS_CAT_INSTANT_MESSAGE)) + +/* Build time ANS supported category bit mask */ +#define BT_ANS_UNALRT_CAT_MASK \ + ((IS_ENABLED(CONFIG_BT_ANS_UNALRT_CAT_SIMPLE_ALERT) << BT_ANS_CAT_SIMPLE_ALERT) | \ + (IS_ENABLED(CONFIG_BT_ANS_UNALRT_CAT_EMAIL) << BT_ANS_CAT_EMAIL) | \ + (IS_ENABLED(CONFIG_BT_ANS_UNALRT_CAT_NEWS) << BT_ANS_CAT_NEWS) | \ + (IS_ENABLED(CONFIG_BT_ANS_UNALRT_CAT_CALL) << BT_ANS_CAT_CALL) | \ + (IS_ENABLED(CONFIG_BT_ANS_UNALRT_CAT_MISSED_CALL) << BT_ANS_CAT_MISSED_CALL) | \ + (IS_ENABLED(CONFIG_BT_ANS_UNALRT_CAT_SMS_MMS) << BT_ANS_CAT_SMS_MMS) | \ + (IS_ENABLED(CONFIG_BT_ANS_UNALRT_CAT_VOICE_MAIL) << BT_ANS_CAT_VOICE_MAIL) | \ + (IS_ENABLED(CONFIG_BT_ANS_UNALRT_CAT_SCHEDULE) << BT_ANS_CAT_SCHEDULE) | \ + (IS_ENABLED(CONFIG_BT_ANS_UNALRT_CAT_HIGH_PRI_ALERT) << BT_ANS_CAT_HIGH_PRI_ALERT) | \ + (IS_ENABLED(CONFIG_BT_ANS_UNALRT_CAT_INSTANT_MESSAGE) << BT_ANS_CAT_INSTANT_MESSAGE)) + +/* As per spec, ensure at least one New Alert category is supported */ +BUILD_ASSERT(BT_ANS_NALRT_CAT_MASK != 0, + "At least one ANS New Alert category must be enabled in Kconfig"); + +static uint16_t alert_sup_cat_bit_mask = BT_ANS_NALRT_CAT_MASK; +static uint16_t unread_sup_cat_bit_mask = BT_ANS_UNALRT_CAT_MASK; + +/* Command ID definitions */ +#define BT_ANS_SEND_ALL_CATEGORY 0xFF +enum bt_ans_command_id { + BT_ANS_CTRL_ENABLE_NEW_ALERT, + BT_ANS_CTRL_ENABLE_UNREAD, + BT_ANS_CTRL_DISABLE_NEW_ALERT, + BT_ANS_CTRL_DISABLE_UNREAD, + BT_ANS_CTRL_NOTIFY_NEW_ALERT_IMMEDIATE, + BT_ANS_CTRL_NOTIFY_UNREAD_IMMEDIATE, +}; + +/* Struct definition for Alert Notification Control Point */ +struct alert_ctrl_p { + uint8_t cmd_id; + uint8_t category; +} __packed; + +/* Struct definition for New Alert */ +struct new_alert { + uint8_t category_id; + uint8_t num_new_alerts; + char text_string[BT_ANS_MAX_TEXT_STR_SIZE + 1]; +} __packed; + +/* Struct definition for Unread Alert */ +struct unread_alert_status { + uint8_t category_id; + uint8_t unread_count; +} __packed; + +/* Mutex for modifying database */ +K_MUTEX_DEFINE(new_alert_mutex); +K_MUTEX_DEFINE(unread_mutex); + +/* Saved messages database */ +static struct new_alert new_alerts[BT_ANS_CAT_NUM]; +static struct unread_alert_status unread_alerts[BT_ANS_CAT_NUM]; + +/* Initialize to 0, it is control point responsibility to enable once connected */ +static uint16_t alert_cat_enabled_map; +static uint16_t unread_cat_enabled_map; + +/* Supported New Alert Category */ +static ssize_t read_supp_new_alert_cat(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + LOG_DBG("Supported New Alert Category Read"); + + /* Return the bit mask of the supported categories */ + return bt_gatt_attr_read(conn, attr, buf, len, offset, &alert_sup_cat_bit_mask, + sizeof(alert_sup_cat_bit_mask)); +} + +/* New Alert Notifications */ +static void new_alert_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + ARG_UNUSED(attr); + + LOG_DBG("New Alert Notifications %s", value == BT_GATT_CCC_NOTIFY ? "enabled" : "disabled"); +} + +/* Supported Unread Alert Category*/ +static ssize_t read_supp_unread_alert_cat(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + LOG_DBG("Supported Unread Alert Category Read"); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &unread_sup_cat_bit_mask, + sizeof(unread_sup_cat_bit_mask)); +} + +/* Unread Alert Status Notifications */ +static void unread_alert_status_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + ARG_UNUSED(attr); + + LOG_DBG("Unread Alert Status Notifications %s", + value == BT_GATT_CCC_NOTIFY ? "enabled" : "disabled"); +} + +/* Alert Notification Control Point */ +static int transmit_new_alert(struct bt_conn *conn, enum bt_ans_cat category); +static int transmit_unread_alert(struct bt_conn *conn, enum bt_ans_cat category); +static ssize_t write_alert_notif_ctrl_point(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, + uint8_t flags) +{ + int rc; + + LOG_DBG("Alert Control Point Written %u", len); + + if (len != sizeof(struct alert_ctrl_p)) { + LOG_DBG("Length of control packet is %u when expected %zu", len, + sizeof(struct alert_ctrl_p)); + return BT_GATT_ERR(BT_ANS_ERR_CMD_NOT_SUP); + } + + struct alert_ctrl_p command = *((const struct alert_ctrl_p *)buf); + + LOG_DBG("Command ID 0x%x", command.cmd_id); + LOG_DBG("Category 0x%x", command.category); + + if (command.category >= BT_ANS_CAT_NUM && command.category != BT_ANS_SEND_ALL_CATEGORY) { + LOG_DBG("Received control point request for category out of bounds: %d", + command.category); + return BT_GATT_ERR(BT_ANS_ERR_CMD_NOT_SUP); + } + + /* + * If category is BT_ANS_SEND_ALL_CATEGORY then only BT_ANS_CTRL_NOTIFY_NEW_ALERT_IMMEDIATE + * or BT_ANS_CTRL_NOTIFY_UNREAD_IMMEDIATE are valid command IDs + */ + if (command.category == BT_ANS_SEND_ALL_CATEGORY) { + switch (command.cmd_id) { + case BT_ANS_CTRL_NOTIFY_NEW_ALERT_IMMEDIATE: + rc = transmit_new_alert(conn, command.category); + return rc ? BT_GATT_ERR(BT_ANS_ERR_CMD_NOT_SUP) : len; + case BT_ANS_CTRL_NOTIFY_UNREAD_IMMEDIATE: + rc = transmit_unread_alert(conn, command.category); + return rc ? BT_GATT_ERR(BT_ANS_ERR_CMD_NOT_SUP) : len; + default: + return BT_GATT_ERR(BT_ANS_ERR_CMD_NOT_SUP); + } + } + + switch (command.cmd_id) { + case BT_ANS_CTRL_ENABLE_NEW_ALERT: + if ((alert_sup_cat_bit_mask & (1U << command.category)) == 0) { + LOG_DBG("Received control point request for unsupported category: " + "%d", + command.category); + return BT_GATT_ERR(BT_ANS_ERR_CMD_NOT_SUP); + } + alert_cat_enabled_map |= (1U << command.category); + break; + case BT_ANS_CTRL_ENABLE_UNREAD: + if ((unread_sup_cat_bit_mask & (1U << command.category)) == 0) { + LOG_DBG("Received control point request for unsupported category: " + "%d", + command.category); + return BT_GATT_ERR(BT_ANS_ERR_CMD_NOT_SUP); + } + unread_cat_enabled_map |= (1U << command.category); + break; + case BT_ANS_CTRL_DISABLE_NEW_ALERT: + if ((alert_sup_cat_bit_mask & (1U << command.category)) == 0) { + LOG_DBG("Received control point request for unsupported category: " + "%d", + command.category); + + return BT_GATT_ERR(BT_ANS_ERR_CMD_NOT_SUP); + } + alert_cat_enabled_map &= ~(1U << command.category); + break; + case BT_ANS_CTRL_DISABLE_UNREAD: + if ((unread_sup_cat_bit_mask & (1U << command.category)) == 0) { + LOG_DBG("Received control point request for unsupported category: " + "%d", + command.category); + return BT_GATT_ERR(BT_ANS_ERR_CMD_NOT_SUP); + } + unread_cat_enabled_map &= ~(1U << command.category); + break; + default: + return BT_GATT_ERR(BT_ANS_ERR_CMD_NOT_SUP); + } + + return len; +} + +static int ans_init(void) +{ + for (int i = 0; i < BT_ANS_CAT_NUM; i++) { + new_alerts[i].category_id = i; + unread_alerts[i].category_id = i; + } + + LOG_INF("ANS initialization complete"); + + return 0; +} + +/* Alert Notification Service Declaration */ +BT_GATT_SERVICE_DEFINE( + ans_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_ANS), + BT_GATT_CHARACTERISTIC(BT_UUID_GATT_SNALRTC, BT_GATT_CHRC_READ, BT_GATT_PERM_READ, + read_supp_new_alert_cat, NULL, NULL), + BT_GATT_CHARACTERISTIC(BT_UUID_GATT_NALRT, BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_NONE, NULL, + NULL, NULL), + BT_GATT_CCC(new_alert_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), + BT_GATT_CHARACTERISTIC(BT_UUID_GATT_SUALRTC, BT_GATT_CHRC_READ, BT_GATT_PERM_READ, + read_supp_unread_alert_cat, NULL, NULL), + BT_GATT_CHARACTERISTIC(BT_UUID_GATT_UALRTS, BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_NONE, NULL, + NULL, NULL), + BT_GATT_CCC(unread_alert_status_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), + BT_GATT_CHARACTERISTIC(BT_UUID_GATT_ALRTNCP, BT_GATT_CHRC_WRITE, BT_GATT_PERM_WRITE, NULL, + write_alert_notif_ctrl_point, NULL)); + +/* Helper to notify a single category */ +static int notify_new_alert_category(struct bt_conn *conn, uint8_t cat) +{ + int rc; + int ret; + + ret = k_mutex_lock(&new_alert_mutex, K_NO_WAIT); + if (ret != 0) { + LOG_ERR("Unable to lock mutex (err: %d)", ret); + return -EAGAIN; + } + + rc = bt_gatt_notify_uuid(conn, BT_UUID_GATT_NALRT, ans_svc.attrs, &new_alerts[cat], + sizeof(struct new_alert)); + + ret = k_mutex_unlock(&new_alert_mutex); + __ASSERT(ret == 0, "Unable to unlock mutex (err: %d)", ret); + + /* If the client is not connected, that is fine */ + if (rc != 0 && rc != -ENOTCONN) { + LOG_DBG("Error notifying New Alert category %d rc: %d", cat, rc); + return rc; + } + + return 0; +} + +/* Transmit New Alert */ +static int transmit_new_alert(struct bt_conn *conn, enum bt_ans_cat category) +{ + int rc; + + uint8_t cat_in = (uint8_t)category; + + /* Nothing to do if notify is disabled */ + if (conn != NULL && !bt_gatt_is_subscribed(conn, &ans_svc.attrs[3], BT_GATT_CCC_NOTIFY)) { + return 0; + } + + /* Special case: send all categories */ + if (cat_in == BT_ANS_SEND_ALL_CATEGORY) { + for (int i = 0; i < BT_ANS_CAT_NUM; i++) { + if (((alert_sup_cat_bit_mask & BIT(i)) != 0) && + ((alert_cat_enabled_map & BIT(i)) != 0)) { + rc = notify_new_alert_category(conn, i); + if (rc < 0) { + return rc; + } + } + } + return 0; + } + + /* Otherwise send just the requested category (if enabled) */ + if ((alert_cat_enabled_map & BIT(cat_in)) != 0) { + return notify_new_alert_category(conn, cat_in); + } + + return 0; +} + +/* Helper to notify a single unread alert category */ +static int notify_unread_alert_category(struct bt_conn *conn, uint8_t cat) +{ + int rc; + int ret; + + ret = k_mutex_lock(&unread_mutex, K_NO_WAIT); + if (ret != 0) { + LOG_ERR("Unable to lock mutex (err: %d)", ret); + return -EAGAIN; + } + + rc = bt_gatt_notify_uuid(conn, BT_UUID_GATT_UALRTS, ans_svc.attrs, &unread_alerts[cat], + sizeof(struct unread_alert_status)); + + ret = k_mutex_unlock(&unread_mutex); + __ASSERT(ret == 0, "Unable to unlock mutex (err: %d)", ret); + + /* If the client is not connected, that is fine */ + if (rc != 0 && rc != -ENOTCONN) { + LOG_DBG("Error notifying Unread Alert category %d rc: %d", cat, rc); + return rc; + } + + return 0; +} + +/* Transmit Unread Alert */ +static int transmit_unread_alert(struct bt_conn *conn, enum bt_ans_cat category) +{ + int rc; + + uint8_t cat_in = (uint8_t)category; + + /* Nothing to do if notify is disabled */ + if (conn != NULL && !bt_gatt_is_subscribed(conn, &ans_svc.attrs[6], BT_GATT_CCC_NOTIFY)) { + return 0; + } + + /* Special case: send all categories */ + if (cat_in == BT_ANS_SEND_ALL_CATEGORY) { + for (int i = 0; i < BT_ANS_CAT_NUM; i++) { + if ((unread_sup_cat_bit_mask & BIT(i)) && + (unread_cat_enabled_map & BIT(i))) { + rc = notify_unread_alert_category(conn, i); + if (rc < 0) { + return rc; + } + } + } + return 0; + } + + /* Otherwise send just the requested category (if enabled) */ + if ((unread_cat_enabled_map & BIT(cat_in)) != 0) { + return notify_unread_alert_category(conn, cat_in); + } + + return 0; +} + +int bt_ans_notify_new_alert(struct bt_conn *conn, enum bt_ans_cat category, uint8_t num_new, + const char *text) +{ + int ret; + + uint8_t cat_in = (uint8_t)category; + + /* Check if the category is supported */ + if ((alert_sup_cat_bit_mask & (1U << cat_in)) == 0) { + LOG_DBG("Category %d unsupported", cat_in); + return -EINVAL; + } + + /* Update the saved value */ + ret = k_mutex_lock(&new_alert_mutex, K_FOREVER); + __ASSERT(ret == 0, "Unable to lock mutex (err: %d)", ret); + new_alerts[cat_in].num_new_alerts = num_new; + utf8_lcpy(new_alerts[cat_in].text_string, text, sizeof(new_alerts[cat_in].text_string)); + ret = k_mutex_unlock(&new_alert_mutex); + __ASSERT(ret == 0, "Unable to unlock mutex (err: %d)", ret); + + return transmit_new_alert(conn, category); +} + +int bt_ans_set_unread_count(struct bt_conn *conn, enum bt_ans_cat category, uint8_t unread) +{ + int ret; + + uint8_t cat_in = (uint8_t)category; + + /* Check if the category is supported */ + if ((unread_sup_cat_bit_mask & (1U << cat_in)) == 0) { + LOG_DBG("Category %d unsupported", cat_in); + return -EINVAL; + } + + /* Update the saved value */ + ret = k_mutex_lock(&unread_mutex, K_FOREVER); + __ASSERT(ret == 0, "Unable to lock mutex (err: %d)", ret); + unread_alerts[cat_in].unread_count = unread; + ret = k_mutex_unlock(&unread_mutex); + __ASSERT(ret == 0, "Unable to unlock mutex (err: %d)", ret); + + return transmit_unread_alert(conn, category); +} + +/* Callback used by bt_conn_foreach() to detect an established connection. */ +static void ans_conn_check_cb(struct bt_conn *conn, void *data) +{ + int err; + bool *has_conn = data; + struct bt_conn_info info; + + err = bt_conn_get_info(conn, &info); + if (err == 0 && info.state == BT_CONN_STATE_CONNECTED) { + *has_conn = true; + } +} + +/* Used to check if an active connect exists, stopping modifications to supported features */ +static bool ans_check_conn_busy(void) +{ + bool has_conn = false; + + bt_conn_foreach(BT_CONN_TYPE_ALL, ans_conn_check_cb, &has_conn); + + if (has_conn) { + /* Cannot change support while connection exists */ + return true; + } + + return false; +} + +int bt_ans_set_new_alert_support_category(uint16_t mask) +{ + if (ans_check_conn_busy()) { + /* Cannot change support while connection exists */ + return -EBUSY; + } + + alert_sup_cat_bit_mask = mask; + + LOG_DBG("New Alert Support Bit Mask: %x", alert_sup_cat_bit_mask); + + return 0; +} + +int bt_ans_set_unread_support_category(uint16_t mask) +{ + if (ans_check_conn_busy()) { + /* Cannot change support while connection exists */ + return -EBUSY; + } + + unread_sup_cat_bit_mask = mask; + + LOG_DBG("Unread Support Bit Mask: %x", unread_sup_cat_bit_mask); + + return 0; +} + +SYS_INIT(ans_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);