diff --git a/prj.conf b/prj.conf index 6e2a1100..ec8593c4 100644 --- a/prj.conf +++ b/prj.conf @@ -85,6 +85,20 @@ CONFIG_I2C_NRFX=y CONFIG_POWEROFF=y +# Auto-off power saving modes: 0=off, 1=minimal, 2=balanced, 3=aggressive. +# Higher modes are more aggressive: shorter timeout, fewer participants can veto. +CONFIG_POWER_SAVING_DEFAULT_MODE=2 + +# Auto-off participant levels are the most aggressive mode where a component +# may veto. Level 2 applies in modes 1 and 2, but not mode 3. +CONFIG_POWER_SAVING_LEVEL_LEAUDIO=3 +CONFIG_POWER_SAVING_LEVEL_SENSOR_MANAGER=2 + +# Auto-off timeouts in minutes for each power saving mode +CONFIG_AUTO_OFF_TIMEOUT_AGGRESSIVE=5 +CONFIG_AUTO_OFF_TIMEOUT_BALANCED=15 +CONFIG_AUTO_OFF_TIMEOUT_MINIMAL=30 + CONFIG_PM=y # may cause delay issues for ISO stream ? CONFIG_PM_DEVICE=y CONFIG_PM_DEVICE_RUNTIME=y diff --git a/src/Battery/AutoOffManager.cpp b/src/Battery/AutoOffManager.cpp new file mode 100644 index 00000000..5e62bf1a --- /dev/null +++ b/src/Battery/AutoOffManager.cpp @@ -0,0 +1,407 @@ +#include "AutoOffManager.h" + +#include +#include +#include + +#include +#include +#include + +#include "PowerManager.h" + +#include +LOG_MODULE_REGISTER(auto_off, LOG_LEVEL_DBG); + +void auto_off_work_handler(struct k_work *work); + +// anonymous namespace for helpers +namespace { + +struct PowerSavingModeConfig { + const char *name; + int timeout_minutes; +}; + +/* Indexed by power_saving_level_t. Keep the order in sync with the enum values. */ +constexpr std::array power_saving_modes = { { + { "Off", 0 }, + { "Minimal", CONFIG_AUTO_OFF_TIMEOUT_MINIMAL }, + { "Balanced", CONFIG_AUTO_OFF_TIMEOUT_BALANCED }, + { "Aggressive", CONFIG_AUTO_OFF_TIMEOUT_AGGRESSIVE }, +} }; +static_assert(power_saving_modes.size() == POWER_SAVING_LEVEL_COUNT, + "power_saving_modes must contain every concrete power saving level"); + +constexpr const char *auto_off_settings_mode_key = "auto_off/mode"; + +power_saving_level_t loaded_mode = (power_saving_level_t)CONFIG_POWER_SAVING_DEFAULT_MODE; +bool loaded_mode_is_valid = false; + +K_MUTEX_DEFINE(auto_off_mutex); +K_WORK_DELAYABLE_DEFINE(auto_off_work, auto_off_work_handler); + +// RAII implementation for k_mutex instead of std::lock_guard +class AutoOffLock { +public: + AutoOffLock() + { + k_mutex_lock(&auto_off_mutex, K_FOREVER); + } + + ~AutoOffLock() + { + k_mutex_unlock(&auto_off_mutex); + } + + AutoOffLock(const AutoOffLock &) = delete; + AutoOffLock &operator=(const AutoOffLock &) = delete; +}; + +int auto_off_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) +{ + if (std::strcmp(name, "mode") != 0) { + return -ENOENT; + } + + if (len != sizeof(int)) { + LOG_WRN("Ignoring auto-off mode setting with invalid size %zu", len); + return 0; + } + + int stored_mode; + const int ret = read_cb(cb_arg, &stored_mode, sizeof(stored_mode)); + if (ret < 0) { + return ret; + } + + if (ret != sizeof(stored_mode)) { + LOG_WRN("Ignoring incomplete auto-off mode setting read: %d", ret); + return 0; + } + + const auto mode = static_cast(stored_mode); + if (!auto_off_mode_is_supported(mode)) { + LOG_WRN("Ignoring invalid stored auto-off mode %d", stored_mode); + return 0; + } + + loaded_mode = mode; + loaded_mode_is_valid = true; + + return 0; +} + +SETTINGS_STATIC_HANDLER_DEFINE(auto_off, "auto_off", nullptr, auto_off_settings_set, nullptr, + nullptr); + +void save_auto_off_mode(power_saving_level_t mode) +{ + const int stored_mode = static_cast(mode); + const int ret = settings_save_one(auto_off_settings_mode_key, &stored_mode, + sizeof(stored_mode)); + if (ret) { + LOG_WRN("Failed to persist auto-off mode %d: %d", stored_mode, ret); + } +} + +} + +bool AutoOffManager::participant_is_considered(const ParticipantEntry &participant) const +{ + if (!participant.registered || current_mode == POWER_SAVING_LEVEL_OFF || + participant.level == POWER_SAVING_LEVEL_OFF) { + return false; + } + + /* + * The participant level is the most aggressive power saving mode where it may veto + * auto-off. Example: a Balanced participant is considered in Minimal and + * Balanced modes, but ignored in Aggressive mode. Aggressive power saving + * considers fewer participant vetoes, so auto-offs are more likely. + */ + return participant.level >= current_mode; +} + +AutoOffManager::ParticipantEntry *AutoOffManager::find_participant(const char *participant_token) +{ + for (auto &entry : participants) { + if (entry.registered && std::strcmp(entry.token, participant_token) == 0) { + return &entry; + } + } + + return nullptr; +} + +bool AutoOffManager::all_considered_participants_allow() const +{ + for (const auto &participant : participants) { + if (participant_is_considered(participant) && !participant.allowed) { + return false; + } + } + + return true; +} + +void AutoOffManager::schedule() const +{ + if (current_mode < POWER_SAVING_LEVEL_OFF || + static_cast(current_mode) >= power_saving_modes.size()) { + LOG_WRN("Cannot schedule auto-off for invalid mode %d", current_mode); + return; + } + + const auto &mode = power_saving_modes[static_cast(current_mode)]; + + (void)k_work_reschedule(&auto_off_work, K_MINUTES(mode.timeout_minutes)); + LOG_INF("Auto-off armed for %d min in %s mode", mode.timeout_minutes, mode.name); +} + +void AutoOffManager::cancel() const +{ + (void)k_work_cancel_delayable(&auto_off_work); +} + +void AutoOffManager::evaluate() +{ + if (!initialized) { + return; + } + + if (current_mode == POWER_SAVING_LEVEL_OFF) { + cancel(); + return; + } + + if (all_considered_participants_allow()) { + schedule(); + } else { + cancel(); + } +} + +int AutoOffManager::init() +{ + AutoOffLock lock; + + if (initialized) { + return -EALREADY; + } + + current_mode = loaded_mode_is_valid ? + loaded_mode : + (power_saving_level_t)CONFIG_POWER_SAVING_DEFAULT_MODE; + + if (!auto_off_mode_is_supported(current_mode)) { + LOG_WRN("Invalid default auto-off mode %d, disabling auto-off", current_mode); + current_mode = POWER_SAVING_LEVEL_OFF; + } + + const auto &mode = power_saving_modes[static_cast(current_mode)]; + + initialized = true; + + LOG_INF("Auto-off initialized in %s mode", mode.name); + evaluate(); + + return 0; +} + +int AutoOffManager::register_participant(const char *participant_token, power_saving_level_t level) +{ + if (participant_token == nullptr || level <= POWER_SAVING_LEVEL_OFF || + static_cast(level) >= power_saving_modes.size()) { + return -EINVAL; + } + + const auto &level_config = power_saving_modes[static_cast(level)]; + + AutoOffLock lock; + + ParticipantEntry *entry = find_participant(participant_token); + if (entry != nullptr) { + entry->level = level; + evaluate(); + return -EALREADY; + } + + ParticipantEntry *free_participant = nullptr; + for (auto &participant : participants) { + if (!participant.registered) { + free_participant = &participant; + break; + } + } + + if (free_participant == nullptr) { + LOG_WRN("No room to register auto-off participant token %s", participant_token); + return -ENOMEM; + } + + free_participant->token = participant_token; + free_participant->level = level; + free_participant->allowed = false; + free_participant->registered = true; + + LOG_INF("Registered auto-off participant token %s at %s level", + participant_token, level_config.name); + evaluate(); + + return 0; +} + +void AutoOffManager::allow(const char *participant_token) +{ + if (participant_token == nullptr) { + return; + } + + AutoOffLock lock; + + ParticipantEntry *entry = find_participant(participant_token); + if (entry == nullptr) { + LOG_WRN("Auto-off allow from unregistered participant token %s", + participant_token); + return; + } + + if (!entry->allowed) { + entry->allowed = true; + LOG_DBG("Auto-off allowed by token %s", participant_token); + evaluate(); + } +} + +void AutoOffManager::prohibit(const char *participant_token) +{ + if (participant_token == nullptr) { + return; + } + + AutoOffLock lock; + + ParticipantEntry *entry = find_participant(participant_token); + if (entry == nullptr) { + LOG_WRN("Auto-off prohibit from unregistered participant token %s", + participant_token); + return; + } + + if (entry->allowed) { + entry->allowed = false; + LOG_DBG("Auto-off prohibited by token %s", participant_token); + evaluate(); + } +} + +void AutoOffManager::set_mode(power_saving_level_t mode) +{ + if (!auto_off_mode_is_supported(mode)) { + LOG_WRN("Ignoring invalid auto-off mode %d", mode); + return; + } + + const auto &mode_config = power_saving_modes[static_cast(mode)]; + bool mode_changed = false; + + { + AutoOffLock lock; + + if (current_mode != mode) { + current_mode = mode; + mode_changed = true; + LOG_INF("Auto-off mode set to %s", mode_config.name); + evaluate(); + } + } + + if (mode_changed) { + save_auto_off_mode(mode); + } +} + +power_saving_level_t AutoOffManager::get_mode() +{ + AutoOffLock lock; + + return current_mode; +} + +void AutoOffManager::handle_timeout() +{ + bool should_power_down; + + { + AutoOffLock lock; + should_power_down = initialized && current_mode != POWER_SAVING_LEVEL_OFF && + all_considered_participants_allow(); + } + + if (!should_power_down) { + LOG_DBG("Auto-off skipped because a participant now prohibits it"); + return; + } + + LOG_INF("Auto-off timeout reached"); + power_manager.power_down(); +} + +void auto_off_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); + auto_off_manager.handle_timeout(); +} + +int auto_off_init(void) +{ + return auto_off_manager.init(); +} + +int auto_off_register_participant(const char *participant_token, power_saving_level_t level) +{ + return auto_off_manager.register_participant(participant_token, level); +} + +void auto_off_allow(const char *participant_token) +{ + auto_off_manager.allow(participant_token); +} + +void auto_off_prohibit(const char *participant_token) +{ + auto_off_manager.prohibit(participant_token); +} + +void auto_off_set_mode(power_saving_level_t mode) +{ + auto_off_manager.set_mode(mode); +} + +power_saving_level_t auto_off_get_mode(void) +{ + return auto_off_manager.get_mode(); +} + +uint8_t auto_off_get_supported_mode_count(void) +{ + return static_cast(power_saving_modes.size()); +} + +const char *auto_off_get_mode_name(power_saving_level_t mode) +{ + if (!auto_off_mode_is_supported(mode)) { + return nullptr; + } + + return power_saving_modes[static_cast(mode)].name; +} + +int auto_off_mode_is_supported(power_saving_level_t mode) +{ + return mode >= POWER_SAVING_LEVEL_OFF && + static_cast(mode) < power_saving_modes.size(); +} + +AutoOffManager auto_off_manager; diff --git a/src/Battery/AutoOffManager.h b/src/Battery/AutoOffManager.h new file mode 100644 index 00000000..7488ca1a --- /dev/null +++ b/src/Battery/AutoOffManager.h @@ -0,0 +1,225 @@ +#ifndef _AUTO_OFF_MANAGER_H +#define _AUTO_OFF_MANAGER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Power saving levels used by AutoOffManager. + * + * Numeric values intentionally match the Kconfig settings: + * 0=off, 1=minimal, 2=balanced, 3=aggressive. + * + * When used as the current mode, a higher numeric value means more aggressive + * power saving: the auto-off timeout is shorter and fewer participants are + * allowed to veto power-off, making auto-off more likely. + * + * When used as a participant level, a higher numeric value means larger veto + * power: that participant may still prohibit auto-off in higher, more + * aggressive power saving modes. + */ +typedef enum power_saving_level { + POWER_SAVING_LEVEL_OFF = 0, + POWER_SAVING_LEVEL_MINIMAL = 1, + POWER_SAVING_LEVEL_BALANCED = 2, + POWER_SAVING_LEVEL_AGGRESSIVE = 3, + /** Number of concrete levels. Not a selectable power saving mode. */ + POWER_SAVING_LEVEL_COUNT, +} power_saving_level_t; + +/** + * @brief C wrapper for AutoOffManager::init(). + * + * See AutoOffManager::init() for documentation. + */ +int auto_off_init(void); + +/** + * @brief C wrapper for AutoOffManager::register_participant(). + * + * See AutoOffManager::register_participant() for documentation. + */ +int auto_off_register_participant(const char *participant_token, power_saving_level_t level); + +/** + * @brief C wrapper for AutoOffManager::allow(). + * + * See AutoOffManager::allow() for documentation. + */ +void auto_off_allow(const char *participant_token); + +/** + * @brief C wrapper for AutoOffManager::prohibit(). + * + * See AutoOffManager::prohibit() for documentation. + */ +void auto_off_prohibit(const char *participant_token); + +/** + * @brief C wrapper for AutoOffManager::set_mode(). + * + * See AutoOffManager::set_mode() for documentation. + */ +void auto_off_set_mode(power_saving_level_t mode); + +/** + * @brief C wrapper for AutoOffManager::get_mode(). + * + * See AutoOffManager::get_mode() for documentation. + */ +power_saving_level_t auto_off_get_mode(void); + +/** + * @brief Get the number of selectable power saving modes. + * + * @return Number of selectable modes in the firmware-defined mode table. + */ +uint8_t auto_off_get_supported_mode_count(void); + +/** + * @brief Get the display name for a selectable power saving mode. + * + * @param mode Power saving mode identifier. + * + * @return Null-terminated mode name, or NULL if @p mode is not supported. + */ +const char *auto_off_get_mode_name(power_saving_level_t mode); + +/** + * @brief Check whether a power saving mode identifier is supported. + * + * @param mode Power saving mode identifier. + * + * @return 1 if @p mode is supported, otherwise 0. + */ +int auto_off_mode_is_supported(power_saving_level_t mode); + +#ifdef __cplusplus +} + +#include + +struct k_work; + +/** + * @brief Coordinates automatic power-off across dynamically registered participants. + * + * The manager uses a participant-based permission model. Firmware components + * that may need to keep the device awake register themselves as auto-off + * participants by providing a unique token and a power saving level. + * + * The current power saving mode controls how strict auto-off is. More + * aggressive modes have shorter timeouts and consider fewer participant vetoes, + * so auto-off is more likely to happen. + * + * A participant's level is its veto power: it defines the most aggressive power + * saving mode in which the participant is still considered by the auto-off + * decision. A participant registered for level 2 (balanced) can prevent + * auto-off in levels 1 and 2, but is ignored in level 3 (aggressive). A + * participant registered for level 3 has the largest veto power and may still + * block auto-off even in aggressive mode. + * + * Once registered, a participant calls prohibit() whenever it enters a critical + * region where power-off would be unsafe or disruptive, for example while + * writing data, streaming audio, or handling a time-sensitive operation. When + * the participant leaves that region, it calls allow(). Auto-off is armed only + * when every participant that is relevant for the current power saving mode is + * currently allowing power-off. If any relevant participant prohibits auto-off, + * the pending auto-off work is cancelled until the system is allowed again. + * + * The current power saving mode controls both which participants are considered + * and which timeout is used before power_down() is called. POWER_SAVING_LEVEL_OFF + * disables auto-off. + * + * Implementation notes: + * - AutoOffManager is used as a process-wide singleton via auto_off_manager. + * The firmware should not create additional instances. + * - Public C++ methods have thin C wrappers such as auto_off_allow() and + * auto_off_register_participant() so C firmware modules can use the same + * manager without depending on C++ linkage. + */ +class AutoOffManager { +public: + /** + * @brief Initialize the manager and load the default mode from Kconfig. + * + * @return 0 on success or -EALREADY if already initialized. + */ + int init(); + + /** + * @brief Register an auto-off participant. + * + * @param participant_token Unique, stable token identifying the participant. + * @param level Most aggressive power saving mode where this participant may + * prohibit auto-off. Higher levels grant larger veto power because the + * participant remains relevant in more aggressive modes. The participant is + * considered when the current mode is less than or equal to this level. + * POWER_SAVING_LEVEL_OFF is not a valid participant level. + * + * @return 0 on success, -EINVAL for invalid arguments, -EALREADY if the + * token was already registered, or -ENOMEM if the registry is full. + */ + int register_participant(const char *participant_token, power_saving_level_t level); + + /** + * @brief Mark a registered participant as allowing auto-off. + * + * @param participant_token The same unique token used for registration. + */ + void allow(const char *participant_token); + + /** + * @brief Mark a registered participant as prohibiting auto-off. + * + * @param participant_token The same unique token used for registration. + */ + void prohibit(const char *participant_token); + + /** + * @brief Set the current power saving mode. + * + * @param mode New power saving mode. + */ + void set_mode(power_saving_level_t mode); + + /** + * @brief Get the current power saving mode. + * + * @return Current power saving mode. + */ + power_saving_level_t get_mode(); + +private: + static constexpr int max_participants = 8; + + struct ParticipantEntry { + const char *token = nullptr; + power_saving_level_t level = POWER_SAVING_LEVEL_OFF; + bool allowed = false; + bool registered = false; + }; + + friend void auto_off_work_handler(struct k_work *work); + + ParticipantEntry *find_participant(const char *participant_token); + bool participant_is_considered(const ParticipantEntry &entry) const; + bool all_considered_participants_allow() const; + void schedule() const; + void cancel() const; + void evaluate(); + void handle_timeout(); + + std::array participants{}; + power_saving_level_t current_mode = POWER_SAVING_LEVEL_OFF; + bool initialized = false; +}; + +/** @brief Global AutoOffManager singleton. */ +extern AutoOffManager auto_off_manager; +#endif + +#endif diff --git a/src/Battery/CMakeLists.txt b/src/Battery/CMakeLists.txt index a1c2dd11..65ad0603 100644 --- a/src/Battery/CMakeLists.txt +++ b/src/Battery/CMakeLists.txt @@ -2,6 +2,7 @@ target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/BQ27220.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BQ25120a.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/AutoOffManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/PowerManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/PowerManager.c ${CMAKE_CURRENT_SOURCE_DIR}/BootState.c diff --git a/src/Battery/Kconfig b/src/Battery/Kconfig index 47b8645d..b0939acd 100644 --- a/src/Battery/Kconfig +++ b/src/Battery/Kconfig @@ -1,5 +1,50 @@ menu "Battery Configuration" +config POWER_SAVING_DEFAULT_MODE + int "Default auto-off power saving mode" + default 2 + help + Default mode for automatic power-off. + Values: 0=off, 1=minimal, 2=balanced, 3=aggressive. Higher + values are more aggressive: the timeout is shorter and fewer + participants can prohibit auto-off. + +config POWER_SAVING_LEVEL_LEAUDIO + int "LE Audio auto-off power saving level" + default 3 + help + Most aggressive power saving mode where LE Audio may prohibit + auto-off. A participant is considered when the current mode is + less than or equal to its level. + See POWER_SAVING_DEFAULT_MODE. + +config POWER_SAVING_LEVEL_SENSOR_MANAGER + int "SensorManager auto-off power saving level" + default 2 + help + Most aggressive power saving mode where SensorManager may prohibit + auto-off. For example, level 2 applies in minimal and balanced + modes, but not in aggressive mode. + See POWER_SAVING_DEFAULT_MODE. + +config AUTO_OFF_TIMEOUT_AGGRESSIVE + int "Aggressive auto-off timeout in minutes" + default 5 + help + Idle time before power-off when auto-off is in aggressive mode. + +config AUTO_OFF_TIMEOUT_BALANCED + int "Balanced auto-off timeout in minutes" + default 15 + help + Idle time before power-off when auto-off is in balanced mode. + +config AUTO_OFF_TIMEOUT_MINIMAL + int "Minimal auto-off timeout in minutes" + default 30 + help + Idle time before power-off when auto-off is in minimal mode. + config BATTERY_SYSDOWN_SET_OFFSET int "System Down Set Voltage Offset (mV)" default 250 diff --git a/src/SensorManager/SensorManager.cpp b/src/SensorManager/SensorManager.cpp index 1c8a994b..8ad2459a 100644 --- a/src/SensorManager/SensorManager.cpp +++ b/src/SensorManager/SensorManager.cpp @@ -1,5 +1,6 @@ #include "SensorManager.h" +#include #include #include "macros_common.h" @@ -16,6 +17,7 @@ #include "openearable_common.h" #include "StateIndicator.h" +#include "AutoOffManager.h" #include #include "../SD_Card/SDLogger/SDLogger.h" @@ -62,6 +64,7 @@ struct k_work_q sensor_work_q; K_THREAD_STACK_DEFINE(sensor_publish_thread_stack, CONFIG_SENSOR_PUB_STACK_SIZE); int active_sensors = 0; +static const char sensor_manager_auto_off_token[] = "SensorManager"; static void config_work_handler(struct k_work *work); @@ -100,6 +103,15 @@ void init_sensor_manager() { k_poll_signal_init(&sensor_manager_sig); sdlogger.init(); + + int ret = auto_off_manager.register_participant( + sensor_manager_auto_off_token, + (power_saving_level_t)CONFIG_POWER_SAVING_LEVEL_SENSOR_MANAGER); + if (ret && ret != -EALREADY) { + LOG_WRN("Failed to register SensorManager with auto-off: %d", ret); + } else { + auto_off_manager.allow(sensor_manager_auto_off_token); + } } void start_sensor_manager() { @@ -136,6 +148,7 @@ void stop_sensor_manager() { Microphone::sensor.stop(); active_sensors = 0; + auto_off_manager.allow(sensor_manager_auto_off_token); k_work_queue_drain(&sensor_work_q, true); @@ -211,6 +224,7 @@ static void config_work_handler(struct k_work *work) { sensor->start(config.sampleRateIndex); if (sensor->is_running()) { active_sensors++; + auto_off_manager.prohibit(sensor_manager_auto_off_token); } } } diff --git a/src/audio/streamctrl.c b/src/audio/streamctrl.c index 5a1e7edb..340b4cc5 100644 --- a/src/audio/streamctrl.c +++ b/src/audio/streamctrl.c @@ -26,6 +26,7 @@ #include "le_audio_rx.h" #include "fw_info_app.h" +#include "AutoOffManager.h" #include "BootState.h" #include @@ -660,6 +661,9 @@ int streamctrl_start() //streamctrl_start ret = bt_mgmt_init(); ERR_CHK(ret); + ret = auto_off_init(); + ERR_CHK(ret); + ret = audio_system_init(); ERR_CHK(ret); diff --git a/src/bluetooth/bt_stream/le_audio.c b/src/bluetooth/bt_stream/le_audio.c index 50a8d5ed..d4f1c50a 100644 --- a/src/bluetooth/bt_stream/le_audio.c +++ b/src/bluetooth/bt_stream/le_audio.c @@ -6,12 +6,69 @@ #include "le_audio.h" +#include + #include #include +#include +#include + +#include "AutoOffManager.h" +#include "zbus_common.h" #include LOG_MODULE_REGISTER(le_audio, CONFIG_BLE_LOG_LEVEL); +static const char le_audio_auto_off_token[] = "LEAudio"; + +ZBUS_CHAN_DECLARE(le_audio_chan); + +static void le_audio_auto_off_event_handler(const struct zbus_channel *chan); +ZBUS_LISTENER_DEFINE(le_audio_auto_off_listener, le_audio_auto_off_event_handler); + +static int le_audio_auto_off_init(void) +{ + int ret; + + ret = auto_off_register_participant(le_audio_auto_off_token, + (power_saving_level_t)CONFIG_POWER_SAVING_LEVEL_LEAUDIO); + if (ret && ret != -EALREADY) { + LOG_WRN("Failed to register LE Audio with auto-off: %d", ret); + return ret; + } + + auto_off_allow(le_audio_auto_off_token); + + ret = zbus_chan_add_obs(&le_audio_chan, &le_audio_auto_off_listener, + ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_WRN("Failed to add LE Audio auto-off listener: %d", ret); + return ret; + } + + return 0; +} + +static void le_audio_auto_off_event_handler(const struct zbus_channel *chan) +{ + const struct le_audio_msg *msg = zbus_chan_const_msg(chan); + + switch (msg->event) { + case LE_AUDIO_EVT_STREAMING: + auto_off_prohibit(le_audio_auto_off_token); + break; + case LE_AUDIO_EVT_NOT_STREAMING: + case LE_AUDIO_EVT_SYNC_LOST: + case LE_AUDIO_EVT_NO_VALID_CFG: + auto_off_allow(le_audio_auto_off_token); + break; + default: + break; + } +} + +SYS_INIT(le_audio_auto_off_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); + int le_audio_ep_state_get(struct bt_bap_ep *ep, uint8_t *state) { int ret; diff --git a/src/bluetooth/gatt_services/CMakeLists.txt b/src/bluetooth/gatt_services/CMakeLists.txt index dc64e7b9..141acb9e 100644 --- a/src/bluetooth/gatt_services/CMakeLists.txt +++ b/src/bluetooth/gatt_services/CMakeLists.txt @@ -4,5 +4,6 @@ target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/button_service.c ${CMAKE_CURRENT_SOURCE_DIR}/sensor_service.c ${CMAKE_CURRENT_SOURCE_DIR}/audio_config_service.c + ${CMAKE_CURRENT_SOURCE_DIR}/power_saving_service.c ${CMAKE_CURRENT_SOURCE_DIR}/led_service.cpp ) diff --git a/src/bluetooth/gatt_services/power_saving_service.c b/src/bluetooth/gatt_services/power_saving_service.c new file mode 100644 index 00000000..faed5082 --- /dev/null +++ b/src/bluetooth/gatt_services/power_saving_service.c @@ -0,0 +1,142 @@ +#include "power_saving_service.h" + +#include +#include +#include +#include + +#include +#include + +#include "AutoOffManager.h" + +#include +LOG_MODULE_REGISTER(power_saving_service, CONFIG_BLE_LOG_LEVEL); + +#define POWER_SAVING_SUPPORTED_MODES_MAX_PAYLOAD_LEN 128 + +static uint8_t supported_modes_payload[POWER_SAVING_SUPPORTED_MODES_MAX_PAYLOAD_LEN]; + +/* + * Build the value returned by the "supported power saving modes" GATT + * characteristic. + * + * The client needs more than the currently selected mode: it also needs to know + * which numeric mode IDs are valid and which labels should be shown in the UI. + * This function serializes the AutoOffManager mode table into a compact, + * self-describing byte stream: + * + * byte 0: number of modes + * repeated for each mode: + * byte 0: mode ID, matching power_saving_level_t + * byte 1: mode name length in bytes + * bytes: mode name, without a terminating NUL + */ +static ssize_t encode_supported_modes(uint8_t *payload, size_t capacity) +{ + uint8_t mode_count = auto_off_get_supported_mode_count(); + size_t payload_len = 0; + + if (capacity < 1) { + return -ENOMEM; + } + + /* Prefix the payload with the number of following mode records. */ + payload[payload_len++] = mode_count; + + for (uint8_t mode_id = 0; mode_id < mode_count; mode_id++) { + const char *name = auto_off_get_mode_name((power_saving_level_t)mode_id); + size_t name_len; + + if (name == NULL) { + return -EINVAL; + } + + name_len = strlen(name); + if (name_len > UINT8_MAX || + payload_len + 2 + name_len > capacity) { + return -ENOMEM; + } + + /* Append one length-prefixed record so clients can parse without NULs. */ + payload[payload_len++] = mode_id; + payload[payload_len++] = (uint8_t)name_len; + memcpy(&payload[payload_len], name, name_len); + payload_len += name_len; + } + + return payload_len; +} + +static ssize_t read_power_saving_mode(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + void *buf, + uint16_t len, + uint16_t offset) +{ + uint8_t mode = (uint8_t)auto_off_get_mode(); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &mode, sizeof(mode)); +} + +static ssize_t write_power_saving_mode(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, + uint16_t len, + uint16_t offset, + uint8_t flags) +{ + const uint8_t *mode_buf = buf; + power_saving_level_t mode; + + ARG_UNUSED(conn); + ARG_UNUSED(attr); + ARG_UNUSED(flags); + + if (offset != 0) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + if (len != sizeof(uint8_t)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + mode = (power_saving_level_t)mode_buf[0]; + if (!auto_off_mode_is_supported(mode)) { + return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED); + } + + auto_off_set_mode(mode); + return len; +} + +static ssize_t read_supported_power_saving_modes(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + void *buf, + uint16_t len, + uint16_t offset) +{ + ssize_t payload_len = encode_supported_modes( + supported_modes_payload, + sizeof(supported_modes_payload)); + + if (payload_len < 0) { + LOG_ERR("Failed to encode supported power saving modes: %d", (int)payload_len); + return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); + } + + return bt_gatt_attr_read(conn, attr, buf, len, offset, supported_modes_payload, + (uint16_t)payload_len); +} + +BT_GATT_SERVICE_DEFINE(power_saving_svc, + BT_GATT_PRIMARY_SERVICE(BT_UUID_POWER_SAVING_SERVICE), + BT_GATT_CHARACTERISTIC(BT_UUID_POWER_SAVING_MODE, + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE, + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, + read_power_saving_mode, write_power_saving_mode, NULL), + BT_GATT_CHARACTERISTIC(BT_UUID_POWER_SAVING_SUPPORTED_MODES, + BT_GATT_CHRC_READ, + BT_GATT_PERM_READ, + read_supported_power_saving_modes, NULL, NULL), +); diff --git a/src/bluetooth/gatt_services/power_saving_service.h b/src/bluetooth/gatt_services/power_saving_service.h new file mode 100644 index 00000000..a03bedbb --- /dev/null +++ b/src/bluetooth/gatt_services/power_saving_service.h @@ -0,0 +1,27 @@ +#ifndef _POWER_SAVING_SERVICE_H_ +#define _POWER_SAVING_SERVICE_H_ + +#include + +/** @brief Power saving service UUID. */ +#define BT_UUID_POWER_SAVING_SERVICE_VAL \ + BT_UUID_128_ENCODE(0xd63fd1f0, 0x5f68, 0x4ebb, 0xa7c7, 0x5e0fb9ae7557) + +/** @brief Current power saving mode characteristic UUID. */ +#define BT_UUID_POWER_SAVING_MODE_VAL \ + BT_UUID_128_ENCODE(0xd63fd1f1, 0x5f68, 0x4ebb, 0xa7c7, 0x5e0fb9ae7557) + +/** @brief Supported power saving modes characteristic UUID. */ +#define BT_UUID_POWER_SAVING_SUPPORTED_MODES_VAL \ + BT_UUID_128_ENCODE(0xd63fd1f2, 0x5f68, 0x4ebb, 0xa7c7, 0x5e0fb9ae7557) + +#define BT_UUID_POWER_SAVING_SERVICE \ + BT_UUID_DECLARE_128(BT_UUID_POWER_SAVING_SERVICE_VAL) + +#define BT_UUID_POWER_SAVING_MODE \ + BT_UUID_DECLARE_128(BT_UUID_POWER_SAVING_MODE_VAL) + +#define BT_UUID_POWER_SAVING_SUPPORTED_MODES \ + BT_UUID_DECLARE_128(BT_UUID_POWER_SAVING_SUPPORTED_MODES_VAL) + +#endif /* _POWER_SAVING_SERVICE_H_ */