diff --git a/.vscode/settings.json b/.vscode/settings.json index 4720ba3..f19cc63 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,23 @@ { "files.associations": { - "relay_chn.h": "c" + "relay_chn.h": "c", + "stdlib.h": "c", + "cstdint": "c", + "relay_chn_run_info.h": "c", + "esp_err.h": "c", + "relay_chn_output.h": "c", + "relay_chn_core.h": "c", + "relay_chn_ctl.h": "c", + "relay_chn_tilt.h": "c", + "relay_chn_defs.h": "c", + "esp_check.h": "c", + "esp_event_base.h": "c", + "esp_event.h": "c", + "queue.h": "c", + "relay_chn_priv_types.h": "c", + "relay_chn_adapter.h": "c", + "relay_chn_types.h": "c" }, - "idf.port": "/dev/ttyUSB0" + "idf.port": "/dev/ttyUSB0", + "idf.pythonInstallPath": "/usr/bin/python" } \ No newline at end of file diff --git a/include/relay_chn_adapter.h b/include/relay_chn_adapter.h new file mode 100644 index 0000000..dd66907 --- /dev/null +++ b/include/relay_chn_adapter.h @@ -0,0 +1,186 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + * + * An adapter header to expose the appropriate API functions to the public API + * depending on the RELAY_CHN_COUNT value which determines single or multi mode. + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#if RELAY_CHN_COUNT > 1 +/** + * @brief Get the current state of a relay channel. + * + * @param[in] chn_id Channel ID to get state for. + * @return Current state of the specified channel, or RELAY_CHN_STATE_UNDEFINED if invalid. + */ +extern relay_chn_state_t relay_chn_ctl_get_state(uint8_t chn_id); + +/** + * @brief Get string representation of a relay channel's state. + * + * @param[in] chn_id Channel ID to get state string for. + * @return String representation of channel state, or "UNDEFINED" if invalid. + */ +extern char *relay_chn_ctl_get_state_str(uint8_t chn_id); + +/** + * @brief Run a relay channel in forward direction. + * + * @param[in] chn_id Channel ID to run forward, or RELAY_CHN_ID_ALL for all channels. + */ +extern void relay_chn_ctl_run_forward(uint8_t chn_id); + +/** + * @brief Run a relay channel in reverse direction. + * + * @param[in] chn_id Channel ID to run reverse, or RELAY_CHN_ID_ALL for all channels. + */ +extern void relay_chn_ctl_run_reverse(uint8_t chn_id); + +/** + * @brief Stop a relay channel. + * + * @param[in] chn_id Channel ID to stop, or RELAY_CHN_ID_ALL for all channels. + */ +extern void relay_chn_ctl_stop(uint8_t chn_id); + +/** + * @brief Flip the running direction of a relay channel. + * + * @param[in] chn_id Channel ID to flip direction for, or RELAY_CHN_ID_ALL for all channels. + */ +extern void relay_chn_ctl_flip_direction(uint8_t chn_id); + +/** + * @brief Get the current direction of a relay channel. + * + * @param[in] chn_id Channel ID to get direction for. + * @return Current direction of the specified channel, or RELAY_CHN_DIRECTION_DEFAULT if invalid. + */ +extern relay_chn_direction_t relay_chn_ctl_get_direction(uint8_t chn_id); + +static inline relay_chn_state_t relay_chn_get_state(uint8_t chn_id) +{ + return relay_chn_ctl_get_state(chn_id); +} + +static inline char *relay_chn_get_state_str(uint8_t chn_id) +{ + return relay_chn_ctl_get_state_str(chn_id); +} + +static inline void relay_chn_run_forward(uint8_t chn_id) +{ + relay_chn_ctl_run_forward(chn_id); +} + +static inline void relay_chn_run_reverse(uint8_t chn_id) +{ + relay_chn_ctl_run_reverse(chn_id); +} + +static inline void relay_chn_stop(uint8_t chn_id) +{ + relay_chn_ctl_stop(chn_id); +} + +static inline void relay_chn_flip_direction(uint8_t chn_id) +{ + relay_chn_ctl_flip_direction(chn_id); +} + +static inline relay_chn_direction_t relay_chn_get_direction(uint8_t chn_id) +{ + return relay_chn_ctl_get_direction(chn_id); +} + +#else + +/** + * @brief Get the current state of the relay channel. + * + * @return Current state of the channel. + */ +extern relay_chn_state_t relay_chn_ctl_get_state(void); + +/** + * @brief Get string representation of the relay channel's state. + * + * @return String representation of channel state. + */ +extern char *relay_chn_ctl_get_state_str(void); + +/** + * @brief Run the relay channel in forward direction. + */ +extern void relay_chn_ctl_run_forward(void); + +/** + * @brief Run the relay channel in reverse direction. + */ +extern void relay_chn_ctl_run_reverse(void); + +/** + * @brief Stop the relay channel. + */ +extern void relay_chn_ctl_stop(void); + +/** + * @brief Flip the running direction of the relay channel. + */ +extern void relay_chn_ctl_flip_direction(void); + +/** + * @brief Get the current direction of the relay channel. + * + * @return Current direction of the channel. + */ +extern relay_chn_direction_t relay_chn_ctl_get_direction(void); + +static inline relay_chn_state_t relay_chn_get_state(void) +{ + return relay_chn_ctl_get_state(); +} + +static inline char *relay_chn_get_state_str(void) +{ + return relay_chn_ctl_get_state_str(); +} + +static inline void relay_chn_run_forward(void) +{ + relay_chn_ctl_run_forward(); +} + +static inline void relay_chn_run_reverse(void) +{ + relay_chn_ctl_run_reverse(); +} + +static inline void relay_chn_stop(void) +{ + relay_chn_ctl_stop(); +} + +static inline void relay_chn_flip_direction(void) +{ + relay_chn_ctl_flip_direction(); +} + +static inline relay_chn_direction_t relay_chn_get_direction(void) +{ + return relay_chn_ctl_get_direction(); +} + +#endif // RELAY_CHN_COUNT > 1 + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/include/relay_chn_defs.h b/include/relay_chn_defs.h new file mode 100644 index 0000000..0000b01 --- /dev/null +++ b/include/relay_chn_defs.h @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + + #pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/* Config defines for covenient writing */ +#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 + +#if RELAY_CHN_COUNT > 1 +#define RELAY_CHN_ID_ALL RELAY_CHN_COUNT /*!< Special ID to address all channels */ +#endif + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/include/relay_chn_types.h b/include/relay_chn_types.h new file mode 100644 index 0000000..90db4ff --- /dev/null +++ b/include/relay_chn_types.h @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include "relay_chn_defs.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Enumeration for relay channel direction. + */ +typedef 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 */ +} relay_chn_direction_t; + + +/** + * @brief Enums that represent the state of a relay channel. + */ +typedef enum relay_chn_state_enum { + RELAY_CHN_STATE_UNDEFINED, /*!< The relay channel state is undefined */ + RELAY_CHN_STATE_IDLE, /*!< 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 */ +#if CONFIG_RELAY_CHN_ENABLE_TILTING == 1 + RELAY_CHN_STATE_TILT_FORWARD, /*!< The relay channel is tilting for forward */ + RELAY_CHN_STATE_TILT_REVERSE, /*!< The relay channel is tilting for reverse */ +#endif +} relay_chn_state_t; + +/** + * @brief Relay channel state change listener. + * + * An optional interface to listen to the channel state change events. + * The listeners SHOULD be implemented as light functions and SHOULD NOT contain + * any blocking calls. Otherwise the relay_chn module would not function properly + * since it is designed as event driven. + * + * @param chn_id The ID of the channel whose state has changed. + * @param old_state The old state of the channel. + * @param new_state The new state of the channel. + */ +typedef void (*relay_chn_state_listener_t)(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/private_include/relay_chn_core.h b/private_include/relay_chn_core.h new file mode 100644 index 0000000..d132e99 --- /dev/null +++ b/private_include/relay_chn_core.h @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include "esp_err.h" +#include "esp_log.h" +#include "esp_event_base.h" +#include "esp_event.h" +#include "esp_timer.h" +#include "relay_chn_defs.h" +#include "relay_chn_types.h" +#include "relay_chn_priv_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/// Event base used by *_core, *_ctl, and *_tilt modules. +ESP_EVENT_DECLARE_BASE(RELAY_CHN_CMD_EVENT); + +/** + * @brief Initializes the relay channel timer. + * + * This function creates a timer for the relay channel to handle direction change inertia. + * Required by *_ctl_* module. + * + * @param chn_ctl Pointer to the relay channel control structure. + * @return esp_err_t ESP_OK on success, or an error code on failure. + */ +esp_err_t relay_chn_init_timer(relay_chn_ctl_t *chn_ctl); + +/** + * @brief Issues a command to the relay channel. + * + * Evaluates the current state of the relay channel and issues the command accordingly. + * Required by *_core, *_ctl_* and *_tilt modules. + * + * @param chn_ctl Pointer to the relay channel control structure. + * @param cmd The command to issue. + */ +void relay_chn_issue_cmd(relay_chn_ctl_t* chn_ctl, relay_chn_cmd_t cmd); + +/** + * @brief Dispatches a relay channel command to the event loop. + * + * @param chn_ctl Pointer to the relay channel control structure. + * @param cmd The command to dispatch. + */ +void relay_chn_dispatch_cmd(relay_chn_ctl_t *chn_ctl, 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. + */ +char *relay_chn_cmd_str(relay_chn_cmd_t cmd); + +/** + * @brief Starts the ESP timer once with the specified time in milliseconds. + * + * Starts the ESP timer to run once after the specified time. + * If the timer is already running, it stops it first and then starts it again. + * Required by *_ctl_* and *_tilt modules. + * + * @param esp_timer The ESP timer handle. + * @param time_ms The time in milliseconds to wait before the timer expires. + * @return esp_err_t ESP_OK on success, or an error code on failure. + */ +esp_err_t relay_chn_start_esp_timer_once(esp_timer_handle_t esp_timer, uint32_t time_ms); + +/** + * @brief Updates the state of the relay channel and notifies listeners. + * + * This function updates the state of the relay channel and notifies all registered listeners + * about the state change. + * Required by *_ctl_* and *_tilt modules. + * + * @param chn_ctl Pointer to the relay channel control structure. + * @param new_state The new state to set for the relay channel. + */ +void relay_chn_update_state(relay_chn_ctl_t *chn_ctl, relay_chn_state_t new_state); + +/** + * @brief Return the text presentation of an state. + * + * @param state A state with type of relay_chn_state_t. + * @return char* The text presentation of the state. "UNKNOWN" if the state is not known. + */ +char *relay_chn_state_str(relay_chn_state_t state); + +#if RELAY_CHN_COUNT > 1 +/** + * @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. + */ +bool relay_chn_is_channel_id_valid(uint8_t chn_id); +#endif // RELAY_CHN_COUNT > 1 + +#if RELAY_CHN_ENABLE_TILTING == 1 +/// Relay channel event loop handle declaration for *_tilt module. +extern esp_event_loop_handle_t relay_chn_event_loop; +#endif // RELAY_CHN_ENABLE_TILTING + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/private_include/relay_chn_ctl.h b/private_include/relay_chn_ctl.h new file mode 100644 index 0000000..8f89a11 --- /dev/null +++ b/private_include/relay_chn_ctl.h @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech developer@kozmotronik.com.tr + * + * SPDX-License-Identifier: MIT + * + * Expose the *_ctl functions required by *_core.c file. + */ + +#pragma once + +#include "relay_chn_priv_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize the relay channel control. + * + * @param output Pointer to the output object(s). + * @param run_info Pointer to the runtime information object(s). + * + * @return esp_err_t Returns ESP_OK on success, or an error code on failure. + */ +esp_err_t relay_chn_ctl_init(relay_chn_output_t *output, relay_chn_run_info_t *run_info); + +/** + * @brief Deinitialize the relay channel control. + * + * This function cleans up resources used by the relay channel control. + */ +void relay_chn_ctl_deinit(void); + +#if RELAY_CHN_COUNT > 1 +/** + * @brief Get the control structure for a specific relay channel. + * + * @param chn_id The ID of the relay channel to retrieve. + * + * @return relay_chn_ctl_t* Pointer to the control structure for the specified channel, or NULL if not found. + */ +relay_chn_ctl_t *relay_chn_ctl_get(uint8_t chn_id); + +/** + * @brief Get the control structures for all relay channels. + * + * @return relay_chn_ctl_t* Pointer to the array of control structures for all channels. + */ +relay_chn_ctl_t *relay_chn_ctl_get_all(void); +#else +relay_chn_ctl_t *relay_chn_ctl_get(void); +#endif // RELAY_CHN_COUNT > 1 + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/private_include/relay_chn_output.h b/private_include/relay_chn_output.h new file mode 100644 index 0000000..27a8de9 --- /dev/null +++ b/private_include/relay_chn_output.h @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + * + * Abstraction layer for controlling relay channel outputs. This is the layer + * that interacts with the GPIO pins to control the relay channels. + */ + +#pragma once + +#include +#include "esp_err.h" +#include "relay_chn_priv_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize relay channel outputs. + * + * Maps relay channels to GPIO pins and prepares them for operation. + * + * @param[in] gpio_map Array of GPIO pin numbers for each relay channel. + * @param[in] gpio_count Number of GPIO pins (relay channels). + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t relay_chn_output_init(const uint8_t* gpio_map, uint8_t gpio_count); + +/** + * @brief Deinitialize relay channel outputs. + * + * Releases resources and resets GPIO pins used for relay channels. + */ +void relay_chn_output_deinit(void); + +#if RELAY_CHN_COUNT > 1 +/** + * @brief Get the relay channel output object for a specific channel. + * + * @param[in] chn_id Channel ID. + * + * @return Pointer to relay channel output object, or NULL if invalid. + */ +relay_chn_output_t *relay_chn_output_get(uint8_t chn_id); + +/** + * @brief Get all relay channel output objects. + * + * @return Pointer to array of relay channel output objects. + */ +relay_chn_output_t *relay_chn_output_get_all(void); +#else +/** + * @brief Get the relay channel output object. + * + * @return Pointer to relay channel output object. + */ +relay_chn_output_t *relay_chn_output_get(void); +#endif // RELAY_CHN_COUNT > 1 + +/** + * @brief Stop the relay channel output. + * + * Sets the relay channel to the stop state. + * + * @param[in] output Pointer to relay channel output object. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t relay_chn_output_stop(relay_chn_output_t *output); + +/** + * @brief Set relay channel output to forward direction. + * + * @param[in] output Pointer to relay channel output object. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t relay_chn_output_forward(relay_chn_output_t *output); + +/** + * @brief Set relay channel output to reverse direction. + * + * @param[in] output Pointer to relay channel output object. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t relay_chn_output_reverse(relay_chn_output_t *output); + +/** + * @brief Flip the direction of the relay channel output. + * + * Changes the direction from forward to reverse or vice versa. + * + * @param[in] output Pointer to relay channel output object. + */ +void relay_chn_output_flip(relay_chn_output_t *output); + +/** + * @brief Get the current direction of the relay channel output. + * + * @param[in] output Pointer to relay channel output object. + * + * @return Current direction of the relay channel. + */ +relay_chn_direction_t relay_chn_output_get_direction(relay_chn_output_t *output); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/private_include/relay_chn_priv_types.h b/private_include/relay_chn_priv_types.h new file mode 100644 index 0000000..0967d86 --- /dev/null +++ b/private_include/relay_chn_priv_types.h @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include "driver/gpio.h" +#include "esp_timer.h" +#include "relay_chn_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Enumeration for relay channel commands. + */ +typedef 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_IDLE /*!< Free the relay channel */ +} relay_chn_cmd_t; + + +/** + * @brief Structure to hold the output configuration of a relay channel. + */ +typedef struct { + 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; + +/** + * @brief Structure to hold runtime information for a relay channel. + */ +typedef struct { + 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; + +#if RELAY_CHN_ENABLE_TILTING == 1 +/// @brief Tilt commands. +typedef 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 */ +} relay_chn_tilt_cmd_t; + +/// Forward declaration for relay_chn_tilt_ctl +typedef struct relay_chn_tilt_ctl relay_chn_tilt_ctl_t; +#endif + +/** + * @brief Structure to hold the state and configuration of a relay channel. + */ +typedef struct { + 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 */ +#if RELAY_CHN_ENABLE_TILTING == 1 + relay_chn_tilt_ctl_t *tilt_ctl; /*!< Pointer to the tilt control structure if tilting is enabled */ +#endif +} relay_chn_ctl_t; + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/private_include/relay_chn_run_info.h b/private_include/relay_chn_run_info.h new file mode 100644 index 0000000..0b14296 --- /dev/null +++ b/private_include/relay_chn_run_info.h @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + * + * This is for managing the run information of relay channels. + */ + +#pragma once + +#include "relay_chn_priv_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize relay channel run information. + * + * Initializes the run information for all relay channels with default values. + */ +void relay_chn_run_info_init(void); + +#if RELAY_CHN_COUNT > 1 +/** + * @brief Get run information object for a specific relay channel. + * + * @param[in] chn_id Channel ID to get run information for. + * @return Pointer to run information structure, or NULL if channel ID is invalid. + */ +relay_chn_run_info_t *relay_chn_run_info_get(uint8_t chn_id); + +/** + * @brief Get run information objects for all relay channels. + * + * @return Pointer to array of run information structures. + */ +relay_chn_run_info_t *relay_chn_run_info_get_all(void); +#else +/** + * @brief Get run information object for the single relay channel. + * + * @return Pointer to run information structure. + */ +relay_chn_run_info_t *relay_chn_run_info_get(void); +#endif // RELAY_CHN_COUNT > 1 + +/** + * @brief Get the last run command for a relay channel. + * + * @param[in] run_info Pointer to run information structure. + * + * @return Last command that was executed, or RELAY_CHN_CMD_NONE if invalid. + */ +relay_chn_cmd_t relay_chn_run_info_get_last_run_cmd(relay_chn_run_info_t *run_info); + +/** + * @brief Set the last run command for a relay channel. + * + * @param[in] run_info Pointer to run information structure. + * @param[in] cmd Command to set as last run command. + */ +void relay_chn_run_info_set_last_run_cmd(relay_chn_run_info_t *run_info, relay_chn_cmd_t cmd); + +/** + * @brief Get the timestamp of the last run command. + * + * @param[in] run_info Pointer to run information structure. + * + * @return Timestamp in milliseconds of last command, or 0 if invalid. + */ +uint32_t relay_chn_run_info_get_last_run_cmd_time_ms(relay_chn_run_info_t *run_info); + +/** + * @brief Set the timestamp for the last run command. + * + * @param[in] run_info Pointer to run information structure. + * @param[in] time_ms Timestamp in milliseconds to set. + */ +void relay_chn_run_info_set_last_run_cmd_time_ms(relay_chn_run_info_t *run_info, uint32_t time_ms); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/private_include/relay_chn_tilt.h b/private_include/relay_chn_tilt.h new file mode 100644 index 0000000..ab1b5c9 --- /dev/null +++ b/private_include/relay_chn_tilt.h @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include "relay_chn_priv_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize relay channel tilt controls. + * + * Sets up tilt functionality for relay channels including timers and event handlers. + * Must be called before using any other tilt functions. + * + * @param[in] chn_ctls Array of relay channel control structures. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t relay_chn_tilt_init(relay_chn_ctl_t *chn_ctls); + +/** + * @brief Deinitialize relay channel tilt controls. + * + * Cleans up tilt resources including timers and event handlers. + * Should be called when tilt functionality is no longer needed. + */ +void relay_chn_tilt_deinit(void); + +/** + * @brief Dispatch a tilt command to a relay channel. + * + * Queues a tilt command for execution on the specified channel. + * + * @param[in] tilt_ctl Pointer to tilt control structure. + * @param[in] cmd Tilt command to execute. + * + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t relay_chn_tilt_dispatch_cmd(relay_chn_tilt_ctl_t *tilt_ctl, relay_chn_tilt_cmd_t cmd); + +/** + * @brief Reset tilt counters for a relay channel. + * + * Resets both forward and reverse tilt counters to zero. + * + * @param[in] tilt_ctl Pointer to tilt control structure. + */ +void relay_chn_tilt_reset_count(relay_chn_tilt_ctl_t *tilt_ctl); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/src/relay_chn.c b/src/relay_chn.c deleted file mode 100644 index 069fe96..0000000 --- a/src/relay_chn.c +++ /dev/null @@ -1,1470 +0,0 @@ -/** - * @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 - -/// @} \ No newline at end of file diff --git a/src/relay_chn_core.c b/src/relay_chn_core.c new file mode 100644 index 0000000..85e30ee --- /dev/null +++ b/src/relay_chn_core.c @@ -0,0 +1,574 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include "esp_check.h" +#include "esp_task.h" +#include "relay_chn_output.h" +#include "relay_chn_run_info.h" +#include "relay_chn_ctl.h" +#if RELAY_CHN_ENABLE_TILTING == 1 +#include "relay_chn_tilt.h" +#endif +#include "relay_chn_core.h" + + +static const char *TAG = "RELAY_CHN_CORE"; + +ESP_EVENT_DEFINE_BASE(RELAY_CHN_CMD_EVENT); + + +// 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; + +// The list that holds references to the registered listeners. +static List_t relay_chn_listener_list; + +// Define the event loop for global access both for this module and tilt module. +esp_event_loop_handle_t relay_chn_event_loop = NULL; + + +// 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); + + +// Timer callback function for relay channel direction change inertia. +static void relay_chn_timer_cb(void* arg) +{ + relay_chn_ctl_t* chn_ctl = (relay_chn_ctl_t*) arg; + // Does channel have a pending command? + if (chn_ctl->pending_cmd != RELAY_CHN_CMD_NONE) { + relay_chn_dispatch_cmd(chn_ctl, chn_ctl->pending_cmd); + chn_ctl->pending_cmd = RELAY_CHN_CMD_NONE; + } + else { + ESP_LOGE(TAG, "relay_chn_timer_cb: No pending cmd for relay channel %d!", chn_ctl->id); + } +} + +esp_err_t relay_chn_init_timer(relay_chn_ctl_t *chn_ctl) +{ + char timer_name[32]; + snprintf(timer_name, sizeof(timer_name), "relay_chn_%d_timer", chn_ctl->id); + esp_timer_create_args_t timer_args = { + .callback = relay_chn_timer_cb, + .arg = chn_ctl, + .name = timer_name + }; + return esp_timer_create(&timer_args, &chn_ctl->inertia_timer); +} + +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 uint8_t* gpio_map, uint8_t gpio_count) +{ + ESP_RETURN_ON_FALSE(gpio_map != NULL, ESP_ERR_INVALID_ARG, TAG, "gpio_map cannot be NULL"); + + esp_err_t ret; + // Initialize the output + ret = relay_chn_output_init(gpio_map, gpio_count); + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize relay channel outputs"); + + // Initialize the run info + relay_chn_run_info_init(); + +#if RELAY_CHN_COUNT > 1 + relay_chn_output_t *outputs = relay_chn_output_get_all(); + relay_chn_run_info_t *run_infos = relay_chn_run_info_get_all(); +#else + relay_chn_output_t *outputs = relay_chn_output_get(); + relay_chn_run_info_t *run_infos = relay_chn_run_info_get(); +#endif + + // Initialize the relay channel controls + ret = relay_chn_ctl_init(outputs, run_infos); + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize relay channel control"); + + // 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 + // Initialize the tilt feature +#if RELAY_CHN_COUNT > 1 + relay_chn_ctl_t *chn_ctls = relay_chn_ctl_get_all(); +#else + relay_chn_ctl_t *chn_ctls = relay_chn_ctl_get(); +#endif // RELAY_CHN_COUNT > 1 + ret = relay_chn_tilt_init(chn_ctls); // 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) +{ +#if RELAY_CHN_ENABLE_TILTING == 1 + relay_chn_tilt_deinit(); +#endif + relay_chn_ctl_deinit(); + relay_chn_output_deinit(); + + // 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); + } +} + +/** + * @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); + } +} + + +// Dispatch relay channel command to its event loop +void relay_chn_dispatch_cmd(relay_chn_ctl_t *chn_ctl, relay_chn_cmd_t cmd) { + if (cmd == RELAY_CHN_CMD_NONE) { + return; + } + + // Since the event_loop library creates a deep copy of the event data, + // and we need to pass the pointer of the relevant channel, here we need + // to pass the pointer to the pointer of the channel (&chn_ctl) so that + // the pointer value is preserved in the event data. + esp_event_post_to(relay_chn_event_loop, + RELAY_CHN_CMD_EVENT, + cmd, + &chn_ctl, + sizeof(chn_ctl), + 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_reset_count(chn_ctl->tilt_ctl); + } +#endif +} + +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; +} + +void relay_chn_update_state(relay_chn_ctl_t *chn_ctl, relay_chn_state_t new_state) +{ + relay_chn_state_t old_state = chn_ctl->state; + + // Only update and notify if the state has actually changed. + if (old_state == new_state) { + return; + } + + chn_ctl->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(chn_ctl->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 chn_ctl The relay channel to issue the command to. + * @param cmd The command to issue. + */ +void relay_chn_issue_cmd(relay_chn_ctl_t* chn_ctl, relay_chn_cmd_t cmd) +{ + if (cmd == RELAY_CHN_CMD_NONE) { + return; + } + + if (cmd == RELAY_CHN_CMD_STOP) { + if (chn_ctl->state == RELAY_CHN_STATE_STOPPED) { + return; // Do nothing if already stopped + } + // If the command is STOP, issue it immediately + relay_chn_dispatch_cmd(chn_ctl, cmd); + return; + } + + relay_chn_cmd_t last_run_cmd = relay_chn_run_info_get_last_run_cmd(chn_ctl->run_info); + // Evaluate the channel's next move depending on its status + switch (chn_ctl->state) + { + case RELAY_CHN_STATE_IDLE: + // If the channel is idle, run the command immediately + relay_chn_dispatch_cmd(chn_ctl, 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(chn_ctl, cmd); + } + break; + + case RELAY_CHN_STATE_STOPPED: + if (last_run_cmd == cmd || 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(chn_ctl->inertia_timer); + chn_ctl->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(chn_ctl, 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 last_run_cmd_time_ms = relay_chn_run_info_get_last_run_cmd_time_ms(chn_ctl->run_info); + uint32_t inertia_time_passed_ms = (uint32_t) (esp_timer_get_time() / 1000) - last_run_cmd_time_ms; + uint32_t inertia_time_ms = RELAY_CHN_OPPOSITE_INERTIA_MS - inertia_time_passed_ms; + if (inertia_time_ms > 0) { + chn_ctl->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(chn_ctl, new_state); + // If the time passed is less than the opposite inertia time, wait for the remaining time + relay_chn_start_esp_timer_once(chn_ctl->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(chn_ctl, 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(chn_ctl, RELAY_CHN_CMD_STOP); + relay_chn_dispatch_cmd(chn_ctl, cmd); + return; + } + + if (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(chn_ctl, RELAY_CHN_CMD_STOP); + + // If the last run command is different from the current command, wait for the opposite inertia time + chn_ctl->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(chn_ctl, new_state); + relay_chn_start_esp_timer_once(chn_ctl->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_tilt_dispatch_cmd(chn_ctl->tilt_ctl, RELAY_CHN_TILT_CMD_STOP); + if (cmd == RELAY_CHN_CMD_FORWARD) { + // Schedule for running forward + chn_ctl->pending_cmd = cmd; + relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_FORWARD_PENDING); + relay_chn_start_esp_timer_once(chn_ctl->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(chn_ctl, cmd); + relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_REVERSE); + } + break; + case RELAY_CHN_STATE_TILT_REVERSE: + // Terminate tilting first + relay_chn_tilt_dispatch_cmd(chn_ctl->tilt_ctl, RELAY_CHN_TILT_CMD_STOP); + if (cmd == RELAY_CHN_CMD_FORWARD) { + // Run directly since it is the same direction + relay_chn_dispatch_cmd(chn_ctl, cmd); + relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_FORWARD); + } else if (cmd == RELAY_CHN_CMD_REVERSE) { + // Schedule for running reverse + chn_ctl->pending_cmd = cmd; + relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_REVERSE_PENDING); + relay_chn_start_esp_timer_once(chn_ctl->inertia_timer, RELAY_CHN_OPPOSITE_INERTIA_MS); + } + break; +#endif + + default: ESP_LOGD(TAG, "relay_chn_evaluate: Unknown relay channel state!"); + } +} + +#if RELAY_CHN_COUNT > 1 +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; +} +#endif // RELAY_CHN_COUNT > 1 + + +static void relay_chn_execute_stop(relay_chn_ctl_t *chn_ctl) +{ + if (relay_chn_output_stop(chn_ctl->output) != ESP_OK) { + ESP_LOGE(TAG, "relay_chn_execute_stop: Failed to output stop for relay channel #%d!", chn_ctl->id); + } + relay_chn_state_t previous_state = chn_ctl->state; + relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_STOPPED); + + // If there is any pending command, cancel it since the STOP command is issued right after it + chn_ctl->pending_cmd = RELAY_CHN_CMD_NONE; + // Invalidate the channel's timer if it is active + esp_timer_stop(chn_ctl->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_set_last_run_cmd_time_ms(chn_ctl->run_info, (uint32_t)(esp_timer_get_time() / 1000)); + // Schedule a free command for the channel + chn_ctl->pending_cmd = RELAY_CHN_CMD_IDLE; + relay_chn_start_esp_timer_once(chn_ctl->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(chn_ctl, RELAY_CHN_CMD_IDLE); + } +} + +static void relay_chn_execute_forward(relay_chn_ctl_t *chn_ctl) +{ + if (relay_chn_output_forward(chn_ctl->output) != ESP_OK) { + ESP_LOGE(TAG, "relay_chn_execute_forward: Failed to output forward for relay channel #%d!", chn_ctl->id); + return; + } + relay_chn_run_info_set_last_run_cmd(chn_ctl->run_info, RELAY_CHN_CMD_FORWARD); + relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_FORWARD); +} + +static void relay_chn_execute_reverse(relay_chn_ctl_t *chn_ctl) +{ + if (relay_chn_output_reverse(chn_ctl->output) != ESP_OK) { + ESP_LOGE(TAG, "relay_chn_execute_reverse: Failed to output reverse for relay channel #%d!", chn_ctl->id); + return; + } + relay_chn_run_info_set_last_run_cmd(chn_ctl->run_info, RELAY_CHN_CMD_REVERSE); + relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_REVERSE); +} + +static void relay_chn_execute_flip(relay_chn_ctl_t *chn_ctl) +{ + relay_chn_output_flip(chn_ctl->output); + // Set an inertia on the channel to prevent any immediate movement + chn_ctl->pending_cmd = RELAY_CHN_CMD_IDLE; + relay_chn_start_esp_timer_once(chn_ctl->inertia_timer, RELAY_CHN_OPPOSITE_INERTIA_MS); +} + +void relay_chn_execute_idle(relay_chn_ctl_t *chn_ctl) +{ + chn_ctl->pending_cmd = RELAY_CHN_CMD_NONE; + // Invalidate the channel's timer if it is active + esp_timer_stop(chn_ctl->inertia_timer); + relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_IDLE); +} + +static void relay_chn_event_handler(void* handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) +{ + relay_chn_ctl_t* chn_ctl = *(relay_chn_ctl_t**) event_data; + ESP_RETURN_VOID_ON_FALSE(chn_ctl != NULL, TAG, "event_data is NULL"); + ESP_LOGD(TAG, "relay_chn_event_handler: Command: %s", relay_chn_cmd_str(event_id)); + + switch (event_id) { + case RELAY_CHN_CMD_STOP: + relay_chn_execute_stop(chn_ctl); + break; + case RELAY_CHN_CMD_FORWARD: + relay_chn_execute_forward(chn_ctl); + break; + case RELAY_CHN_CMD_REVERSE: + relay_chn_execute_reverse(chn_ctl); + break; + case RELAY_CHN_CMD_FLIP: + relay_chn_execute_flip(chn_ctl); + break; + case RELAY_CHN_CMD_IDLE: + relay_chn_execute_idle(chn_ctl); + break; + default: + ESP_LOGD(TAG, "Unknown relay channel command!"); + } +} + +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_IDLE: + return "IDLE"; + default: + return "UNKNOWN"; + } +} + +char *relay_chn_state_str(relay_chn_state_t state) +{ + switch (state) { + case RELAY_CHN_STATE_IDLE: + return "IDLE"; + 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"; + } +} \ No newline at end of file diff --git a/src/relay_chn_ctl_multi.c b/src/relay_chn_ctl_multi.c new file mode 100644 index 0000000..13c97e8 --- /dev/null +++ b/src/relay_chn_ctl_multi.c @@ -0,0 +1,137 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + +#include "esp_check.h" +#include "relay_chn_priv_types.h" +#include "relay_chn_core.h" +#include "relay_chn_ctl.h" +#include "relay_chn_output.h" + +static const char *TAG = "RELAY_CHN_CTL"; + +static relay_chn_ctl_t chn_ctls[RELAY_CHN_COUNT]; + + +esp_err_t relay_chn_ctl_init(relay_chn_output_t *outputs, relay_chn_run_info_t *run_infos) +{ + // Initialize all relay channels + esp_err_t ret; + for (int i = 0; i < RELAY_CHN_COUNT; i++) { + relay_chn_ctl_t* chn_ctl = &chn_ctls[i]; + relay_chn_output_t* output = &outputs[i]; + relay_chn_run_info_t* run_info = &run_infos[i]; + + chn_ctl->id = i; + chn_ctl->state = RELAY_CHN_STATE_IDLE; + chn_ctl->pending_cmd = RELAY_CHN_CMD_NONE; + + chn_ctl->output = output; + chn_ctl->run_info = run_info; + ret = relay_chn_init_timer(chn_ctl); // Create direction change inertia timer + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to create relay channel timer for channel %d", i); + } + return ESP_OK; +} + +void relay_chn_ctl_deinit() +{ + for (int i = 0; i < RELAY_CHN_COUNT; i++) { + relay_chn_ctl_t* chn_ctl = &chn_ctls[i]; + if (chn_ctl->inertia_timer != NULL) { + esp_timer_delete(chn_ctl->inertia_timer); + chn_ctl->inertia_timer = NULL; + } + } +} + +relay_chn_state_t relay_chn_ctl_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 chn_ctls[chn_id].state; +} + +char *relay_chn_ctl_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(chn_ctls[chn_id].state); +} + +static void relay_chn_ctl_issue_cmd_on_all_channels(relay_chn_cmd_t cmd) +{ + for (int i = 0; i < RELAY_CHN_COUNT; i++) { + relay_chn_issue_cmd(&chn_ctls[i], cmd); + } +} + +void relay_chn_ctl_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_ctl_issue_cmd_on_all_channels(RELAY_CHN_CMD_FORWARD); + return; + } + relay_chn_issue_cmd(&chn_ctls[chn_id], RELAY_CHN_CMD_FORWARD); +} + +void relay_chn_ctl_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_ctl_issue_cmd_on_all_channels(RELAY_CHN_CMD_REVERSE); + return; + } + relay_chn_issue_cmd(&chn_ctls[chn_id], RELAY_CHN_CMD_REVERSE); +} + +void relay_chn_ctl_stop(uint8_t chn_id) +{ + if (!relay_chn_is_channel_id_valid(chn_id)) return; + + if (chn_id == RELAY_CHN_ID_ALL) { + relay_chn_ctl_issue_cmd_on_all_channels(RELAY_CHN_CMD_STOP); + return; + } + relay_chn_issue_cmd(&chn_ctls[chn_id], RELAY_CHN_CMD_STOP); +} + +void relay_chn_ctl_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_ctl_issue_cmd_on_all_channels(RELAY_CHN_CMD_FLIP); + return; + } + relay_chn_issue_cmd(&chn_ctls[chn_id], RELAY_CHN_CMD_FLIP); +} + +relay_chn_direction_t relay_chn_ctl_get_direction(uint8_t chn_id) +{ + if (!relay_chn_is_channel_id_valid(chn_id)) { + return RELAY_CHN_DIRECTION_DEFAULT; + } + relay_chn_ctl_t *chn_ctl = &chn_ctls[chn_id]; + return relay_chn_output_get_direction(chn_ctl->output); +} + +relay_chn_ctl_t *relay_chn_ctl_get(uint8_t chn_id) +{ + if (!relay_chn_is_channel_id_valid(chn_id)) { + return NULL; + } + return &chn_ctls[chn_id]; +} + +relay_chn_ctl_t *relay_chn_ctl_get_all(void) +{ + return chn_ctls; +} \ No newline at end of file diff --git a/src/relay_chn_ctl_single.c b/src/relay_chn_ctl_single.c new file mode 100644 index 0000000..0435b33 --- /dev/null +++ b/src/relay_chn_ctl_single.c @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + +#include "relay_chn_priv_types.h" +#include "relay_chn_core.h" +#include "relay_chn_ctl.h" +#include "relay_chn_output.h" + + +static relay_chn_ctl_t chn_ctl; + + +esp_err_t relay_chn_ctl_init(relay_chn_output_t *output, relay_chn_run_info_t *run_info) +{ + // Initialize the relay channel + chn_ctl.id = 0; // Single channel, so ID is 0 + chn_ctl.state = RELAY_CHN_STATE_IDLE; + chn_ctl.pending_cmd = RELAY_CHN_CMD_NONE; + chn_ctl.output = output; + chn_ctl.run_info = run_info; + return relay_chn_init_timer(&chn_ctl); // Create direction change inertia timer +} + + +void relay_chn_ctl_deinit() +{ + if (chn_ctl.inertia_timer != NULL) { + esp_timer_delete(chn_ctl.inertia_timer); + chn_ctl.inertia_timer = NULL; + } +} + +/* relay_chn APIs */ +relay_chn_state_t relay_chn_ctl_get_state() +{ + return chn_ctl.state; +} + +char *relay_chn_ctl_get_state_str() +{ + return relay_chn_state_str(chn_ctl.state); +} + +void relay_chn_ctl_run_forward() +{ + relay_chn_issue_cmd(&chn_ctl, RELAY_CHN_CMD_FORWARD); +} + +void relay_chn_ctl_run_reverse() +{ + relay_chn_issue_cmd(&chn_ctl, RELAY_CHN_CMD_REVERSE); +} + +void relay_chn_ctl_stop() +{ + relay_chn_issue_cmd(&chn_ctl, RELAY_CHN_CMD_STOP); +} + +void relay_chn_ctl_flip_direction() +{ + relay_chn_issue_cmd(&chn_ctl, RELAY_CHN_CMD_FLIP); +} + +relay_chn_direction_t relay_chn_ctl_get_direction() +{ + return relay_chn_output_get_direction(chn_ctl.output); +} +/* relay_chn APIs */ + +relay_chn_ctl_t *relay_chn_ctl_get() +{ + return &chn_ctl; +} \ No newline at end of file diff --git a/src/relay_chn_output.c b/src/relay_chn_output.c new file mode 100644 index 0000000..4f819d1 --- /dev/null +++ b/src/relay_chn_output.c @@ -0,0 +1,168 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + +#include "esp_check.h" +#include "esp_log.h" +#include "relay_chn_defs.h" +#include "relay_chn_output.h" +#include "relay_chn_core.h" + + +static const char *TAG = "RELAY_CHN_OUTPUT"; + +#if RELAY_CHN_COUNT > 1 +static relay_chn_output_t outputs[RELAY_CHN_COUNT]; +#else +static relay_chn_output_t output; +#endif + + +static esp_err_t relay_chn_output_check_gpio_capabilities(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; + } + return ESP_OK; +} + +static esp_err_t relay_chn_output_ctl_init(relay_chn_output_t *output, gpio_num_t forward_pin, gpio_num_t reverse_pin) +{ + ESP_RETURN_ON_FALSE(GPIO_IS_VALID_OUTPUT_GPIO(forward_pin), ESP_ERR_INVALID_ARG, TAG, + "Invalid GPIO pin number for forward_pin: %d", forward_pin); + ESP_RETURN_ON_FALSE(GPIO_IS_VALID_OUTPUT_GPIO(reverse_pin), ESP_ERR_INVALID_ARG, TAG, + "Invalid GPIO pin number for reverse_pin: %d", reverse_pin); + + // Check if the GPIOs are valid + esp_err_t ret; + // Initialize the GPIOs + ret = gpio_reset_pin(forward_pin); + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to reset GPIO forward pin: %d", forward_pin); + ret = gpio_set_direction(forward_pin, GPIO_MODE_OUTPUT); + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set GPIO direction for forward pin: %d", forward_pin); + + ret = gpio_reset_pin(reverse_pin); + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to reset GPIO reverse pin: %d", reverse_pin); + ret = gpio_set_direction(reverse_pin, GPIO_MODE_OUTPUT); + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set GPIO direction for reverse pin: %d", reverse_pin); + // Initialize the GPIOs + + // Initialize the relay channel output + output->forward_pin = forward_pin; + output->reverse_pin = reverse_pin; + output->direction = RELAY_CHN_DIRECTION_DEFAULT; + return ESP_OK; +} + +esp_err_t relay_chn_output_init(const uint8_t* gpio_map, uint8_t gpio_count) +{ + esp_err_t ret; + ret = relay_chn_output_check_gpio_capabilities(gpio_count); + ESP_RETURN_ON_ERROR(ret, TAG, "Device does not support the provided GPIOs"); + +#if RELAY_CHN_COUNT > 1 + for (int i = 0; i < RELAY_CHN_COUNT; i++) { + relay_chn_output_t* output = &outputs[i]; + int gpio_index = i << 1; // gpio_index = i * 2 + gpio_num_t forward_pin = (gpio_num_t) gpio_map[gpio_index]; + gpio_num_t reverse_pin = (gpio_num_t) gpio_map[gpio_index + 1]; + + ret = relay_chn_output_ctl_init(output, forward_pin, reverse_pin); + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize relay channel %d", i); + } +#else + ret = relay_chn_output_ctl_init(&output, gpio_map[0], gpio_map[1]); + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize relay channel"); +#endif + return ESP_OK; +} + +static void relay_chn_output_ctl_deinit(relay_chn_output_t *output) +{ + gpio_reset_pin(output->forward_pin); + gpio_reset_pin(output->reverse_pin); +} + +void relay_chn_output_deinit() +{ +#if RELAY_CHN_COUNT > 1 + for (int i = 0; i < RELAY_CHN_COUNT; i++) { + relay_chn_output_ctl_deinit(&outputs[i]); + } +#else + relay_chn_output_ctl_deinit(&output); +#endif // RELAY_CHN_COUNT > 1 +} + +#if RELAY_CHN_COUNT > 1 +relay_chn_output_t *relay_chn_output_get(uint8_t chn_id) +{ + if (!relay_chn_is_channel_id_valid(chn_id)) { + return NULL; + } + return &outputs[chn_id]; +} + +relay_chn_output_t *relay_chn_output_get_all(void) +{ + return outputs; +} +#else +relay_chn_output_t *relay_chn_output_get(void) +{ + return &output; +} +#endif // RELAY_CHN_COUNT > 1 + +esp_err_t relay_chn_output_stop(relay_chn_output_t *output) +{ + esp_err_t ret; + ret = gpio_set_level(output->forward_pin, 0); + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set forward pin to LOW"); + return gpio_set_level(output->reverse_pin, 0); +} + +esp_err_t relay_chn_output_forward(relay_chn_output_t *output) +{ + esp_err_t ret; + ret = gpio_set_level(output->forward_pin, 1); + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set forward pin to HIGH"); + return gpio_set_level(output->reverse_pin, 0); +} + +esp_err_t relay_chn_output_reverse(relay_chn_output_t *output) +{ + esp_err_t ret; + ret = gpio_set_level(output->forward_pin, 0); + ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set forward pin to LOW"); + return gpio_set_level(output->reverse_pin, 1); +} + +void relay_chn_output_flip(relay_chn_output_t *output) +{ + // Flip the output GPIO pins + gpio_num_t temp = output->forward_pin; + output->forward_pin = output->reverse_pin; + output->reverse_pin = temp; + // Flip the direction + output->direction = (output->direction == RELAY_CHN_DIRECTION_DEFAULT) + ? RELAY_CHN_DIRECTION_FLIPPED + : RELAY_CHN_DIRECTION_DEFAULT; +} + +relay_chn_direction_t relay_chn_output_get_direction(relay_chn_output_t *output) +{ + return output->direction; +} \ No newline at end of file diff --git a/src/relay_chn_run_info.c b/src/relay_chn_run_info.c new file mode 100644 index 0000000..e021023 --- /dev/null +++ b/src/relay_chn_run_info.c @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + +#include "relay_chn_core.h" +#include "relay_chn_run_info.h" + + +#if RELAY_CHN_COUNT > 1 +static relay_chn_run_info_t run_infos[RELAY_CHN_COUNT]; +#else +static relay_chn_run_info_t run_info; +#endif + +void relay_chn_run_info_init() +{ +#if RELAY_CHN_COUNT > 1 + for (int i = 0; i < RELAY_CHN_COUNT; i++) { + run_infos[i].last_run_cmd = RELAY_CHN_CMD_NONE; + run_infos[i].last_run_cmd_time_ms = 0; + } +#else + run_info.last_run_cmd = RELAY_CHN_CMD_NONE; + run_info.last_run_cmd_time_ms = 0; +#endif +} + +#if RELAY_CHN_COUNT > 1 +relay_chn_run_info_t *relay_chn_run_info_get(uint8_t chn_id) +{ + if (!relay_chn_is_channel_id_valid(chn_id)) { + return NULL; + } + return &run_infos[chn_id]; +} + +relay_chn_run_info_t *relay_chn_run_info_get_all() +{ + return run_infos; +} +#else +relay_chn_run_info_t *relay_chn_run_info_get() +{ + return &run_info; +} +#endif // RELAY_CHN_COUNT > 1 + +relay_chn_cmd_t relay_chn_run_info_get_last_run_cmd(relay_chn_run_info_t *run_info) +{ + return run_info == NULL ? RELAY_CHN_CMD_NONE : run_info->last_run_cmd; +} + +void relay_chn_run_info_set_last_run_cmd(relay_chn_run_info_t *run_info, relay_chn_cmd_t cmd) +{ + if (!run_info) { + return; + } + run_info->last_run_cmd = cmd; +} + +uint32_t relay_chn_run_info_get_last_run_cmd_time_ms(relay_chn_run_info_t *run_info) +{ + return run_info == NULL ? 0 : run_info->last_run_cmd_time_ms; +} + +void relay_chn_run_info_set_last_run_cmd_time_ms(relay_chn_run_info_t *run_info, uint32_t time_ms) +{ + if (!run_info) { + return; + } + run_info->last_run_cmd_time_ms = time_ms; +} \ No newline at end of file diff --git a/src/relay_chn_tilt.c b/src/relay_chn_tilt.c new file mode 100644 index 0000000..de7610f --- /dev/null +++ b/src/relay_chn_tilt.c @@ -0,0 +1,634 @@ +/* + * SPDX-FileCopyrightText: 2025 Kozmotronik Tech + * + * SPDX-License-Identifier: MIT + */ + +#include "esp_check.h" +#include "relay_chn_core.h" +#include "relay_chn_output.h" +#include "relay_chn_run_info.h" +#include "relay_chn_tilt.h" + + +static const char *TAG = "RELAY_CHN_TILT"; + +/**@{*/ +/* + * 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 steps. +typedef 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 */ +} relay_chn_tilt_step_t; + +/// @brief Tilt timing structure to manage tilt pattern timing. +typedef 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 { + 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_ctl { + relay_chn_ctl_t *chn_ctl; /*!< The relay channel control structure */ + 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_ctl_t; + + +#if RELAY_CHN_COUNT > 1 +static relay_chn_tilt_ctl_t tilt_ctls[RELAY_CHN_COUNT]; +#else +static relay_chn_tilt_ctl_t tilt_ctl; +#endif + + +esp_err_t relay_chn_tilt_dispatch_cmd(relay_chn_tilt_ctl_t *tilt_ctl, relay_chn_tilt_cmd_t cmd) +{ + if (cmd == RELAY_CHN_TILT_CMD_NONE) return ESP_ERR_INVALID_ARG; + + // Since the event_loop library creates a deep copy of the event data, + // and we need to pass the pointer of the relevant tilt control, here we need + // to pass the pointer to the pointer of the tilt_control (&tilt_ctl) so that + // the pointer value is preserved in the event data. + return esp_event_post_to(relay_chn_event_loop, + RELAY_CHN_TILT_CMD_EVENT_BASE, + cmd, + &tilt_ctl, + sizeof(tilt_ctl), portMAX_DELAY); +} + +// Returns the required timing before tilting depending on the last run. +static uint32_t relay_chn_tilt_get_required_timing_before_tilting(relay_chn_tilt_ctl_t *tilt_ctl, relay_chn_tilt_cmd_t cmd) +{ + relay_chn_cmd_t last_run_cmd = relay_chn_run_info_get_last_run_cmd(tilt_ctl->chn_ctl->run_info); + if (cmd == RELAY_CHN_TILT_CMD_FORWARD && last_run_cmd == RELAY_CHN_CMD_REVERSE) + return 0; + else if (cmd == RELAY_CHN_TILT_CMD_REVERSE && last_run_cmd == RELAY_CHN_CMD_FORWARD) + return 0; + + uint32_t last_run_cmd_time_ms = relay_chn_run_info_get_last_run_cmd_time_ms(tilt_ctl->chn_ctl->run_info); + uint32_t inertia_time_passed_ms = (uint32_t) (esp_timer_get_time() / 1000) - last_run_cmd_time_ms; + return RELAY_CHN_OPPOSITE_INERTIA_MS - inertia_time_passed_ms; +} + +// Issue a tilt command to a specific relay channel. +static void relay_chn_tilt_issue_cmd(relay_chn_tilt_ctl_t *tilt_ctl, relay_chn_tilt_cmd_t cmd) +{ + if (relay_chn_run_info_get_last_run_cmd(tilt_ctl->chn_ctl->run_info) == RELAY_CHN_CMD_NONE) { + // Do not tilt if the channel hasn't been run before + ESP_LOGD(TAG, "relay_chn_tilt_issue_cmd: Tilt will not be executed since the channel hasn't been run yet"); + return; + } + + if (tilt_ctl->cmd == cmd) { + ESP_LOGD(TAG, "relay_chn_tilt_issue_cmd: There is already a tilt command in progress!"); + return; + } + + // Set the command that will be processed + tilt_ctl->cmd = cmd; + switch (tilt_ctl->chn_ctl->state) { + case RELAY_CHN_STATE_IDLE: + // Relay channel is free, tilt can be issued immediately + relay_chn_tilt_dispatch_cmd(tilt_ctl, 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(tilt_ctl->chn_ctl, 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_tilt_get_required_timing_before_tilting(tilt_ctl, cmd); + if (req_timing_ms == 0) { + relay_chn_tilt_dispatch_cmd(tilt_ctl, cmd); + } else { + // Channel needs timing before running tilting action, schedule it + tilt_ctl->step = RELAY_CHN_TILT_STEP_PENDING; + relay_chn_start_esp_timer_once(tilt_ctl->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(tilt_ctl->chn_ctl, RELAY_CHN_CMD_STOP); + // Schedule for tilting + tilt_ctl->step = RELAY_CHN_TILT_STEP_PENDING; + relay_chn_start_esp_timer_once(tilt_ctl->tilt_timer, RELAY_CHN_OPPOSITE_INERTIA_MS); + } else if (cmd == RELAY_CHN_TILT_CMD_REVERSE) { + // Stop the running channel first + relay_chn_dispatch_cmd(tilt_ctl->chn_ctl, RELAY_CHN_CMD_STOP); + // If the tilt cmd is TILT_REVERSE then dispatch it immediately + relay_chn_tilt_dispatch_cmd(tilt_ctl, cmd); + } + break; + + case RELAY_CHN_STATE_REVERSE: + if (cmd == RELAY_CHN_TILT_CMD_REVERSE) { + // Stop the running channel first + relay_chn_dispatch_cmd(tilt_ctl->chn_ctl, RELAY_CHN_CMD_STOP); + // Schedule for tilting + tilt_ctl->step = RELAY_CHN_TILT_STEP_PENDING; + relay_chn_start_esp_timer_once(tilt_ctl->tilt_timer, RELAY_CHN_OPPOSITE_INERTIA_MS); + } else if (cmd == RELAY_CHN_TILT_CMD_FORWARD) { + // Stop the running channel first + relay_chn_dispatch_cmd(tilt_ctl->chn_ctl, RELAY_CHN_CMD_STOP); + // If the tilt cmd is TILT_FORWARD then dispatch it immediately + relay_chn_tilt_dispatch_cmd(tilt_ctl, cmd); + } + break; + + default: + ESP_LOGD(TAG, "relay_chn_tilt_issue_cmd: Unexpected relay channel state: %s!", relay_chn_state_str(tilt_ctl->chn_ctl->state)); + } +} + +static void relay_chn_tilt_issue_auto(relay_chn_tilt_ctl_t *tilt_ctl) +{ + relay_chn_cmd_t last_run_cmd = relay_chn_run_info_get_last_run_cmd(tilt_ctl->chn_ctl->run_info); + if (last_run_cmd == RELAY_CHN_CMD_FORWARD || tilt_ctl->chn_ctl->state == RELAY_CHN_STATE_FORWARD) { + relay_chn_tilt_issue_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_FORWARD); + } + else if (last_run_cmd == RELAY_CHN_CMD_REVERSE || tilt_ctl->chn_ctl->state == RELAY_CHN_STATE_REVERSE) { + relay_chn_tilt_issue_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_REVERSE); + } +} + +#if RELAY_CHN_COUNT > 1 +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_tilt_issue_auto(&tilt_ctls[i]); + } + } + // Execute for a single channel + else { + relay_chn_tilt_ctl_t* tilt_ctl = &tilt_ctls[chn_id]; + relay_chn_tilt_issue_auto(tilt_ctl); + } +} + +static void relay_chn_tilt_issue_cmd_on_all_channels(relay_chn_tilt_cmd_t cmd) +{ + for (int i = 0; i < RELAY_CHN_COUNT; i++) { + relay_chn_tilt_ctl_t* tilt_ctl = &tilt_ctls[i]; + relay_chn_tilt_issue_cmd(tilt_ctl, cmd); + } +} + +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_tilt_issue_cmd_on_all_channels(RELAY_CHN_TILT_CMD_FORWARD); + else { + relay_chn_tilt_ctl_t* tilt_ctl = &tilt_ctls[chn_id]; + relay_chn_tilt_issue_cmd(tilt_ctl, 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_tilt_issue_cmd_on_all_channels(RELAY_CHN_TILT_CMD_REVERSE); + else { + relay_chn_tilt_ctl_t* tilt_ctl = &tilt_ctls[chn_id]; + relay_chn_tilt_issue_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_REVERSE); + } +} + +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_dispatch_cmd(&tilt_ctls[i], RELAY_CHN_TILT_CMD_STOP); + } + } + else { + relay_chn_tilt_dispatch_cmd(&tilt_ctls[chn_id], RELAY_CHN_TILT_CMD_STOP); + } +} + +#else // RELAY_CHN_COUNT > 1 + +void relay_chn_tilt_auto() +{ + relay_chn_tilt_issue_auto(&tilt_ctl); +} + +void relay_chn_tilt_forward() +{ + relay_chn_tilt_issue_cmd(&tilt_ctl, RELAY_CHN_TILT_CMD_FORWARD); +} + +void relay_chn_tilt_reverse() +{ + relay_chn_tilt_issue_cmd(&tilt_ctl, RELAY_CHN_TILT_CMD_REVERSE); +} + +void relay_chn_tilt_stop() +{ + relay_chn_tilt_dispatch_cmd(&tilt_ctl, RELAY_CHN_TILT_CMD_STOP); +} +#endif // RELAY_CHN_COUNT > 1 + +static void relay_chn_tilt_set_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_tilt_ctl_t *tilt_ctl, uint8_t sensitivity) +{ + if (sensitivity >= 100) { + relay_chn_tilt_set_timing_values(&tilt_ctl->tilt_timing, + 100, + RELAY_CHN_TILT_RUN_MAX_MS, + RELAY_CHN_TILT_PAUSE_MAX_MS); + } + else if (sensitivity == 0) { + relay_chn_tilt_set_timing_values(&tilt_ctl->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_tilt_set_timing_values(&tilt_ctl->tilt_timing, + sensitivity, + tilt_run_time_ms, + tilt_pause_time_ms); + } +} + +#if RELAY_CHN_COUNT > 1 +void relay_chn_tilt_set_sensitivity(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(&tilt_ctls[i], sensitivity); + } + } + else { + _relay_chn_tilt_sensitivity_set(&tilt_ctls[chn_id], sensitivity); + } +} + +esp_err_t relay_chn_tilt_get_sensitivity(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_get_sensitivity: 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_get_sensitivity: 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] = tilt_ctls[i].tilt_timing.sensitivity; + } + return ESP_OK; + } + *sensitivity = tilt_ctls[chn_id].tilt_timing.sensitivity; + return ESP_OK; +} + +#else + +void relay_chn_tilt_set_sensitivity(uint8_t sensitivity) +{ + _relay_chn_tilt_sensitivity_set(&tilt_ctl, sensitivity); +} + +uint8_t relay_chn_tilt_get_sensitivity() +{ + return tilt_ctl.tilt_timing.sensitivity; +} +#endif // RELAY_CHN_COUNT > 1 + +void relay_chn_tilt_reset_count(relay_chn_tilt_ctl_t *tilt_ctl) +{ + tilt_ctl->tilt_counter.tilt_forward_count = 0; + tilt_ctl->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_type::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_type::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 tilt_ctl 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_tilt_ctl_t *tilt_ctl) +{ + relay_chn_cmd_t last_run_cmd = relay_chn_run_info_get_last_run_cmd(tilt_ctl->chn_ctl->run_info); + if (last_run_cmd == RELAY_CHN_CMD_FORWARD) { + if (tilt_ctl->cmd == RELAY_CHN_TILT_CMD_FORWARD) { + return ++tilt_ctl->tilt_counter.tilt_forward_count; + } + else if (tilt_ctl->cmd == RELAY_CHN_TILT_CMD_REVERSE) { + if (tilt_ctl->tilt_counter.tilt_forward_count > 0) { + --tilt_ctl->tilt_counter.tilt_forward_count; + // Still should do one more move, return non-zero value + return 1; + } + else + return 0; + } + else { + relay_chn_tilt_reset_count(tilt_ctl); + return 0; + } + } + else if (last_run_cmd == RELAY_CHN_CMD_REVERSE) { + if (tilt_ctl->cmd == RELAY_CHN_TILT_CMD_REVERSE) { + return ++tilt_ctl->tilt_counter.tilt_reverse_count; + } + else if (tilt_ctl->cmd == RELAY_CHN_TILT_CMD_FORWARD) { + if (tilt_ctl->tilt_counter.tilt_reverse_count > 0) { + --tilt_ctl->tilt_counter.tilt_reverse_count; + // Still should do one more move, return non-zero value + return 1; + } + else + return 0; + } + else { + relay_chn_tilt_reset_count(tilt_ctl); + return 0; + } + } + return 0; +} + +static void relay_chn_tilt_execute_stop(relay_chn_tilt_ctl_t *tilt_ctl) +{ + // Stop the channel's timer if active + esp_timer_stop(tilt_ctl->tilt_timer); + // Invalidate tilt cmd and step + tilt_ctl->cmd = RELAY_CHN_TILT_CMD_NONE; + tilt_ctl->step = RELAY_CHN_TILT_STEP_NONE; + // Stop the channel + if (relay_chn_output_stop(tilt_ctl->chn_ctl->output) != ESP_OK) { + ESP_LOGE(TAG, "relay_chn_tilt_execute_stop: Failed to output stop for relay channel #%d!", tilt_ctl->chn_ctl->id); + } + relay_chn_dispatch_cmd(tilt_ctl->chn_ctl, RELAY_CHN_CMD_STOP); +} + +static void relay_chn_tilt_execute_forward(relay_chn_tilt_ctl_t *tilt_ctl) +{ + if (relay_chn_output_reverse(tilt_ctl->chn_ctl->output) != ESP_OK) { + ESP_LOGE(TAG, "relay_chn_tilt_execute_forward: Failed to output reverse for relay channel #%d!", tilt_ctl->chn_ctl->id); + // Stop tilting because of the error + relay_chn_tilt_dispatch_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_STOP); + return; + } + // Set the move time timer + relay_chn_start_esp_timer_once(tilt_ctl->tilt_timer, tilt_ctl->tilt_timing.move_time_ms); + // Set to pause step + tilt_ctl->step = RELAY_CHN_TILT_STEP_PAUSE; +} + +static void relay_chn_tilt_execute_reverse(relay_chn_tilt_ctl_t *tilt_ctl) +{ + if (relay_chn_output_forward(tilt_ctl->chn_ctl->output) != ESP_OK) { + ESP_LOGE(TAG, "relay_chn_tilt_execute_reverse: Failed to output forward for relay channel #%d!", tilt_ctl->chn_ctl->id); + // Stop tilting because of the error + relay_chn_tilt_dispatch_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_STOP); + return; + } + // Set the move time timer + relay_chn_start_esp_timer_once(tilt_ctl->tilt_timer, tilt_ctl->tilt_timing.move_time_ms); + // Set to pause step + tilt_ctl->step = RELAY_CHN_TILT_STEP_PAUSE; +} + +static void relay_chn_tilt_execute_pause(relay_chn_tilt_ctl_t *tilt_ctl) +{ + // Pause the channel + if (relay_chn_output_stop(tilt_ctl->chn_ctl->output) != ESP_OK) { + ESP_LOGE(TAG, "relay_chn_tilt_execute_pause: Failed to output stop for relay channel #%d!", tilt_ctl->chn_ctl->id); + // Stop tilting because of the error + relay_chn_tilt_dispatch_cmd(tilt_ctl, 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(tilt_ctl) == 0) { + ESP_LOGD(TAG, "relay_chn_tilt_execute_pause: Relay channel cannot tilt anymore"); + // Stop tilting since the tilting limit has been reached + relay_chn_tilt_dispatch_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_STOP); + return; + } + + // Set the pause time timer + relay_chn_start_esp_timer_once(tilt_ctl->tilt_timer, tilt_ctl->tilt_timing.pause_time_ms); + // Set to move step + tilt_ctl->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) +{ + relay_chn_tilt_ctl_t* tilt_ctl = *(relay_chn_tilt_ctl_t**) event_data; + ESP_RETURN_VOID_ON_FALSE(tilt_ctl != NULL, TAG, "event_data is NULL"); + ESP_LOGD(TAG, "relay_chn_event_handler: Command: %s", relay_chn_cmd_str(event_id)); + + switch(event_id) { + case RELAY_CHN_TILT_CMD_STOP: + relay_chn_tilt_execute_stop(tilt_ctl); + break; + case RELAY_CHN_TILT_CMD_FORWARD: + relay_chn_tilt_execute_forward(tilt_ctl); + // Update channel state + relay_chn_update_state(tilt_ctl->chn_ctl, RELAY_CHN_STATE_TILT_FORWARD); + break; + case RELAY_CHN_TILT_CMD_REVERSE: + relay_chn_tilt_execute_reverse(tilt_ctl); + // Update channel state + relay_chn_update_state(tilt_ctl->chn_ctl, 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) +{ + relay_chn_tilt_ctl_t* tilt_ctl = (relay_chn_tilt_ctl_t*) arg; + ESP_RETURN_VOID_ON_FALSE(tilt_ctl != NULL, TAG, "relay_chn_tilt_timer_cb: event_data is NULL"); + + switch (tilt_ctl->step) + { + case RELAY_CHN_TILT_STEP_MOVE: + if (tilt_ctl->cmd == RELAY_CHN_TILT_CMD_FORWARD) { + relay_chn_tilt_execute_forward(tilt_ctl); + } + else if (tilt_ctl->cmd == RELAY_CHN_TILT_CMD_REVERSE) { + relay_chn_tilt_execute_reverse(tilt_ctl); + } + break; + + case RELAY_CHN_TILT_STEP_PAUSE: + relay_chn_tilt_execute_pause(tilt_ctl); + break; + + case RELAY_CHN_TILT_STEP_PENDING: + // Just dispatch the pending tilt command + relay_chn_tilt_dispatch_cmd(tilt_ctl, tilt_ctl->cmd); + break; + + default: + break; + } +} + +esp_err_t relay_chn_tilt_ctl_init(relay_chn_tilt_ctl_t *tilt_ctl, relay_chn_ctl_t *chn_ctl) +{ + tilt_ctl->cmd = RELAY_CHN_TILT_CMD_NONE; + tilt_ctl->step = RELAY_CHN_TILT_STEP_NONE; + tilt_ctl->tilt_timing.sensitivity = RELAY_CHN_TILT_DEFAULT_SENSITIVITY; + tilt_ctl->tilt_timing.move_time_ms = RELAY_CHN_TILT_DEFAULT_RUN_MS; + tilt_ctl->tilt_timing.pause_time_ms = RELAY_CHN_TILT_DEFAULT_PAUSE_MS; + relay_chn_tilt_reset_count(tilt_ctl); + + tilt_ctl->chn_ctl = chn_ctl; + tilt_ctl->chn_ctl->tilt_ctl = tilt_ctl; // + + // Create tilt timer for the channel + char timer_name[32]; + snprintf(timer_name, sizeof(timer_name), "relay_chn_%2d_tilt_timer", chn_ctl->id); + esp_timer_create_args_t timer_args = { + .callback = relay_chn_tilt_timer_cb, + .arg = tilt_ctl, + .name = timer_name + }; + return esp_timer_create(&timer_args, &tilt_ctl->tilt_timer); +} + +esp_err_t relay_chn_tilt_init(relay_chn_ctl_t *chn_ctls) +{ +#if RELAY_CHN_COUNT > 1 + for (int i = 0; i < RELAY_CHN_COUNT; i++) { + relay_chn_tilt_ctl_init(&tilt_ctls[i], &chn_ctls[i]); + } +#else + relay_chn_tilt_ctl_init(&tilt_ctl, chn_ctls); +#endif // RELAY_CHN_COUNT > 1 + + return 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); +} + +void relay_chn_tilt_ctl_deinit(relay_chn_tilt_ctl_t *tilt_ctl) +{ + if (tilt_ctl->tilt_timer != NULL) { + esp_timer_delete(tilt_ctl->tilt_timer); + tilt_ctl->tilt_timer = NULL; + } +} + +void relay_chn_tilt_deinit() +{ +#if RELAY_CHN_COUNT > 1 + for (int i = 0; i < RELAY_CHN_COUNT; i++) { + relay_chn_tilt_ctl_deinit(&tilt_ctls[i]); + } +#else + relay_chn_tilt_ctl_deinit(&tilt_ctl); +#endif // RELAY_CHN_COUNT > 1 + esp_event_handler_unregister_with(relay_chn_event_loop, + RELAY_CHN_TILT_CMD_EVENT_BASE, + ESP_EVENT_ANY_ID, + relay_chn_tilt_event_handler); +} \ No newline at end of file