bluetooth: ANS: Add Alert Notification Service

Add alert notification service (ANS) to Bluetooth
subsystem and accompanying sample.

Signed-off-by: Sean Kyer <sean.actor@gmail.com>
This commit is contained in:
Sean Kyer
2025-09-05 12:56:02 -07:00
committed by Anas Nashif
parent 4d6dd7c6a0
commit 05634c3d6c
10 changed files with 901 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
/*
* 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.4
* @version 0.1.0
*
* @ingroup bluetooth
* @{
*/
#include <stdint.h>
#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. */
/** @cond INTERNAL_HIDDEN */
BT_ANS_CAT_NUM, /**< Marker for the number of categories. */
/** @endcond */
/* 1015 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_ */

View File

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

View File

@@ -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 Bluetooth LE app (ADI Attach, nRF Connect, etc.) or dedicated Bluetooth LE sniffer
Building and Running
********************
To start receiving alerts over the connection, refer to
`GATT Specification Supplement <https://btprodspecificationrefs.blob.core.windows.net/gatt-specification-supplement/GATT_Specification_Supplement.pdf>`_
section 3.12 for byte array to enable/disable notifications and control the service.
See :zephyr:code-sample-category:`bluetooth` samples for details.

View File

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

View File

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

View File

@@ -0,0 +1,128 @@
/*
* Copyright (c) 2025 Sean Kyer
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/services/ans.h>
#include <zephyr/logging/log.h>
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;
}

View File

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

View File

@@ -6,6 +6,8 @@
menu "GATT Services"
depends on BT_CONN
rsource "Kconfig.ans"
rsource "Kconfig.dis"
rsource "Kconfig.cts"

View File

@@ -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 [EXPERIMENTAL]"
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

View File

@@ -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 <errno.h>
#include <string.h>
#include <zephyr/init.h>
#include <zephyr/types.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/services/ans.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util_utf8.h>
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);