Add notification system for relay channel state changes

- Introduced a new notification module to handle state change listeners.
- Added functions to register and unregister listeners for relay channel state changes.
- Implemented a queue-based system to manage notifications and listener callbacks.
- Updated core relay channel logic to utilize the new notification system.
- Removed old listener management code from relay channel core.
- Refactored the former listener tests to notify tests and added tests for the notification system, including handling of multiple listeners and queue overflow scenarios.
- Updated CMakeLists.txt to include new source files and headers for the notification module.
- Revised README.md to include warnings about callback execution context and performance considerations.

Refs #1096, #1085 and closes #1097
This commit is contained in:
2025-09-02 15:46:48 +03:00
parent d2b38a5b4e
commit 5e8e5a4cab
13 changed files with 678 additions and 272 deletions

View File

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

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"
// --- 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;
}