/** * @file relay_chn.c * * @author * Ismail Sahillioglu * * @date 2025.02.08 * * @ingroup relay_chn * * @brief This file contains the implementation of the relay channel component. * @{ */ #include #include #include "esp_err.h" #include "esp_check.h" #include "esp_log.h" #include "esp_task.h" #include "driver/gpio.h" #include "esp_timer.h" #include "esp_event_base.h" #include "esp_event.h" #include "relay_chn.h" #include "freertos/idf_additions.h" #include "sdkconfig.h" #define RELAY_CHN_OPPOSITE_INERTIA_MS CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS #define RELAY_CHN_COUNT CONFIG_RELAY_CHN_COUNT #define RELAY_CHN_ENABLE_TILTING CONFIG_RELAY_CHN_ENABLE_TILTING static const char *TAG = "relay_chn"; ESP_EVENT_DEFINE_BASE(RELAY_CHN_CMD_EVENT); /** * @brief Enumeration for relay channel commands. */ enum relay_chn_cmd_enum { RELAY_CHN_CMD_NONE, ///< No command. RELAY_CHN_CMD_STOP, ///< Stop the relay channel. RELAY_CHN_CMD_FORWARD, ///< Run the relay channel in the forward direction. RELAY_CHN_CMD_REVERSE, ///< Run the relay channel in the reverse direction. RELAY_CHN_CMD_FLIP, ///< Flip the direction of the relay channel. RELAY_CHN_CMD_FREE ///< Free the relay channel. }; /// @brief Alias for the enum type relay_chn_cmd_enum. typedef enum relay_chn_cmd_enum relay_chn_cmd_t; /** * @brief Structure to hold runtime information for a relay channel. */ typedef struct relay_chn_run_info_type { relay_chn_cmd_t last_run_cmd; ///< The last run command issued on the relay channel; forward or reverse. uint32_t last_run_cmd_time_ms; ///< The time in milliseconds when the last run command was issued. } relay_chn_run_info_t; /** * @brief Structure to hold the output configuration of a relay channel. */ typedef struct relay_chn_output_type { gpio_num_t forward_pin; ///< GPIO pin number for the forward direction. gpio_num_t reverse_pin; ///< GPIO pin number for the reverse direction. relay_chn_direction_t direction; ///< The current direction of the relay channel. } relay_chn_output_t; typedef struct relay_chn_type relay_chn_t; // Forward declaration /** * @brief Function pointer type for relay channel command execution functions. * @param relay_chn Pointer to the relay channel to execute the command on. */ typedef void(*relay_chn_cmd_fn_t)(relay_chn_t*); #if RELAY_CHN_ENABLE_TILTING == 0 /** * @brief Structure to hold the state and configuration of a relay channel. */ typedef struct relay_chn_type { uint8_t id; ///< The ID of the relay channel. relay_chn_state_t state; ///< The current state of the relay channel. relay_chn_run_info_t run_info; ///< Runtime information of the relay channel. relay_chn_output_t output; ///< Output configuration of the relay channel. relay_chn_cmd_t pending_cmd; ///< The command that is pending to be issued esp_timer_handle_t inertia_timer; ///< Timer to handle the opposite direction inertia time. } relay_chn_t; #else /** * @name Tilt Pattern Timing Definitions * @{ * The min and max timing definitions as well as the default timing definitions. * These definitions are used to define and adjust the tilt sensitivity. */ #define RELAY_CHN_TILT_RUN_MIN_MS 50 #define RELAY_CHN_TILT_RUN_MAX_MS 10 #define RELAY_CHN_TILT_PAUSE_MIN_MS 450 #define RELAY_CHN_TILT_PAUSE_MAX_MS 90 #define RELAY_CHN_TILT_DEFAULT_RUN_MS 15 #define RELAY_CHN_TILT_DEFAULT_PAUSE_MS 150 #define RELAY_CHN_TILT_DEFAULT_SENSITIVITY \ ( (RELAY_CHN_TILT_DEFAULT_RUN_MS - RELAY_CHN_TILT_RUN_MIN_MS) \ * 100 / (RELAY_CHN_TILT_RUN_MAX_MS - RELAY_CHN_TILT_RUN_MIN_MS) ) /// @} ESP_EVENT_DEFINE_BASE(RELAY_CHN_TILT_CMD_EVENT_BASE); /// @brief Tilt commands. enum relay_chn_tilt_cmd_enum { RELAY_CHN_TILT_CMD_NONE, ///< No command. RELAY_CHN_TILT_CMD_STOP, ///< Tilt command stop. RELAY_CHN_TILT_CMD_FORWARD, ///< Tilt command for forward. RELAY_CHN_TILT_CMD_REVERSE ///< Tilt command for reverse. }; /// @brief Alias for the enum type relay_chn_tilt_cmd_enum. typedef enum relay_chn_tilt_cmd_enum relay_chn_tilt_cmd_t; /// @brief Tilt steps. enum relay_chn_tilt_step_enum { RELAY_CHN_TILT_STEP_NONE, ///< No step. RELAY_CHN_TILT_STEP_PENDING, ///< Pending step. RELAY_CHN_TILT_STEP_MOVE, ///< Move step. Tilt is driving either for forward or reverse. RELAY_CHN_TILT_STEP_PAUSE ///< Pause step. Tilt is paused. }; /// @brief Alias for the enum relay_chn_tilt_step_enum. typedef enum relay_chn_tilt_step_enum relay_chn_tilt_step_t; /// @brief Tilt timing structure to manage tilt pattern timing. typedef struct relay_chn_tilt_timing_struct { uint8_t sensitivity; ///< Tilt sensitivity in percentage (%). uint32_t move_time_ms; ///< Move time in milliseconds. uint32_t pause_time_ms; ///< Pause time in milliseconds. } relay_chn_tilt_timing_t; /// @brief Tilt counter structure to manage tilt count. typedef struct relay_chn_tilt_counter_struct { uint32_t tilt_forward_count; ///< Tilt forward count. uint32_t tilt_reverse_count; ///< Tilt reverse count. } relay_chn_tilt_counter_t; /// @brief Tilt control structure to manage tilt operations. typedef struct relay_chn_tilt_control_struct { relay_chn_tilt_cmd_t cmd; ///< The tilt command in process. relay_chn_tilt_step_t step; ///< Current tilt step. relay_chn_tilt_timing_t tilt_timing; ///< Tilt timing structure. relay_chn_tilt_counter_t tilt_counter; ///< Tilt counter structure. esp_timer_handle_t tilt_timer; ///< Tilt timer handle. } relay_chn_tilt_control_t; /** * @brief Structure to hold the state and configuration of a relay channel. */ typedef struct relay_chn_type { uint8_t id; ///< The ID of the relay channel. relay_chn_state_t state; ///< The current state of the relay channel. relay_chn_run_info_t run_info; ///< Runtime information of the relay channel. relay_chn_output_t output; ///< Output configuration of the relay channel. relay_chn_cmd_t pending_cmd; ///< The command that is pending to be issued esp_timer_handle_t inertia_timer; ///< Timer to handle the opposite direction inertia time. relay_chn_tilt_control_t tilt_control; ///< Tilt control block. } relay_chn_t; static esp_err_t relay_chn_init_tilt_control(relay_chn_t *relay_chn); static esp_err_t relay_chn_tilt_init(void); static void relay_chn_tilt_count_reset(relay_chn_t *relay_chn); static esp_err_t relay_chn_dispatch_tilt_cmd(relay_chn_t *relay_chn, relay_chn_tilt_cmd_t cmd); #endif // RELAY_CHN_ENABLE_TILTING /** * @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 The list that holds references to the registered listeners. * * Uses a FreeRTOS list for safe and dynamic management of listeners. */ static List_t relay_chn_listener_list; static relay_chn_t relay_channels[RELAY_CHN_COUNT]; static esp_event_loop_handle_t relay_chn_event_loop; // Private function declarations // Event handler for the relay channel command event static void relay_chn_event_handler(void* handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data); /** * @brief Check if the provided channel ID is valid. * * @param chn_id Channel ID to check. * @return true Channel ID is valid. * @return false Channel ID is invalid. */ static bool relay_chn_is_channel_id_valid(uint8_t chn_id); /** * @brief Dispatches a relay channel command to the event loop. * * @param relay_chn The relay channel. * @param cmd The command to dispatch. */ static void relay_chn_dispatch_cmd(relay_chn_t *relay_chn, relay_chn_cmd_t cmd); /** * @brief Returns the string representation of a relay channel command. * * @param cmd The relay channel command. * @return char* The string representation of the command. */ static char *relay_chn_cmd_str(relay_chn_cmd_t cmd); /** * @brief Timer callback function for relay channel direction change inertia. * * This function is called when the opposite direction inertia timer expires. It checks if the channel * has a pending command and dispatches it if there is one. * * @param arg The channel ID of the relay channel. */ static void relay_chn_timer_cb(void* arg) { uint8_t chn_id = *(uint8_t*) arg; if (!relay_chn_is_channel_id_valid(chn_id)) { ESP_LOGE(TAG, "relay_chn_timer_cb: Invalid relay channel ID!"); return; } relay_chn_t* relay_chn = &relay_channels[chn_id]; // Does channel have a pending command? if (relay_chn->pending_cmd != RELAY_CHN_CMD_NONE) { relay_chn_dispatch_cmd(relay_chn, relay_chn->pending_cmd); relay_chn->pending_cmd = RELAY_CHN_CMD_NONE; } else { ESP_LOGE(TAG, "relay_chn_timer_cb: No pending cmd for relay channel %d!", chn_id); } } static esp_err_t relay_chn_init_timer(relay_chn_t *relay_chn) { char timer_name[32]; snprintf(timer_name, sizeof(timer_name), "relay_chn_%d_timer", relay_chn->id); esp_timer_create_args_t timer_args = { .callback = relay_chn_timer_cb, .arg = &relay_chn->id, .name = timer_name }; return esp_timer_create(&timer_args, &relay_chn->inertia_timer); } /** * @brief Check if the provided GPIO pin number is valid for the current device. * * @param gpio The GPIO pin number to check. * @return true GPIO pin number is valid. * @return false GPIO pin number is invalid. */ static bool relay_chn_is_gpio_valid(gpio_num_t gpio) { return gpio >= 0 && gpio < GPIO_PIN_COUNT; } static esp_err_t relay_chn_create_event_loop() { esp_event_loop_args_t loop_args = { .queue_size = RELAY_CHN_COUNT * 8, .task_name = "relay_chn_event_loop", .task_priority = ESP_TASKD_EVENT_PRIO - 1, .task_stack_size = 2048, .task_core_id = tskNO_AFFINITY }; esp_err_t ret = esp_event_loop_create(&loop_args, &relay_chn_event_loop); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to create event loop for relay channel"); ret = esp_event_handler_register_with(relay_chn_event_loop, RELAY_CHN_CMD_EVENT, ESP_EVENT_ANY_ID, relay_chn_event_handler, NULL); return ret; } esp_err_t relay_chn_create(const gpio_num_t* gpio_map, uint8_t gpio_count) { ESP_RETURN_ON_FALSE(gpio_map, ESP_ERR_INVALID_ARG, TAG, "gpio_map cannot be NULL"); // Check if the device's GPIOs are enough for the number of channels if (RELAY_CHN_COUNT > (GPIO_PIN_COUNT / 2)) { ESP_LOGE(TAG, "Not enough GPIOs for the number of channels!"); ESP_LOGE(TAG, "Max available num of channels: %d, requested channels: %d", GPIO_PIN_COUNT / 2, RELAY_CHN_COUNT); return ESP_ERR_INVALID_ARG; } // Check if the provided GPIOs correspond to the number of channels if (gpio_count != RELAY_CHN_COUNT * 2) { ESP_LOGE(TAG, "Invalid number of GPIOs provided: %d", gpio_count); ESP_LOGE(TAG, "Expected number of GPIOs: %d", RELAY_CHN_COUNT * 2); return ESP_ERR_INVALID_ARG; } esp_err_t ret; for (int i = 0; i < RELAY_CHN_COUNT; i++) { int gpio_index = i << 1; // gpio_index = i * 2 gpio_num_t forward_pin = gpio_map[gpio_index]; gpio_num_t reverse_pin = gpio_map[gpio_index + 1]; // Check if the GPIOs are valid if (!relay_chn_is_gpio_valid(forward_pin)) { ESP_LOGE(TAG, "Invalid GPIO pin number: %d", forward_pin); return ESP_ERR_INVALID_ARG; } if (!relay_chn_is_gpio_valid(reverse_pin)) { ESP_LOGE(TAG, "Invalid GPIO pin number: %d", reverse_pin); return ESP_ERR_INVALID_ARG; } // Check if the GPIOs are valid // Initialize the GPIOs ret = gpio_reset_pin(forward_pin); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to reset GPIO forward pin for channel %d", i); ret = gpio_set_direction(forward_pin, GPIO_MODE_OUTPUT); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set GPIO direction for forward pin for channel %d", i); ret = gpio_reset_pin(reverse_pin); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to reset GPIO reverse pin for channel %d", i); ret = gpio_set_direction(reverse_pin, GPIO_MODE_OUTPUT); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set GPIO direction for reverse pin for channel %d", i); // Initialize the GPIOs // Initialize the relay channel relay_chn_t* relay_chn = &relay_channels[i]; relay_chn->id = i; relay_chn->output.forward_pin = forward_pin; relay_chn->output.reverse_pin = reverse_pin; relay_chn->output.direction = RELAY_CHN_DIRECTION_DEFAULT; relay_chn->state = RELAY_CHN_STATE_FREE; relay_chn->pending_cmd = RELAY_CHN_CMD_NONE; relay_chn->run_info.last_run_cmd = RELAY_CHN_CMD_NONE; ret = relay_chn_init_timer(relay_chn); // Create direction change inertia timer ESP_RETURN_ON_ERROR(ret, TAG, "Failed to create relay channel timer for channel %d", i); #if RELAY_CHN_ENABLE_TILTING == 1 ret = relay_chn_init_tilt_control(relay_chn); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize tilt control for channel %d", i); #endif } // Create relay channel command event loop ret = relay_chn_create_event_loop(); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to create relay channel event loop"); #if RELAY_CHN_ENABLE_TILTING == 1 // Must call after the event loop is initialized ret = relay_chn_tilt_init(); // Initialize tilt feature ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize tilt feature"); #endif // Init the state listener list vListInitialise(&relay_chn_listener_list); return ret; } void relay_chn_destroy(void) { // Destroy the event loop esp_event_loop_delete(relay_chn_event_loop); relay_chn_event_loop = NULL; // 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); } // Destroy the timers and reset GPIOs for (int i = 0; i < RELAY_CHN_COUNT; i++) { relay_chn_t* relay_chn = &relay_channels[i]; if (relay_chn->inertia_timer != NULL) { esp_timer_delete(relay_chn->inertia_timer); relay_chn->inertia_timer = NULL; } #if RELAY_CHN_ENABLE_TILTING == 1 if (relay_chn->tilt_control.tilt_timer != NULL) { esp_timer_delete(relay_chn->tilt_control.tilt_timer); relay_chn->tilt_control.tilt_timer = NULL; } #endif // RELAY_CHN_ENABLE_TILTING gpio_reset_pin(relay_chn->output.forward_pin); gpio_reset_pin(relay_chn->output.reverse_pin); } } /** * @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); } } /** * @brief Check channel ID validity * * @param chn_id Channel ID to check * @return true If channel is valid * @return false If channel is invalid */ static bool relay_chn_is_channel_id_valid(uint8_t chn_id) { bool valid = (chn_id < RELAY_CHN_COUNT) || chn_id == RELAY_CHN_ID_ALL; if (!valid) { ESP_LOGE(TAG, "Invalid channel ID: %d", chn_id); } return valid; } // Dispatch relay channel command to its event loop static void relay_chn_dispatch_cmd(relay_chn_t *relay_chn, relay_chn_cmd_t cmd) { if (cmd == RELAY_CHN_CMD_NONE) { return; } esp_event_post_to(relay_chn_event_loop, RELAY_CHN_CMD_EVENT, cmd, &relay_chn->id, sizeof(relay_chn->id), portMAX_DELAY); #if RELAY_CHN_ENABLE_TILTING == 1 // Reset the tilt counter when the command is either FORWARD or REVERSE if (cmd == RELAY_CHN_CMD_FORWARD || cmd == RELAY_CHN_CMD_REVERSE) { relay_chn_tilt_count_reset(relay_chn); } #endif } static 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); if (ret == ESP_ERR_INVALID_STATE) { // This timer is already running, stop the timer first ret = esp_timer_stop(esp_timer); if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { return ret; } ret = esp_timer_start_once(esp_timer, time_ms * 1000); } return ret; } static void relay_chn_update_state(relay_chn_t *relay_chn, relay_chn_state_t new_state) { relay_chn_state_t old_state = relay_chn->state; // Only update and notify if the state has actually changed. if (old_state == new_state) { return; } relay_chn->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(relay_chn->id, old_state, new_state); } } } /** * @brief The command issuer function. * * This function is the deciding logic for issuing a command to a relay channel. It evaluates * the current state of the channel before issuing the command. Then it decides whether to run * the command immediately or wait for the opposite inertia time. * * The STOP command is an exception, it is always run immediately since it is safe in any case. * * Another special consideration is the FLIP command. If the channel is running, the FLIP command * is issued after the channel is stopped. If the channel is stopped, the FLIP command is issued * immediately. * * @param relay_chn The relay channel to issue the command to. * @param cmd The command to issue. */ static void relay_chn_issue_cmd(relay_chn_t* relay_chn, relay_chn_cmd_t cmd) { if (cmd == RELAY_CHN_CMD_NONE) { return; } if (cmd == RELAY_CHN_CMD_STOP) { if (relay_chn->state == RELAY_CHN_STATE_STOPPED) { return; // Do nothing if already stopped } // If the command is STOP, issue it immediately relay_chn_dispatch_cmd(relay_chn, cmd); return; } // Evaluate the channel's next move depending on its status switch (relay_chn->state) { case RELAY_CHN_STATE_FREE: // If the channel is free, run the command immediately relay_chn_dispatch_cmd(relay_chn, cmd); break; case RELAY_CHN_STATE_FORWARD_PENDING: case RELAY_CHN_STATE_REVERSE_PENDING: // The channel is already waiting for the opposite inertia time, // so do nothing unless the command is STOP if (cmd == RELAY_CHN_CMD_STOP) { relay_chn_dispatch_cmd(relay_chn, cmd); } break; case RELAY_CHN_STATE_STOPPED: if (relay_chn->run_info.last_run_cmd == cmd || relay_chn->run_info.last_run_cmd == RELAY_CHN_CMD_NONE) { // Since the state is STOPPED, the inertia timer should be running and must be invalidated // with the pending FREE command esp_timer_stop(relay_chn->inertia_timer); relay_chn->pending_cmd = RELAY_CHN_CMD_NONE; // If this is the first run or the last run command is the same as the current command, // run the command immediately relay_chn_dispatch_cmd(relay_chn, cmd); } else { // If the last run command is different from the current command, calculate the time passed // since the last run command stopped and decide whether to run the command immediately or wait uint32_t inertia_time_passed_ms = (uint32_t) (esp_timer_get_time() / 1000) - relay_chn->run_info.last_run_cmd_time_ms; uint32_t inertia_time_ms = RELAY_CHN_OPPOSITE_INERTIA_MS - inertia_time_passed_ms; if (inertia_time_ms > 0) { relay_chn->pending_cmd = cmd; relay_chn_state_t new_state = cmd == RELAY_CHN_CMD_FORWARD ? RELAY_CHN_STATE_FORWARD_PENDING : RELAY_CHN_STATE_REVERSE_PENDING; relay_chn_update_state(relay_chn, new_state); // If the time passed is less than the opposite inertia time, wait for the remaining time relay_chn_start_esp_timer_once(relay_chn->inertia_timer, inertia_time_ms); } else { // If the time passed is more than the opposite inertia time, run the command immediately relay_chn_dispatch_cmd(relay_chn, cmd); } } break; case RELAY_CHN_STATE_FORWARD: case RELAY_CHN_STATE_REVERSE: if (cmd == RELAY_CHN_CMD_FLIP) { // If the command is FLIP, stop the running channel first, then issue the FLIP command relay_chn_dispatch_cmd(relay_chn, RELAY_CHN_CMD_STOP); relay_chn_dispatch_cmd(relay_chn, cmd); return; } if (relay_chn->run_info.last_run_cmd == cmd) { // If the last run command is the same as the current command, do nothing return; } // Stop the channel first before the schedule relay_chn_dispatch_cmd(relay_chn, RELAY_CHN_CMD_STOP); // If the last run command is different from the current command, wait for the opposite inertia time relay_chn->pending_cmd = cmd; relay_chn_state_t new_state = cmd == RELAY_CHN_CMD_FORWARD ? RELAY_CHN_STATE_FORWARD_PENDING : RELAY_CHN_STATE_REVERSE_PENDING; relay_chn_update_state(relay_chn, new_state); relay_chn_start_esp_timer_once(relay_chn->inertia_timer, RELAY_CHN_OPPOSITE_INERTIA_MS); break; #if RELAY_CHN_ENABLE_TILTING == 1 case RELAY_CHN_STATE_TILT_FORWARD: // Terminate tilting first relay_chn_dispatch_tilt_cmd(relay_chn, RELAY_CHN_TILT_CMD_STOP); if (cmd == RELAY_CHN_CMD_FORWARD) { // Schedule for running forward relay_chn->pending_cmd = cmd; relay_chn_update_state(relay_chn, RELAY_CHN_STATE_FORWARD_PENDING); relay_chn_start_esp_timer_once(relay_chn->inertia_timer, RELAY_CHN_OPPOSITE_INERTIA_MS); } else if (cmd == RELAY_CHN_CMD_REVERSE) { // Run directly since it is the same direction relay_chn_dispatch_cmd(relay_chn, cmd); relay_chn_update_state(relay_chn, RELAY_CHN_STATE_REVERSE); } break; case RELAY_CHN_STATE_TILT_REVERSE: // Terminate tilting first relay_chn_dispatch_tilt_cmd(relay_chn, RELAY_CHN_TILT_CMD_STOP); if (cmd == RELAY_CHN_CMD_FORWARD) { // Run directly since it is the same direction relay_chn_dispatch_cmd(relay_chn, cmd); relay_chn_update_state(relay_chn, RELAY_CHN_STATE_FORWARD); } else if (cmd == RELAY_CHN_CMD_REVERSE) { // Schedule for running reverse relay_chn->pending_cmd = cmd; relay_chn_update_state(relay_chn, RELAY_CHN_STATE_REVERSE_PENDING); relay_chn_start_esp_timer_once(relay_chn->inertia_timer, RELAY_CHN_OPPOSITE_INERTIA_MS); } break; #endif default: ESP_LOGD(TAG, "relay_chn_evaluate: Unknown relay channel state!"); } } /* relay_chn APIs */ relay_chn_state_t relay_chn_get_state(uint8_t chn_id) { if (!relay_chn_is_channel_id_valid(chn_id) || chn_id == RELAY_CHN_ID_ALL) { return RELAY_CHN_STATE_UNDEFINED; } return relay_channels[chn_id].state; } char *relay_chn_get_state_str(uint8_t chn_id) { if (!relay_chn_is_channel_id_valid(chn_id) || chn_id == RELAY_CHN_ID_ALL) { return relay_chn_state_str(RELAY_CHN_STATE_UNDEFINED); } return relay_chn_state_str(relay_channels[chn_id].state); } static void relay_chn_issue_cmd_on_all_channels(relay_chn_cmd_t cmd) { for (int i = 0; i < RELAY_CHN_COUNT; i++) { relay_chn_issue_cmd(&relay_channels[i], cmd); } } void relay_chn_run_forward(uint8_t chn_id) { if (!relay_chn_is_channel_id_valid(chn_id)) return; if (chn_id == RELAY_CHN_ID_ALL) { relay_chn_issue_cmd_on_all_channels(RELAY_CHN_CMD_FORWARD); return; } relay_chn_t* relay_chn = &relay_channels[chn_id]; relay_chn_issue_cmd(relay_chn, RELAY_CHN_CMD_FORWARD); } void relay_chn_run_reverse(uint8_t chn_id) { if (!relay_chn_is_channel_id_valid(chn_id)) return; if (chn_id == RELAY_CHN_ID_ALL) { relay_chn_issue_cmd_on_all_channels(RELAY_CHN_CMD_REVERSE); return; } relay_chn_t* relay_chn = &relay_channels[chn_id]; relay_chn_issue_cmd(relay_chn, RELAY_CHN_CMD_REVERSE); } void relay_chn_stop(uint8_t chn_id) { if (!relay_chn_is_channel_id_valid(chn_id)) return; if (chn_id == RELAY_CHN_ID_ALL) { relay_chn_issue_cmd_on_all_channels(RELAY_CHN_CMD_STOP); return; } relay_chn_t* relay_chn = &relay_channels[chn_id]; relay_chn_issue_cmd(relay_chn, RELAY_CHN_CMD_STOP); } void relay_chn_flip_direction(uint8_t chn_id) { if (!relay_chn_is_channel_id_valid(chn_id)) return; if (chn_id == RELAY_CHN_ID_ALL) { relay_chn_issue_cmd_on_all_channels(RELAY_CHN_CMD_FLIP); return; } relay_chn_t* relay_chn = &relay_channels[chn_id]; relay_chn_issue_cmd(relay_chn, RELAY_CHN_CMD_FLIP); } relay_chn_direction_t relay_chn_get_direction(uint8_t chn_id) { if (!relay_chn_is_channel_id_valid(chn_id)) { return RELAY_CHN_DIRECTION_DEFAULT; } return relay_channels[chn_id].output.direction; } /* relay_chn APIs */ static esp_err_t relay_chn_output_stop(relay_chn_t *relay_chn) { esp_err_t ret; ret = gpio_set_level(relay_chn->output.forward_pin, 0); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set forward pin to LOW for relay channel #%d", relay_chn->id); ret = gpio_set_level(relay_chn->output.reverse_pin, 0); return ret; } static esp_err_t relay_chn_output_forward(relay_chn_t *relay_chn) { esp_err_t ret; ret = gpio_set_level(relay_chn->output.forward_pin, 1); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set forward pin to HIGH for relay channel #%d", relay_chn->id); ret = gpio_set_level(relay_chn->output.reverse_pin, 0); return ret; } static esp_err_t relay_chn_output_reverse(relay_chn_t *relay_chn) { esp_err_t ret; ret = gpio_set_level(relay_chn->output.forward_pin, 0); ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set forward pin to LOW for relay channel #%d", relay_chn->id); ret = gpio_set_level(relay_chn->output.reverse_pin, 1); return ret; } static void relay_chn_execute_stop(relay_chn_t *relay_chn) { if (relay_chn_output_stop(relay_chn) != ESP_OK) { ESP_LOGE(TAG, "relay_chn_execute_stop: Failed to output stop for relay channel #%d!", relay_chn->id); } relay_chn_state_t previous_state = relay_chn->state; relay_chn_update_state(relay_chn, RELAY_CHN_STATE_STOPPED); // If there is any pending command, cancel it since the STOP command is issued right after it relay_chn->pending_cmd = RELAY_CHN_CMD_NONE; // Invalidate the channel's timer if it is active esp_timer_stop(relay_chn->inertia_timer); // Save the last run time only if the previous state was either STATE FORWARD // or STATE_REVERSE. Then schedule a free command. if (previous_state == RELAY_CHN_STATE_FORWARD || previous_state == RELAY_CHN_STATE_REVERSE) { // Record the command's last run time relay_chn->run_info.last_run_cmd_time_ms = esp_timer_get_time() / 1000; // Schedule a free command for the channel relay_chn->pending_cmd = RELAY_CHN_CMD_FREE; relay_chn_start_esp_timer_once(relay_chn->inertia_timer, RELAY_CHN_OPPOSITE_INERTIA_MS); } else { // If the channel was not running one of the run or fwd, issue a free command immediately relay_chn_dispatch_cmd(relay_chn, RELAY_CHN_CMD_FREE); } } static void relay_chn_execute_forward(relay_chn_t *relay_chn) { if (relay_chn_output_forward(relay_chn) != ESP_OK) { ESP_LOGE(TAG, "relay_chn_execute_forward: Failed to output forward for relay channel #%d!", relay_chn->id); return; } relay_chn->run_info.last_run_cmd = RELAY_CHN_CMD_FORWARD; relay_chn_update_state(relay_chn, RELAY_CHN_STATE_FORWARD); } static void relay_chn_execute_reverse(relay_chn_t *relay_chn) { if (relay_chn_output_reverse(relay_chn) != ESP_OK) { ESP_LOGE(TAG, "relay_chn_execute_reverse: Failed to output reverse for relay channel #%d!", relay_chn->id); return; } relay_chn->run_info.last_run_cmd = RELAY_CHN_CMD_REVERSE; relay_chn_update_state(relay_chn, RELAY_CHN_STATE_REVERSE); } static void relay_chn_execute_flip(relay_chn_t *relay_chn) { // Flip the output GPIO pins gpio_num_t temp = relay_chn->output.forward_pin; relay_chn->output.forward_pin = relay_chn->output.reverse_pin; relay_chn->output.reverse_pin = temp; // Flip the direction relay_chn->output.direction = (relay_chn->output.direction == RELAY_CHN_DIRECTION_DEFAULT) ? RELAY_CHN_DIRECTION_FLIPPED : RELAY_CHN_DIRECTION_DEFAULT; // Set an inertia on the channel to prevent any immediate movement relay_chn->pending_cmd = RELAY_CHN_CMD_FREE; relay_chn_start_esp_timer_once(relay_chn->inertia_timer, RELAY_CHN_OPPOSITE_INERTIA_MS); } void relay_chn_execute_free(relay_chn_t *relay_chn) { relay_chn->pending_cmd = RELAY_CHN_CMD_NONE; // Invalidate the channel's timer if it is active esp_timer_stop(relay_chn->inertia_timer); relay_chn_update_state(relay_chn, RELAY_CHN_STATE_FREE); } static void relay_chn_event_handler(void* handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { uint8_t chn_id = *(uint8_t*) event_data; if (!relay_chn_is_channel_id_valid(chn_id)) { return; } relay_chn_t* relay_chn = &relay_channels[chn_id]; ESP_LOGD(TAG, "relay_chn_event_handler: Channel %d, Command: %s", relay_chn->id, relay_chn_cmd_str(event_id)); switch (event_id) { case RELAY_CHN_CMD_STOP: relay_chn_execute_stop(relay_chn); break; case RELAY_CHN_CMD_FORWARD: relay_chn_execute_forward(relay_chn); break; case RELAY_CHN_CMD_REVERSE: relay_chn_execute_reverse(relay_chn); break; case RELAY_CHN_CMD_FLIP: relay_chn_execute_flip(relay_chn); break; case RELAY_CHN_CMD_FREE: relay_chn_execute_free(relay_chn); break; default: ESP_LOGD(TAG, "Unknown relay channel command!"); } } static char *relay_chn_cmd_str(relay_chn_cmd_t cmd) { switch (cmd) { case RELAY_CHN_CMD_STOP: return "STOP"; case RELAY_CHN_CMD_FORWARD: return "FORWARD"; case RELAY_CHN_CMD_REVERSE: return "REVERSE"; case RELAY_CHN_CMD_FLIP: return "FLIP"; case RELAY_CHN_CMD_FREE: return "FREE"; default: return "UNKNOWN"; } } char *relay_chn_state_str(relay_chn_state_t state) { switch (state) { case RELAY_CHN_STATE_FREE: return "FREE"; case RELAY_CHN_STATE_STOPPED: return "STOPPED"; case RELAY_CHN_STATE_FORWARD: return "FORWARD"; case RELAY_CHN_STATE_REVERSE: return "REVERSE"; case RELAY_CHN_STATE_FORWARD_PENDING: return "FORWARD_PENDING"; case RELAY_CHN_STATE_REVERSE_PENDING: return "REVERSE_PENDING"; #if RELAY_CHN_ENABLE_TILTING == 1 case RELAY_CHN_STATE_TILT_FORWARD: return "TILT_FORWARD"; case RELAY_CHN_STATE_TILT_REVERSE: return "TILT_REVERSE"; #endif default: return "UNKNOWN"; } } #if RELAY_CHN_ENABLE_TILTING == 1 /** * @brief Dispatch a tilt command to the relay channel event loop. * * @param relay_chn The relay channel to send the command to. * @param cmd The tilt command. * @return * - ESP_OK on success. * - ESP_ERR_INVALID_ARG if the command is none. * - Other error codes on failure. */ static esp_err_t relay_chn_dispatch_tilt_cmd(relay_chn_t *relay_chn, relay_chn_tilt_cmd_t cmd) { if (cmd == RELAY_CHN_TILT_CMD_NONE) return ESP_ERR_INVALID_ARG; return esp_event_post_to(relay_chn_event_loop, RELAY_CHN_TILT_CMD_EVENT_BASE, cmd, &relay_chn->id, sizeof(relay_chn->id), portMAX_DELAY); } /** * @brief Get the required timing before tilting depending on the last run. * * @param relay_chn the relay channel. * @param cmd The tilt command. * @return The time that is required in ms. */ static uint32_t relay_chn_get_required_timing_before_tilting(relay_chn_t *relay_chn, relay_chn_tilt_cmd_t cmd) { if (cmd == RELAY_CHN_TILT_CMD_FORWARD && relay_chn->run_info.last_run_cmd == RELAY_CHN_CMD_REVERSE) return 0; else if (cmd == RELAY_CHN_TILT_CMD_REVERSE && relay_chn->run_info.last_run_cmd == RELAY_CHN_CMD_FORWARD) return 0; uint32_t inertia_time_passed_ms = (uint32_t) (esp_timer_get_time() / 1000) - relay_chn->run_info.last_run_cmd_time_ms; return RELAY_CHN_OPPOSITE_INERTIA_MS - inertia_time_passed_ms; } /** * @brief Issue a tilt command to a specific relay channel. * * @param chn_id The channel ID. * @param cmd The tilt command. */ static void relay_chn_issue_tilt_cmd(uint8_t chn_id, relay_chn_tilt_cmd_t cmd) { relay_chn_t* relay_chn = &relay_channels[chn_id]; if (relay_chn->run_info.last_run_cmd == RELAY_CHN_CMD_NONE) { // Do not tilt if the channel hasn't been run before ESP_LOGD(TAG, "relay_chn_issue_tilt_cmd: Tilt will not be executed since the channel hasn't been run yet"); return; } if (relay_chn->tilt_control.cmd == cmd) { ESP_LOGD(TAG, "relay_chn_issue_tilt_cmd: There is already a tilt command in progress!"); return; } // Set the command that will be processed relay_chn->tilt_control.cmd = cmd; switch (relay_chn->state) { case RELAY_CHN_STATE_FREE: // Relay channel is free, tilt can be issued immediately relay_chn_dispatch_tilt_cmd(relay_chn, cmd); break; case RELAY_CHN_STATE_FORWARD_PENDING: case RELAY_CHN_STATE_REVERSE_PENDING: // Issue a stop command first so that the timer and pending cmd get cleared relay_chn_dispatch_cmd(relay_chn, RELAY_CHN_CMD_STOP); // break not put intentionally case RELAY_CHN_STATE_STOPPED: { // Check if channel needs timing before tilting uint32_t req_timing_ms = relay_chn_get_required_timing_before_tilting(relay_chn, cmd); if (req_timing_ms == 0) { relay_chn_dispatch_tilt_cmd(relay_chn, cmd); } else { // Channel needs timing before running tilting action, schedule it relay_chn->tilt_control.step = RELAY_CHN_TILT_STEP_PENDING; relay_chn_start_esp_timer_once(relay_chn->tilt_control.tilt_timer, req_timing_ms); } break; } case RELAY_CHN_STATE_FORWARD: if (cmd == RELAY_CHN_TILT_CMD_FORWARD) { // Stop the running channel first relay_chn_dispatch_cmd(relay_chn, RELAY_CHN_CMD_STOP); // Schedule for tilting relay_chn->tilt_control.step = RELAY_CHN_TILT_STEP_PENDING; relay_chn_start_esp_timer_once(relay_chn->tilt_control.tilt_timer, RELAY_CHN_OPPOSITE_INERTIA_MS); } else if (cmd == RELAY_CHN_TILT_CMD_REVERSE) { // Stop the running channel first relay_chn_dispatch_cmd(relay_chn, RELAY_CHN_CMD_STOP); // If the tilt cmd is TILT_REVERSE then dispatch it immediately relay_chn_dispatch_tilt_cmd(relay_chn, cmd); } break; case RELAY_CHN_STATE_REVERSE: if (cmd == RELAY_CHN_TILT_CMD_REVERSE) { // Stop the running channel first relay_chn_dispatch_cmd(relay_chn, RELAY_CHN_CMD_STOP); // Schedule for tilting relay_chn->tilt_control.step = RELAY_CHN_TILT_STEP_PENDING; relay_chn_start_esp_timer_once(relay_chn->tilt_control.tilt_timer, RELAY_CHN_OPPOSITE_INERTIA_MS); } else if (cmd == RELAY_CHN_TILT_CMD_FORWARD) { // Stop the running channel first relay_chn_dispatch_cmd(relay_chn, RELAY_CHN_CMD_STOP); // If the tilt cmd is TILT_FORWARD then dispatch it immediately relay_chn_dispatch_tilt_cmd(relay_chn, cmd); } break; default: ESP_LOGD(TAG, "relay_chn_issue_tilt_cmd: Unexpected relay channel state: %s!", relay_chn_state_str(relay_chn->state)); } } static void relay_chn_issue_tilt_cmd_on_all_channels(relay_chn_tilt_cmd_t cmd) { for (int i = 0; i < RELAY_CHN_COUNT; i++) { relay_chn_issue_tilt_cmd(i, cmd); } } static void relay_chn_issue_tilt_auto(uint8_t chn_id) { relay_chn_t* relay_chn = &relay_channels[chn_id]; if (relay_chn->run_info.last_run_cmd == RELAY_CHN_CMD_FORWARD || relay_chn->state == RELAY_CHN_STATE_FORWARD) { relay_chn_issue_tilt_cmd(chn_id, RELAY_CHN_TILT_CMD_FORWARD); } else if (relay_chn->run_info.last_run_cmd == RELAY_CHN_CMD_REVERSE || relay_chn->state == RELAY_CHN_STATE_REVERSE) { relay_chn_issue_tilt_cmd(chn_id, RELAY_CHN_TILT_CMD_REVERSE); } } void relay_chn_tilt_auto(uint8_t chn_id) { if (!relay_chn_is_channel_id_valid(chn_id)) { return; } // Execute for all channels if (chn_id == RELAY_CHN_ID_ALL) { for (int i = 0; i < RELAY_CHN_COUNT; i++) { relay_chn_issue_tilt_auto(i); } return; } // Execute for a single channel else relay_chn_issue_tilt_auto(chn_id); } void relay_chn_tilt_forward(uint8_t chn_id) { if (!relay_chn_is_channel_id_valid(chn_id)) { return; } if (chn_id == RELAY_CHN_ID_ALL) relay_chn_issue_tilt_cmd_on_all_channels(RELAY_CHN_TILT_CMD_FORWARD); else relay_chn_issue_tilt_cmd(chn_id, RELAY_CHN_TILT_CMD_FORWARD); } void relay_chn_tilt_reverse(uint8_t chn_id) { if (!relay_chn_is_channel_id_valid(chn_id)) { return; } if (chn_id == RELAY_CHN_ID_ALL) relay_chn_issue_tilt_cmd_on_all_channels(RELAY_CHN_TILT_CMD_REVERSE); else relay_chn_issue_tilt_cmd(chn_id, RELAY_CHN_TILT_CMD_REVERSE); } static void _relay_chn_tilt_stop(uint8_t chn_id) { relay_chn_t* relay_chn = &relay_channels[chn_id]; if (relay_chn->tilt_control.cmd != RELAY_CHN_TILT_CMD_NONE) { esp_event_post_to(relay_chn_event_loop, RELAY_CHN_TILT_CMD_EVENT_BASE, RELAY_CHN_TILT_CMD_STOP, &relay_chn->id, sizeof(relay_chn->id), portMAX_DELAY); } } void relay_chn_tilt_stop(uint8_t chn_id) { if (!relay_chn_is_channel_id_valid(chn_id)) { return; } if (chn_id == RELAY_CHN_ID_ALL) { for (int i = 0; i < RELAY_CHN_COUNT; i++) { _relay_chn_tilt_stop(i); } } else { _relay_chn_tilt_stop(chn_id); } } static void relay_chn_set_tilt_timing_values(relay_chn_tilt_timing_t *tilt_timing, uint8_t sensitivity, uint32_t run_time_ms, uint32_t pause_time_ms) { tilt_timing->sensitivity = sensitivity; tilt_timing->move_time_ms = run_time_ms; tilt_timing->pause_time_ms = pause_time_ms; } static void _relay_chn_tilt_sensitivity_set(relay_chn_t *relay_chn, uint8_t sensitivity) { if (sensitivity >= 100) { relay_chn_set_tilt_timing_values(&relay_chn->tilt_control.tilt_timing, 100, RELAY_CHN_TILT_RUN_MAX_MS, RELAY_CHN_TILT_PAUSE_MAX_MS); } else if (sensitivity == 0) { relay_chn_set_tilt_timing_values(&relay_chn->tilt_control.tilt_timing, 0, RELAY_CHN_TILT_RUN_MIN_MS, RELAY_CHN_TILT_PAUSE_MIN_MS); } else { // Compute the new timing values from the sensitivity percent value by using linear interpolation uint32_t tilt_run_time_ms = 0, tilt_pause_time_ms = 0; tilt_run_time_ms = RELAY_CHN_TILT_RUN_MIN_MS + (sensitivity * (RELAY_CHN_TILT_RUN_MAX_MS - RELAY_CHN_TILT_RUN_MIN_MS) / 100); tilt_pause_time_ms = RELAY_CHN_TILT_PAUSE_MIN_MS + (sensitivity * (RELAY_CHN_TILT_PAUSE_MAX_MS - RELAY_CHN_TILT_PAUSE_MIN_MS) / 100); relay_chn_set_tilt_timing_values(&relay_chn->tilt_control.tilt_timing, sensitivity, tilt_run_time_ms, tilt_pause_time_ms); } } void relay_chn_tilt_sensitivity_set(uint8_t chn_id, uint8_t sensitivity) { if (!relay_chn_is_channel_id_valid(chn_id)) { return; } if (chn_id == RELAY_CHN_ID_ALL) { for (int i = 0; i < RELAY_CHN_COUNT; i++) { _relay_chn_tilt_sensitivity_set(&relay_channels[i], sensitivity); } } else { _relay_chn_tilt_sensitivity_set(&relay_channels[chn_id], sensitivity); } } esp_err_t relay_chn_tilt_sensitivity_get(uint8_t chn_id, uint8_t *sensitivity, size_t length) { if (!relay_chn_is_channel_id_valid(chn_id)) { return ESP_ERR_INVALID_ARG; } if (sensitivity == NULL) { ESP_LOGD(TAG, "relay_chn_tilt_sensitivity_get: sensitivity is NULL"); return ESP_ERR_INVALID_ARG; } if (chn_id == RELAY_CHN_ID_ALL) { if (length < RELAY_CHN_COUNT) { ESP_LOGD(TAG, "relay_chn_tilt_sensitivity_get: length is too short to store all sensitivity values"); return ESP_ERR_INVALID_ARG; } for (int i = 0; i < RELAY_CHN_COUNT; i++) { sensitivity[i] = relay_channels[i].tilt_control.tilt_timing.sensitivity; } return ESP_OK; } *sensitivity = relay_channels[chn_id].tilt_control.tilt_timing.sensitivity; return ESP_OK; } static void relay_chn_tilt_count_reset(relay_chn_t *relay_chn) { relay_chn->tilt_control.tilt_counter.tilt_forward_count = 0; relay_chn->tilt_control.tilt_counter.tilt_reverse_count = 0; } /** * @brief Update tilt count automatically and return the current value. * * This helper function updates the relevant tilt count depending on the * last run info and helps the tilt module in deciding whether the requested * tilt should execute or not. * This is useful to control reverse tilting particularly. For example: * - If the channel's last run was FORWARD and a TILT_FORWARD is requested, * then the tilt counter will count up on the * relay_chn_tilt_counter_struct::tilt_forward_count and the function will * return the actual count. * - If the channel's last run was FORWARD and a TILT_REVERSE is requested, * then the relay_chn_tilt_counter_struct::tilt_forward_count will be checked * against zero first, and then it will count down and return the actual count * if it is greater than 0, else the function will return 0. * - If the tilt command is irrelevant then the function will return 0. * - If the last run is irrelevant then the function will return 0. * * @param relay_chn The relay channel handle. * @return uint32_t The actual value of the relevant counter. * @return 0 if: * - related counter is already 0. * - tilt command is irrelevant. * - last run info is irrelevant. */ static uint32_t relay_chn_tilt_count_update(relay_chn_t *relay_chn) { if (relay_chn->run_info.last_run_cmd == RELAY_CHN_CMD_FORWARD) { if (relay_chn->tilt_control.cmd == RELAY_CHN_TILT_CMD_FORWARD) { return ++relay_chn->tilt_control.tilt_counter.tilt_forward_count; } else if (relay_chn->tilt_control.cmd == RELAY_CHN_TILT_CMD_REVERSE) { if (relay_chn->tilt_control.tilt_counter.tilt_forward_count > 0) { --relay_chn->tilt_control.tilt_counter.tilt_forward_count; // Still should do one more move, return non-zero value return 1; } else return 0; } else { relay_chn_tilt_count_reset(relay_chn); return 0; } } else if (relay_chn->run_info.last_run_cmd == RELAY_CHN_CMD_REVERSE) { if (relay_chn->tilt_control.cmd == RELAY_CHN_TILT_CMD_REVERSE) { return ++relay_chn->tilt_control.tilt_counter.tilt_reverse_count; } else if (relay_chn->tilt_control.cmd == RELAY_CHN_TILT_CMD_FORWARD) { if (relay_chn->tilt_control.tilt_counter.tilt_reverse_count > 0) { --relay_chn->tilt_control.tilt_counter.tilt_reverse_count; // Still should do one more move, return non-zero value return 1; } else return 0; } else { relay_chn_tilt_count_reset(relay_chn); return 0; } } return 0; } static void relay_chn_tilt_execute_tilt_stop(relay_chn_t *relay_chn) { // Stop the channel's timer if active esp_timer_stop(relay_chn->tilt_control.tilt_timer); // Invalidate tilt cmd and step relay_chn->tilt_control.cmd = RELAY_CHN_TILT_CMD_NONE; relay_chn->tilt_control.step = RELAY_CHN_TILT_STEP_NONE; // Stop the channel if (relay_chn_output_stop(relay_chn) != ESP_OK) { ESP_LOGE(TAG, "relay_chn_tilt_execute_tilt_stop: Failed to output stop for relay channel #%d!", relay_chn->id); } relay_chn_dispatch_cmd(relay_chn, RELAY_CHN_CMD_STOP); } static void relay_chn_tilt_execute_tilt_forward(relay_chn_t *relay_chn) { if (relay_chn_output_reverse(relay_chn) != ESP_OK) { ESP_LOGE(TAG, "relay_chn_tilt_execute_tilt_forward: Failed to output reverse for relay channel #%d!", relay_chn->id); // Stop tilting because of the error relay_chn_dispatch_tilt_cmd(relay_chn, RELAY_CHN_TILT_CMD_STOP); return; } // Set the move time timer relay_chn_start_esp_timer_once(relay_chn->tilt_control.tilt_timer, relay_chn->tilt_control.tilt_timing.move_time_ms); // Set to pause step relay_chn->tilt_control.step = RELAY_CHN_TILT_STEP_PAUSE; } static void relay_chn_tilt_execute_tilt_reverse(relay_chn_t *relay_chn) { if (relay_chn_output_forward(relay_chn) != ESP_OK) { ESP_LOGE(TAG, "relay_chn_tilt_execute_tilt_reverse: Failed to output forward for relay channel #%d!", relay_chn->id); // Stop tilting because of the error relay_chn_dispatch_tilt_cmd(relay_chn, RELAY_CHN_TILT_CMD_STOP); return; } // Set the move time timer relay_chn_start_esp_timer_once(relay_chn->tilt_control.tilt_timer, relay_chn->tilt_control.tilt_timing.move_time_ms); // Set to pause step relay_chn->tilt_control.step = RELAY_CHN_TILT_STEP_PAUSE; } static void relay_chn_tilt_execute_tilt_pause(relay_chn_t *relay_chn) { // Pause the channel if (relay_chn_output_stop(relay_chn) != ESP_OK) { ESP_LOGE(TAG, "relay_chn_tilt_execute_tilt_stop: Failed to output stop for relay channel #%d!", relay_chn->id); // Stop tilting because of the error relay_chn_dispatch_tilt_cmd(relay_chn, RELAY_CHN_TILT_CMD_STOP); return; } // Update the tilt counter before the next move and expect the return value to be greater than 0 if (relay_chn_tilt_count_update(relay_chn) == 0) { ESP_LOGD(TAG, "relay_chn_tilt_execute_tilt_stop: Relay channel cannot tilt anymore"); // Stop tilting since the tilting limit has been reached relay_chn_dispatch_tilt_cmd(relay_chn, RELAY_CHN_TILT_CMD_STOP); return; } // Set the pause time timer relay_chn_start_esp_timer_once(relay_chn->tilt_control.tilt_timer, relay_chn->tilt_control.tilt_timing.pause_time_ms); // Set to move step relay_chn->tilt_control.step = RELAY_CHN_TILT_STEP_MOVE; } static void relay_chn_tilt_event_handler(void *handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { uint8_t chn_id = *(uint8_t*) event_data; if (!relay_chn_is_channel_id_valid(chn_id)) { return; } relay_chn_t* relay_chn = &relay_channels[chn_id]; ESP_LOGD(TAG, "relay_chn_event_handler: Channel %d, Command: %s", relay_chn->id, relay_chn_cmd_str(event_id)); switch(event_id) { case RELAY_CHN_TILT_CMD_STOP: relay_chn_tilt_execute_tilt_stop(relay_chn); break; case RELAY_CHN_TILT_CMD_FORWARD: relay_chn_tilt_execute_tilt_forward(relay_chn); // Update channel state relay_chn_update_state(relay_chn, RELAY_CHN_STATE_TILT_FORWARD); break; case RELAY_CHN_TILT_CMD_REVERSE: relay_chn_tilt_execute_tilt_reverse(relay_chn); // Update channel state relay_chn_update_state(relay_chn, RELAY_CHN_STATE_TILT_REVERSE); break; default: ESP_LOGW(TAG, "Unexpected relay channel tilt command: %ld!", event_id); } } // Timer callback for the relay_chn_tilt_control_t::tilt_timer static void relay_chn_tilt_timer_cb(void *arg) { uint8_t chn_id = *(uint8_t*) arg; if (!relay_chn_is_channel_id_valid(chn_id)) { ESP_LOGE(TAG, "relay_chn_tilt_timer_cb: Invalid relay channel ID!"); return; } relay_chn_t* relay_chn = &relay_channels[chn_id]; switch (relay_chn->tilt_control.step) { case RELAY_CHN_TILT_STEP_MOVE: if (relay_chn->tilt_control.cmd == RELAY_CHN_TILT_CMD_FORWARD) { relay_chn_tilt_execute_tilt_forward(relay_chn); } else if (relay_chn->tilt_control.cmd == RELAY_CHN_TILT_CMD_REVERSE) { relay_chn_tilt_execute_tilt_reverse(relay_chn); } break; case RELAY_CHN_TILT_STEP_PAUSE: relay_chn_tilt_execute_tilt_pause(relay_chn); break; case RELAY_CHN_TILT_STEP_PENDING: // Just dispatch the pending tilt command relay_chn_dispatch_tilt_cmd(relay_chn, relay_chn->tilt_control.cmd); break; default: break; } } static esp_err_t relay_chn_init_tilt_control(relay_chn_t *relay_chn) { relay_chn_tilt_control_t *tilt_control = &relay_chn->tilt_control; tilt_control->cmd = RELAY_CHN_TILT_CMD_NONE; tilt_control->step = RELAY_CHN_TILT_STEP_NONE; tilt_control->tilt_timing.sensitivity = RELAY_CHN_TILT_DEFAULT_SENSITIVITY; tilt_control->tilt_timing.move_time_ms = RELAY_CHN_TILT_DEFAULT_RUN_MS; tilt_control->tilt_timing.pause_time_ms = RELAY_CHN_TILT_DEFAULT_PAUSE_MS; relay_chn_tilt_count_reset(relay_chn); // Create tilt timer for the channel char timer_name[32]; snprintf(timer_name, sizeof(timer_name), "relay_chn_%2d_tilt_timer", relay_chn->id); esp_timer_create_args_t timer_args = { .callback = relay_chn_tilt_timer_cb, .arg = &relay_chn->id, .name = timer_name }; return esp_timer_create(&timer_args, &relay_chn->tilt_control.tilt_timer); } // Should call once from relay_chn_init static esp_err_t relay_chn_tilt_init(void) { esp_err_t ret; ret = esp_event_handler_register_with(relay_chn_event_loop, RELAY_CHN_TILT_CMD_EVENT_BASE, ESP_EVENT_ANY_ID, relay_chn_tilt_event_handler, NULL); return ret; } #endif // RELAY_CHN_ENABLE_TILTING /// @}