diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f103e7..cdc9e67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,8 @@ set(priv_include_dirs "private_include") set(srcs "src/relay_chn_core.c" "src/relay_chn_output.c" - "src/relay_chn_run_info.c") + "src/relay_chn_run_info.c" + "src/relay_chn_notify.c") if(CONFIG_RELAY_CHN_ENABLE_TILTING) list(APPEND srcs "src/relay_chn_tilt.c") diff --git a/README.md b/README.md index 0814795..4bf96a0 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,9 @@ relay_chn_flip_direction_all(); ### 3. Monitor channel state +> [!WARNING] +> Listener callbacks are executed from the context of the notification dispatcher task. To ensure system responsiveness and prevent event loss, callbacks must be lightweight and non-blocking. Avoid any long-running operations or functions that may block, such as `vTaskDelay()` or semaphore takes with long timeouts, inside the callback. + For single mode: ```c diff --git a/include/relay_chn_adapter.h b/include/relay_chn_adapter.h index 85b0093..a6d27c2 100644 --- a/include/relay_chn_adapter.h +++ b/include/relay_chn_adapter.h @@ -13,6 +13,36 @@ extern "C" { #endif +/** + * @brief Register a channel state change listener. + * + * @param listener A function that implements relay_chn_state_listener_t interface. + * + * @return + * - ESP_OK: Success + * - ESP_ERR_INVALID_ARG: Invalid argument + * - ESP_ERR_NO_MEM: No enough memory + * - ESP_FAIL: General failure + */ +extern esp_err_t relay_chn_notify_add_listener(relay_chn_state_listener_t listener); + +/** + * @brief Unregister a channel state change listener. + * + * @param listener A function that implements relay_chn_state_listener_t interface. + */ +extern void relay_chn_notify_remove_listener(relay_chn_state_listener_t listener); + +static inline esp_err_t relay_chn_register_listener(relay_chn_state_listener_t listener) +{ + return relay_chn_notify_add_listener(listener); +} + +static inline void relay_chn_unregister_listener(relay_chn_state_listener_t listener) +{ + relay_chn_notify_remove_listener(listener); +} + #if CONFIG_RELAY_CHN_COUNT > 1 /** * @brief Get the current state of a relay channel. diff --git a/private_include/relay_chn_notify.h b/private_include/relay_chn_notify.h new file mode 100644 index 0000000..d862acd --- /dev/null +++ b/private_include/relay_chn_notify.h @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include "esp_err.h" +#include +#include "relay_chn_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Init the notify module. + * + * @return + * - ESP_OK: Success + * - ESP_ERR_NO_MEM: Not enough memory to create notify queue + */ +esp_err_t relay_chn_notify_init(void); + +/** + * @brief Deinit the notify module. + * + * This function cleans up resources used by the notify module. + */ +void relay_chn_notify_deinit(void); + +/** + * @brief Notify all registered listeners about a state change. + * + * This function sends a state change event to an internal queue, which will then + * be processed by a dedicated task to notify all registered listeners. This + * function is typically called internally by the relay channel core logic. + * + * @param chn_id The ID of the relay channel whose state has changed. + * @param old_state The previous state of the relay channel. + * @param new_state The new state of the relay channel. + */ +esp_err_t relay_chn_notify_state_change(uint8_t chn_id, + relay_chn_state_t old_state, + relay_chn_state_t new_state); + +#ifdef __cplusplus +} +#endif + diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index 89fb816..459c81d 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -10,7 +10,7 @@ if [[ -z "$IDF_PATH" ]]; then fi # ==== 2. Valid Modes and Defaults ==== -valid_test_tags=("core" "tilt" "listener" "all" "relay_chn" "nvs" "run_limit" "batch" "inertia" "direction" "auto" "sensitivity" "counter" "interrupt") +valid_test_tags=("core" "tilt" "notify" "all" "relay_chn" "nvs" "run_limit" "batch" "inertia" "direction" "auto" "sensitivity" "counter" "interrupt") valid_test_profiles=("run_limit" "tilt" "nvs" "nvs_custom" "multi" "full_single" "full_multi") arg_tag="all" # Default to 'all' if no tag specified arg_profile="full_multi" # Default to 'full_multi' if no profile specified @@ -24,7 +24,7 @@ print_help() { echo "This script builds and runs tests for the relay_chn component using QEMU." echo "" echo "Arguments:" - echo " -t, --tag [relay_chn|core|tilt|listener|nvs|run_limit|batch|inertia|direction|auto|sensitivity|counter|interrupt|all] Specify which test tag to run." + echo " -t, --tag [relay_chn|core|tilt|notify|nvs|run_limit|batch|inertia|direction|auto|sensitivity|counter|interrupt|all] Specify which test tag to run." echo "" echo " If no tag is specified, it defaults to 'all'." echo "" diff --git a/src/relay_chn_core.c b/src/relay_chn_core.c index a8c7d08..64e44ca 100644 --- a/src/relay_chn_core.c +++ b/src/relay_chn_core.c @@ -11,6 +11,7 @@ #include "relay_chn_output.h" #include "relay_chn_run_info.h" #include "relay_chn_ctl.h" +#include "relay_chn_notify.h" #if CONFIG_RELAY_CHN_ENABLE_TILTING #include "relay_chn_tilt.h" @@ -26,15 +27,6 @@ static const char *TAG = "RELAY_CHN_CORE"; -// Structure to hold a listener entry in the linked list. -typedef struct relay_chn_listener_entry_type { - relay_chn_state_listener_t listener; /*!< The listener function pointer */ - ListItem_t list_item; /*!< FreeRTOS list item */ -} relay_chn_listener_entry_t; - -// The list that holds references to the registered listeners. -static List_t relay_chn_listener_list; - #if CONFIG_RELAY_CHN_ENABLE_RUN_LIMIT /* * Run limit timer callback immediately dispatches a STOP command for the @@ -125,9 +117,9 @@ esp_err_t relay_chn_create(const uint8_t* gpio_map, uint8_t gpio_count) ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize tilt feature"); #endif - // Init the state listener list - vListInitialise(&relay_chn_listener_list); - + // Initialize the notify feature + ret = relay_chn_notify_init(); + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize notify feature"); return ret; } @@ -136,96 +128,15 @@ void relay_chn_destroy(void) #if CONFIG_RELAY_CHN_ENABLE_TILTING relay_chn_tilt_deinit(); #endif + relay_chn_notify_deinit(); relay_chn_ctl_deinit(); relay_chn_output_deinit(); #if CONFIG_RELAY_CHN_ENABLE_NVS relay_chn_nvs_deinit(); #endif - - // Free the listeners - while (listCURRENT_LIST_LENGTH(&relay_chn_listener_list) > 0) { - ListItem_t *pxItem = listGET_HEAD_ENTRY(&relay_chn_listener_list); - relay_chn_listener_entry_t *entry = listGET_LIST_ITEM_OWNER(pxItem); - uxListRemove(pxItem); - free(entry); - } } -/** - * @brief Find a listener entry in the list by its function pointer. - * - * This function replaces the old index-based search and is used to check - * for the existence of a listener before registration or for finding it - * during unregistration. - * - * @param listener The listener function pointer to find. - * @return Pointer to the listener entry if found, otherwise NULL. - */ -static relay_chn_listener_entry_t* find_listener_entry(relay_chn_state_listener_t listener) -{ - // Iterate through the linked list of listeners - for (ListItem_t *pxListItem = listGET_HEAD_ENTRY(&relay_chn_listener_list); - pxListItem != listGET_END_MARKER(&relay_chn_listener_list); - pxListItem = listGET_NEXT(pxListItem)) { - - relay_chn_listener_entry_t *entry = (relay_chn_listener_entry_t *) listGET_LIST_ITEM_OWNER(pxListItem); - if (entry->listener == listener) { - // Found the listener, return the entry - return entry; - } - } - - // Listener was not found in the list - return NULL; -} - -esp_err_t relay_chn_register_listener(relay_chn_state_listener_t listener) -{ - ESP_RETURN_ON_FALSE(listener, ESP_ERR_INVALID_ARG, TAG, "Listener cannot be NULL"); - - // Check for duplicates - if (find_listener_entry(listener) != NULL) { - ESP_LOGD(TAG, "Listener %p already registered", listener); - return ESP_OK; - } - - // Allocate memory for the new listener entry - relay_chn_listener_entry_t *entry = malloc(sizeof(relay_chn_listener_entry_t)); - ESP_RETURN_ON_FALSE(entry, ESP_ERR_NO_MEM, TAG, "Failed to allocate memory for listener"); - - // Initialize and insert the new listener - entry->listener = listener; - vListInitialiseItem(&(entry->list_item)); - listSET_LIST_ITEM_OWNER(&(entry->list_item), (void *)entry); - vListInsertEnd(&relay_chn_listener_list, &(entry->list_item)); - - ESP_LOGD(TAG, "Registered listener %p", listener); - return ESP_OK; -} - -void relay_chn_unregister_listener(relay_chn_state_listener_t listener) -{ - if (listener == NULL) - { - ESP_LOGD(TAG, "Cannot unregister a NULL listener."); - return; - } - - // Find the listener entry in the list - relay_chn_listener_entry_t *entry = find_listener_entry(listener); - - if (entry != NULL) { - // Remove the item from the list and free the allocated memory - uxListRemove(&(entry->list_item)); - free(entry); - ESP_LOGD(TAG, "Unregistered listener %p", listener); - } else { - ESP_LOGD(TAG, "Listener %p not found for unregistration.", listener); - } -} - - esp_err_t relay_chn_start_esp_timer_once(esp_timer_handle_t esp_timer, uint32_t time_ms) { esp_err_t ret = esp_timer_start_once(esp_timer, time_ms * 1000); @@ -251,16 +162,7 @@ void relay_chn_update_state(relay_chn_ctl_t *chn_ctl, relay_chn_state_t new_stat chn_ctl->state = new_state; - // Iterate through the linked list of listeners and notify them. - for (ListItem_t *pxListItem = listGET_HEAD_ENTRY(&relay_chn_listener_list); - pxListItem != listGET_END_MARKER(&relay_chn_listener_list); - pxListItem = listGET_NEXT(pxListItem)) { - relay_chn_listener_entry_t *entry = (relay_chn_listener_entry_t *) listGET_LIST_ITEM_OWNER(pxListItem); - if (entry && entry->listener) { - // Emit the state change to the listeners - entry->listener(chn_ctl->id, old_state, new_state); - } - } + relay_chn_notify_state_change(chn_ctl->id, old_state, new_state); } static void relay_chn_execute_idle(relay_chn_ctl_t *chn_ctl); diff --git a/src/relay_chn_notify.c b/src/relay_chn_notify.c new file mode 100644 index 0000000..0051d2a --- /dev/null +++ b/src/relay_chn_notify.c @@ -0,0 +1,258 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "esp_log.h" +#include "esp_check.h" +#include "relay_chn_notify.h" + +static const char *TAG = "RELAY_CHN_NOTIFY"; + +// --- Config --- +#define RELAY_CHN_NOTIFY_QUEUE_LEN (16 + CONFIG_RELAY_CHN_COUNT * 4) +#define RELAY_CHN_NOTIFY_TASK_STACK 2048 +#define RELAY_CHN_NOTIFY_TASK_PRIO (tskIDLE_PRIORITY + 5) + +/// @brief Structure to hold a listener entry in the linked list. +typedef struct relay_chn_listener_entry_type { + relay_chn_state_listener_t listener; /*!< The listener function pointer */ + ListItem_t list_item; /*!< FreeRTOS list item */ +} relay_chn_listener_entry_t; + +/// @brief Command types for the notification queue. +typedef enum { + RELAY_CHN_NOTIFY_CMD_BROADCAST, /*!< A relay channel state has changed. */ + RELAY_CHN_NOTIFY_CMD_ADD_LISTENER, /*!< Request to add a new listener. */ + RELAY_CHN_NOTIFY_CMD_REMOVE_LISTENER, /*!< Request to remove a listener. */ +} relay_chn_notify_cmd_t; + +/// @brief Payload for a state change event. +typedef struct { + uint8_t chn_id; + relay_chn_state_t old_state; + relay_chn_state_t new_state; +} relay_chn_notify_event_data_t; + +/// @brief The command structure sent to the notification queue. +typedef struct { + relay_chn_notify_cmd_t cmd; + union { + relay_chn_notify_event_data_t event_data; /*!< Used for RELAY_CHN_NOTIFY_CMD_BROADCAST */ + relay_chn_state_listener_t listener; /*!< Used for ADD/REMOVE listener commands */ + } payload; +} relay_chn_notify_msg_t; + +// The list that holds references to the registered listeners. +static List_t listeners; + +static QueueHandle_t notify_msg_queue = NULL; +static TaskHandle_t notify_task_handle = NULL; + +static void relay_chn_notify_task(void *arg); + + +esp_err_t relay_chn_notify_init(void) +{ + if (notify_msg_queue != NULL) { + return ESP_OK; + } + + notify_msg_queue = xQueueCreate(RELAY_CHN_NOTIFY_QUEUE_LEN, sizeof(relay_chn_notify_msg_t)); + if (!notify_msg_queue) { + ESP_LOGE(TAG, "Failed to create notify queue"); + return ESP_ERR_NO_MEM; + } + + // Create the notify dispatcher task + BaseType_t ret = xTaskCreate(relay_chn_notify_task, "task_rlch_ntfy", + RELAY_CHN_NOTIFY_TASK_STACK, NULL, + RELAY_CHN_NOTIFY_TASK_PRIO, ¬ify_task_handle); + if (ret != pdPASS) { + ESP_LOGE(TAG, "Failed to create notify task"); + return ESP_ERR_NO_MEM; + } + + // Init the state listener list + vListInitialise(&listeners); + + return ESP_OK; +} + +void relay_chn_notify_deinit(void) +{ + if (notify_task_handle != NULL) { + vTaskDelete(notify_task_handle); + notify_task_handle = NULL; + } + + if (notify_msg_queue != NULL) { + vQueueDelete(notify_msg_queue); + notify_msg_queue = NULL; + } + + if (!listLIST_IS_EMPTY(&listeners)) { + // Free the listeners + while (listCURRENT_LIST_LENGTH(&listeners) > 0) { + ListItem_t *pxItem = listGET_HEAD_ENTRY(&listeners); + relay_chn_listener_entry_t *entry = listGET_LIST_ITEM_OWNER(pxItem); + uxListRemove(pxItem); + free(entry); + } + } +} + +/** + * @brief Find a listener entry in the list by its function pointer. + * + * This function replaces the old index-based search and is used to check + * for the existence of a listener before registration or for finding it + * during unregistration. + * + * @param listener The listener function pointer to find. + * @return Pointer to the listener entry if found, otherwise NULL. + */ +static relay_chn_listener_entry_t* find_listener_entry(relay_chn_state_listener_t listener) +{ + // Iterate through the linked list of listeners + for (ListItem_t *pxListItem = listGET_HEAD_ENTRY(&listeners); + pxListItem != listGET_END_MARKER(&listeners); + pxListItem = listGET_NEXT(pxListItem)) { + + relay_chn_listener_entry_t *entry = (relay_chn_listener_entry_t *) listGET_LIST_ITEM_OWNER(pxListItem); + if (entry->listener == listener) { + // Found the listener, return the entry + return entry; + } + } + + // Listener was not found in the list + return NULL; +} + +static void do_add_listener(relay_chn_state_listener_t listener) +{ + // This is now only called from the dispatcher task, so no mutex needed. + if (find_listener_entry(listener) != NULL) { + ESP_LOGD(TAG, "Listener %p already registered", listener); + return; + } + relay_chn_listener_entry_t *entry = malloc(sizeof(relay_chn_listener_entry_t)); + if (!entry) { + ESP_LOGE(TAG, "Failed to allocate memory for listener"); + return; + } + entry->listener = listener; + vListInitialiseItem(&(entry->list_item)); + listSET_LIST_ITEM_OWNER(&(entry->list_item), (void *)entry); + vListInsertEnd(&listeners, &(entry->list_item)); + ESP_LOGD(TAG, "Registered listener %p", listener); +} + +static void do_remove_listener(relay_chn_state_listener_t listener) +{ + // This is now only called from the dispatcher task, so no mutex needed. + relay_chn_listener_entry_t *entry = find_listener_entry(listener); + if (entry != NULL) { + uxListRemove(&(entry->list_item)); + free(entry); + ESP_LOGD(TAG, "Unregistered listener %p", listener); + } else { + ESP_LOGD(TAG, "Listener %p not found for unregistration.", listener); + } +} + +esp_err_t relay_chn_notify_add_listener(relay_chn_state_listener_t listener) +{ + ESP_RETURN_ON_FALSE(listener, ESP_ERR_INVALID_ARG, TAG, "Listener cannot be NULL"); + ESP_RETURN_ON_FALSE(notify_msg_queue, ESP_ERR_INVALID_STATE, TAG, "Notify module not initialized"); + + relay_chn_notify_msg_t msg = { .cmd = RELAY_CHN_NOTIFY_CMD_ADD_LISTENER, .payload.listener = listener }; + if (xQueueSend(notify_msg_queue, &msg, 0) != pdTRUE) { + ESP_LOGE(TAG, "Notify queue is full, failed to queue add_listener"); + return ESP_FAIL; + } + return ESP_OK; +} + +void relay_chn_notify_remove_listener(relay_chn_state_listener_t listener) +{ + if (listener == NULL) { + ESP_LOGD(TAG, "Cannot unregister a NULL listener."); + return; + } + if (!notify_msg_queue) { + ESP_LOGE(TAG, "Notify module not initialized, cannot remove listener"); + return; + } + + relay_chn_notify_msg_t msg = { .cmd = RELAY_CHN_NOTIFY_CMD_REMOVE_LISTENER, .payload.listener = listener }; + if (xQueueSend(notify_msg_queue, &msg, 0) != pdTRUE) { + ESP_LOGW(TAG, "Notify queue is full, failed to queue remove_listener"); + } +} + +esp_err_t relay_chn_notify_state_change(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) +{ + if (!notify_msg_queue) { + return ESP_ERR_INVALID_STATE; + } + + relay_chn_notify_msg_t msg = { + .cmd = RELAY_CHN_NOTIFY_CMD_BROADCAST, + .payload.event_data.chn_id = chn_id, + .payload.event_data.old_state = old_state, + .payload.event_data.new_state = new_state, + }; + + // Try to send, do not wait if the queue is full + if (xQueueSend(notify_msg_queue, &msg, 0) != pdTRUE) { + ESP_LOGW(TAG, "Notify queue is full, dropping event: %d -> %d for #%d", old_state, new_state, chn_id); + return ESP_FAIL; + } + return ESP_OK; +} + +static void do_notify(relay_chn_notify_event_data_t *event_data) +{ + // Iterate through the linked list of listeners and notify them. + // No mutex is needed as this is the only task accessing the list. + for (ListItem_t *pxListItem = listGET_HEAD_ENTRY(&listeners); + pxListItem != listGET_END_MARKER(&listeners); + pxListItem = listGET_NEXT(pxListItem)) { + relay_chn_listener_entry_t *entry = (relay_chn_listener_entry_t *) listGET_LIST_ITEM_OWNER(pxListItem); + if (entry && entry->listener) { + // Emit the state change to the listeners + entry->listener(event_data->chn_id, event_data->old_state, event_data->new_state); + } + } +} + +// ---- Notify Task ---- +static void relay_chn_notify_task(void *arg) +{ + relay_chn_notify_msg_t msg; + for (;;) { + if (xQueueReceive(notify_msg_queue, &msg, portMAX_DELAY) == pdTRUE) { + switch (msg.cmd) { + case RELAY_CHN_NOTIFY_CMD_BROADCAST: { + do_notify(&msg.payload.event_data); + break; + } + case RELAY_CHN_NOTIFY_CMD_ADD_LISTENER: + do_add_listener(msg.payload.listener); + break; + case RELAY_CHN_NOTIFY_CMD_REMOVE_LISTENER: + do_remove_listener(msg.payload.listener); + break; + default: + ESP_LOGE(TAG, "Unknown command type in notify queue: %d", msg.cmd); + break; + } + } + } +} \ No newline at end of file diff --git a/test_apps/main/CMakeLists.txt b/test_apps/main/CMakeLists.txt index f148b59..3b8ff4d 100644 --- a/test_apps/main/CMakeLists.txt +++ b/test_apps/main/CMakeLists.txt @@ -1,5 +1,6 @@ # === These files must be included in any case === set(srcs "test_common.c" + "test_relay_chn_notify_common.c" "test_app_main.c") set(incdirs ".") @@ -7,10 +8,10 @@ set(incdirs ".") # === Selective compilation based on channel count === if(CONFIG_RELAY_CHN_COUNT GREATER 1) list(APPEND srcs "test_relay_chn_core_multi.c" - "test_relay_chn_listener_multi.c") + "test_relay_chn_notify_multi.c") else() list(APPEND srcs "test_relay_chn_core_single.c" - "test_relay_chn_listener_single.c") + "test_relay_chn_notify_single.c") endif() if(CONFIG_RELAY_CHN_ENABLE_TILTING) diff --git a/test_apps/main/test_relay_chn_listener_multi.c b/test_apps/main/test_relay_chn_listener_multi.c deleted file mode 100644 index aca0ea0..0000000 --- a/test_apps/main/test_relay_chn_listener_multi.c +++ /dev/null @@ -1,127 +0,0 @@ -#include "test_common.h" - - -// --- Listener Test Globals --- -typedef struct { - uint8_t chn_id; - relay_chn_state_t old_state; - relay_chn_state_t new_state; - int call_count; -} listener_callback_info_t; - -static listener_callback_info_t listener1_info; -static listener_callback_info_t listener2_info; - -// --- Listener Test Helper Functions --- - -// Clear the memory from possible garbage values -static void reset_listener_info(listener_callback_info_t* info) { - memset(info, 0, sizeof(listener_callback_info_t)); -} - -static void test_listener_1(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) { - listener1_info.chn_id = chn_id; - listener1_info.old_state = old_state; - listener1_info.new_state = new_state; - listener1_info.call_count++; -} - -static void test_listener_2(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) { - listener2_info.chn_id = chn_id; - listener2_info.old_state = old_state; - listener2_info.new_state = new_state; - listener2_info.call_count++; -} - -// ### Listener Functionality Tests - -TEST_CASE("Listener is called on state change", "[relay_chn][listener]") -{ - uint8_t ch = 0; - reset_listener_info(&listener1_info); - - // 1. Register the listener - TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); - - // 2. Trigger a state change - relay_chn_run_forward(ch); - vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow event to be processed - - // 3. Verify the listener was called with correct parameters - TEST_ASSERT_EQUAL(1, listener1_info.call_count); - TEST_ASSERT_EQUAL(ch, listener1_info.chn_id); - TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, listener1_info.old_state); - TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener1_info.new_state); - - // 4. Unregister to clean up - relay_chn_unregister_listener(test_listener_1); -} - -TEST_CASE("Unregistered listener is not called", "[relay_chn][listener]") -{ - uint8_t ch = 0; - reset_listener_info(&listener1_info); - - // 1. Register and then immediately unregister the listener - TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); - relay_chn_unregister_listener(test_listener_1); - - // 2. Trigger a state change - relay_chn_run_forward(ch); - vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); - - // 3. Verify the listener was NOT called - TEST_ASSERT_EQUAL(0, listener1_info.call_count); -} - -TEST_CASE("Multiple listeners are called on state change", "[relay_chn][listener]") -{ - uint8_t ch = 0; - reset_listener_info(&listener1_info); - reset_listener_info(&listener2_info); - - // 1. Register two different listeners - TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); - TEST_ESP_OK(relay_chn_register_listener(test_listener_2)); - - // 2. Trigger a state change - relay_chn_run_forward(ch); - vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); - - // 3. Verify listener 1 was called correctly - TEST_ASSERT_EQUAL(1, listener1_info.call_count); - TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, listener1_info.old_state); - TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener1_info.new_state); - - // 4. Verify listener 2 was also called correctly - TEST_ASSERT_EQUAL(1, listener2_info.call_count); - TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, listener2_info.old_state); - TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener2_info.new_state); - - // 5. Clean up - relay_chn_unregister_listener(test_listener_1); - relay_chn_unregister_listener(test_listener_2); -} - -TEST_CASE("Listener registration handles invalid arguments and duplicates", "[relay_chn][listener]") -{ - reset_listener_info(&listener1_info); - - // 1. Registering a NULL listener should fail - TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_register_listener(NULL)); - - // 2. Unregistering a NULL listener should not crash - relay_chn_unregister_listener(NULL); - - // 3. Registering the same listener twice should be handled gracefully - TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); - TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); // Second call should be a no-op - - // 4. Trigger a state change and verify the listener is only called ONCE - relay_chn_run_forward(0); - vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); - TEST_ASSERT_EQUAL(1, listener1_info.call_count); - - // 5. Clean up - relay_chn_unregister_listener(test_listener_1); -} diff --git a/test_apps/main/test_relay_chn_notify_common.c b/test_apps/main/test_relay_chn_notify_common.c new file mode 100644 index 0000000..f033d16 --- /dev/null +++ b/test_apps/main/test_relay_chn_notify_common.c @@ -0,0 +1,49 @@ +#include "test_relay_chn_notify_common.h" + +listener_callback_info_t listener1_info; +listener_callback_info_t listener2_info; + +// --- Globals for Advanced Tests --- +SemaphoreHandle_t blocking_listener_sem = NULL; +SemaphoreHandle_t log_check_sem = NULL; +volatile int blocking_listener_call_count = 0; +vprintf_like_t original_vprintf = NULL; + +// --- Listener Test Helper Functions --- + +// Clear the memory from possible garbage values +void reset_listener_info(listener_callback_info_t* info) { + memset(info, 0, sizeof(listener_callback_info_t)); +} + +void test_listener_1(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) { + listener1_info.chn_id = chn_id; + listener1_info.old_state = old_state; + listener1_info.new_state = new_state; + listener1_info.call_count++; +} + +void test_listener_2(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) { + listener2_info.chn_id = chn_id; + listener2_info.old_state = old_state; + listener2_info.new_state = new_state; + listener2_info.call_count++; +} + +void blocking_listener(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) { + blocking_listener_call_count++; + // Block until the main test task unblocks us + xSemaphoreTake(blocking_listener_sem, portMAX_DELAY); +} + +int log_check_vprintf(const char *format, va_list args) { + // Buffer to hold the formatted log message + char buffer[256]; + vsnprintf(buffer, sizeof(buffer), format, args); + + if (strstr(buffer, "Notify queue is full")) { + xSemaphoreGive(log_check_sem); + } + + return original_vprintf(format, args); +} \ No newline at end of file diff --git a/test_apps/main/test_relay_chn_notify_common.h b/test_apps/main/test_relay_chn_notify_common.h new file mode 100644 index 0000000..436f4af --- /dev/null +++ b/test_apps/main/test_relay_chn_notify_common.h @@ -0,0 +1,48 @@ +#pragma once + +#include "test_common.h" +#include "freertos/semphr.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// This is defined in the source file, we redefine it here for the test. +// The test build must have the same CONFIG_RELAY_CHN_COUNT. +#define TEST_RELAY_CHN_NOTIFY_QUEUE_LEN (16 + CONFIG_RELAY_CHN_COUNT * 4) + +// --- Listener Test Globals --- +typedef struct { + uint8_t chn_id; + relay_chn_state_t old_state; + relay_chn_state_t new_state; + int call_count; +} listener_callback_info_t; + +// --- Listener callback infos to be defined --- +extern listener_callback_info_t listener1_info; +extern listener_callback_info_t listener2_info; + +// --- Globals for Advanced Tests --- +extern SemaphoreHandle_t blocking_listener_sem; +extern SemaphoreHandle_t log_check_sem; +extern volatile int blocking_listener_call_count; +extern vprintf_like_t original_vprintf; + + +// --- Listener Test Helper Functions --- + +// Clear the memory from possible garbage values +void reset_listener_info(listener_callback_info_t* info); + +// State listeners for notify module testing +void test_listener_1(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state); +void test_listener_2(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state); +void blocking_listener(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state); + +int log_check_vprintf(const char *format, va_list args); + +#ifdef __cplusplus +} +#endif diff --git a/test_apps/main/test_relay_chn_notify_multi.c b/test_apps/main/test_relay_chn_notify_multi.c new file mode 100644 index 0000000..35844bc --- /dev/null +++ b/test_apps/main/test_relay_chn_notify_multi.c @@ -0,0 +1,166 @@ +#include "test_relay_chn_notify_common.h" + +// This is a private header, but we need it for direct notification calls and queue length. +// It's included conditionally in the build via CMakeLists.txt when NVS is enabled. +#include "relay_chn_notify.h" + +// ### Listener Functionality Tests + +TEST_CASE("Listener is called on state change for each channel", "[relay_chn][notify]") +{ + // 1. Register the listener and reset info + reset_listener_info(&listener1_info); + TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow registration to be processed + + // Loop through each channel + for (uint8_t ch = 0; ch < CONFIG_RELAY_CHN_COUNT; ch++) { + // 2. Trigger a state change on the current channel. + // tearDown() ensures each channel starts as IDLE. + relay_chn_run_forward(ch); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow event to be processed + + // 3. Verify the listener was called with correct parameters for this channel. + // The listener_info struct is overwritten each time, but we check it before the next iteration. + TEST_ASSERT_EQUAL(ch, listener1_info.chn_id); + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, listener1_info.old_state); + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener1_info.new_state); + } + + // 4. Verify the total call count after the loop + TEST_ASSERT_EQUAL(CONFIG_RELAY_CHN_COUNT, listener1_info.call_count); + + // 5. Unregister to clean up + relay_chn_unregister_listener(test_listener_1); +} + +TEST_CASE("Unregistered listener is not called for any channel", "[relay_chn][notify]") +{ + reset_listener_info(&listener1_info); + + // 1. Register and then immediately unregister the listener + TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); + relay_chn_unregister_listener(test_listener_1); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow commands to process + + // 2. Trigger a state change on all channels + relay_chn_run_forward_all(); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS * CONFIG_RELAY_CHN_COUNT)); // Allow all events to be processed + + // 3. Verify the listener was NOT called + TEST_ASSERT_EQUAL(0, listener1_info.call_count); +} + +TEST_CASE("Multiple listeners are called on state change for each channel", "[relay_chn][notify]") +{ + // 1. Register listeners and reset info + reset_listener_info(&listener1_info); + reset_listener_info(&listener2_info); + TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); + TEST_ESP_OK(relay_chn_register_listener(test_listener_2)); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow registration commands to be processed + + // Loop through each channel + for (uint8_t ch = 0; ch < CONFIG_RELAY_CHN_COUNT; ch++) { + // 2. Trigger a state change on the current channel + relay_chn_run_forward(ch); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); + + // 3. Verify listener 1 was called correctly for this channel + TEST_ASSERT_EQUAL(ch, listener1_info.chn_id); + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, listener1_info.old_state); + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener1_info.new_state); + + // 4. Verify listener 2 was also called correctly for this channel + TEST_ASSERT_EQUAL(ch, listener2_info.chn_id); + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, listener2_info.old_state); + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener2_info.new_state); + } + + // 5. Verify total call counts + TEST_ASSERT_EQUAL(CONFIG_RELAY_CHN_COUNT, listener1_info.call_count); + TEST_ASSERT_EQUAL(CONFIG_RELAY_CHN_COUNT, listener2_info.call_count); + + // 6. Clean up + relay_chn_unregister_listener(test_listener_1); + relay_chn_unregister_listener(test_listener_2); +} + +TEST_CASE("Listener registration handles invalid arguments and duplicates", "[relay_chn][notify]") +{ + reset_listener_info(&listener1_info); + + // 1. Registering a NULL listener should fail + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_register_listener(NULL)); + + // 2. Unregistering a NULL listener should not crash + relay_chn_unregister_listener(NULL); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow commands to process + + // 3. Registering the same listener twice should be handled gracefully + TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); + TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); // Second call should be a no-op + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow registration commands to be processed + + // 4. Trigger a state change on all channels and verify the listener is only called ONCE per channel + relay_chn_run_forward_all(); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS * CONFIG_RELAY_CHN_COUNT)); // Allow all events to be processed + TEST_ASSERT_EQUAL(CONFIG_RELAY_CHN_COUNT, listener1_info.call_count); + + // 5. Clean up + relay_chn_unregister_listener(test_listener_1); +} + +TEST_CASE("Notify queue full scenario is handled gracefully", "[relay_chn][notify]") +{ + // 1. Setup + blocking_listener_sem = xSemaphoreCreateBinary(); + log_check_sem = xSemaphoreCreateBinary(); + blocking_listener_call_count = 0; + + // Intercept logs to check for the "queue full" warning + original_vprintf = esp_log_set_vprintf(log_check_vprintf); + + // 2. Register a listener that will block, allowing the queue to fill up + TEST_ESP_OK(relay_chn_register_listener(blocking_listener)); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow task to start + + // 3. Fill the queue. The first event will be consumed immediately by the dispatcher, + // which will then call the blocking_listener and block. The remaining (LEN - 1) + // events will sit in the queue, leaving one empty slot. + // Use different channel IDs to make the test more robust. + for (int i = 0; i < TEST_RELAY_CHN_NOTIFY_QUEUE_LEN; i++) { + uint8_t ch = i % CONFIG_RELAY_CHN_COUNT; + TEST_ESP_OK(relay_chn_notify_state_change(ch, RELAY_CHN_STATE_IDLE, RELAY_CHN_STATE_FORWARD)); + } + + // 4. Send one more event to fill the last slot in the queue. This should succeed. + TEST_ESP_OK(relay_chn_notify_state_change(0, RELAY_CHN_STATE_IDLE, RELAY_CHN_STATE_FORWARD)); // Use any valid channel + + // 5. Now the queue is full. Trigger one more event to cause an overflow. + // This call should fail and log the warning. + TEST_ASSERT_EQUAL(ESP_FAIL, relay_chn_notify_state_change(1 % CONFIG_RELAY_CHN_COUNT, RELAY_CHN_STATE_IDLE, RELAY_CHN_STATE_FORWARD)); + + // 6. Wait for the "queue full" log message to be captured by our vprintf hook + TEST_ASSERT_TRUE_MESSAGE(xSemaphoreTake(log_check_sem, pdMS_TO_TICKS(1000)) == pdTRUE, "Did not receive 'queue full' log message"); + + // 7. Unblock the listener so it can process all queued items. + // There was 1 initial event + QUEUE_LEN events that were successfully queued. + for (int i = 0; i < TEST_RELAY_CHN_NOTIFY_QUEUE_LEN + 1; i++) { + xSemaphoreGive(blocking_listener_sem); + // Give the dispatcher task a moment to process one item from the queue + vTaskDelay(pdMS_TO_TICKS(10)); + } + + // 8. Verify the listener was called exactly QUEUE_LEN + 1 times + TEST_ASSERT_EQUAL_INT(TEST_RELAY_CHN_NOTIFY_QUEUE_LEN + 1, blocking_listener_call_count); + + // 9. Cleanup + esp_log_set_vprintf(original_vprintf); + relay_chn_unregister_listener(blocking_listener); + vSemaphoreDelete(blocking_listener_sem); + vSemaphoreDelete(log_check_sem); + blocking_listener_sem = NULL; + log_check_sem = NULL; + original_vprintf = NULL; +} diff --git a/test_apps/main/test_relay_chn_listener_single.c b/test_apps/main/test_relay_chn_notify_single.c similarity index 51% rename from test_apps/main/test_relay_chn_listener_single.c rename to test_apps/main/test_relay_chn_notify_single.c index 155972c..66e2a92 100644 --- a/test_apps/main/test_relay_chn_listener_single.c +++ b/test_apps/main/test_relay_chn_notify_single.c @@ -1,40 +1,12 @@ -#include "test_common.h" +#include "test_relay_chn_notify_common.h" - -// --- Listener Test Globals --- -typedef struct { - relay_chn_state_t old_state; - relay_chn_state_t new_state; - int call_count; -} listener_callback_info_t; - -static listener_callback_info_t listener1_info; -static listener_callback_info_t listener2_info; - -// --- Listener Test Helper Functions --- - -// Clear the memory from possible garbage values -static void reset_listener_info(listener_callback_info_t* info) { - memset(info, 0, sizeof(listener_callback_info_t)); -} - -static void test_listener_1(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) { - /* Just ignore the channel id */ - listener1_info.old_state = old_state; - listener1_info.new_state = new_state; - listener1_info.call_count++; -} - -static void test_listener_2(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) { - /* Just ignore the channel id */ - listener2_info.old_state = old_state; - listener2_info.new_state = new_state; - listener2_info.call_count++; -} +// This is a private header, but we need it for direct notification calls and queue length. +// It's included conditionally in the build via CMakeLists.txt when NVS is enabled. +#include "relay_chn_notify.h" // ### Listener Functionality Tests -TEST_CASE("Listener is called on state change", "[relay_chn][listener]") +TEST_CASE("Listener is called on state change", "[relay_chn][notify]") { reset_listener_info(&listener1_info); @@ -54,7 +26,7 @@ TEST_CASE("Listener is called on state change", "[relay_chn][listener]") relay_chn_unregister_listener(test_listener_1); } -TEST_CASE("Unregistered listener is not called", "[relay_chn][listener]") +TEST_CASE("Unregistered listener is not called", "[relay_chn][notify]") { reset_listener_info(&listener1_info); @@ -70,7 +42,7 @@ TEST_CASE("Unregistered listener is not called", "[relay_chn][listener]") TEST_ASSERT_EQUAL(0, listener1_info.call_count); } -TEST_CASE("Multiple listeners are called on state change", "[relay_chn][listener]") +TEST_CASE("Multiple listeners are called on state change", "[relay_chn][notify]") { reset_listener_info(&listener1_info); reset_listener_info(&listener2_info); @@ -98,7 +70,7 @@ TEST_CASE("Multiple listeners are called on state change", "[relay_chn][listener relay_chn_unregister_listener(test_listener_2); } -TEST_CASE("Listener registration handles invalid arguments and duplicates", "[relay_chn][listener]") +TEST_CASE("Listener registration handles invalid arguments and duplicates", "[relay_chn][notify]") { reset_listener_info(&listener1_info); @@ -120,3 +92,55 @@ TEST_CASE("Listener registration handles invalid arguments and duplicates", "[re // 5. Clean up relay_chn_unregister_listener(test_listener_1); } + +TEST_CASE("Notify queue full scenario is handled gracefully", "[relay_chn][notify]") +{ + // 1. Setup + blocking_listener_sem = xSemaphoreCreateBinary(); + log_check_sem = xSemaphoreCreateBinary(); + blocking_listener_call_count = 0; + + // Intercept logs to check for the "queue full" warning + original_vprintf = esp_log_set_vprintf(log_check_vprintf); + + // 2. Register a listener that will block, allowing the queue to fill up + TEST_ESP_OK(relay_chn_register_listener(blocking_listener)); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow task to start + + // 3. Fill the queue. The first event will be consumed immediately by the dispatcher, + // which will then call the blocking_listener and block. The remaining (LEN - 1) + // events will sit in the queue, leaving one empty slot. + for (int i = 0; i < TEST_RELAY_CHN_NOTIFY_QUEUE_LEN; i++) { + TEST_ESP_OK(relay_chn_notify_state_change(0, RELAY_CHN_STATE_IDLE, RELAY_CHN_STATE_FORWARD)); + } + + // 4. Send one more event to fill the last slot in the queue. This should succeed. + TEST_ESP_OK(relay_chn_notify_state_change(0, RELAY_CHN_STATE_IDLE, RELAY_CHN_STATE_FORWARD)); + + // 5. Now the queue is full. Trigger one more event to cause an overflow. + // This call should fail and log the warning. + TEST_ASSERT_EQUAL(ESP_FAIL, relay_chn_notify_state_change(0, RELAY_CHN_STATE_IDLE, RELAY_CHN_STATE_FORWARD)); + + // 6. Wait for the "queue full" log message to be captured by our vprintf hook + TEST_ASSERT_TRUE_MESSAGE(xSemaphoreTake(log_check_sem, pdMS_TO_TICKS(1000)) == pdTRUE, "Did not receive 'queue full' log message"); + + // 7. Unblock the listener so it can process all queued items. + // There was 1 initial event + QUEUE_LEN events that were successfully queued. + for (int i = 0; i < TEST_RELAY_CHN_NOTIFY_QUEUE_LEN + 1; i++) { + xSemaphoreGive(blocking_listener_sem); + // Give the dispatcher task a moment to process one item from the queue + vTaskDelay(pdMS_TO_TICKS(10)); + } + + // 8. Verify the listener was called exactly QUEUE_LEN + 1 times + TEST_ASSERT_EQUAL_INT(TEST_RELAY_CHN_NOTIFY_QUEUE_LEN + 1, blocking_listener_call_count); + + // 9. Cleanup + esp_log_set_vprintf(original_vprintf); + relay_chn_unregister_listener(blocking_listener); + vSemaphoreDelete(blocking_listener_sem); + vSemaphoreDelete(log_check_sem); + blocking_listener_sem = NULL; + log_check_sem = NULL; + original_vprintf = NULL; +}