/* * 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 s_listeners; static QueueHandle_t s_notify_queue = NULL; static TaskHandle_t s_notify_task = NULL; static void relay_chn_notify_task(void *arg); esp_err_t relay_chn_notify_init(void) { if (s_notify_queue != NULL) { return ESP_OK; } s_notify_queue = xQueueCreate(RELAY_CHN_NOTIFY_QUEUE_LEN, sizeof(relay_chn_notify_msg_t)); if (!s_notify_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, &s_notify_task); if (ret != pdPASS) { ESP_LOGE(TAG, "Failed to create notify task"); return ESP_ERR_NO_MEM; } // Init the state listener list vListInitialise(&s_listeners); return ESP_OK; } void relay_chn_notify_deinit(void) { if (s_notify_task != NULL) { vTaskDelete(s_notify_task); s_notify_task = NULL; } if (s_notify_queue != NULL) { vQueueDelete(s_notify_queue); s_notify_queue = NULL; } if (!listLIST_IS_EMPTY(&s_listeners)) { // Free the listeners while (listCURRENT_LIST_LENGTH(&s_listeners) > 0) { ListItem_t *pxItem = listGET_HEAD_ENTRY(&s_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) { if (listLIST_IS_EMPTY(&s_listeners)) { ESP_LOGD(TAG, "No listeners registered"); return NULL; } // Iterate through the linked list of listeners for (ListItem_t *pxListItem = listGET_HEAD_ENTRY(&s_listeners); pxListItem != listGET_END_MARKER(&s_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(&s_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); } if (listLIST_IS_EMPTY(&s_listeners)) { // Flush all pending notifications in the queue xQueueReset(s_notify_queue); } } 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(s_notify_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(s_notify_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 (!s_notify_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 (xQueueSendToFront(s_notify_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 (!s_notify_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(s_notify_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(&s_listeners); pxListItem != listGET_END_MARKER(&s_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(s_notify_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; } } } }