42 Commits

Author SHA1 Message Date
1776c81c8d Merge pull request 'release-0.5.0' (!31) from release-0.5.0 into main
Reviewed-on: #31
2025-07-23 17:48:18 +03:00
2e81966afb Bump version to 0.5.0 and update repo URLs. 2025-07-23 17:37:27 +03:00
5734f47cd3 Merge pull request 'feat/1030-more-unit-tests' (!29) from feat/1030-more-unit-tests into dev
Reviewed-on: #29
2025-07-22 16:59:47 +03:00
d884f5f45c Add missing test cases for tilt API.
Added missing test cases for the tilt API.

Closes #1056.
2025-07-22 10:02:40 +03:00
c7678d6084 Add restart chip to make qemu exit in tests. 2025-07-22 10:00:33 +03:00
8527ebea83 Fix imbalanced tilt counts.
Fixed the tilt count logic that causes an imbalance in tilting within the same run frame but opposite direction.

Fixes #1057.
2025-07-22 09:58:59 +03:00
f31eae649f Add a shell utility for running tests. 2025-07-21 18:47:41 +03:00
a143484748 Break down tests into categories.
Break down tests into categories to improve maintainability and test granularity. This makes it easier to execute unit tests in CI/CD pipelines.

Closes #1054.
2025-07-21 15:37:49 +03:00
a9a8169710 Add test cases for ID_ALL channel id.
Add test cases to test all relevant functions that support operating with the `RELAY_CHN_ID_ALL` channel id.

Closes #1052.
2025-07-16 11:42:44 +03:00
74f4341c1d Add test cases for direction flip.
Closes #1051.
2025-07-15 16:14:19 +03:00
a587036093 Add tests for init error handling.
Added tests for covering initialization error handling cases like; NULL pointer, invalid GPIO count etc.

This changed implied removing the `relay_chn_create` from the Unity's `setUp` function and place it in each testcase.

Refs #1050, #1030.
2025-07-15 12:23:21 +03:00
82312ba7c3 Add NULL handling for the gpio_num pointer.
Fixes #1050.
2025-07-15 12:17:46 +03:00
db62a7b5b2 Fix listener memory allocation bug.
- Replaced the buggy, oldschool, plain pointer based list approach with more robust FreeRTOS linked list implementation for the listener API. Fixes #1049.

- Added relevant test cases. Refs #1030.
2025-07-14 18:49:47 +03:00
1ee70be715 Fix invalid ID test loops. 2025-07-14 18:49:47 +03:00
f1cb928341 Add missing destroy function.
The component allocates resources (timers, event loop) in relay_chn_create but never frees them. This is a resource leak.
Hence, a destroy function added to free the resources gracefully.

Fixes #1048.
2025-07-14 11:37:14 +03:00
a90649a4d3 Clean and add more tests.
Clean the unnecessary codes and logging macros to keep the test output cleaner.
Add fail tests for the `get_state*` functions.
2025-07-11 18:32:28 +03:00
b239b50abe Fix invalid channel ID handling.
Fix the issue where get_state* functions do not handle when id is RELAY_CHN_ID_ALL.
Fixes #1037.
2025-07-11 18:28:59 +03:00
9b2274ed7c Add default configs for unit tests. 2025-07-11 17:35:50 +03:00
5a38956146 Restructure the project tree.
Restructured the project tree to align with Espressif's project tree pattern and practice. Also updated the affected cmake files.

Fixes: #1033
2025-07-07 22:57:02 +03:00
82168f34eb Delete this ghost file appeared out of nowhere. 2025-07-07 20:25:45 +03:00
2165e9d571 Merge pull request 'release-0.4.0' (!28) from release-0.4.0 into main
Reviewed-on: KozmotronikTech/relay_chn_component#28
2025-07-07 14:45:01 +03:00
0ebe1c791e Merge pull request 'fix/134-testing-issues' (!27) from fix/134-testing-issues into dev
Reviewed-on: KozmotronikTech/relay_chn_component#27
2025-07-07 14:31:27 +03:00
bacbe03e12 Update the manifest file with the latest information. 2025-07-07 09:50:36 +03:00
be09cb883a Correct the installation description.
Fixes  #1027
2025-07-07 09:39:29 +03:00
925fd5de74 Update descriptions about the inertia timing.
Fixes #1027
2025-07-07 09:27:28 +03:00
2e3e92bb63 Fix testing issues and add more tests.
Fixes #134.
Fix unit testing issues. Add more tests to cover more code.
2025-07-04 17:55:33 +03:00
c4482b8d49 Fix unhandled tilt to run mode transitions.
Fixes #1028.
This commit add unhandled logic to the relay_chn_issue_cmd function to handle transitions from tilt mode to run mode.
2025-07-04 17:31:31 +03:00
41c292cc89 Restructure the project tree for unit testing
Restructure the whole project tree so that the component can be unit tested. Also update some cmake files to update the modified paths, update test cases etc.
2025-07-04 00:38:57 +03:00
ed5b86e863 Fix CMakeLists.txt definitions and test cases. 2025-07-03 18:58:09 +03:00
c0c7fbf3df Merge pull request 'Fix error handling issues.' (!26) from release-0.3.2 into main
Reviewed-on: https://dev.kozmotronik.com.tr/gitea/KozmotronikTech/relay_chn/pulls/26
2025-04-03 10:21:37 +03:00
a1c66d51c7 Merge pull request 'Fix error handling issues.' from fix/172-fix-error-handling into dev
Reviewed-on: https://dev.kozmotronik.com.tr/gitea/KozmotronikTech/relay_chn/pulls/25
2025-04-02 14:39:49 +03:00
421dea7d69 Merge pull request 'fix/172-fix-error-handling' (!24) from fix/172-fix-error-handling into fix/134-testing-issues
Reviewed-on: https://dev.kozmotronik.com.tr/gitea/KozmotronikTech/relay_chn/pulls/24
2025-04-02 14:38:32 +03:00
99d753238b Fix error handling issues.
Fix error handling so that the value in ret variable does not become corrupt.
2025-04-02 14:05:48 +03:00
7afe6144bd Set dev branch's upstream. 2025-03-24 09:31:33 +03:00
4f39308f13 Merge pull request 'fix/162-fix-error-handling' from fix/162-fix-error-handling into fix/134-testing-issues
Reviewed-on: https://dev.kozmotronik.com.tr/gitea/KozmotronikTech/relay_chn/pulls/23
2025-03-21 17:30:13 +03:00
fb425edc4b Merge pull request 'release-0.3.1' from release-0.3.1 into main
Reviewed-on: https://dev.kozmotronik.com.tr/gitea/KozmotronikTech/relay_chn/pulls/22
2025-03-21 17:06:05 +03:00
805df016fe Merge pull request 'fix/162-fix-error-handling' from fix/162-fix-error-handling into dev
Reviewed-on: https://dev.kozmotronik.com.tr/gitea/KozmotronikTech/relay_chn/pulls/21
2025-03-21 16:44:34 +03:00
f230477cad Fix error handling in the init function. 2025-03-21 16:42:06 +03:00
e19bd09389 Update gitignore as per esp-idf gitignore. 2025-03-21 16:41:22 +03:00
11786b7a06 Remove unnecessary unity functions. 2025-03-04 09:49:04 +03:00
7c18ddcc04 Fix declarations as per the documents. 2025-03-04 09:48:24 +03:00
e8303a9418 Fix test directory structure. 2025-03-03 16:20:29 +03:00
22 changed files with 4321 additions and 304 deletions

180
.gitignore vendored
View File

@@ -1,107 +1,111 @@
# ---> C
# Prerequisites
*.d
# Object files
.config
*.o
*.ko
*.obj
*.elf
*.pyc
# Linker output
*.ilk
*.map
*.exp
# gtags
GTAGS
GRTAGS
GPATH
# Precompiled Headers
*.gch
*.pch
# emacs
.dir-locals.el
# Libraries
*.lib
*.a
*.la
*.lo
# emacs temp file suffixes
*~
.#*
\#*#
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# eclipse setting
.settings
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# MacOS directory files
.DS_Store
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# cache dir
.cache/
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
# Doc build artifacts
docs/_build/
docs/doxygen_sqlite3.db
# ---> C++
# Prerequisites
*.d
# Downloaded font files
docs/_static/DejaVuSans.ttf
docs/_static/NotoSansSC-Regular.otf
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Components Unit Test Apps files
components/**/build/
components/**/build_*_*/
components/**/sdkconfig
components/**/sdkconfig.old
# Precompiled Headers
*.gch
*.pch
# Example project files
examples/**/build/
examples/**/build_*_*/
examples/**/sdkconfig
examples/**/sdkconfig.old
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Unit test app files
tools/unit-test-app/build
tools/unit-test-app/build_*_*/
tools/unit-test-app/sdkconfig
tools/unit-test-app/sdkconfig.old
# Fortran module files
*.mod
*.smod
# test application build files
tools/test_apps/**/build/
tools/test_apps/**/build_*_*/
tools/test_apps/**/sdkconfig
tools/test_apps/**/sdkconfig.old
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
TEST_LOGS/
build_summary_*.xml
# Executables
*.exe
*.out
*.app
# gcov coverage reports
*.gcda
*.gcno
coverage.info
coverage_report/
# ---> CMake
CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
CMakeScripts
Testing
Makefile
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps
CMakeUserPresets.json
test_multi_heap_host
# Build directory
# VS Code Settings
.vscode/
# VIM files
*.swp
*.swo
# Sublime Text files
*.sublime-project
*.sublime-workspace
# Clion IDE CMake build & config
.idea/
cmake-build-*/
# Results for the checking of the Python coding style and static analysis
.mypy_cache
flake8_output.txt
# ESP-IDF default build directory name
build
# unity-app directory
unity-app
# lock files for examples and components
dependencies.lock
# managed_components for examples
managed_components
# pytest log
pytest-embedded/
# legacy one
pytest_embedded_log/
list_job*.txt
size_info*.txt
XUNIT_RESULT*.xml
.manifest_sha
# clang config (for LSP)
.clangd
# Vale
.vale/styles/*

View File

@@ -1,5 +1,6 @@
{
"files.associations": {
"relay_chn.h": "c"
}
},
"idf.port": "/dev/ttyUSB0"
}

View File

@@ -15,7 +15,7 @@ An ESP-IDF component for controlling relay channels, specifically designed for d
## Description
Each relay channel consists of 2 output relays controlled by 2 GPIO pins. The component provides APIs to control these relay pairs while ensuring safe operation, particularly for driving bipolar motors. It prevents short-circuits by automatically managing direction changes with configurable inertia timing.
Each relay channel consists of 2 output relays controlled by 2 GPIO pins. The component provides APIs to control these relay pairs while ensuring safe operation, particularly for driving bipolar motors. To prevent mechanical strain on the motor, the component automatically manages direction changes with a configurable inertia delay, protecting it from abrupt reversals.
It also provides an optional tilting interface per channel base. Tilting makes a channel move with a specific pattern moving with small steps at a time. Tilting is specifically designed for controlling some types of curtains that need to be adjusted to let enter specific amount of day light.
Since it operates on relays, the switching frequency is limited to 10Hz which complies with the most of the general purpose relays' requirements. The minimum frequency is 2Hz and the duty cycle is about 10% in all ranges.
@@ -31,13 +31,14 @@ Configure the component through menuconfig under "Relay Channel Driver Configura
## Installation
1. Copy the component to your project's components directory
2. Add dependency to your project's `idf_component.yml`:
Just add it as a custom dependency to your project's `idf_component.yml`:
```yaml
dependencies:
# Add as a custom component from git repository
relay_chn:
version: "^0.1.0"
git: https://git.kozmotronik.com.tr/KozmotronikTech/relay_chn.git
version: '>=0.5.0'
```
## Usage

View File

@@ -1,7 +1,6 @@
name: relay_chn
version: 0.1.0
description: Custom component for relay channel control
dependencies:
idf:
version: ">=4.0"
# TODO: Repo ve belgelendirme bağlantılarını ekle.
version: "0.5.0"
description: "Custom component for relay channel control"
license: "MIT"
url: "https://git.kozmotronik.com.tr/KozmotronikTech/relay_chn"
repository: "https://git.kozmotronik.com.tr/KozmotronikTech/relay_chn.git"

View File

@@ -14,7 +14,8 @@
* One relay channel consists of 2 output relays, hence 2 GPIO pins are required for each relay channel.
* This module provides an API to control the relay channels, specifically to drive bipolar motors.
* It also provides APIs to control the direction of the relay channel, bipolar motors in mind.
* The module also automatically manages the direction change inertia to prevent short-circuiting the motor.
* To prevent mechanical strain on the motor, the component automatically manages direction changes
* with a configurable inertia delay, protecting it from abrupt reversals.
* The STOP command overrides any other command and clears the pending command if any.
*
* The module internally uses a custom esp event loop to handle relay commands serially to ensure
@@ -48,6 +49,7 @@ typedef enum relay_chn_direction_enum relay_chn_direction_t;
* @brief Enums that represent the state of a relay channel.
*/
enum relay_chn_state_enum {
RELAY_CHN_STATE_UNDEFINED, ///< The relay channel state is undefined.
RELAY_CHN_STATE_FREE, ///< The relay channel is free to run or execute commands.
RELAY_CHN_STATE_STOPPED, ///< The relay channel is stopped and not running.
RELAY_CHN_STATE_FORWARD, ///< The relay channel is running in the forward direction.
@@ -95,6 +97,13 @@ typedef void (*relay_chn_state_listener_t)(uint8_t chn_id, relay_chn_state_t old
*/
esp_err_t relay_chn_create(const gpio_num_t* gpio_map, uint8_t gpio_count);
/**
* @brief Destroy the relay channels and free resources.
*
* This function cleans up the relay channels and releases any resources allocated during their creation.
*/
void relay_chn_destroy(void);
/**
* @brief Register a channel state change listener.
*

156
scripts/run_tests.sh Executable file
View File

@@ -0,0 +1,156 @@
#!/usr/bin/env bash
set -e
# ==== 1. Check ESP-IDF environment ====
if [[ -z "$IDF_PATH" ]]; then
echo "❌ ESP-IDF environment not found. Please source the export.sh file first:"
echo "'. $HOME/esp/esp-idf/export.sh' or wherever the ESP-IDF is installed"
exit 1
fi
# ==== 2. Valid Modes and Defaults ====
valid_test_tags=("core" "tilt" "listener" "all" "relay_chn")
arg_tag="all" # Default to 'all' if no tag specified
arg_clean=false
arg_log=false
arg_dry_run=false
arg_sdkconfig_file=""
flag_file=false
print_help() {
echo "Usage: $0 -t <tags> [OPTIONS]"
echo ""
echo "This script builds and runs tests for the relay_chn component using QEMU."
echo ""
echo "Arguments:"
echo " -t, --tag [relay_chn|core|tilt|listener|all] Specify which test tag to run."
echo ""
echo " If no tag is specified, it defaults to 'all'."
echo ""
echo "Options:"
echo " -f, --file <path> Specify a custom sdkconfig file to use for the build."
echo " Defaults to 'sdkconfig.defaults' if not provided."
echo " -c, --clean Perform a 'fullclean' before building the tests."
echo " -l, --log Log the test output to a timestamped file."
echo " -n, --dry-run Build the project without running qemu."
echo " -h, --help Show this help message and exit."
}
help() {
print_help
exit 0
}
usage() {
print_help
exit 1
}
# ==== 3. Argument Parsing ====
while [[ $# -gt 0 ]]; do
case $1 in
--tag|-t)
arg_tag="$2"
shift 2
;;
--file|-f)
arg_sdkconfig_file="$2"
flag_file=true
shift 2
;;
--clean|-c)
arg_clean=true
shift
;;
--log|-l)
arg_log=true
shift
;;
--dry-run|-n)
arg_dry_run=true
shift
;;
--help|-h)
help
;;
*)
usage
;;
esac
done
# ==== 4. Validity Check ====
if [[ ! " ${valid_test_tags[*]} " =~ " $arg_tag " ]]; then
echo "❌ Invalid mode: '$arg_tag'"
usage
fi
# ==== 5. Resolve Paths and Switch to Working Directory ====
script_dir=$(dirname "$(readlink -f "$0")")
project_root=$(dirname "$script_dir")
echo "🔍 Searching for 'test_apps' directory in '$project_root'..."
test_apps_dir=$(find "$project_root" -type d -name "test_apps" | head -n 1)
if [[ -z "$test_apps_dir" || ! -d "$test_apps_dir" ]]; then
echo "❌ 'test_apps' directory not found within the project root: '$project_root'"
echo " Please ensure the script is in a 'scripts' directory and 'test_apps' is a sibling."
exit 1
fi
echo "✅ Found 'test_apps' at: $test_apps_dir"
if $flag_file; then
if [[ -z "$arg_sdkconfig_file" || ! -f "$arg_sdkconfig_file" ]]; then
echo "❌ Invalid or missing file: '$arg_sdkconfig_file'"
usage
fi
# Resolve to an absolute path to work correctly after changing directory
arg_sdkconfig_file=$(readlink -f "$arg_sdkconfig_file")
else
echo "⚠️ No SDK configuration file provided. Using default sdkconfig."
arg_sdkconfig_file="$test_apps_dir/sdkconfig.defaults"
fi
echo "🧪 Test mode: $arg_tag"
echo "🧹 Clean: $arg_clean | 📄 Log: $arg_log"
echo "📂 Changing to working directory: $test_apps_dir"
cd "$test_apps_dir" || exit 1
# ==== 6. Clean if requested ====
if $arg_clean; then
echo "🧹 Doing Fullclean..."
idf.py fullclean
rm sdkconfig
fi
# ==== 7. Building and Running Tests ====
# In some locales, we can get errors like: "Error: unknown opcode or format name 'wsr.IBREAKA1'"
# The 'LC_ALL=C' env variable is set to ensure consistent locale settings.
LC_ALL=C \
SDKCONFIG_DEFAULTS="$arg_sdkconfig_file" \
RELAY_CHN_UNITY_TEST_GROUP_TAG="$arg_tag" \
idf.py reconfigure build
echo "🚀 Running test with QEMU..."
if $arg_log; then
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
LOGFILE="test_log_${arg_tag}_$TIMESTAMP.txt"
if $arg_dry_run; then
echo "🔍 Dry run mode: Logging to $LOGFILE but not executing." | tee "$LOGFILE"
echo "Command: idf.py qemu" | tee "$LOGFILE"
else
echo "📜 Logging test output to: $LOGFILE"
idf.py qemu --qemu-extra-args "-no-reboot" | tee "$LOGFILE"
fi
else
if $arg_dry_run; then
echo "🔍 Dry run mode: Not executing idf.py qemu."
echo "Command: idf.py qemu"
else
echo "🚀 Running idf.py qemu..."
idf.py qemu --qemu-extra-args "-no-reboot"
fi
fi

View File

@@ -15,6 +15,7 @@
#include <stdio.h>
#include <stdlib.h>
#include "esp_err.h"
#include "esp_check.h"
#include "esp_log.h"
#include "esp_task.h"
#include "driver/gpio.h"
@@ -22,6 +23,7 @@
#include "esp_event_base.h"
#include "esp_event.h"
#include "relay_chn.h"
#include "freertos/idf_additions.h"
#include "sdkconfig.h"
@@ -173,18 +175,25 @@ typedef struct relay_chn_type {
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 manage the state change listeners.
* @brief Structure to hold a listener entry in the linked list.
*/
struct relay_chn_state_listener_manager_type {
uint8_t listener_count; ///< The number of registered listeners.
relay_chn_state_listener_t *listeners; ///< The list that holds references to the registered listeners.
} relay_chn_state_listener_manager;
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;
@@ -279,7 +288,8 @@ static esp_err_t relay_chn_create_event_loop()
.task_core_id = tskNO_AFFINITY
};
esp_err_t ret = esp_event_loop_create(&loop_args, &relay_chn_event_loop);
ret |= esp_event_handler_register_with(relay_chn_event_loop,
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);
@@ -288,6 +298,8 @@ static esp_err_t relay_chn_create_event_loop()
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!");
@@ -320,14 +332,14 @@ esp_err_t relay_chn_create(const gpio_num_t* gpio_map, uint8_t gpio_count)
// Initialize the GPIOs
ret = gpio_reset_pin(forward_pin);
ret |= gpio_set_direction(forward_pin, GPIO_MODE_OUTPUT);
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);
ret |= gpio_set_direction(reverse_pin, GPIO_MODE_OUTPUT);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize GPIOs relay channel %d!", i);
return ret;
}
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
@@ -339,102 +351,135 @@ esp_err_t relay_chn_create(const gpio_num_t* gpio_map, uint8_t gpio_count)
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
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);
ret = relay_chn_init_tilt_control(relay_chn);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize tilt control for channel %d", i);
#endif
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize relay channel %d!", i);
return ret;
}
}
// Create relay channel command event loop
ret |= relay_chn_create_event_loop();
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
ret = relay_chn_tilt_init(); // Initialize tilt feature
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize tilt feature");
#endif
// Init the state listener manager
relay_chn_state_listener_manager.listeners = malloc(sizeof(relay_chn_state_listener_t*));
if (relay_chn_state_listener_manager.listeners == NULL) {
ESP_LOGE(TAG, "Failed to initialize memory for the listeners!");
ret = ESP_ERR_NO_MEM;
}
// Init the state listener list
vListInitialise(&relay_chn_listener_list);
return ret;
}
static int relay_chn_listener_index(relay_chn_state_listener_t listener)
void relay_chn_destroy(void)
{
for (int i = 0; i < relay_chn_state_listener_manager.listener_count; i++) {
if (relay_chn_state_listener_manager.listeners[i] == listener) {
// This is the listener to unregister. Check if it is in the middle
ESP_LOGD(TAG, "relay_chn_listener_index: Listener %p; found at index %d.", listener, i);
return i;
// 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;
}
}
return -1;
// Listener was not found in the list
return NULL;
}
esp_err_t relay_chn_register_listener(relay_chn_state_listener_t listener)
{
if (listener == NULL) {
ESP_LOGE(TAG, "relay_chn_register_listener: A NULL listener given.");
return ESP_ERR_INVALID_ARG;
}
ESP_RETURN_ON_FALSE(listener, ESP_ERR_INVALID_ARG, TAG, "Listener cannot be NULL");
if (relay_chn_listener_index(listener) > -1) {
ESP_LOGD(TAG, "relay_chn_register_listener: The listener %p is already registered.", listener);
// Check for duplicates
if (find_listener_entry(listener) != NULL) {
ESP_LOGD(TAG, "Listener %p already registered", listener);
return ESP_OK;
}
ESP_LOGD(TAG, "relay_chn_register_listener: Register listener: %p", listener);
relay_chn_state_listener_manager.listeners[relay_chn_state_listener_manager.listener_count] = listener;
// Update listener count
relay_chn_state_listener_manager.listener_count++;
// 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, "relay_chn_unregister_listener: A NULL listener given, nothing to do.");
return;
}
// Search the listener in the listeners list and get its index if exists
int i = relay_chn_listener_index(listener);
if (i == -1) {
ESP_LOGD(TAG, "relay_chn_unregister_listener: %p is not registered already.", listener);
if (listener == NULL)
{
ESP_LOGD(TAG, "Cannot unregister a NULL listener.");
return;
}
uint8_t max_index = relay_chn_state_listener_manager.listener_count - 1;
// Check whether the listener's index is in the middle
if (i == max_index) {
// free(&relay_chn_state_listener_manager.listeners[i]);
relay_chn_state_listener_manager.listeners[i] = NULL;
// 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);
}
else {
// It is in the middle, so align the next elements in the list and then free the last empty pointer
// Align the next elements
uint8_t num_of_elements = max_index - i;
relay_chn_state_listener_t *pnext = NULL;
// (i + j): current index; (i + j + 1): next index
for (uint8_t j = 0; j < num_of_elements; j++) {
uint8_t current_index = i + j;
uint8_t next_index = current_index + 1;
pnext = &relay_chn_state_listener_manager.listeners[next_index];
relay_chn_state_listener_manager.listeners[current_index] = *pnext;
}
// free(&relay_chn_state_listener_manager.listeners[max_index]); // Free the last element
relay_chn_state_listener_manager.listeners[max_index] = NULL; // Free the last element
}
// Decrease listener count
relay_chn_state_listener_manager.listener_count--;
}
/**
@@ -489,17 +534,24 @@ static esp_err_t relay_chn_start_esp_timer_once(esp_timer_handle_t esp_timer, ui
static void relay_chn_update_state(relay_chn_t *relay_chn, relay_chn_state_t new_state)
{
relay_chn_state_t old = relay_chn->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;
for (uint8_t i = 0; i < relay_chn_state_listener_manager.listener_count; i++) {
relay_chn_state_listener_t listener = relay_chn_state_listener_manager.listeners[i];
if (listener == NULL) {
relay_chn_state_listener_manager.listener_count -= 1;
ESP_LOGD(TAG, "relay_chn_update_state: A listener is NULL at index: %u", i);
}
// 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
listener(relay_chn->id, old, new_state);
entry->listener(relay_chn->id, old_state, new_state);
}
}
}
@@ -607,6 +659,37 @@ static void relay_chn_issue_cmd(relay_chn_t* relay_chn, relay_chn_cmd_t cmd)
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!");
}
}
@@ -614,16 +697,16 @@ static void relay_chn_issue_cmd(relay_chn_t* relay_chn, relay_chn_cmd_t cmd)
/* relay_chn APIs */
relay_chn_state_t relay_chn_get_state(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) {
return RELAY_CHN_STATE_STOPPED;
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)) {
return "INVALID";
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);
}
@@ -700,7 +783,8 @@ 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);
ret |= gpio_set_level(relay_chn->output.reverse_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;
}
@@ -708,7 +792,8 @@ 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);
ret |= gpio_set_level(relay_chn->output.reverse_pin, 0);
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;
}
@@ -716,7 +801,8 @@ 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);
ret |= gpio_set_level(relay_chn->output.reverse_pin, 1);
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;
}
@@ -1182,8 +1268,11 @@ static uint32_t relay_chn_tilt_count_update(relay_chn_t *relay_chn)
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)
return --relay_chn->tilt_control.tilt_counter.tilt_forward_count;
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;
}
@@ -1197,8 +1286,11 @@ static uint32_t relay_chn_tilt_count_update(relay_chn_t *relay_chn)
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)
return --relay_chn->tilt_control.tilt_counter.tilt_reverse_count;
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;
}

View File

@@ -1,8 +0,0 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components"
"../../relay_chn")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(relay_chn_test)

View File

@@ -1,3 +0,0 @@
idf_component_register(SRCS_DIRS "."
PRIV_INCLUDE_DIRS "."
PRIV_REQUIRES unity test_utils relay_chn)

View File

@@ -1,95 +0,0 @@
#include "driver/gpio.h"
#include "unity.h"
#include "unity_test_utils.h"
#include "relay_chn.h"
const gpio_num_t gpio_map[] = {GPIO_NUM_4, GPIO_NUM_5, GPIO_NUM_18, GPIO_NUM_19};
const uint8_t gpio_count = sizeof(gpio_map) / sizeof(gpio_map[0]);
const uint8_t relay_chn_count = gpio_count / 2;
TEST_CASE("relay chn inits correctly", "[relay_chn]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
}
TEST_CASE("Relay channels run forward and update state", "[relay_chn][forward]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
// Test forward run on all channels
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i));
relay_chn_run_forward(i); // Run the channel forward
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(i));
relay_chn_stop(i); // Stop the channel
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i));
relay_chn_flip_direction(i); // Flip the direction
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_FLIPPED, relay_chn_get_direction(i));
relay_chn_run_forward(i); // Run the channel forward
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(i));
relay_chn_stop(i); // Stop the channel
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i));
}
}
TEST_CASE("Relay channels run reverse and update state", "[relay_chn][reverse]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
// Test reverse run on all channels
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i));
relay_chn_run_reverse(i); // Run the channel reverse
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(i));
relay_chn_stop(i); // Stop the channel
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i));
relay_chn_flip_direction(i); // Flip the direction
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_FLIPPED, relay_chn_get_direction(i));
relay_chn_run_reverse(i); // Run the channel forward
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(i));
relay_chn_stop(i); // Stop the channel
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i));
}
}
static void check_channels_state_unchanged(void)
{
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i));
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_DEFAULT, relay_chn_get_direction(i));
}
}
TEST_CASE("Relay channels do not change state for invalid channel", "[relay_chn][invalid]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
// Test invalid channel run
relay_chn_run_forward(relay_chn_count + 1); // Run the channel forward
check_channels_state_unchanged();
relay_chn_run_reverse(relay_chn_count + 1); // Run the channel reverse
check_channels_state_unchanged();
relay_chn_stop(relay_chn_count + 1); // Stop the channel
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(relay_chn_count + 1));
check_channels_state_unchanged();
relay_chn_flip_direction(relay_chn_count + 1); // Flip the direction
check_channels_state_unchanged();
}
void setUp(void)
{
// Run before each test
}
void tearDown(void)
{
// Run after each test
}
// Test app entry point
void app_main(void)
{
// Run the Unity tests menu
unity_run_menu();
}

View File

@@ -1,5 +0,0 @@
# For IDF 5.0
CONFIG_ESP_TASK_WDT_EN=n
# For IDF4.4
CONFIG_ESP_TASK_WDT=n

20
test_apps/CMakeLists.txt Normal file
View File

@@ -0,0 +1,20 @@
# This is the project CMakeLists.txt file for the test subproject
cmake_minimum_required(VERSION 3.5)
# Define component search paths
# IMPORTANT: We should tell to the ESP-IDF
# where it can find relay_chn component.
# We add the 'relay_chn' root directory to the EXTRA_COMPONENT_DIRS by specifying: "../"
set(EXTRA_COMPONENT_DIRS "../")
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
set(COMPONENTS main)
# Include ESP-IDF project build system
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# Inject the test tag into the build
add_compile_definitions(RELAY_CHN_UNITY_TEST_GROUP_TAG=\"$ENV{RELAY_CHN_UNITY_TEST_GROUP_TAG}\")
# Define the name of this project
project(relay_chn_test)

View File

@@ -0,0 +1,20 @@
# === These files must be included in any case ===
set(srcs "test_common.c"
"test_app_main.c"
"test_relay_chn_core.c"
"test_relay_chn_listener.c")
if(CONFIG_RELAY_CHN_ENABLE_TILTING)
list(APPEND srcs "test_relay_chn_tilt.c")
endif()
message(STATUS "srcs=${srcs}")
# In order for the cases defined by `TEST_CASE` to be linked into the final elf,
# the component can be registered as WHOLE_ARCHIVE
idf_component_register(
SRCS ${srcs}
INCLUDE_DIRS "."
REQUIRES unity relay_chn
WHOLE_ARCHIVE
)

View File

@@ -0,0 +1,49 @@
#include "esp_log.h"
#include "esp_system.h"
#include "test_common.h"
#include "unity.h"
#include "unity_internals.h"
#include "unity_test_runner.h"
#include <stdbool.h>
#ifndef RELAY_CHN_UNITY_TEST_GROUP_TAG
#warning "RELAY_CHN_UNITY_TEST_GROUP_TAG is not defined, using default 'relay_chn'"
#define RELAY_CHN_UNITY_TEST_GROUP_TAG "relay_chn"
#endif
void setUp()
{
g_is_component_initialized = false;
}
void tearDown()
{
// Clean up after each test
if (g_is_component_initialized) {
relay_chn_destroy();
g_is_component_initialized = false;
}
}
void app_main(void)
{
UNITY_BEGIN();
// Log general test information
ESP_LOGI(TEST_TAG, "Available test count: %d", unity_get_test_count());
ESP_LOGI(TEST_TAG, "Running tests for tag: %s", RELAY_CHN_UNITY_TEST_GROUP_TAG);
if (strncmp(RELAY_CHN_UNITY_TEST_GROUP_TAG, "all", strlen("all")) == 0) {
unity_run_all_tests();
}
else {
unity_run_tests_by_tag(RELAY_CHN_UNITY_TEST_GROUP_TAG, false);
}
UNITY_END();
ESP_LOGI(TEST_TAG, "All tests complete.");
esp_restart(); // Restart to invoke qemu exit
}

View File

@@ -0,0 +1,17 @@
#include "test_common.h"
const char *TEST_TAG = "RELAY_CHN_TEST";
// GPIO eşlemesi (örn: GPIO_NUM_4 vs GPIO_NUM_5)
const gpio_num_t gpio_map[] = {
GPIO_NUM_4, GPIO_NUM_5, GPIO_NUM_18, GPIO_NUM_19
};
const uint8_t gpio_count = sizeof(gpio_map) / sizeof(gpio_map[0]);
const uint8_t relay_chn_count = gpio_count / 2;
// Konfigürasyon tabanlı inertia süresi
const uint32_t opposite_inertia_ms = CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS;
const uint32_t test_delay_margin_ms = 50; // ms toleransı
bool g_is_component_initialized = false;

View File

@@ -0,0 +1,25 @@
#pragma once
#include <string.h> // For memset
#include "unity.h"
#include "relay_chn.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// Test log tag
extern const char *TEST_TAG;
// GPIO konfigürasyonları
extern const gpio_num_t gpio_map[];
extern const uint8_t gpio_count;
extern const uint8_t relay_chn_count;
// Config parametreleri
extern const uint32_t opposite_inertia_ms;
extern const uint32_t test_delay_margin_ms;
// Init durumu
extern bool g_is_component_initialized;

View File

@@ -0,0 +1,403 @@
#include "test_common.h"
// --- Initialization Tests ---
TEST_CASE("relay_chn_create handles invalid arguments", "[relay_chn][core]")
{
// 1. Test with NULL gpio_map
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_create(NULL, gpio_count));
// 2. Test with incorrect gpio_count (must be RELAY_CHN_COUNT * 2)
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_create(gpio_map, gpio_count - 1));
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_create(gpio_map, 1));
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_create(gpio_map, 0));
// 3. Test with invalid GPIO numbers (GPIO_NUM_MAX is an invalid GPIO for output)
gpio_num_t invalid_gpio_map[] = {GPIO_NUM_4, GPIO_NUM_MAX, GPIO_NUM_18, GPIO_NUM_19};
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_create(invalid_gpio_map, gpio_count));
}
// --- Basic Functionality Tests ---
// TEST_CASE: Test that relay channels initialize correctly to RELAY_CHN_STATE_FREE
TEST_CASE("Relay channels initialize correctly to FREE state", "[relay_chn][core]") {
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(i));
}
}
// TEST_CASE: Test that relays do nothing when an invlid channel id given
TEST_CASE("Run forward does nothing if channel id is invalid", "[relay_chn][core]") {
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
for (uint8_t i = 0; i < relay_chn_count; i++) {
int invalid_id = relay_chn_count * 2 + i;
relay_chn_run_forward(invalid_id); // relay_chn_run_forward returns void
// Short delay for state to update
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(i));
}
}
// TEST_CASE: Test that relays run in the forward direction and update their state
TEST_CASE("Relay channels run forward and update state", "[relay_chn][core]") {
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
for (uint8_t i = 0; i < relay_chn_count; i++) {
relay_chn_run_forward(i); // relay_chn_run_forward returns void
// Short delay for state to update
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(i));
}
}
// TEST_CASE: Test that relays do nothing when an invlid channel id given
TEST_CASE("Run reverse does nothing if channel id is invalid", "[relay_chn][core]") {
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// Verify that no valid channels were affected
for (uint8_t i = 0; i < relay_chn_count; i++) {
int invalid_id = relay_chn_count * 2 + i;
// Call run_reverse with an invalid ID
relay_chn_run_reverse(invalid_id);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(i));
}
}
// TEST_CASE: Test that relays run in the reverse direction and update their state
TEST_CASE("Relay channels run reverse and update state", "[relay_chn][core]") {
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
for (uint8_t i = 0; i < relay_chn_count; i++) {
relay_chn_run_reverse(i); // relay_chn_run_reverse returns void
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(i));
}
}
// ### Broadcast Command (RELAY_CHN_ID_ALL) Tests
TEST_CASE("run_forward with ID_ALL sets all channels to FORWARD", "[relay_chn][core][id_all]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
relay_chn_run_forward(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(i));
}
}
TEST_CASE("run_reverse with ID_ALL sets all channels to REVERSE", "[relay_chn][core][id_all]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
relay_chn_run_reverse(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(i));
}
}
TEST_CASE("stop with ID_ALL stops all running channels", "[relay_chn][core][id_all]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// 1. Start all channels forward to ensure they are in a known running state
relay_chn_run_forward(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
// 2. Stop all channels using the broadcast command
relay_chn_stop(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
// 3. Verify all channels have transitioned to the FREE state
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(i));
}
}
// TEST_CASE: Test that relays stop and transition to RELAY_CHN_STATE_FREE
// This test also verifies the transition to FREE state after a STOP command.
TEST_CASE("Relay channels stop and update to FREE state", "[relay_chn][core]") {
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
for (uint8_t i = 0; i < relay_chn_count; i++) {
// First, run forward to test stopping and transitioning to FREE state
relay_chn_run_forward(i); // relay_chn_run_forward returns void
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(i));
// Now, issue the stop command
relay_chn_stop(i); // relay_chn_stop returns void
// Immediately after stop, state should be STOPPED
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i));
// Then, wait for the inertia period for it to transition to RELAY_CHN_STATE_FREE
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(i));
}
}
// TEST_CASE: Get state should return UNDEFINED when id is not valid
TEST_CASE("Get state returns UNDEFINED when id is invalid", "[relay_chn][core]") {
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
for (uint8_t i = 0; i < relay_chn_count; i++) {
int invalid_id = relay_chn_count * 2 + i;
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_UNDEFINED, relay_chn_get_state(invalid_id));
}
// Test for running states also
relay_chn_run_forward(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
for (uint8_t i = 0; i < relay_chn_count; i++) {
int invalid_id = relay_chn_count * 2 + i;
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_UNDEFINED, relay_chn_get_state(invalid_id));
}
}
// TEST_CASE: Get state string should return "UNKNOWN" when id is not valid
TEST_CASE("Get state string returns UNKNOWN when id is invalid", "[relay_chn][core]") {
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
for (uint8_t i = 0; i < relay_chn_count; i++) {
int invalid_id = relay_chn_count * 2 + i;
TEST_ASSERT_EQUAL_STRING("UNKNOWN", relay_chn_get_state_str(invalid_id));
}
// Test for running states also
relay_chn_run_forward(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
for (uint8_t i = 0; i < relay_chn_count; i++) {
int invalid_id = relay_chn_count * 2 + i;
TEST_ASSERT_EQUAL_STRING("UNKNOWN", relay_chn_get_state_str(invalid_id));
}
}
// TEST_CASE: Test independent operation of multiple relay channels
TEST_CASE("Multiple channels can operate independently", "[relay_chn][core]") {
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
if (relay_chn_count >= 2) {
// Start Channel 0 in forward direction
relay_chn_run_forward(0); // relay_chn_run_forward returns void
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(0));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(1)); // Other channel should not be affected
// Start Channel 1 in reverse direction
relay_chn_run_reverse(1); // relay_chn_run_reverse returns void
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(0));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(1));
// Stop Channel 0 and wait for it to become FREE
relay_chn_stop(0); // relay_chn_stop returns void
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(0));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(1)); // Other channel should continue running
// Stop Channel 1 and wait for it to become FREE
relay_chn_stop(1); // relay_chn_stop returns void
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(0));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(1));
} else {
ESP_LOGW("TEST", "Skipping 'Multiple channels can operate independently' test: Not enough channels available.");
}
}
// ### Inertia and State Transition Tests
// This section specifically targets the inertia periods and complex state transitions as per the component's logic.
// TEST_CASE: Test transition from forward to reverse with inertia and state checks
// Scenario: RELAY_CHN_STATE_FORWARD -> (relay_chn_run_reverse) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_REVERSE
TEST_CASE("Forward to Reverse transition with opposite inertia", "[relay_chn][core][inertia]") {
uint8_t ch = 0; // Channel to test
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// 1. Start in forward direction
relay_chn_run_forward(ch); // relay_chn_run_forward returns void
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Short delay for state stabilization
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch));
// 2. Issue reverse command
relay_chn_run_reverse(ch); // relay_chn_run_reverse returns void
// Immediately after the command, the motor should be stopped
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE_PENDING, relay_chn_get_state(ch));
// Wait for the inertia period (after which the reverse command will be dispatched)
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(ch)); // Should now be in reverse state
}
// TEST_CASE: Test transition from reverse to forward with inertia and state checks
// Scenario: RELAY_CHN_STATE_REVERSE -> (relay_chn_run_forward) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_FORWARD
TEST_CASE("Reverse to Forward transition with opposite inertia", "[relay_chn][core][inertia]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// 1. Start in reverse direction
relay_chn_run_reverse(ch); // relay_chn_run_reverse returns void
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(ch));
// 2. Issue forward command
relay_chn_run_forward(ch); // relay_chn_run_forward returns void
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD_PENDING, relay_chn_get_state(ch));
// Wait for inertia
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch));
}
// TEST_CASE: Test issuing the same run command while already running (no inertia expected)
// Scenario: RELAY_CHN_STATE_FORWARD -> (relay_chn_run_forward) -> RELAY_CHN_STATE_FORWARD
TEST_CASE("Running in same direction does not incur inertia", "[relay_chn][core][inertia]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// 1. Start in forward direction
relay_chn_run_forward(ch); // relay_chn_run_forward returns void
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch));
// 2. Issue the same forward command again
relay_chn_run_forward(ch); // relay_chn_run_forward returns void
// As per the code, is_direction_opposite_to_current_motion should return false, so no inertia.
// Just a short delay to check state remains the same.
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch));
}
// TEST_CASE: Test transition from FREE state to running (no inertia expected)
// Scenario: RELAY_CHN_STATE_FREE -> (relay_chn_run_forward) -> RELAY_CHN_STATE_FORWARD
TEST_CASE("FREE to Running transition without inertia", "[relay_chn][core][inertia]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// setUp() should have already brought the channel to FREE state
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(ch));
// Start in forward direction
relay_chn_run_forward(ch); // relay_chn_run_forward returns void
// No inertia is expected when starting from FREE state.
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch));
}
// ### Direction Flipping Tests
TEST_CASE("Single channel direction can be flipped", "[relay_chn][core][direction]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
const uint8_t ch = 0;
// 1. Initial direction should be default
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_DEFAULT, relay_chn_get_direction(ch));
// 2. Flip the direction
relay_chn_flip_direction(ch);
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); // Wait for flip inertia
// 3. Verify direction is flipped
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_FLIPPED, relay_chn_get_direction(ch));
// 4. Flip back
relay_chn_flip_direction(ch);
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); // Wait for flip inertia
// 5. Verify direction is back to default
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_DEFAULT, relay_chn_get_direction(ch));
}
TEST_CASE("All channels direction can be flipped simultaneously", "[relay_chn][core][direction][id_all]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// 1. Flip all channels
relay_chn_flip_direction(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
// 2. Verify all channels are flipped
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_FLIPPED, relay_chn_get_direction(i));
}
// 3. Flip all back
relay_chn_flip_direction(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
// 4. Verify all channels are back to default
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_DEFAULT, relay_chn_get_direction(i));
}
}
TEST_CASE("Flipping a running channel stops it and flips direction", "[relay_chn][core][direction]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
const uint8_t ch = 0;
// 1. Start channel running and verify state
relay_chn_run_forward(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch));
// 2. Flip the direction while running
relay_chn_flip_direction(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Give time for events to process
// 3. The channel should stop as part of the flip process
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(ch));
// 4. Wait for the flip inertia to pass, after which it should be FREE and FLIPPED
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(ch));
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_FLIPPED, relay_chn_get_direction(ch));
}
TEST_CASE("Direction flip handles invalid channel ID gracefully", "[relay_chn][core][direction]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
const uint8_t invalid_ch = relay_chn_count + 5;
relay_chn_flip_direction(invalid_ch); // Call with an invalid ID
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_DEFAULT, relay_chn_get_direction(invalid_ch));
}

View File

@@ -0,0 +1,135 @@
#include "test_common.h"
// --- Listener Test Globals ---
typedef struct {
uint8_t chn_id;
relay_chn_state_t old_state;
relay_chn_state_t new_state;
int call_count;
} listener_callback_info_t;
static listener_callback_info_t listener1_info;
static listener_callback_info_t listener2_info;
// --- Listener Test Helper Functions ---
// Clear the memory from possible garbage values
static void reset_listener_info(listener_callback_info_t* info) {
memset(info, 0, sizeof(listener_callback_info_t));
}
static void test_listener_1(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) {
listener1_info.chn_id = chn_id;
listener1_info.old_state = old_state;
listener1_info.new_state = new_state;
listener1_info.call_count++;
}
static void test_listener_2(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) {
listener2_info.chn_id = chn_id;
listener2_info.old_state = old_state;
listener2_info.new_state = new_state;
listener2_info.call_count++;
}
// ### Listener Functionality Tests
TEST_CASE("Listener is called on state change", "[relay_chn][listener]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
reset_listener_info(&listener1_info);
// 1. Register the listener
TEST_ESP_OK(relay_chn_register_listener(test_listener_1));
// 2. Trigger a state change
relay_chn_run_forward(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Allow event to be processed
// 3. Verify the listener was called with correct parameters
TEST_ASSERT_EQUAL(1, listener1_info.call_count);
TEST_ASSERT_EQUAL(ch, listener1_info.chn_id);
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, listener1_info.old_state);
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener1_info.new_state);
// 4. Unregister to clean up
relay_chn_unregister_listener(test_listener_1);
}
TEST_CASE("Unregistered listener is not called", "[relay_chn][listener]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
reset_listener_info(&listener1_info);
// 1. Register and then immediately unregister the listener
TEST_ESP_OK(relay_chn_register_listener(test_listener_1));
relay_chn_unregister_listener(test_listener_1);
// 2. Trigger a state change
relay_chn_run_forward(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
// 3. Verify the listener was NOT called
TEST_ASSERT_EQUAL(0, listener1_info.call_count);
}
TEST_CASE("Multiple listeners are called on state change", "[relay_chn][listener]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
reset_listener_info(&listener1_info);
reset_listener_info(&listener2_info);
// 1. Register two different listeners
TEST_ESP_OK(relay_chn_register_listener(test_listener_1));
TEST_ESP_OK(relay_chn_register_listener(test_listener_2));
// 2. Trigger a state change
relay_chn_run_forward(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
// 3. Verify listener 1 was called correctly
TEST_ASSERT_EQUAL(1, listener1_info.call_count);
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, listener1_info.old_state);
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener1_info.new_state);
// 4. Verify listener 2 was also called correctly
TEST_ASSERT_EQUAL(1, listener2_info.call_count);
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, listener2_info.old_state);
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener2_info.new_state);
// 5. Clean up
relay_chn_unregister_listener(test_listener_1);
relay_chn_unregister_listener(test_listener_2);
}
TEST_CASE("Listener registration handles invalid arguments and duplicates", "[relay_chn][listener]") {
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
reset_listener_info(&listener1_info);
// 1. Registering a NULL listener should fail
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_register_listener(NULL));
// 2. Unregistering a NULL listener should not crash
relay_chn_unregister_listener(NULL);
// 3. Registering the same listener twice should be handled gracefully
TEST_ESP_OK(relay_chn_register_listener(test_listener_1));
TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); // Second call should be a no-op
// 4. Trigger a state change and verify the listener is only called ONCE
relay_chn_run_forward(0);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(1, listener1_info.call_count);
// 5. Clean up
relay_chn_unregister_listener(test_listener_1);
}

View File

@@ -0,0 +1,389 @@
#include "test_common.h"
// ### Tilt Functionality Tests (Conditional)
// This section will only be compiled if **`CONFIG_RELAY_CHN_ENABLE_TILTING`** is defined as **`1`** in `sdkconfig`.
#ifndef CONFIG_RELAY_CHN_ENABLE_TILTING
#error "This test requires CONFIG_RELAY_CHN_ENABLE_TILTING"
#endif
#define RELAY_CHN_CMD_FORWARD 1
#define RELAY_CHN_CMD_REVERSE 2
// Helper function to prepare channel for tilt tests
void prepare_channel_for_tilt(uint8_t chn_id, int initial_cmd) {
// Ensure the channel has had a 'last_run_cmd'
if (initial_cmd == RELAY_CHN_CMD_FORWARD) {
relay_chn_run_forward(chn_id);
} else { // Assuming initial_cmd is RELAY_CHN_CMD_REVERSE
relay_chn_run_reverse(chn_id);
}
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Allow command to process
relay_chn_stop(chn_id); // Stop it to set last_run_cmd but return to FREE for next test
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(chn_id));
}
// TEST_CASE: Test transition from running forward to tilt forward
// Scenario: RELAY_CHN_STATE_FORWARD -> (relay_chn_tilt_forward) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_TILT_FORWARD
TEST_CASE("Run Forward to Tilt Forward transition with inertia", "[relay_chn][tilt][inertia]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// Prepare channel by running forward first to set last_run_cmd
prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD);
// 1. Start in forward direction
relay_chn_run_forward(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch));
// 2. Issue tilt forward command
relay_chn_tilt_forward(ch);
// After tilt command, it should immediately stop and then trigger inertia.
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(ch));
// Wait for the inertia period (after which the tilt command will be dispatched)
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch));
}
// TEST_CASE: Test transition from running reverse to tilt reverse
// Scenario: RELAY_CHN_STATE_REVERSE -> (relay_chn_tilt_reverse) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_TILT_REVERSE
TEST_CASE("Run Reverse to Tilt Reverse transition with inertia", "[relay_chn][tilt][inertia]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// Prepare channel by running reverse first to set last_run_cmd
prepare_channel_for_tilt(ch, RELAY_CHN_CMD_REVERSE);
// 1. Start in reverse direction
relay_chn_run_reverse(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(ch));
// 2. Issue tilt reverse command
relay_chn_tilt_reverse(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(ch));
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state(ch));
}
// TEST_CASE: Test transition from FREE state to tilt forward (now with preparation)
// Scenario: RELAY_CHN_STATE_FREE -> (prepare) -> RELAY_CHN_STATE_FREE -> (relay_chn_tilt_forward) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_TILT_FORWARD
TEST_CASE("FREE to Tilt Forward transition with inertia (prepared)", "[relay_chn][tilt][inertia]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// Prepare channel by running forward first to set last_run_cmd
prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD);
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(ch)); // Ensure we are back to FREE
// Issue tilt forward command
relay_chn_tilt_forward(ch);
// From FREE state, tilt command should still incur the inertia due to the internal timer logic
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch));
}
// TEST_CASE: Test transition from FREE state to tilt reverse (now with preparation)
// Scenario: RELAY_CHN_STATE_FREE -> (prepare) -> RELAY_CHN_STATE_FREE -> (relay_chn_tilt_reverse) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_TILT_REVERSE
TEST_CASE("FREE to Tilt Reverse transition with inertia (prepared)", "[relay_chn][tilt][inertia]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// Prepare channel by running reverse first to set last_run_cmd
prepare_channel_for_tilt(ch, RELAY_CHN_CMD_REVERSE);
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(ch)); // Ensure we are back to FREE
// Issue tilt reverse command
relay_chn_tilt_reverse(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state(ch));
}
// TEST_CASE: Test transition from tilt forward to run forward (inertia expected for run)
// Scenario: RELAY_CHN_STATE_TILT_FORWARD -> (relay_chn_run_forward) -> RELAY_CHN_STATE_FORWARD
TEST_CASE("Tilt Forward to Run Forward transition with inertia", "[relay_chn][tilt][inertia]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// Prepare channel by running forward first to set last_run_cmd, then tilt
prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD);
relay_chn_tilt_forward(ch); // Go to tilt state
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch));
// 2. Issue run forward command
relay_chn_run_forward(ch);
// From Tilt to Run in the same logical name but in the opposite direction, inertia is expected.
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD_PENDING, relay_chn_get_state(ch));
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch));
}
// TEST_CASE: Test transition from tilt reverse to run reverse (no inertia expected for run)
// Scenario: RELAY_CHN_STATE_TILT_REVERSE -> (relay_chn_run_reverse) -> RELAY_CHN_STATE_REVERSE
TEST_CASE("Tilt Reverse to Run Reverse transition with inertia", "[relay_chn][tilt][inertia]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// Prepare channel by running reverse first to set last_run_cmd, then tilt
prepare_channel_for_tilt(ch, RELAY_CHN_CMD_REVERSE);
relay_chn_tilt_reverse(ch); // Go to tilt state
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state(ch));
// 2. Issue run reverse command
relay_chn_run_reverse(ch);
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE_PENDING, relay_chn_get_state(ch));
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(ch));
}
// TEST_CASE: Test transition from tilt forward to run reverse (without inertia)
// Scenario: RELAY_CHN_STATE_TILT_FORWARD -> (relay_chn_run_reverse) -> RELAY_CHN_STATE_REVERSE
TEST_CASE("Tilt Forward to Run Reverse transition without inertia", "[relay_chn][tilt][inertia]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// Prepare channel by running forward first to set last_run_cmd, then tilt
prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD);
relay_chn_tilt_forward(ch); // Go to tilt state
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch));
// 2. Issue run reverse command (opposite direction)
relay_chn_run_reverse(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(ch));
}
// TEST_CASE: Test stopping from a tilt state (no inertia for stop command itself)
// Scenario: RELAY_CHN_STATE_TILT_FORWARD -> (relay_chn_stop) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_FREE
TEST_CASE("Tilt to Stop transition without immediate inertia for stop", "[relay_chn][tilt][inertia]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// Prepare channel by running forward first to set last_run_cmd, then tilt
prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD);
relay_chn_tilt_forward(ch); // Go to tilt state
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch));
// 2. Issue stop command
relay_chn_stop(ch);
// Stop command should apply immediately, setting state to FREE since last state was tilt.
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(ch));
}
// ### Tilt Broadcast Command (RELAY_CHN_ID_ALL) Tests
TEST_CASE("tilt_forward with ID_ALL sets all channels to TILT_FORWARD", "[relay_chn][tilt][id_all]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// 1. Prepare all channels.
for (uint8_t i = 0; i < relay_chn_count; i++) {
prepare_channel_for_tilt(i, RELAY_CHN_CMD_FORWARD);
}
// 2. Issue tilt forward to all channels
relay_chn_tilt_forward(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Tilt from FREE doesn't have stop-inertia
// 3. Verify all channels are tilting forward
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(i));
}
}
TEST_CASE("tilt_reverse with ID_ALL sets all channels to TILT_REVERSE", "[relay_chn][tilt][id_all]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// 1. Prepare all channels.
for (uint8_t i = 0; i < relay_chn_count; i++) {
prepare_channel_for_tilt(i, RELAY_CHN_CMD_REVERSE);
}
// 2. Issue tilt reverse to all channels
relay_chn_tilt_reverse(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
// 3. Verify all channels are tilting reverse
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state(i));
}
}
TEST_CASE("tilt_stop with ID_ALL stops all tilting channels", "[relay_chn][tilt][id_all]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// 1. Prepare and start all channels tilting forward
for (uint8_t i = 0; i < relay_chn_count; i++) {
prepare_channel_for_tilt(i, RELAY_CHN_CMD_REVERSE);
}
relay_chn_tilt_forward(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
// 2. Stop tilting on all channels
relay_chn_tilt_stop(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
// 3. Verify all channels are free
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(i));
}
}
TEST_CASE("tilt_auto with ID_ALL tilts channels based on last run direction", "[relay_chn][tilt][id_all]")
{
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// This test requires at least 2 channels to demonstrate different behaviors
TEST_ASSERT_GREATER_OR_EQUAL_MESSAGE(2, relay_chn_count, "Test requires at least 2 channels");
// 1. Prepare channel 0 with last run FORWARD and channel 1 with last run REVERSE
prepare_channel_for_tilt(0, RELAY_CHN_CMD_FORWARD);
prepare_channel_for_tilt(1, RELAY_CHN_CMD_REVERSE);
// 2. Issue auto tilt command to all channels
relay_chn_tilt_auto(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Tilt from FREE state is dispatched immediately
// 3. Verify channel 0 tilts forward (last run was forward) and channel 1 tilts reverse (last run was reverse)
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(0));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state(1));
}
// Test relay_chn_tilt_auto() chooses correct tilt direction
TEST_CASE("relay_chn_tilt_auto chooses correct direction", "[relay_chn][tilt][auto]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
// Prepare FORWARD
prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD);
relay_chn_tilt_auto(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch));
relay_chn_tilt_stop(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
// Prepare REVERSE
prepare_channel_for_tilt(ch, RELAY_CHN_CMD_REVERSE);
relay_chn_tilt_auto(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state(ch));
}
// Test sensitivity set/get
TEST_CASE("relay_chn_tilt_sensitivity_set and get", "[relay_chn][tilt][sensitivity]") {
uint8_t ch = 0;
uint8_t val = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
relay_chn_tilt_sensitivity_set(ch, 0);
TEST_ESP_OK(relay_chn_tilt_sensitivity_get(ch, &val, 1));
TEST_ASSERT_EQUAL_UINT8(0, val);
relay_chn_tilt_sensitivity_set(ch, 50);
TEST_ESP_OK(relay_chn_tilt_sensitivity_get(ch, &val, 1));
TEST_ASSERT_EQUAL_UINT8(50, val);
relay_chn_tilt_sensitivity_set(ch, 100);
TEST_ESP_OK(relay_chn_tilt_sensitivity_get(ch, &val, 1));
TEST_ASSERT_EQUAL_UINT8(100, val);
// Set all channels
relay_chn_tilt_sensitivity_set(RELAY_CHN_ID_ALL, 42);
uint8_t vals[CONFIG_RELAY_CHN_COUNT] = {0};
TEST_ESP_OK(relay_chn_tilt_sensitivity_get(RELAY_CHN_ID_ALL, vals, relay_chn_count));
for (int i = 0; i < relay_chn_count; ++i) {
TEST_ASSERT_EQUAL_UINT8(42, vals[i]);
}
}
// Test tilt counter logic: forward x3, reverse x3, extra reverse fails
TEST_CASE("tilt counter logic: forward and reverse consumption", "[relay_chn][tilt][counter]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD);
// Tilt forward 3 times
for (int i = 0; i < 3; ++i) {
relay_chn_tilt_forward(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch));
relay_chn_tilt_stop(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
}
// Now tilt reverse 3 times (should succeed)
for (int i = 0; i < 3; ++i) {
relay_chn_tilt_reverse(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
if (i < 3) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state(ch));
relay_chn_tilt_stop(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
}
}
// Extra reverse tilt should fail (counter exhausted)
relay_chn_tilt_reverse(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
// Should not enter TILT_REVERSE, should remain FREE or STOPPED
relay_chn_state_t state = relay_chn_get_state(ch);
TEST_ASSERT(state != RELAY_CHN_STATE_TILT_REVERSE);
}
// Test run command during TILT state
TEST_CASE("run command during TILT state transitions correctly", "[relay_chn][tilt][run-during-tilt]") {
uint8_t ch = 0;
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
g_is_component_initialized = true;
prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD);
relay_chn_tilt_forward(ch);
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch));
// Issue run reverse while in TILT_FORWARD
relay_chn_run_reverse(ch);
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
// Should transition to REVERSE or REVERSE_PENDING depending on inertia logic
relay_chn_state_t state = relay_chn_get_state(ch);
TEST_ASSERT(state == RELAY_CHN_STATE_REVERSE || state == RELAY_CHN_STATE_REVERSE_PENDING);
}

1400
test_apps/sdkconfig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
# Disable task WDT for tests
CONFIG_ESP_TASK_WDT_INIT=n
# Relay Channel Driver Default Configuration for Testing
# Keep this as short as possible for tests
CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS=200
CONFIG_RELAY_CHN_COUNT=2
CONFIG_RELAY_CHN_ENABLE_TILTING=y

1400
test_apps/sdkconfig.old Normal file

File diff suppressed because it is too large Load Diff