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:
258
src/relay_chn_notify.c
Normal file
258
src/relay_chn_notify.c
Normal 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, ¬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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user