release-1.0.0 #39

Merged
ismail merged 78 commits from release-1.0.0 into main 2025-09-13 10:55:49 +02:00
13 changed files with 678 additions and 272 deletions
Showing only changes of commit 5e8e5a4cab - Show all commits

View File

@@ -3,7 +3,8 @@ set(priv_include_dirs "private_include")
set(srcs "src/relay_chn_core.c" set(srcs "src/relay_chn_core.c"
"src/relay_chn_output.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) if(CONFIG_RELAY_CHN_ENABLE_TILTING)
list(APPEND srcs "src/relay_chn_tilt.c") list(APPEND srcs "src/relay_chn_tilt.c")

View File

@@ -200,6 +200,9 @@ relay_chn_flip_direction_all();
### 3. Monitor channel state ### 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: For single mode:
```c ```c

View File

@@ -13,6 +13,36 @@
extern "C" { extern "C" {
#endif #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 #if CONFIG_RELAY_CHN_COUNT > 1
/** /**
* @brief Get the current state of a relay channel. * @brief Get the current state of a relay channel.

View File

@@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "esp_err.h"
#include <stdint.h>
#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

View File

@@ -10,7 +10,7 @@ if [[ -z "$IDF_PATH" ]]; then
fi fi
# ==== 2. Valid Modes and Defaults ==== # ==== 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") 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_tag="all" # Default to 'all' if no tag specified
arg_profile="full_multi" # Default to 'full_multi' if no profile 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 "This script builds and runs tests for the relay_chn component using QEMU."
echo "" echo ""
echo "Arguments:" 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 ""
echo " If no tag is specified, it defaults to 'all'." echo " If no tag is specified, it defaults to 'all'."
echo "" echo ""

View File

@@ -11,6 +11,7 @@
#include "relay_chn_output.h" #include "relay_chn_output.h"
#include "relay_chn_run_info.h" #include "relay_chn_run_info.h"
#include "relay_chn_ctl.h" #include "relay_chn_ctl.h"
#include "relay_chn_notify.h"
#if CONFIG_RELAY_CHN_ENABLE_TILTING #if CONFIG_RELAY_CHN_ENABLE_TILTING
#include "relay_chn_tilt.h" #include "relay_chn_tilt.h"
@@ -26,15 +27,6 @@
static const char *TAG = "RELAY_CHN_CORE"; 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 #if CONFIG_RELAY_CHN_ENABLE_RUN_LIMIT
/* /*
* Run limit timer callback immediately dispatches a STOP command for the * 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"); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize tilt feature");
#endif #endif
// Init the state listener list // Initialize the notify feature
vListInitialise(&relay_chn_listener_list); ret = relay_chn_notify_init();
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize notify feature");
return ret; return ret;
} }
@@ -136,95 +128,14 @@ void relay_chn_destroy(void)
#if CONFIG_RELAY_CHN_ENABLE_TILTING #if CONFIG_RELAY_CHN_ENABLE_TILTING
relay_chn_tilt_deinit(); relay_chn_tilt_deinit();
#endif #endif
relay_chn_notify_deinit();
relay_chn_ctl_deinit(); relay_chn_ctl_deinit();
relay_chn_output_deinit(); relay_chn_output_deinit();
#if CONFIG_RELAY_CHN_ENABLE_NVS #if CONFIG_RELAY_CHN_ENABLE_NVS
relay_chn_nvs_deinit(); relay_chn_nvs_deinit();
#endif #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 relay_chn_start_esp_timer_once(esp_timer_handle_t esp_timer, uint32_t time_ms)
{ {
@@ -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; chn_ctl->state = new_state;
// Iterate through the linked list of listeners and notify them. relay_chn_notify_state_change(chn_ctl->id, old_state, new_state);
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);
}
}
} }
static void relay_chn_execute_idle(relay_chn_ctl_t *chn_ctl); static void relay_chn_execute_idle(relay_chn_ctl_t *chn_ctl);

258
src/relay_chn_notify.c Normal file
View File

@@ -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, &notify_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;
}
}
}
}

View File

@@ -1,5 +1,6 @@
# === These files must be included in any case === # === These files must be included in any case ===
set(srcs "test_common.c" set(srcs "test_common.c"
"test_relay_chn_notify_common.c"
"test_app_main.c") "test_app_main.c")
set(incdirs ".") set(incdirs ".")
@@ -7,10 +8,10 @@ set(incdirs ".")
# === Selective compilation based on channel count === # === Selective compilation based on channel count ===
if(CONFIG_RELAY_CHN_COUNT GREATER 1) if(CONFIG_RELAY_CHN_COUNT GREATER 1)
list(APPEND srcs "test_relay_chn_core_multi.c" list(APPEND srcs "test_relay_chn_core_multi.c"
"test_relay_chn_listener_multi.c") "test_relay_chn_notify_multi.c")
else() else()
list(APPEND srcs "test_relay_chn_core_single.c" list(APPEND srcs "test_relay_chn_core_single.c"
"test_relay_chn_listener_single.c") "test_relay_chn_notify_single.c")
endif() endif()
if(CONFIG_RELAY_CHN_ENABLE_TILTING) if(CONFIG_RELAY_CHN_ENABLE_TILTING)

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
#pragma once
#include "test_common.h"
#include "freertos/semphr.h"
#include <stdarg.h>
#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

View File

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

View File

@@ -1,40 +1,12 @@
#include "test_common.h" #include "test_relay_chn_notify_common.h"
// This is a private header, but we need it for direct notification calls and queue length.
// --- Listener Test Globals --- // It's included conditionally in the build via CMakeLists.txt when NVS is enabled.
typedef struct { #include "relay_chn_notify.h"
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++;
}
// ### Listener Functionality Tests // ### 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); 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); 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); 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_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(&listener1_info);
reset_listener_info(&listener2_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); 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); reset_listener_info(&listener1_info);
@@ -120,3 +92,55 @@ TEST_CASE("Listener registration handles invalid arguments and duplicates", "[re
// 5. Clean up // 5. Clean up
relay_chn_unregister_listener(test_listener_1); 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;
}