Files
relay_chn/src/relay_chn_core.c
ismail 087deb338e Fix STOP command issuing when idle
Fixed almost unconditional STOP command issuing when the
channel is idle. Refs #1104, #1105 and closes #1107.
2025-09-09 09:26:24 +03:00

498 lines
18 KiB
C

/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#include <stdio.h>
#include <stdlib.h>
#include "esp_check.h"
#include "esp_task.h"
#include "relay_chn_output.h"
#include "relay_chn_run_info.h"
#include "relay_chn_ctl.h"
#include "relay_chn_notify.h"
#if CONFIG_RELAY_CHN_ENABLE_TILTING
#include "relay_chn_tilt.h"
#endif
#if CONFIG_RELAY_CHN_ENABLE_NVS
#include "relay_chn_nvs.h"
#endif
#include "relay_chn_core.h"
static const char *TAG = "RELAY_CHN_CORE";
#if CONFIG_RELAY_CHN_ENABLE_RUN_LIMIT
/*
* Run limit timer callback immediately dispatches a STOP command for the
* relevant channel as soon as the run limit time times out
*/
static void relay_chn_run_limit_timer_cb(void* arg)
{
relay_chn_ctl_t* chn_ctl = (relay_chn_ctl_t*) arg;
relay_chn_dispatch_cmd(chn_ctl, RELAY_CHN_CMD_STOP);
}
esp_err_t relay_chn_init_run_limit_timer(relay_chn_ctl_t *chn_ctl)
{
char timer_name[32];
snprintf(timer_name, sizeof(timer_name), "ch_%d_rlimit_timer", chn_ctl->id);
esp_timer_create_args_t timer_args = {
.callback = relay_chn_run_limit_timer_cb,
.arg = chn_ctl,
.name = timer_name
};
return esp_timer_create(&timer_args, &chn_ctl->run_limit_timer);
}
#endif
// 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);
}
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;
#if CONFIG_RELAY_CHN_ENABLE_NVS
ret = relay_chn_nvs_init();
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize NVS for relay channel");
#endif
// 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 CONFIG_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");
#if CONFIG_RELAY_CHN_ENABLE_TILTING
// Initialize the tilt feature
#if CONFIG_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 // CONFIG_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
// Initialize the notify feature
ret = relay_chn_notify_init();
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize notify feature");
return ret;
}
void relay_chn_destroy(void)
{
#if CONFIG_RELAY_CHN_ENABLE_TILTING
relay_chn_tilt_deinit();
#endif
relay_chn_notify_deinit();
relay_chn_ctl_deinit();
relay_chn_output_deinit();
#if CONFIG_RELAY_CHN_ENABLE_NVS
relay_chn_nvs_deinit();
#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;
relay_chn_notify_state_change(chn_ctl->id, old_state, new_state);
}
static void relay_chn_execute_idle(relay_chn_ctl_t *chn_ctl);
static void relay_chn_start_timer_or_idle(relay_chn_ctl_t *chn_ctl, esp_timer_handle_t timer, uint32_t time_ms, const char* timer_name)
{
if (relay_chn_start_esp_timer_once(timer, time_ms) != ESP_OK) {
ESP_LOGE(TAG, "Failed to start %s timer for ch %d", timer_name, chn_ctl->id);
// Attempt to go to a safe state.
// relay_chn_execute_idle is safe to call, it stops timers and sets state.
relay_chn_execute_idle(chn_ctl);
}
}
static void relay_chn_stop_prv(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);
// 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));
}
}
/**
* @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 || chn_ctl->state == RELAY_CHN_STATE_IDLE) {
return; // Do nothing if already stopped or idle
}
// 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 current_time_ms = (uint32_t)(esp_timer_get_time() / 1000);
if (current_time_ms < last_run_cmd_time_ms) { // Timer overflow
// If timer overflowed, it's been a long time. Run immediately.
relay_chn_dispatch_cmd(chn_ctl, cmd);
} else {
uint32_t inertia_time_passed_ms = current_time_ms - last_run_cmd_time_ms;
if (inertia_time_passed_ms < CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS) {
uint32_t inertia_time_ms = CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS - inertia_time_passed_ms;
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
if (relay_chn_start_esp_timer_once(chn_ctl->inertia_timer, inertia_time_ms) != ESP_OK) {
ESP_LOGE(TAG, "Failed to start inertia timer for ch %d", chn_ctl->id);
relay_chn_execute_idle(chn_ctl);
}
} 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_stop_prv(chn_ctl);
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_stop_prv(chn_ctl);
// 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_timer_or_idle(chn_ctl, chn_ctl->inertia_timer, CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS, "inertia");
break;
#if CONFIG_RELAY_CHN_ENABLE_TILTING
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, CONFIG_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, CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS);
}
break;
#endif
default: ESP_LOGD(TAG, "relay_chn_evaluate: Unknown relay channel state!");
}
}
#if CONFIG_RELAY_CHN_COUNT > 1
bool relay_chn_is_channel_id_valid(uint8_t chn_id)
{
bool valid = chn_id < CONFIG_RELAY_CHN_COUNT;
if (!valid) {
ESP_LOGE(TAG, "Invalid channel ID: %d", chn_id);
}
return valid;
}
#endif // CONFIG_RELAY_CHN_COUNT > 1
static 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_execute_stop(relay_chn_ctl_t *chn_ctl)
{
relay_chn_stop_prv(chn_ctl);
#if CONFIG_RELAY_CHN_ENABLE_RUN_LIMIT
esp_timer_stop(chn_ctl->run_limit_timer);
#endif
// Invalidate the channel's timer if it is active
esp_timer_stop(chn_ctl->inertia_timer);
relay_chn_cmd_t last_run_cmd = relay_chn_run_info_get_last_run_cmd(chn_ctl->run_info);
if (last_run_cmd == RELAY_CHN_CMD_FORWARD || last_run_cmd == RELAY_CHN_CMD_REVERSE ) {
// Schedule IDLE
chn_ctl->pending_cmd = RELAY_CHN_CMD_IDLE;
relay_chn_start_timer_or_idle(chn_ctl, chn_ctl->inertia_timer, CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS, "idle");
} else {
// If the channel was not running forward or reverse, issue a free command immediately
relay_chn_execute_idle(chn_ctl);
}
}
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);
#if CONFIG_RELAY_CHN_ENABLE_RUN_LIMIT
relay_chn_start_timer_or_idle(chn_ctl, chn_ctl->run_limit_timer, chn_ctl->run_limit_sec * 1000, "run limit");
#endif
}
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);
#if CONFIG_RELAY_CHN_ENABLE_RUN_LIMIT
relay_chn_start_timer_or_idle(chn_ctl, chn_ctl->run_limit_timer, chn_ctl->run_limit_sec * 1000, "run limit");
#endif
}
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_timer_or_idle(chn_ctl, chn_ctl->inertia_timer, CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS, "flip inertia");
}
// Dispatch relay channel command
void relay_chn_dispatch_cmd(relay_chn_ctl_t *chn_ctl, relay_chn_cmd_t cmd) {
ESP_LOGD(TAG, "relay_chn_dispatch_cmd: Command: %d", cmd);
switch (cmd) {
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!");
}
#if CONFIG_RELAY_CHN_ENABLE_TILTING
// 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
}
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 CONFIG_RELAY_CHN_ENABLE_TILTING
case RELAY_CHN_STATE_TILT_FORWARD:
return "TILT_FORWARD";
case RELAY_CHN_STATE_TILT_REVERSE:
return "TILT_REVERSE";
#endif
default:
return "UNKNOWN";
}
}