diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a6f9403 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "relay_chn.c" + INCLUDE_DIRS include + REQUIRES driver + PRIV_REQUIRES esp_timer esp_event) diff --git a/Kconfig b/Kconfig new file mode 100644 index 0000000..e3b236d --- /dev/null +++ b/Kconfig @@ -0,0 +1,20 @@ +menu "Relay Channel Driver Configuration" + + config RELAY_CHN_OPPOSITE_INERTIA_MS + int "Inertia time before it runs opposite direction (ms)" + range 200 1500 + default 800 + help + Time to wait after changing the direction of the output before + starting the output. This is useful for the motors or some other + mechanical actuators to allow them to stop and settle before + changing the direction. + + config RELAY_CHN_COUNT + int "Number of relay channels" + range 1 8 + default 1 + help + Number of relay channels between 1 and 8. + +endmenu \ No newline at end of file diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 0000000..f511074 --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,7 @@ +name: relay_chn +version: 0.1.0 +description: Custom component for relay channel control +dependencies: + idf: + version: ">=4.0" +# TODO: Repo ve belgelendirme bağlantılarını ekle. \ No newline at end of file diff --git a/include/relay_chn.h b/include/relay_chn.h new file mode 100644 index 0000000..c7087c5 --- /dev/null +++ b/include/relay_chn.h @@ -0,0 +1,162 @@ +#ifndef RELAY_CHN_H +#define RELAY_CHN_H +/** + * @file relay_chn.h + * + * @author + * Ismail Sahillioglu + * + * @date 2025.02.08 + * + * @defgroup relay_chn Relay Channel Controller + * @ingroup components + * @{ + * One relay channel consists of 2 output relays, hence 2 GPIO pins are required for each relay channel. + * This module provides an API to control the relay channels, specifically to drive bipolar motors. + * It also provides APIs to control the direction of the relay channel, bipolar motors in mind. + * The module also automatically manages the direction change inertia to prevent short-circuiting the motor. + * The STOP command overrides any other command and clears the pending command if any. + * + * The module internally uses a custom esp event loop to handle relay commands serially to ensure + * reliability and prevent conflict operations. Also, the esp timer is used to manage the direction change inertia. + */ + +#include "esp_err.h" +#include "driver/gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Enumeration for relay channel direction. + */ +enum relay_chn_direction_enum { + RELAY_CHN_DIRECTION_DEFAULT, ///< Default direction of the relay channel. + RELAY_CHN_DIRECTION_FLIPPED ///< Flipped direction of the relay channel. +}; + +/** + * @brief Alias for the enum type relay_chn_direction_enum. + */ +typedef enum relay_chn_direction_enum relay_chn_direction_t; + +/** + * @brief Enums that represent the state of a relay channel. + */ +enum relay_chn_state_enum { + RELAY_CHN_STATE_FREE, ///< The relay channel is free to run or execute commands. + RELAY_CHN_STATE_STOPPED, ///< The relay channel is stopped and not running. + RELAY_CHN_STATE_FORWARD, ///< The relay channel is running in the forward direction. + RELAY_CHN_STATE_REVERSE, ///< The relay channel is running in the reverse direction. + RELAY_CHN_STATE_FORWARD_PENDING, ///< The relay channel is pending to run in the forward direction. + RELAY_CHN_STATE_REVERSE_PENDING, ///< The relay channel is pending to run in the reverse direction. +}; + +/** + * @brief Alias for the enum type relay_chn_state_enum. + */ +typedef enum relay_chn_state_enum relay_chn_state_t; + + +/** + * @brief Create and initialize relay channels. + * + * This function initializes the relay channels based on the provided GPIO map. + * + * @param gpio_map Pointer to an array of GPIO numbers that correspond to the relay channels. + * @param gpio_count The number of GPIOs in the gpio_map array. + * + * @return + * - ESP_OK: Success + * - ESP_ERR_INVALID_ARG: Invalid argument + * - ESP_FAIL: General failure + */ +esp_err_t relay_chn_create(const gpio_num_t* gpio_map, uint8_t gpio_count); + +/** + * @brief Get the state of the specified relay channel. + * + * This function retrieves the current state of the relay channel identified by the given channel ID. + * + * @param chn_id The ID of the relay channel whose state is to be retrieved. + * @return The current state of the specified relay channel. + */ +relay_chn_state_t relay_chn_get_state(uint8_t chn_id); + +/** + * @brief Get the state string of the specified relay channel. + * + * This function returns a string representation of the state of the relay + * channel identified by the given channel ID. + * + * @param chn_id The ID of the relay channel whose state is to be retrieved. + * The valid range of channel IDs depends on the specific hardware + * and implementation. + * + * @return A pointer to a string representing the state of the specified relay + * channel. The returned string is managed internally and should not be + * modified or freed by the caller. + */ +char *relay_chn_get_state_str(uint8_t chn_id); + +/** + * @brief Runs the relay channel in the forward direction. + * + * This function activates the specified relay channel to run in the forward direction. + * + * @param chn_id The ID of the relay channel to be activated. + */ +void relay_chn_run_forward(uint8_t chn_id); + +/** + * @brief Runs the relay channel in reverse. + * + * This function activates the specified relay channel to run in reverse. + * + * @param chn_id The ID of the relay channel to be reversed. + */ +void relay_chn_run_reverse(uint8_t chn_id); + +/** + * @brief Stops the relay channel specified by the channel ID. + * + * This function stops the operation of the relay channel identified by the + * provided channel ID. It is typically used to turn off or disable the relay + * channel. + * + * @param chn_id The ID of the relay channel to stop. + */ +void relay_chn_stop(uint8_t chn_id); + +/** + * @brief Flips the direction of the specified relay channel. + * + * This function toggles the direction of the relay channel identified by the + * given channel ID. It is typically used to change the state of the relay + * from its current direction to the opposite direction. + * + * @param chn_id The ID of the relay channel to flip. This should be a valid + * channel ID within the range of available relay channels. + */ +void relay_chn_flip_direction(uint8_t chn_id); + +/** + * @brief Get the direction of the specified relay channel. + * + * This function retrieves the direction configuration of a relay channel + * identified by the given channel ID. + * + * @param chn_id The ID of the relay channel to query. + * @return The direction of the specified relay channel as a value of type + * relay_chn_direction_t. + */ +relay_chn_direction_t relay_chn_get_direction(uint8_t chn_id); + +#ifdef __cplusplus +} +#endif + +/// @} + +#endif // RELAY_CHN_H \ No newline at end of file diff --git a/relay_chn.c b/relay_chn.c new file mode 100644 index 0000000..cb89241 --- /dev/null +++ b/relay_chn.c @@ -0,0 +1,580 @@ +/** + * @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 "esp_err.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 "sdkconfig.h" + +// TODO: on_state change API si ekle + +#define RELAY_CHN_OPPOSITE_INERTIA_MS CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS +#define RELAY_CHN_COUNT CONFIG_RELAY_CHN_COUNT + +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*); + +/** + * @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 timer; ///< Timer to handle the opposite direction inertia time. +} relay_chn_t; + + +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->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 = 10, + .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); + 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) +{ + // 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++) { + gpio_num_t forward_pin = gpio_map[i]; + gpio_num_t reverse_pin = gpio_map[i+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); + ret |= gpio_set_direction(forward_pin, GPIO_MODE_OUTPUT); + + ret |= gpio_reset_pin(reverse_pin); + ret |= gpio_set_direction(reverse_pin, GPIO_MODE_OUTPUT); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize GPIOs relay channel %d!", i); + return ret; + } + // 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_STOPPED; + 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 + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize relay channel %d!", i); + return ret; + } + } + + // Create relay channel command event loop + ret |= relay_chn_create_event_loop(); + + return ret; +} + +/** + * @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 >= 0 && chn_id < RELAY_CHN_COUNT; + 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); +} + +static esp_err_t relay_chn_invalidate_timer(relay_chn_t *relay_chn) +{ + if (esp_timer_is_active(relay_chn->timer)) { + return esp_timer_stop(relay_chn->timer); + } + return ESP_OK; +} + +static esp_err_t relay_chn_start_timer(relay_chn_t *relay_chn, uint32_t time_ms) +{ + // Invalidate the channel's timer if it is active + relay_chn_invalidate_timer(relay_chn); + return esp_timer_start_once(relay_chn->timer, time_ms * 1000); +} + +/** + * @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) { + // If 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 = cmd == RELAY_CHN_CMD_FORWARD + ? RELAY_CHN_STATE_FORWARD_PENDING + : RELAY_CHN_STATE_REVERSE_PENDING; + // If the time passed is less than the opposite inertia time, wait for the remaining time + relay_chn_start_timer(relay_chn, 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; + } + + // 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 = cmd == RELAY_CHN_CMD_FORWARD ? RELAY_CHN_STATE_FORWARD_PENDING : RELAY_CHN_STATE_REVERSE_PENDING; + relay_chn_start_timer(relay_chn, RELAY_CHN_OPPOSITE_INERTIA_MS); + break; + + 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)) { + return RELAY_CHN_STATE_STOPPED; + } + 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)) { + return "INVALID"; + } + switch (relay_channels[chn_id].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"; + default: + return "UNKNOWN"; + } +} + +void relay_chn_run_forward(uint8_t chn_id) +{ + if (!relay_chn_is_channel_id_valid(chn_id)) 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; + + 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; + + 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; + + 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 void relay_chn_execute_stop(relay_chn_t *relay_chn) +{ + gpio_set_level(relay_chn->output.forward_pin, 0); + gpio_set_level(relay_chn->output.reverse_pin, 0); + relay_chn->state = 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 + relay_chn_invalidate_timer(relay_chn); + + // If the channel was running, schedule a free command for the channel + relay_chn_cmd_t last_run_cmd = relay_chn->run_info.last_run_cmd; + if (last_run_cmd == RELAY_CHN_CMD_FORWARD || last_run_cmd == RELAY_CHN_CMD_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_timer(relay_chn, RELAY_CHN_OPPOSITE_INERTIA_MS); + } else { + // If the channel was not running, 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) +{ + gpio_set_level(relay_chn->output.reverse_pin, 0); + gpio_set_level(relay_chn->output.forward_pin, 1); + relay_chn->state = RELAY_CHN_STATE_FORWARD; + relay_chn->run_info.last_run_cmd = RELAY_CHN_CMD_FORWARD; +} + +static void relay_chn_execute_reverse(relay_chn_t *relay_chn) +{ + gpio_set_level(relay_chn->output.forward_pin, 0); + gpio_set_level(relay_chn->output.reverse_pin, 1); + relay_chn->state = RELAY_CHN_STATE_REVERSE; + relay_chn->run_info.last_run_cmd = RELAY_CHN_CMD_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_timer(relay_chn, RELAY_CHN_OPPOSITE_INERTIA_MS); +} + +void relay_chn_execute_free(relay_chn_t *relay_chn) +{ + relay_chn->state = RELAY_CHN_STATE_FREE; + relay_chn->pending_cmd = RELAY_CHN_CMD_NONE; + // Invalidate the channel's timer if it is active + relay_chn_invalidate_timer(relay_chn); +} + +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"; + } +} + +/// @} \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..b596410 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components" + "../../relay_chn") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(relay_chn_test) \ No newline at end of file diff --git a/test/main/CMakeLists.txt b/test/main/CMakeLists.txt new file mode 100644 index 0000000..8f4dc87 --- /dev/null +++ b/test/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS_DIRS "." + PRIV_INCLUDE_DIRS "." + PRIV_REQUIRES unity test_utils relay_chn) \ No newline at end of file diff --git a/test/main/relay_chn_test.c b/test/main/relay_chn_test.c new file mode 100644 index 0000000..d8c18e2 --- /dev/null +++ b/test/main/relay_chn_test.c @@ -0,0 +1,75 @@ +#include "unity.h" +#include "relay_chn.h" + + +const gpip_num_t gpio_map[] = {GPIO_NUM_4, GPIO_NUM_5, GPIO_NUM_18, GPIO_NUM_19}; +const uint8_t gpio_count = sizeof(gpio_map) / sizeof(gpio_map[0]); +const uint8_t relay_chn_count = gpio_count / 2; + +TEST_CASE("relay chn inits correctly", "[relay_chn]") +{ + TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); +} + +TEST_CASE("Relay channels run forward and update state", "[relay_chn][forward]") +{ + TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); + // Test forward run on all channels + for (uint8_t i = 0; i < relay_chn_count; i++) { + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i)); + relay_chn_run_forward(i); // Run the channel forward + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(i)); + relay_chn_stop(i); // Stop the channel + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i)); + + relay_chn_flip_direction(i); // Flip the direction + TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_FLIPPED, relay_chn_get_direction(i)); + relay_chn_run_forward(i); // Run the channel forward + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(i)); + relay_chn_stop(i); // Stop the channel + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i)); + } +} + +TEST_CASE("Relay channels run reverse and update state", "[relay_chn][reverse]") +{ + TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); + // Test reverse run on all channels + for (uint8_t i = 0; i < relay_chn_count; i++) { + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i)); + relay_chn_run_reverse(i); // Run the channel reverse + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(i)); + relay_chn_stop(i); // Stop the channel + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i)); + + relay_chn_flip_direction(i); // Flip the direction + TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_FLIPPED, relay_chn_get_direction(i)); + relay_chn_run_reverse(i); // Run the channel forward + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(i)); + relay_chn_stop(i); // Stop the channel + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i)); + } +} + +static void check_channels_state_unchanged(void) +{ + for (uint8_t i = 0; i < relay_chn_count; i++) { + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i)); + TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_DEFAULT, relay_chn_get_direction(i)); + } +} + +TEST_CASE("Relay channels do not change state for invalid channel", "[relay_chn][invalid]") +{ + TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); + // Test invalid channel run + relay_chn_run_forward(relay_chn_count + 1); // Run the channel forward + check_channels_state_unchanged(); + relay_chn_run_reverse(relay_chn_count + 1); // Run the channel reverse + check_channels_state_unchanged(); + relay_chn_stop(relay_chn_count + 1); // Stop the channel + TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(relay_chn_count + 1)); + check_channels_state_unchanged(); + relay_chn_flip_direction(relay_chn_count + 1); // Flip the direction + check_channels_state_unchanged(); +} \ No newline at end of file diff --git a/test/main/relay_chn_test_main.c b/test/main/relay_chn_test_main.c new file mode 100644 index 0000000..8b007a3 --- /dev/null +++ b/test/main/relay_chn_test_main.c @@ -0,0 +1,21 @@ +#include +#include +#include "unity.h" +#include "unity_test_runner.h" + + +static void print_banner(const char*); + +void app_main(void) { + + print_banner("Starting interactive test menu"); + /* This function will not return, and will be busy waiting for UART input. + * Make sure that task watchdog is disabled if you use this function. + */ + unity_run_menu(); +} + +static void print_banner(const char* text) +{ + printf("\n##### %s #####\n\n", text); +} \ No newline at end of file diff --git a/test/sdkconfig.defaults b/test/sdkconfig.defaults new file mode 100644 index 0000000..c43f2b5 --- /dev/null +++ b/test/sdkconfig.defaults @@ -0,0 +1,5 @@ +# For IDF 5.0 +CONFIG_ESP_TASK_WDT_EN=n + +# For IDF4.4 +CONFIG_ESP_TASK_WDT=n \ No newline at end of file