diff --git a/doc/connectivity/bluetooth/api/services.rst b/doc/connectivity/bluetooth/api/services.rst index 990add1ce62..0e43a3e8b85 100644 --- a/doc/connectivity/bluetooth/api/services.rst +++ b/doc/connectivity/bluetooth/api/services.rst @@ -8,6 +8,11 @@ Battery Service .. doxygengroup:: bt_bas +Current Time Service +******************** + +.. doxygengroup:: bt_cts + Heart Rate Service ****************** diff --git a/include/zephyr/bluetooth/services/cts.h b/include/zephyr/bluetooth/services/cts.h new file mode 100644 index 00000000000..46fb7808cc4 --- /dev/null +++ b/include/zephyr/bluetooth/services/cts.h @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2024 Croxel Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_CTS_H_ +#define ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_CTS_H_ + +/** + * @brief Current Time Service (CTS) + * @defgroup bt_cts Current Time Service (CTS) + * @ingroup bluetooth + * @{ + * + */ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief CTS time update reason bits as defined in the specification + */ +enum bt_cts_update_reason { + /* Unknown reason of update no bit is set */ + BT_CTS_UPDATE_REASON_UNKNOWN = 0, + /* When time is changed manually e.g. through UI */ + BT_CTS_UPDATE_REASON_MANUAL = BIT(0), + /* If time is changed through external reference */ + BT_CTS_UPDATE_REASON_EXTERNAL_REF = BIT(1), + /* time changed due to timezone adjust */ + BT_CTS_UPDATE_REASON_TIME_ZONE_CHANGE = BIT(2), + /* time changed due to dst offset change */ + BT_CTS_UPDATE_REASON_DAYLIGHT_SAVING = BIT(3), +}; + +/** + * @brief Current Time service data format, Please refer to + * specifications for more details + */ +struct bt_cts_time_format { + uint16_t year; + uint8_t mon; + uint8_t mday; + uint8_t hours; + uint8_t min; + uint8_t sec; + uint8_t wday; + uint8_t fractions256; + uint8_t reason; +} __packed; + +/** @brief Current Time Service callback structure */ +struct bt_cts_cb { + /** @brief Current Time Service notifications changed + * + * @param enabled True if notifications are enabled, false if disabled + */ + void (*notification_changed)(bool enabled); + + /** + * @brief The Current Time has been updated by a peer. + * It is the responsibility of the application to store the new time. + * + * @param cts_time [IN] updated time + * + * @return 0 application has decoded it successfully + * @return negative error codes on failure + * + */ + int (*cts_time_write)(struct bt_cts_time_format *cts_time); + + /** + * @brief When current time Read request or notification is triggered, CTS uses + * this callback to retrieve current time information from application. Application + * must implement it and provide cts formatted current time information + * + * @note this callback is mandatory + * + * @param cts_time [IN] updated time + * + * @return 0 application has encoded it successfully + * @return negative error codes on failure + */ + int (*fill_current_cts_time)(struct bt_cts_time_format *cts_time); +}; + +/** + * @brief This API should be called at application init. + * it is safe to call this API before or after bt_enable API + * + * @param cb pointer to required callback + * + * @return 0 on success + * @return negative error codes on failure + */ +int bt_cts_init(const struct bt_cts_cb *cb); + +/** + * @brief Notify all connected clients that have enabled the + * current time update notification + * + * @param reason update reason to be sent to the clients + * + * @return 0 on success + * @return negative error codes on failure + */ +int bt_cts_send_notification(enum bt_cts_update_reason reason); + +/** + * @brief Helper API to decode CTS formatted time into milliseconds since epoch + * + * @note @kconfig{CONFIG_BT_CTS_HELPER_API} needs to be enabled to use this API. + * + * @param ct_time [IN] cts time formatted time + * @param unix_ms [OUT] pointer to store parsed millisecond since epoch + * + * @return 0 on success + * @return negative error codes on failure + */ +int bt_cts_time_to_unix_ms(const struct bt_cts_time_format *ct_time, int64_t *unix_ms); + +/** + * @brief Helper API to encode milliseconds since epoch to CTS formatted time + * + * @note @kconfig{CONFIG_BT_CTS_HELPER_API} needs to be enabled to use this API. + * + * @param ct_time [OUT] Pointer to store CTS formatted time + * @param unix_ms [IN] milliseconds since epoch to be converted + * + * @return 0 on success + * @return negative error codes on failure + */ +int bt_cts_time_from_unix_ms(struct bt_cts_time_format *ct_time, int64_t unix_ms); + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#endif /* ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_CTS_H_ */ diff --git a/subsys/bluetooth/Kconfig.logging b/subsys/bluetooth/Kconfig.logging index e98587821d9..9c2884214e2 100644 --- a/subsys/bluetooth/Kconfig.logging +++ b/subsys/bluetooth/Kconfig.logging @@ -492,6 +492,14 @@ module-str = BAS source "subsys/logging/Kconfig.template.log_config" endif # BT_BAS +# CTS + +if BT_CTS +module = BT_CTS +module-str = CTS +source "subsys/logging/Kconfig.template.log_config" +endif # BT_CTS + # HRS if BT_HRS diff --git a/subsys/bluetooth/services/CMakeLists.txt b/subsys/bluetooth/services/CMakeLists.txt index c02eb796112..bb69916e301 100644 --- a/subsys/bluetooth/services/CMakeLists.txt +++ b/subsys/bluetooth/services/CMakeLists.txt @@ -3,6 +3,8 @@ zephyr_sources_ifdef(CONFIG_BT_DIS dis.c) +zephyr_sources_ifdef(CONFIG_BT_CTS cts.c) + zephyr_sources_ifdef(CONFIG_BT_HRS hrs.c) zephyr_sources_ifdef(CONFIG_BT_TPS tps.c) diff --git a/subsys/bluetooth/services/Kconfig b/subsys/bluetooth/services/Kconfig index 0377c068c93..a57d4998f38 100644 --- a/subsys/bluetooth/services/Kconfig +++ b/subsys/bluetooth/services/Kconfig @@ -8,6 +8,8 @@ menu "GATT Services" rsource "Kconfig.dis" +rsource "Kconfig.cts" + rsource "Kconfig.hrs" rsource "Kconfig.tps" diff --git a/subsys/bluetooth/services/Kconfig.cts b/subsys/bluetooth/services/Kconfig.cts new file mode 100644 index 00000000000..951e66e35a9 --- /dev/null +++ b/subsys/bluetooth/services/Kconfig.cts @@ -0,0 +1,14 @@ +# Bluetooth GATT Battery service + +# Copyright (c) 2024 Croxel Inc. +# SPDX-License-Identifier: Apache-2.0 + +config BT_CTS + bool "GATT Current Time service" + +if BT_CTS + +config BT_CTS_HELPER_API + bool "Helper APIs to encode and decode CTS formatted time" + +endif diff --git a/subsys/bluetooth/services/cts.c b/subsys/bluetooth/services/cts.c new file mode 100644 index 00000000000..ce72f7728e0 --- /dev/null +++ b/subsys/bluetooth/services/cts.c @@ -0,0 +1,189 @@ +/** @file + * @brief GATT Current Time Service + */ + +/* + * Copyright (c) 2024 Croxel Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include + +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(cts, CONFIG_BT_CTS_LOG_LEVEL); + +#define BT_CTS_ATT_ERR_VALUES_IGNORED 0x80 +#define BT_CTS_FRACTION_256_MAX_VALUE 255 + +static const struct bt_cts_cb *cts_cb; + +#ifdef CONFIG_BT_CTS_HELPER_API + +#include +#include + +int bt_cts_time_to_unix_ms(const struct bt_cts_time_format *ct_time, int64_t *unix_ms) +{ + struct tm date_time; + /* fill date time structure */ + date_time.tm_year = sys_le16_to_cpu(ct_time->year); /* year (little endian) */ + date_time.tm_year -= TIME_UTILS_BASE_YEAR; + date_time.tm_mon = ct_time->mon - 1; /* month start from 1, but need from 0 */ + date_time.tm_mday = ct_time->mday; /* day of month */ + date_time.tm_hour = ct_time->hours; /* hours of day */ + date_time.tm_min = ct_time->min; /* minute of hour */ + date_time.tm_sec = ct_time->sec; /* seconds of minute */ + date_time.tm_wday = ct_time->wday % 7; /* for sundays convert to 0, else keep same */ + + LOG_DBG("CTS Write Time: %d/%d/%d %d:%d:%d", date_time.tm_year, date_time.tm_mon, + date_time.tm_mday, date_time.tm_hour, date_time.tm_min, date_time.tm_sec); + /* get unit timestamp from datetime */ + (*unix_ms) = timeutil_timegm64(&date_time); + if ((*unix_ms) == ((time_t)-1)) { + return -EOVERFLOW; + } + LOG_DBG("CTS Write Unix: %lld", (*unix_ms)); + (*unix_ms) *= MSEC_PER_SEC; + /* add fraction 256 part*/ + (*unix_ms) += ((ct_time->fractions256 * MSEC_PER_SEC) / BT_CTS_FRACTION_256_MAX_VALUE); + + return 0; +} + +int bt_cts_time_from_unix_ms(struct bt_cts_time_format *ct_time, int64_t unix_ms) +{ + struct tm date_time; + time_t unix_ts = unix_ms / MSEC_PER_SEC; + + /* 'Fractions 256 part of 'Exact Time 256' */ + unix_ms %= MSEC_PER_SEC; + unix_ms *= BT_CTS_FRACTION_256_MAX_VALUE; + unix_ms /= MSEC_PER_SEC; + ct_time->fractions256 = unix_ms; + + /* convert unix_ts to */ + LOG_DBG("CTS Read Unix: %lld", unix_ts); + /* generate date time from unix timestamp */ + if (gmtime_r(&unix_ts, &date_time) == NULL) { + return -EOVERFLOW; + } + date_time.tm_year += TIME_UTILS_BASE_YEAR; + + LOG_DBG("CTS Read Time: %d/%d/%d %d:%d:%d", date_time.tm_year, date_time.tm_mon, + date_time.tm_mday, date_time.tm_hour, date_time.tm_min, date_time.tm_sec); + + /* 'Exact Time 256' contains 'Day Date Time' which contains + * 'Date Time' - characteristic contains fields for: + * year, month, day, hours, minutes and seconds. + */ + ct_time->year = sys_cpu_to_le16(date_time.tm_year); + ct_time->mon = date_time.tm_mon + 1; /* months starting from 1 */ + ct_time->mday = date_time.tm_mday; /* Day of month */ + ct_time->hours = date_time.tm_hour; /* hours */ + ct_time->min = date_time.tm_min; /* minutes */ + ct_time->sec = date_time.tm_sec; /* seconds */ + /* day of week starting from 1-monday, 7-sunday */ + ct_time->wday = date_time.tm_wday; + if (ct_time->wday == 0) { + ct_time->wday = 7; /* sunday is represented as 7 */ + } + return 0; +} + +#endif /* CONFIG_BT_CTS_HELPER_API */ + +static void ct_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + bool notif_enabled = (value == BT_GATT_CCC_NOTIFY); + + LOG_INF("CTS Notifications %s", notif_enabled ? "enabled" : "disabled"); + + if (cts_cb->notification_changed) { + cts_cb->notification_changed(notif_enabled); + } +} + +static ssize_t read_ct(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) +{ + int err; + struct bt_cts_time_format ct_time; + + err = cts_cb->fill_current_cts_time(&ct_time); + ct_time.reason = BT_CTS_UPDATE_REASON_UNKNOWN; + + if (!err) { + return bt_gatt_attr_read(conn, attr, buf, len, offset, &ct_time, sizeof(ct_time)); + } else { + return BT_GATT_ERR(BT_ATT_ERR_OUT_OF_RANGE); + } +} + +static ssize_t write_ct(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, + uint16_t len, uint16_t offset, uint8_t flags) +{ + int err; + struct bt_cts_time_format ct_time; + + if (cts_cb->cts_time_write == NULL) { + return BT_GATT_ERR(BT_ATT_ERR_INSUFFICIENT_RESOURCES); + } + + if ((offset != 0) || (offset + len != sizeof(ct_time))) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + memcpy(&ct_time, buf, sizeof(ct_time)); + err = cts_cb->cts_time_write(&ct_time); + if (err) { + return BT_GATT_ERR(BT_CTS_ATT_ERR_VALUES_IGNORED); + } + + err = bt_cts_send_notification(BT_CTS_UPDATE_REASON_MANUAL); + if (err) { + return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); + } + + return len; +} + +/* Current Time Service Declaration */ +BT_GATT_SERVICE_DEFINE(cts_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_CTS), + BT_GATT_CHARACTERISTIC(BT_UUID_CTS_CURRENT_TIME, + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | + BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, read_ct, + write_ct, NULL), + BT_GATT_CCC(ct_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE)); + +int bt_cts_init(const struct bt_cts_cb *cb) +{ + __ASSERT(cb == NULL, "Current Time service need valid `struct bt_cts_cb` callback"); + __ASSERT(cb->fill_current_cts_time == NULL, + "`fill_current_cts_time` callback api is required for functioning of CTS"); + if (!cb || !cb->fill_current_cts_time) { + return -EINVAL; + } + cts_cb = cb; + return 0; +} + +int bt_cts_send_notification(enum bt_cts_update_reason reason) +{ + int err; + struct bt_cts_time_format ct_time; + + err = cts_cb->fill_current_cts_time(&ct_time); + ct_time.reason = reason; + if (err) { + return err; + } + return bt_gatt_notify(NULL, &cts_svc.attrs[1], &ct_time, sizeof(ct_time)); +}