/* * 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" #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"; } }