36 Commits

Author SHA1 Message Date
b99622bd23 Fix and document tilt key names.
Fix key names with more approprite ones and add documentation for them. Fixes #1081.
2025-08-20 11:17:40 +03:00
c5fa8a63ae Fix NVS module's tag value.
Fix NVS module's tag string value to match the module name. IssueID #1081.
2025-08-20 11:15:29 +03:00
aeeda44a2c Optimize and refactor tilt counting
- Optimized tilt counting data by reducing the tilt counter variables into one for smaller memory footprint. So the `relay_chn_tilt_counter_t` type is replaced by a single `uint16_t` variable in the `relay_chn_tilt_ctl_t` structure. Hence the `relay_chn_tilt_counter_t` type has been removed since it is not necessary anymore.
- Refactored tilt count handling in NVS: consolidate forward and reverse counts into a single tilt count parameter.
- Updated NVS test files that affected by the data type and function signature changes.

Fixes #1079
2025-08-20 11:07:50 +03:00
dc2aa93d2d Merge pull request 'feat(nvs): Add NVS support for relay channel persistence (#1074)' (!33) from feat/1074-add-nvs-storage into dev
Reviewed-on: #33
2025-08-19 18:55:11 +03:00
96bb139751 Add new utility script and update test tags
- Added new utility script run_tests_all_cfgs.sh to run tests for all configurations and with test tag support.
- Updated run_tests.sh to add the new test tag for NVS unit tests.
2025-08-19 17:40:26 +03:00
61edf11b75 Refactor to remove redundant initialization and add NVS storage tests
- Removed unnecessary calls to relay_chn_create and g_is_component_initialized in multiple test cases across test_relay_chn_core_single.c, test_relay_chn_listener_multi.c, and test_relay_chn_listener_single.c.
- Introduced new test files for NVS functionality: test_relay_chn_nvs_multi.c and test_relay_chn_nvs_single.c, covering initialization, direction setting, invalid parameters, and erase operations.
- Updated partition table configuration to support NVS storage, including the addition of a new partition file part_nvs.csv.
- Adjusted sdkconfig files to enable NVS support and configure custom partition settings for relay channels.
2025-08-19 17:36:23 +03:00
b19f0c553b Add NVS support for relay channel config persistence
- Introduced NVS configuration options in Kconfig.
- Implemented NVS initialization and deinitialization in relay_chn_core.
- Added functions for storing and retrieving relay channel direction and tilt sensitivity in NVS.
- Updated relay_chn_tilt and relay_chn_output to utilize NVS for state management.
- Created relay_chn_nvs.c and relay_chn_nvs.h for NVS-related functionalities.

Closes #1074.
2025-08-19 17:33:45 +03:00
f04632dc77 Unignore .vscode folder.
Unignore .vscode folder and add necessary configuration files for a
synced development environment.
2025-08-14 16:55:25 +03:00
b29768edad Merge pull request 'feat/957-single-channel-mode' (!32) from feat/957-single-channel-mode into dev
Reviewed-on: #32
2025-08-14 09:50:36 +03:00
f1cda4531d Merge remote-tracking branch 'origin/main' into feat/957-single-channel-mode 2025-08-13 18:36:48 +03:00
f8d6e74f23 Refactor and update configuration for single channel mode
- Created `test_relay_chn_tilt_multi.c` and `test_relay_chn_tilt_single.c` to implement comprehensive tests for the tilt functionality of relay channels, covering various scenarios including transitions between states and sensitivity settings.
- Introduced a new partition table in `partitionTable.csv` for proper memory management.
- Updated `sdkconfig` to set the relay channel count to 1 for single channel testing and adjusted related configurations.
- Added default configuration file `sdkconfig.defaults.single` for streamlined testing setup.
2025-08-13 18:36:48 +03:00
9f1134763e Refactor and update the relay_chn component.
Refactor relay channel component to support single and multi-channel modes; update CMake configuration and enhance API documentation.
2025-08-13 18:36:48 +03:00
61f8ed440e Add single channel mode feature.
The addition of a single-channel mode implied further modularisation of the component. This commit has broken the component down into the following modules to avoid a huge single source file and to make unit testing easier.

The modules:

- Separation of public and private code
- *types and *defs
- public relay_chn API
- *adapter
- *output
- *run_info
- *core
- *ctl (control)
- *tilt

Closes #957.
2025-08-13 18:31:05 +03:00
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
22668b6759 Merge pull request 'release-0.5.0' (!30) from release-0.5.0 into dev
Reviewed-on: #30
2025-07-23 17:45:26 +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
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
59 changed files with 8120 additions and 6095 deletions

2
.gitignore vendored
View File

@@ -68,7 +68,7 @@ coverage_report/
test_multi_heap_host
# VS Code Settings
.vscode/
# .vscode/
# VIM files
*.swp

23
.vscode/c_cpp_properties.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
"configurations": [
{
"name": "ESP-IDF",
"compilerPath": "${config:idf.toolsPath}/tools/riscv32-esp-elf/esp-14.2.0_20241119/riscv32-esp-elf/bin/riscv32-esp-elf-gcc",
"compileCommands": "${config:idf.buildPath}/compile_commands.json",
"includePath": [
"${config:idf.espIdfPath}/components/**",
"${config:idf.espIdfPathWin}/components/**",
"${workspaceFolder}/**"
],
"browse": {
"path": [
"${config:idf.espIdfPath}/components",
"${config:idf.espIdfPathWin}/components",
"${workspaceFolder}"
],
"limitSymbolsToIncludedHeaders": true
}
}
],
"version": 4
}

15
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "gdbtarget",
"request": "attach",
"name": "Eclipse CDT GDB Adapter"
},
{
"type": "espidf",
"name": "Launch",
"request": "launch"
}
]
}

View File

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

26
CMakeLists.txt Normal file
View File

@@ -0,0 +1,26 @@
set(include_dirs "include")
set(priv_include_dirs "private_include")
set(srcs "src/relay_chn_core.c"
"src/relay_chn_output.c"
"src/relay_chn_run_info.c")
if(CONFIG_RELAY_CHN_ENABLE_TILTING)
list(APPEND srcs "src/relay_chn_tilt.c")
endif()
if(CONFIG_RELAY_CHN_COUNT GREATER 1)
list(APPEND srcs "src/relay_chn_ctl_multi.c")
else()
list(APPEND srcs "src/relay_chn_ctl_single.c")
endif()
if(CONFIG_RELAY_CHN_NVS)
list(APPEND srcs "src/relay_chn_nvs.c")
endif()
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS ${include_dirs}
PRIV_INCLUDE_DIRS ${priv_include_dirs}
REQUIRES driver esp_timer esp_event nvs_flash)

64
Kconfig Normal file
View File

@@ -0,0 +1,64 @@
menu "Relay Channel Driver Configuration"
config RELAY_CHN_OPPOSITE_INERTIA_MS
int "Inertia time before it runs opposite direction (ms)"
range 200 1500
default 800
help
Time to wait after changing the direction of the output before
starting the output. This is useful for the motors or some other
mechanical actuators to allow them to stop and settle before
changing the direction.
config RELAY_CHN_COUNT
int "Number of relay channels"
range 1 8
default 1
help
Number of relay channels between 1 and 8.
config RELAY_CHN_ENABLE_TILTING
bool "Enable tilting on relay channels"
default n
help
This option controls enabling tilting on channels. 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.
config RELAY_CHN_ENABLE_NVS
bool "Enable persistent NVS storage for relay channel"
default n
help
If enabled, relay channel configuration will be stored in NVS.
endmenu
menu "Relay Channel NVS Storage Configuration"
depends on RELAY_CHN_ENABLE_NVS
config RELAY_CHN_NVS_NAMESPACE
string "NVS namespace for relay channel storage"
default "relay_chn"
help
The NVS namespace used for storing relay channel configuration.
This should be unique to avoid conflicts with other components.
config RELAY_CHN_NVS_CUSTOM_PARTITION
bool "Use custom NVS partition for relay channel storage"
default n
help
If enabled, a custom NVS partition will be used for storing
relay channel configuration. If disabled, the default NVS
partition will be used.
config RELAY_CHN_NVS_CUSTOM_PARTITION_NAME
string "Custom NVS partition name"
depends on RELAY_CHN_NVS_CUSTOM_PARTITION
default "app_data"
help
The name of the custom NVS partition used for storing relay channel
configuration. Make sure the name is exactly the same as label defined
in the relevant partition table.
endmenu

140
README.md
View File

@@ -38,16 +38,33 @@ dependencies:
# Add as a custom component from git repository
relay_chn:
git: https://git.kozmotronik.com.tr/KozmotronikTech/relay_chn.git
version: '>=0.4.0'
version: '>=0.5.0'
```
## Usage
The `relay_chn` component can be used in two different modes, which are determined by the 'CONFIG_RELAY_CHN_COUNT' configuration:
- Single channel mode (`CONFIG_RELAY_CHN_COUNT == 1`)
- Multi channel mode (`CONFIG_RELAY_CHN_COUNT > 1`)
Depending on the mode, the component will be built selectively, so the signatures of some available API functions may vary, either including or excluding a channel ID parameter:
```c
relay_chn_run_forward(); // No channel ID parameter for single channel mode
// or
relay_chn_run_forward(2); // Channel ID parameters will be needed in multi channel mode
```
See the examples for further reference
### 1. Initialize relay channels
```c
// Define GPIO pins for relay channels
const gpio_num_t gpio_map[] = {GPIO_NUM_4, GPIO_NUM_5}; // One channel example
const uint8_t gpio_map[] = {4, 5}; // One channel example
/*------------------------------------------------------------------------*/
const uint8_t gpio_map[] = {4, 5, 9, 10, 18, 19}; // Or a 3 channel example
const uint8_t gpio_count = sizeof(gpio_map) / sizeof(gpio_map[0]);
// Create and initialize relay channels
@@ -59,53 +76,138 @@ if (ret != ESP_OK) {
### 2. Control relay channels
For single mode:
```c
// Run channel 0 forward
// Run the channel forward
relay_chn_run_forward();
// Run the channel reverse
relay_chn_run_reverse();
// Stop the channel
relay_chn_stop();
// Flip the direction of the channel
relay_chn_flip_direction();
```
For multi mode
```c
// Run channel #0 forward
relay_chn_run_forward(0);
// Run all channels forward
relay_chn_run_forward(RELAY_CHN_ID_ALL);
// Run channel 0 reverse
relay_chn_run_reverse(0);
// Run channel #1 reverse
relay_chn_run_reverse(1);
// Run all channels reverse
relay_chn_run_reverse(RELAY_CHN_ID_ALL);
// Stop channel 0
relay_chn_stop(0);
// Stop channel #1
relay_chn_stop(1);
// Stop all channels
relay_chn_stop(RELAY_CHN_ID_ALL);
// Flip direction of channel 0
// Flip direction of channel #0
relay_chn_flip_direction(0);
// Flip direction of all channels
relay_chn_flip_direction(RELAY_CHN_ID_ALL);
```
### 3. Monitor channel state
For single mode:
```c
// Get channel state
relay_chn_state_t state = relay_chn_get_state(0);
char *state_str = relay_chn_get_state_str(0);
relay_chn_state_t state = relay_chn_get_state();
// Get the string representation of the state of the channel
char *state_str = relay_chn_get_state_str();
// Get channel direction
relay_chn_direction_t direction = relay_chn_get_direction();
// Listen to relay channel state changes
static void relay_chn_listener(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) {
/* The channel id can be ignored in single mode */
/* Handle state changes */
}
// Register the listener callback
relay_chn_register_listener(relay_chn_listener);
// Unregister the listener when it is not needed anymore
relay_chn_unregister_listener(relay_chn_listener);
```
For multi mode:
```c
// Get channel #0 state
relay_chn_state_t state = relay_chn_get_state(0);
// Get the string representation of the state of the channel #0
char *state_str = relay_chn_get_state_str(0);
// Get channel #0 direction
relay_chn_direction_t direction = relay_chn_get_direction(0);
/* The listener is same for multi mode */
```
### 4. Tilting Interface (if enabled)
For single mode:
```c
// Assuming CONFIG_RELAY_CHN_ENABLE_TILTING is enabled
// Start tilting automatically (channel 0)
// Start tilting automatically
relay_chn_tilt_auto();
// Tilt forward
relay_chn_tilt_forward();
// Tilt reverse
relay_chn_tilt_reverse();
// Stop tilting
relay_chn_tilt_stop();
// Set tilting sensitivity (sensitivity as percentage)
relay_chn_tilt_sensitivity_set(90);
// Get tilting sensitivity (sensitivty as percentage)
uint8_t sensitivity = relay_chn_tilt_get_sensitivity();
```
For multi mode:
```c
// Assuming CONFIG_RELAY_CHN_ENABLE_TILTING is enabled
// Start tilting automatically on channel #0
relay_chn_tilt_auto(0);
relay_chn_tilt_auto(RELAY_CHN_ID_ALL); // on all channels
// Tilt forward (channel 0)
relay_chn_tilt_forward(0);
// Tilt forward on channel #1
relay_chn_tilt_forward(1);
relay_chn_tilt_forward(RELAY_CHN_ID_ALL);
// Tilt reverse (channel 0)
relay_chn_tilt_reverse(0);
// Tilt reverse on channel #2
relay_chn_tilt_reverse(2);
relay_chn_tilt_reverse(RELAY_CHN_ID_ALL);
// Stop tilting (channel 0)
// Stop tilting on channel #0
relay_chn_tilt_stop(0);
relay_chn_tilt_stop(RELAY_CHN_ID_ALL);
// Set tilting sensitivity (channel 0, sensitivity as percentage)
// Set tilting sensitivity (sensitivity as percentage) for channel #0
relay_chn_tilt_sensitivity_set(0, 90);
relay_chn_tilt_sensitivity_set(RELAY_CHN_ID_ALL, 90);
// Get tilting sensitivity (channel 0, sensitivty as percentage)
uint8_t sensitivity = relay_chn_tilt_sensitivity_get(0);
// Get tilting sensitivity (sensitivty as percentage)
uint8_t sensitivity;
relay_chn_tilt_get_sensitivity(0, &sensitivity, sizeof(sensitivity));
```
## License

View File

@@ -1,13 +0,0 @@
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' directory to the COMPONENT_DIRS by specifying: ../relay_chn
set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/../relay_chn")
# Include ESP-IDF project build system
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# Define the name of this project
project(relay_chn_app_test)

View File

@@ -1,3 +0,0 @@
idf_component_register(SRCS "test_relay_chn.c"
INCLUDE_DIRS "."
REQUIRES unity relay_chn)

View File

@@ -1,443 +0,0 @@
#include "driver/gpio.h"
#include "unity.h"
#include "unity_test_utils.h"
#include "relay_chn.h" // Main header file for the relay_chn component
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "sdkconfig.h" // For accessing CONFIG_* values
// Test GPIOs and channel IDs
// Please ensure these GPIOs are correct and suitable for your board.
// Two channels (4 GPIOs) are used as an example.
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]);
// Assuming 2 GPIOs are used per channel
const uint8_t relay_chn_count = gpio_count / 2;
// Retrieve inertia value from SDKconfig
#ifndef CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS
#define CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS 500 // Default if not defined in SDKconfig
#endif
const uint32_t opposite_inertia_ms = CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS;
// Tolerant delay margin to ensure operations complete, especially after inertia.
const uint32_t test_delay_margin_ms = 50;
// --- Test Setup/Teardown Functions ---
void setUp(void) {
ESP_LOGI("TEST_SETUP", "Running setUp for relay_chn tests.");
// Re-create the component before each test. relay_chn_create returns esp_err_t.
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
// Ensure all relays are stopped at the beginning, and transition to FREE state
for (uint8_t i = 0; i < relay_chn_count; i++) {
relay_chn_stop(i); // relay_chn_stop returns void
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); // Wait for FREE state
}
ESP_LOGI("TEST_SETUP", "All channels initialized to RELAY_CHN_STATE_FREE.");
}
void tearDown(void) {
ESP_LOGI("TEST_TEARDOWN", "Running tearDown for relay_chn tests.");
// Stop all relays after each test, and transition to FREE state
for (uint8_t i = 0; i < relay_chn_count; i++) {
relay_chn_stop(i); // relay_chn_stop returns void
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); // Wait for FREE state
}
ESP_LOGI("TEST_TEARDOWN", "All channels returned to RELAY_CHN_STATE_FREE.");
}
// --- Basic Functionality Tests ---
// TEST_CASE 1: Test that relay channels initialize correctly to RELAY_CHN_STATE_FREE
TEST_CASE("Relay channels initialize correctly to FREE state", "[relay_chn]") {
ESP_LOGI("TEST", "Running test: Relay channels initialize correctly to 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 2: Test that relays run in the forward direction and update their state
TEST_CASE("Relay channels run forward and update state", "[relay_chn]") {
ESP_LOGI("TEST", "Running test: Relay channels run forward and update state");
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 3: Test that relays run in the reverse direction and update their state
TEST_CASE("Relay channels run reverse and update state", "[relay_chn]") {
ESP_LOGI("TEST", "Running test: Relay channels run reverse and update state");
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));
}
}
// TEST_CASE 4: 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]") {
ESP_LOGI("TEST", "Running test: Relay channels stop and update to FREE state");
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 5: Test function calls with invalid channel IDs
// TEST_CASE("Invalid channel ID handling", "[relay_chn]") {
// ESP_LOGI("TEST", "Running test: Invalid channel ID handling");
// uint8_t invalid_channel_id = relay_chn_count + 1; // An ID that is out of bounds
// // These calls are expected to return ESP_ERR_INVALID_ARG, so TEST_ASSERT_EQUAL is appropriate.
// TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_run_forward(invalid_channel_id));
// TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_run_reverse(invalid_channel_id));
// TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_stop(invalid_channel_id));
// // Test tilt commands only if tilt functionality is enabled
// #if CONFIG_RELAY_CHN_ENABLE_TILTING == 1
// TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_tilt_forward(invalid_channel_id));
// TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_tilt_reverse(invalid_channel_id));
// #endif
// TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_get_state(invalid_channel_id));
// }
// TEST_CASE 6: Test independent operation of multiple relay channels
TEST_CASE("Multiple channels can operate independently", "[relay_chn]") {
ESP_LOGI("TEST", "Running test: Multiple channels can operate independently");
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 7: 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][inertia]") {
ESP_LOGI("TEST", "Running test: Forward to Reverse transition with opposite inertia");
uint8_t ch = 0; // Channel to test
// 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 8: 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][inertia]") {
ESP_LOGI("TEST", "Running test: Reverse to Forward transition with opposite inertia");
uint8_t ch = 0;
// 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 9: 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][inertia]") {
ESP_LOGI("TEST", "Running test: Running in same direction does not incur inertia");
uint8_t ch = 0;
// 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 10: 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][inertia]") {
ESP_LOGI("TEST", "Running test: FREE to Running transition without inertia");
uint8_t ch = 0;
// 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));
}
// ### Tilt Functionality Tests (Conditional)
// This section will only be compiled if **`CONFIG_RELAY_CHN_ENABLE_TILTING`** is defined as **`1`** in `sdkconfig`.
#if CONFIG_RELAY_CHN_ENABLE_TILTING == 1
#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 11: 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]") {
ESP_LOGI("TEST", "Running test: Run Forward to Tilt Forward transition with inertia");
uint8_t ch = 0;
// 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 12: 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]") {
ESP_LOGI("TEST", "Running test: Run Reverse to Tilt Reverse transition with inertia");
uint8_t ch = 0;
// 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 13: 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]") {
ESP_LOGI("TEST", "Running test: FREE to Tilt Forward transition with inertia (prepared)");
uint8_t ch = 0;
// 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 14: 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]") {
ESP_LOGI("TEST", "Running test: FREE to Tilt Reverse transition with inertia (prepared)");
uint8_t ch = 0;
// 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 15: 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]") {
ESP_LOGI("TEST", "Running test: Tilt Forward to Run Forward transition with inertia");
uint8_t ch = 0;
// 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 16: 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]") {
ESP_LOGI("TEST", "Running test: Tilt Reverse to Run Reverse transition with inertia");
uint8_t ch = 0;
// 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 17: 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]") {
ESP_LOGI("TEST", "Running test: Tilt Forward to Run Reverse transition without inertia");
uint8_t ch = 0;
// 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 18: 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]") {
ESP_LOGI("TEST", "Running test: Tilt to Stop transition without immediate inertia for stop");
uint8_t ch = 0;
// 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));
}
#else // CONFIG_RELAY_CHN_ENABLE_TILTING == 0
// If tilt functionality is disabled, these tests are skipped.
// A dummy test case is added to indicate this in the test output.
TEST_CASE("Tilt functionality is disabled, skipping tilt tests", "[relay_chn][tilt_disabled]") {
ESP_LOGI("TEST", "Tilt functionality is disabled (CONFIG_RELAY_CHN_ENABLE_TILTING is 0). Skipping tilt tests.");
TEST_ASSERT_TRUE(true); // Just to ensure at least one test passes for visibility
}
#endif // CONFIG_RELAY_CHN_ENABLE_TILTING
// ### `app_main` Function
// --- app_main function ---
void app_main(void) {
ESP_LOGI("APP_MAIN", "Starting relay_chn unit tests...");
// Run the Unity test runner
unity_run_all_tests();
// After tests complete, instead of restarting, the device will halt.
ESP_LOGI("APP_MAIN", "All relay_chn tests completed. Device halted.");
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000)); // Wait with low power consumption
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

0
dev
View File

View File

@@ -1,6 +1,6 @@
name: relay_chn
version: "0.4.0"
version: "0.5.0"
description: "Custom component for relay channel control"
license: "MIT"
url: "https://git.kozmotronik.com.tr/KozmotronikTech/relay_chn_component"
repository: "https://git.kozmotronik.com.tr/KozmotronikTech/relay_chn_component.git"
url: "https://git.kozmotronik.com.tr/KozmotronikTech/relay_chn"
repository: "https://git.kozmotronik.com.tr/KozmotronikTech/relay_chn.git"

View File

@@ -1,16 +1,8 @@
#ifndef RELAY_CHN_H
#define RELAY_CHN_H
/**
* @file relay_chn.h
*
* @author
* Ismail Sahillioglu <ismailsahillioglu@gmail.com>
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*
* @date 2025.02.08
*
* @defgroup relay_chn Relay Channel Controller
* @ingroup components
* @{
* One relay channel consists of 2 output relays, hence 2 GPIO pins are required for each relay channel.
* This module provides an API to control the relay channels, specifically to drive bipolar motors.
* It also provides APIs to control the direction of the relay channel, bipolar motors in mind.
@@ -22,65 +14,17 @@
* reliability and prevent conflict operations. Also, the esp timer is used to manage the direction change inertia.
*/
#pragma once
#include "esp_err.h"
#include "driver/gpio.h"
#include <stdint.h>
#include "relay_chn_defs.h"
#include "relay_chn_types.h"
#include "relay_chn_adapter.h"
#ifdef __cplusplus
extern "C" {
#endif
#define RELAY_CHN_ID_ALL CONFIG_RELAY_CHN_COUNT ///< Special ID to address all channels
/**
* @brief Enumeration for relay channel direction.
*/
enum relay_chn_direction_enum {
RELAY_CHN_DIRECTION_DEFAULT, ///< Default direction of the relay channel.
RELAY_CHN_DIRECTION_FLIPPED ///< Flipped direction of the relay channel.
};
/**
* @brief Alias for the enum type relay_chn_direction_enum.
*/
typedef enum relay_chn_direction_enum relay_chn_direction_t;
/**
* @brief Enums that represent the state of a relay channel.
*/
enum relay_chn_state_enum {
RELAY_CHN_STATE_FREE, ///< The relay channel is free to run or execute commands.
RELAY_CHN_STATE_STOPPED, ///< The relay channel is stopped and not running.
RELAY_CHN_STATE_FORWARD, ///< The relay channel is running in the forward direction.
RELAY_CHN_STATE_REVERSE, ///< The relay channel is running in the reverse direction.
RELAY_CHN_STATE_FORWARD_PENDING, ///< The relay channel is pending to run in the forward direction.
RELAY_CHN_STATE_REVERSE_PENDING, ///< The relay channel is pending to run in the reverse direction.
#if CONFIG_RELAY_CHN_ENABLE_TILTING == 1
RELAY_CHN_STATE_TILT_FORWARD, ///< The relay channel is tilting for forward.
RELAY_CHN_STATE_TILT_REVERSE, ///< The relay channel is tilting for reverse.
#endif
};
/**
* @brief Alias for the enum type relay_chn_state_enum.
*/
typedef enum relay_chn_state_enum relay_chn_state_t;
/**
* @brief Relay channel state change listener.
*
* An optional interface to listen to the channel state change events.
* The listeners SHOULD be implemented as light functions and SHOULD NOT contain
* any blocking calls. Otherwise the relay_chn module would not function properly
* since it is designed as event driven.
*
* @param chn_id The ID of the channel whose state has changed.
* @param old_state The old state of the channel.
* @param new_state The new state of the channel.
*/
typedef void (*relay_chn_state_listener_t)(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state);
/**
* @brief Create and initialize relay channels.
*
@@ -94,7 +38,14 @@ typedef void (*relay_chn_state_listener_t)(uint8_t chn_id, relay_chn_state_t old
* - ESP_ERR_INVALID_ARG: Invalid argument
* - ESP_FAIL: General failure
*/
esp_err_t relay_chn_create(const gpio_num_t* gpio_map, uint8_t gpio_count);
esp_err_t relay_chn_create(const uint8_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.
@@ -116,6 +67,7 @@ esp_err_t relay_chn_register_listener(relay_chn_state_listener_t listener);
*/
void relay_chn_unregister_listener(relay_chn_state_listener_t listener);
#if RELAY_CHN_COUNT > 1
/**
* @brief Get the state of the specified relay channel.
*
@@ -142,14 +94,6 @@ relay_chn_state_t relay_chn_get_state(uint8_t chn_id);
*/
char *relay_chn_get_state_str(uint8_t chn_id);
/**
* @brief Return the text presentation of an state.
*
* @param state A state with type of relay_chn_state_t.
* @return char* The text presentation of the state. "UNKNOWN" if the state is not known.
*/
char *relay_chn_state_str(relay_chn_state_t state);
/**
* @brief Runs the relay channel in the forward direction.
*
@@ -183,8 +127,7 @@ void relay_chn_stop(uint8_t chn_id);
* @brief Flips the direction of the specified relay channel.
*
* This function toggles the direction of the relay channel identified by the
* given channel ID. It is typically used to change the state of the relay
* from its current direction to the opposite direction.
* given channel ID.
*
* @param chn_id The ID of the relay channel to flip. This should be a valid
* channel ID within the range of available relay channels.
@@ -209,9 +152,9 @@ relay_chn_direction_t relay_chn_get_direction(uint8_t chn_id);
/**
* @brief Enables automatic tilting for the specified relay channel.
*
* This function enables automatic tilting mode for the given relay channel. The channel will automatically
* switch between forward and reverse tilting based on some internal sensing mechanism (not detailed here).
* Requires appropriate hardware support and configuration.
* This function enables automatic tilting mode for the given relay channel.
* The channel will automatically switch between forward and reverse tilting
* based on the last movement of the channel
*
* @param chn_id The ID of the relay channel to enable automatic tilting.
*/
@@ -220,8 +163,7 @@ void relay_chn_tilt_auto(uint8_t chn_id);
/**
* @brief Tilts the specified relay channel forward.
*
* This function initiates a forward tilting action for the specified relay channel. This is a manual tilting
* operation, unlike `relay_chn_tilt_auto()`.
* This function initiates a forward tilting action for the specified relay channel.
*
* @param chn_id The ID of the relay channel to tilt forward.
*/
@@ -230,8 +172,7 @@ void relay_chn_tilt_forward(uint8_t chn_id);
/**
* @brief Tilts the specified relay channel reverse.
*
* This function initiates a reverse tilting action for the specified relay channel. This is a manual tilting
* operation, unlike `relay_chn_tilt_auto()`.
* This function initiates a reverse tilting action for the specified relay channel.
*
* @param chn_id The ID of the relay channel to tilt reverse.
*/
@@ -255,7 +196,7 @@ void relay_chn_tilt_stop(uint8_t chn_id);
* @param chn_id The ID of the relay channel to set the sensitivity for.
* @param sensitivity The sensitivity in percentage: 0 - 100%.
*/
void relay_chn_tilt_sensitivity_set(uint8_t chn_id, uint8_t sensitivity);
void relay_chn_tilt_set_sensitivity(uint8_t chn_id, uint8_t sensitivity);
/**
* @brief Gets the tilting sensitivity for the specified relay channel.
@@ -270,14 +211,127 @@ void relay_chn_tilt_sensitivity_set(uint8_t chn_id, uint8_t sensitivity);
* - ESP_OK: Success
* - ESP_ERR_INVALID_ARG: Invalid argument
*/
esp_err_t relay_chn_tilt_sensitivity_get(uint8_t chn_id, uint8_t *sensitivity, size_t length);
esp_err_t relay_chn_tilt_get_sensitivity(uint8_t chn_id, uint8_t *sensitivity, size_t length);
#endif // CONFIG_RELAY_CHN_ENABLE_TILTING
#else // RELAY_CHN_COUNT > 1
/**
* @brief Get the state of the relay channel.
*
* This function retrieves the current state of the relay channel.
*
* @return The current state of the relay channel.
*/
relay_chn_state_t relay_chn_get_state(void);
/**
* @brief Get the state string of the relay channel.
*
* This function returns a string representation of the state of the relay channel.
*
* @return A pointer to a string representing the state of the relay
* channel. The returned string is managed internally and should not be
* modified or freed by the caller.
*/
char *relay_chn_get_state_str(void);
/**
* @brief Runs the relay channel in the forward direction.
*
* This function activates the relay channel to run in the forward direction.
*/
void relay_chn_run_forward(void);
/**
* @brief Runs the relay channel in reverse.
*
* This function activates the relay channel to run in reverse.
*/
void relay_chn_run_reverse(void);
/**
* @brief Stops the relay channel.
*
* This function stops the operation of the relay channel.
*/
void relay_chn_stop(void);
/**
* @brief Flips the direction of the relay channel.
*
* This function toggles the direction of the relay channel.
*/
void relay_chn_flip_direction(void);
/**
* @brief Get the direction of the relay channel.
*
* This function retrieves the direction configuration of a relay channel.
*
* @return The direction of the relay channel as a value of type
* relay_chn_direction_t.
*/
relay_chn_direction_t relay_chn_get_direction(void);
#if CONFIG_RELAY_CHN_ENABLE_TILTING == 1
/**
* @brief Enables automatic tilting for the relay channel.
*
* This function enables automatic tilting mode for the given relay channel.
* The channel will automatically switch between forward and reverse tilting
* based on the last movement of the channel
*/
void relay_chn_tilt_auto(void);
/**
* @brief Tilts the relay channel forward.
*
* This function initiates a forward tilting action for the relay channel.
*/
void relay_chn_tilt_forward(void);
/**
* @brief Tilts the relay channel reverse.
*
* This function initiates a reverse tilting action for the relay channel.
*/
void relay_chn_tilt_reverse(void);
/**
* @brief Stops the tilting action on the relay channel.
*
* This function stops any ongoing tilting action (automatic or manual) on the relay channel.
*/
void relay_chn_tilt_stop(void);
/**
* @brief Sets the tilting sensitivity for the relay channel.
*
* This function sets the sensitivity for the automatic tilting mechanism. A higher sensitivity value
* typically means the channel will react more readily to tilting events.
*
* @param sensitivity The sensitivity in percentage: 0 - 100%.
*/
void relay_chn_tilt_set_sensitivity(uint8_t sensitivity);
/**
* @brief Gets the tilting sensitivity for the relay channel.
*
* This function retrieves the currently set sensitivity for the relay channel's automatic
* tilting mechanism.
*
* @return Sensitivity value in percentage: 0 - 100%.
*/
uint8_t relay_chn_tilt_get_sensitivity(void);
#endif // CONFIG_RELAY_CHN_ENABLE_TILTING
#endif // RELAY_CHN_COUNT > 1
#ifdef __cplusplus
}
#endif
/// @}
#endif // RELAY_CHN_H
#endif

186
include/relay_chn_adapter.h Normal file
View File

@@ -0,0 +1,186 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*
* An adapter header to expose the appropriate API functions to the public API
* depending on the RELAY_CHN_COUNT value which determines single or multi mode.
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#if RELAY_CHN_COUNT > 1
/**
* @brief Get the current state of a relay channel.
*
* @param[in] chn_id Channel ID to get state for.
* @return Current state of the specified channel, or RELAY_CHN_STATE_UNDEFINED if invalid.
*/
extern relay_chn_state_t relay_chn_ctl_get_state(uint8_t chn_id);
/**
* @brief Get string representation of a relay channel's state.
*
* @param[in] chn_id Channel ID to get state string for.
* @return String representation of channel state, or "UNDEFINED" if invalid.
*/
extern char *relay_chn_ctl_get_state_str(uint8_t chn_id);
/**
* @brief Run a relay channel in forward direction.
*
* @param[in] chn_id Channel ID to run forward, or RELAY_CHN_ID_ALL for all channels.
*/
extern void relay_chn_ctl_run_forward(uint8_t chn_id);
/**
* @brief Run a relay channel in reverse direction.
*
* @param[in] chn_id Channel ID to run reverse, or RELAY_CHN_ID_ALL for all channels.
*/
extern void relay_chn_ctl_run_reverse(uint8_t chn_id);
/**
* @brief Stop a relay channel.
*
* @param[in] chn_id Channel ID to stop, or RELAY_CHN_ID_ALL for all channels.
*/
extern void relay_chn_ctl_stop(uint8_t chn_id);
/**
* @brief Flip the running direction of a relay channel.
*
* @param[in] chn_id Channel ID to flip direction for, or RELAY_CHN_ID_ALL for all channels.
*/
extern void relay_chn_ctl_flip_direction(uint8_t chn_id);
/**
* @brief Get the current direction of a relay channel.
*
* @param[in] chn_id Channel ID to get direction for.
* @return Current direction of the specified channel, or RELAY_CHN_DIRECTION_DEFAULT if invalid.
*/
extern relay_chn_direction_t relay_chn_ctl_get_direction(uint8_t chn_id);
static inline relay_chn_state_t relay_chn_get_state(uint8_t chn_id)
{
return relay_chn_ctl_get_state(chn_id);
}
static inline char *relay_chn_get_state_str(uint8_t chn_id)
{
return relay_chn_ctl_get_state_str(chn_id);
}
static inline void relay_chn_run_forward(uint8_t chn_id)
{
relay_chn_ctl_run_forward(chn_id);
}
static inline void relay_chn_run_reverse(uint8_t chn_id)
{
relay_chn_ctl_run_reverse(chn_id);
}
static inline void relay_chn_stop(uint8_t chn_id)
{
relay_chn_ctl_stop(chn_id);
}
static inline void relay_chn_flip_direction(uint8_t chn_id)
{
relay_chn_ctl_flip_direction(chn_id);
}
static inline relay_chn_direction_t relay_chn_get_direction(uint8_t chn_id)
{
return relay_chn_ctl_get_direction(chn_id);
}
#else
/**
* @brief Get the current state of the relay channel.
*
* @return Current state of the channel.
*/
extern relay_chn_state_t relay_chn_ctl_get_state(void);
/**
* @brief Get string representation of the relay channel's state.
*
* @return String representation of channel state.
*/
extern char *relay_chn_ctl_get_state_str(void);
/**
* @brief Run the relay channel in forward direction.
*/
extern void relay_chn_ctl_run_forward(void);
/**
* @brief Run the relay channel in reverse direction.
*/
extern void relay_chn_ctl_run_reverse(void);
/**
* @brief Stop the relay channel.
*/
extern void relay_chn_ctl_stop(void);
/**
* @brief Flip the running direction of the relay channel.
*/
extern void relay_chn_ctl_flip_direction(void);
/**
* @brief Get the current direction of the relay channel.
*
* @return Current direction of the channel.
*/
extern relay_chn_direction_t relay_chn_ctl_get_direction(void);
static inline relay_chn_state_t relay_chn_get_state(void)
{
return relay_chn_ctl_get_state();
}
static inline char *relay_chn_get_state_str(void)
{
return relay_chn_ctl_get_state_str();
}
static inline void relay_chn_run_forward(void)
{
relay_chn_ctl_run_forward();
}
static inline void relay_chn_run_reverse(void)
{
relay_chn_ctl_run_reverse();
}
static inline void relay_chn_stop(void)
{
relay_chn_ctl_stop();
}
static inline void relay_chn_flip_direction(void)
{
relay_chn_ctl_flip_direction();
}
static inline relay_chn_direction_t relay_chn_get_direction(void)
{
return relay_chn_ctl_get_direction();
}
#endif // RELAY_CHN_COUNT > 1
#ifdef __cplusplus
}
#endif

33
include/relay_chn_defs.h Normal file
View File

@@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/* Config defines for covenient writing */
#define RELAY_CHN_OPPOSITE_INERTIA_MS CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS
#define RELAY_CHN_COUNT CONFIG_RELAY_CHN_COUNT
#define RELAY_CHN_ENABLE_TILTING CONFIG_RELAY_CHN_ENABLE_TILTING
#define RELAY_CHN_ENABLE_NVS CONFIG_RELAY_CHN_ENABLE_NVS
#if RELAY_CHN_ENABLE_NVS == 1
#define RELAY_CHN_NVS_NAMESPACE CONFIG_RELAY_CHN_NVS_NAMESPACE
#define RELAY_CHN_NVS_CUSTOM_PARTITION CONFIG_RELAY_CHN_NVS_CUSTOM_PARTITION
#if RELAY_CHN_NVS_CUSTOM_PARTITION == 1
#define RELAY_CHN_NVS_CUSTOM_PARTITION_NAME CONFIG_RELAY_CHN_NVS_CUSTOM_PARTITION_NAME
#endif
#endif
#if RELAY_CHN_COUNT > 1
#define RELAY_CHN_ID_ALL RELAY_CHN_COUNT /*!< Special ID to address all channels */
#endif
#ifdef __cplusplus
}
#endif

58
include/relay_chn_types.h Normal file
View File

@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <stdint.h>
#include "relay_chn_defs.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Enumeration for relay channel direction.
*/
typedef enum relay_chn_direction_enum {
RELAY_CHN_DIRECTION_DEFAULT, /*!< Default direction of the relay channel */
RELAY_CHN_DIRECTION_FLIPPED /*!< Flipped direction of the relay channel */
} relay_chn_direction_t;
/**
* @brief Enums that represent the state of a relay channel.
*/
typedef enum relay_chn_state_enum {
RELAY_CHN_STATE_UNDEFINED, /*!< The relay channel state is undefined */
RELAY_CHN_STATE_IDLE, /*!< The relay channel is free to run or execute commands */
RELAY_CHN_STATE_STOPPED, /*!< The relay channel is stopped and not running */
RELAY_CHN_STATE_FORWARD, /*!< The relay channel is running in the forward direction */
RELAY_CHN_STATE_REVERSE, /*!< The relay channel is running in the reverse direction */
RELAY_CHN_STATE_FORWARD_PENDING, /*!< The relay channel is pending to run in the forward direction */
RELAY_CHN_STATE_REVERSE_PENDING, /*!< The relay channel is pending to run in the reverse direction */
#if CONFIG_RELAY_CHN_ENABLE_TILTING == 1
RELAY_CHN_STATE_TILT_FORWARD, /*!< The relay channel is tilting for forward */
RELAY_CHN_STATE_TILT_REVERSE, /*!< The relay channel is tilting for reverse */
#endif
} relay_chn_state_t;
/**
* @brief Relay channel state change listener.
*
* An optional interface to listen to the channel state change events.
* The listeners SHOULD be implemented as light functions and SHOULD NOT contain
* any blocking calls. Otherwise the relay_chn module would not function properly
* since it is designed as event driven.
*
* @param chn_id The ID of the channel whose state has changed.
* @param old_state The old state of the channel.
* @param new_state The new state of the channel.
*/
typedef void (*relay_chn_state_listener_t)(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,114 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "esp_err.h"
#include "esp_log.h"
#include "esp_event_base.h"
#include "esp_event.h"
#include "esp_timer.h"
#include "relay_chn_defs.h"
#include "relay_chn_types.h"
#include "relay_chn_priv_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/// Event base used by *_core, *_ctl, and *_tilt modules.
ESP_EVENT_DECLARE_BASE(RELAY_CHN_CMD_EVENT);
/**
* @brief Initializes the relay channel timer.
*
* This function creates a timer for the relay channel to handle direction change inertia.
* Required by *_ctl_* module.
*
* @param chn_ctl Pointer to the relay channel control structure.
* @return esp_err_t ESP_OK on success, or an error code on failure.
*/
esp_err_t relay_chn_init_timer(relay_chn_ctl_t *chn_ctl);
/**
* @brief Issues a command to the relay channel.
*
* Evaluates the current state of the relay channel and issues the command accordingly.
* Required by *_core, *_ctl_* and *_tilt modules.
*
* @param chn_ctl Pointer to the relay channel control structure.
* @param cmd The command to issue.
*/
void relay_chn_issue_cmd(relay_chn_ctl_t* chn_ctl, relay_chn_cmd_t cmd);
/**
* @brief Dispatches a relay channel command to the event loop.
*
* @param chn_ctl Pointer to the relay channel control structure.
* @param cmd The command to dispatch.
*/
void relay_chn_dispatch_cmd(relay_chn_ctl_t *chn_ctl, relay_chn_cmd_t cmd);
/**
* @brief Returns the string representation of a relay channel command.
*
* @param cmd The relay channel command.
* @return char* The string representation of the command.
*/
char *relay_chn_cmd_str(relay_chn_cmd_t cmd);
/**
* @brief Starts the ESP timer once with the specified time in milliseconds.
*
* Starts the ESP timer to run once after the specified time.
* If the timer is already running, it stops it first and then starts it again.
* Required by *_ctl_* and *_tilt modules.
*
* @param esp_timer The ESP timer handle.
* @param time_ms The time in milliseconds to wait before the timer expires.
* @return esp_err_t ESP_OK on success, or an error code on failure.
*/
esp_err_t relay_chn_start_esp_timer_once(esp_timer_handle_t esp_timer, uint32_t time_ms);
/**
* @brief Updates the state of the relay channel and notifies listeners.
*
* This function updates the state of the relay channel and notifies all registered listeners
* about the state change.
* Required by *_ctl_* and *_tilt modules.
*
* @param chn_ctl Pointer to the relay channel control structure.
* @param new_state The new state to set for the relay channel.
*/
void relay_chn_update_state(relay_chn_ctl_t *chn_ctl, relay_chn_state_t new_state);
/**
* @brief Return the text presentation of an state.
*
* @param state A state with type of relay_chn_state_t.
* @return char* The text presentation of the state. "UNKNOWN" if the state is not known.
*/
char *relay_chn_state_str(relay_chn_state_t state);
#if RELAY_CHN_COUNT > 1
/**
* @brief Check if the provided channel ID is valid.
*
* @param chn_id Channel ID to check.
* @return true Channel ID is valid.
* @return false Channel ID is invalid.
*/
bool relay_chn_is_channel_id_valid(uint8_t chn_id);
#endif // RELAY_CHN_COUNT > 1
#if RELAY_CHN_ENABLE_TILTING == 1
/// Relay channel event loop handle declaration for *_tilt module.
extern esp_event_loop_handle_t relay_chn_event_loop;
#endif // RELAY_CHN_ENABLE_TILTING
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech developer@kozmotronik.com.tr
*
* SPDX-License-Identifier: MIT
*
* Expose the *_ctl functions required by *_core.c file.
*/
#pragma once
#include "relay_chn_priv_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize the relay channel control.
*
* @param output Pointer to the output object(s).
* @param run_info Pointer to the runtime information object(s).
*
* @return esp_err_t Returns ESP_OK on success, or an error code on failure.
*/
esp_err_t relay_chn_ctl_init(relay_chn_output_t *output, relay_chn_run_info_t *run_info);
/**
* @brief Deinitialize the relay channel control.
*
* This function cleans up resources used by the relay channel control.
*/
void relay_chn_ctl_deinit(void);
#if RELAY_CHN_COUNT > 1
/**
* @brief Get the control structure for a specific relay channel.
*
* @param chn_id The ID of the relay channel to retrieve.
*
* @return relay_chn_ctl_t* Pointer to the control structure for the specified channel, or NULL if not found.
*/
relay_chn_ctl_t *relay_chn_ctl_get(uint8_t chn_id);
/**
* @brief Get the control structures for all relay channels.
*
* @return relay_chn_ctl_t* Pointer to the array of control structures for all channels.
*/
relay_chn_ctl_t *relay_chn_ctl_get_all(void);
#else
relay_chn_ctl_t *relay_chn_ctl_get(void);
#endif // RELAY_CHN_COUNT > 1
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,104 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "relay_chn_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize NVS storage for relay channels.
*
* @attention Before calling this function, make sure the NVS flash is initialised
* using either the nvs_flash_init() function for the default NVS partition or the
* nvs_flash_init_partition() function for a custom partition.
*
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_nvs_init(void);
/**
* @brief Store relay channel direction in NVS.
*
* @param[in] ch Channel number.
* @param[in] direction Direction to store.
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_nvs_set_direction(uint8_t ch, relay_chn_direction_t direction);
/**
* @brief Retrieve relay channel direction from NVS.
*
* @param[in] ch Channel number.
* @param[out] direction Pointer to store retrieved direction.
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_nvs_get_direction(uint8_t ch, relay_chn_direction_t *direction);
#ifdef RELAY_CHN_ENABLE_TILTING
/**
* @brief Store tilt sensitivity in NVS.
*
* @param[in] ch Channel number.
* @param[in] sensitivity Sensitivity value to store.
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_nvs_set_tilt_sensitivity(uint8_t ch, uint8_t sensitivity);
/**
* @brief Retrieve tilt sensitivity from NVS.
*
* @param[in] ch Channel number.
* @param[out] sensitivity Pointer to store retrieved sensitivity.
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_nvs_get_tilt_sensitivity(uint8_t ch, uint8_t *sensitivity);
/**
* @brief Store tilt counters in NVS.
*
* @param[in] ch Channel number.
* @param[in] tilt_count Tilt count value.
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_nvs_set_tilt_count(uint8_t ch, uint16_t tilt_count);
/**
* @brief Retrieve tilt counters from NVS.
*
* @param[in] ch Channel number.
* @param[out] tilt_count Pointer to store tilt count.
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_nvs_get_tilt_count(uint8_t ch, uint16_t *tilt_count);
#endif // RELAY_CHN_ENABLE_TILTING
/**
* @brief Erase all keys in the NVS namespace.
*
* This function will erase all key-value pairs in the NVS namespace used by relay channels.
*
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_nvs_erase_all(void);
/**
* @brief Deinitialize NVS storage for relay channels.
*
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_nvs_deinit(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,113 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*
* Abstraction layer for controlling relay channel outputs. This is the layer
* that interacts with the GPIO pins to control the relay channels.
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#include "relay_chn_priv_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize relay channel outputs.
*
* Maps relay channels to GPIO pins and prepares them for operation.
*
* @param[in] gpio_map Array of GPIO pin numbers for each relay channel.
* @param[in] gpio_count Number of GPIO pins (relay channels).
*
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_output_init(const uint8_t* gpio_map, uint8_t gpio_count);
/**
* @brief Deinitialize relay channel outputs.
*
* Releases resources and resets GPIO pins used for relay channels.
*/
void relay_chn_output_deinit(void);
#if RELAY_CHN_COUNT > 1
/**
* @brief Get the relay channel output object for a specific channel.
*
* @param[in] chn_id Channel ID.
*
* @return Pointer to relay channel output object, or NULL if invalid.
*/
relay_chn_output_t *relay_chn_output_get(uint8_t chn_id);
/**
* @brief Get all relay channel output objects.
*
* @return Pointer to array of relay channel output objects.
*/
relay_chn_output_t *relay_chn_output_get_all(void);
#else
/**
* @brief Get the relay channel output object.
*
* @return Pointer to relay channel output object.
*/
relay_chn_output_t *relay_chn_output_get(void);
#endif // RELAY_CHN_COUNT > 1
/**
* @brief Stop the relay channel output.
*
* Sets the relay channel to the stop state.
*
* @param[in] output Pointer to relay channel output object.
*
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_output_stop(relay_chn_output_t *output);
/**
* @brief Set relay channel output to forward direction.
*
* @param[in] output Pointer to relay channel output object.
*
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_output_forward(relay_chn_output_t *output);
/**
* @brief Set relay channel output to reverse direction.
*
* @param[in] output Pointer to relay channel output object.
*
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_output_reverse(relay_chn_output_t *output);
/**
* @brief Flip the direction of the relay channel output.
*
* Changes the direction from forward to reverse or vice versa.
*
* @param[in] output Pointer to relay channel output object.
*/
void relay_chn_output_flip(relay_chn_output_t *output);
/**
* @brief Get the current direction of the relay channel output.
*
* @param[in] output Pointer to relay channel output object.
*
* @return Current direction of the relay channel.
*/
relay_chn_direction_t relay_chn_output_get_direction(relay_chn_output_t *output);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,78 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <stdint.h>
#include "driver/gpio.h"
#include "esp_timer.h"
#include "relay_chn_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Enumeration for relay channel commands.
*/
typedef enum {
RELAY_CHN_CMD_NONE, /*!< No command */
RELAY_CHN_CMD_STOP, /*!< Stop the relay channel */
RELAY_CHN_CMD_FORWARD, /*!< Run the relay channel in the forward direction */
RELAY_CHN_CMD_REVERSE, /*!< Run the relay channel in the reverse direction */
RELAY_CHN_CMD_FLIP, /*!< Flip the direction of the relay channel */
RELAY_CHN_CMD_IDLE /*!< Free the relay channel */
} relay_chn_cmd_t;
/**
* @brief Structure to hold the output configuration of a relay channel.
*/
typedef struct {
gpio_num_t forward_pin; /*!< GPIO pin number for the forward direction */
gpio_num_t reverse_pin; /*!< GPIO pin number for the reverse direction */
relay_chn_direction_t direction; /*!< The current direction of the relay channel */
} relay_chn_output_t;
/**
* @brief Structure to hold runtime information for a relay channel.
*/
typedef struct {
relay_chn_cmd_t last_run_cmd; /*!< The last run command issued on the relay channel; forward or reverse */
uint32_t last_run_cmd_time_ms; /*!< The time in milliseconds when the last run command was issued */
} relay_chn_run_info_t;
#if RELAY_CHN_ENABLE_TILTING == 1
/// @brief Tilt commands.
typedef enum {
RELAY_CHN_TILT_CMD_NONE, /*!< No command */
RELAY_CHN_TILT_CMD_STOP, /*!< Tilt command stop */
RELAY_CHN_TILT_CMD_FORWARD, /*!< Tilt command for forward */
RELAY_CHN_TILT_CMD_REVERSE /*!< Tilt command for reverse */
} relay_chn_tilt_cmd_t;
/// Forward declaration for relay_chn_tilt_ctl
typedef struct relay_chn_tilt_ctl relay_chn_tilt_ctl_t;
#endif
/**
* @brief Structure to hold the state and configuration of a relay channel.
*/
typedef struct {
uint8_t id; /*!< The ID of the relay channel */
relay_chn_state_t state; /*!< The current state of the relay channel */
relay_chn_run_info_t *run_info; /*!< Runtime information of the relay channel */
relay_chn_output_t *output; /*!< Output configuration of the relay channel */
relay_chn_cmd_t pending_cmd; /*!< The command that is pending to be issued */
esp_timer_handle_t inertia_timer; /*!< Timer to handle the opposite direction inertia time */
#if RELAY_CHN_ENABLE_TILTING == 1
relay_chn_tilt_ctl_t *tilt_ctl; /*!< Pointer to the tilt control structure if tilting is enabled */
#endif
} relay_chn_ctl_t;
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,84 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*
* This is for managing the run information of relay channels.
*/
#pragma once
#include "relay_chn_priv_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize relay channel run information.
*
* Initializes the run information for all relay channels with default values.
*/
void relay_chn_run_info_init(void);
#if RELAY_CHN_COUNT > 1
/**
* @brief Get run information object for a specific relay channel.
*
* @param[in] chn_id Channel ID to get run information for.
* @return Pointer to run information structure, or NULL if channel ID is invalid.
*/
relay_chn_run_info_t *relay_chn_run_info_get(uint8_t chn_id);
/**
* @brief Get run information objects for all relay channels.
*
* @return Pointer to array of run information structures.
*/
relay_chn_run_info_t *relay_chn_run_info_get_all(void);
#else
/**
* @brief Get run information object for the single relay channel.
*
* @return Pointer to run information structure.
*/
relay_chn_run_info_t *relay_chn_run_info_get(void);
#endif // RELAY_CHN_COUNT > 1
/**
* @brief Get the last run command for a relay channel.
*
* @param[in] run_info Pointer to run information structure.
*
* @return Last command that was executed, or RELAY_CHN_CMD_NONE if invalid.
*/
relay_chn_cmd_t relay_chn_run_info_get_last_run_cmd(relay_chn_run_info_t *run_info);
/**
* @brief Set the last run command for a relay channel.
*
* @param[in] run_info Pointer to run information structure.
* @param[in] cmd Command to set as last run command.
*/
void relay_chn_run_info_set_last_run_cmd(relay_chn_run_info_t *run_info, relay_chn_cmd_t cmd);
/**
* @brief Get the timestamp of the last run command.
*
* @param[in] run_info Pointer to run information structure.
*
* @return Timestamp in milliseconds of last command, or 0 if invalid.
*/
uint32_t relay_chn_run_info_get_last_run_cmd_time_ms(relay_chn_run_info_t *run_info);
/**
* @brief Set the timestamp for the last run command.
*
* @param[in] run_info Pointer to run information structure.
* @param[in] time_ms Timestamp in milliseconds to set.
*/
void relay_chn_run_info_set_last_run_cmd_time_ms(relay_chn_run_info_t *run_info, uint32_t time_ms);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "relay_chn_priv_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize relay channel tilt controls.
*
* Sets up tilt functionality for relay channels including timers and event handlers.
* Must be called before using any other tilt functions.
*
* @param[in] chn_ctls Array of relay channel control structures.
*
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_tilt_init(relay_chn_ctl_t *chn_ctls);
/**
* @brief Deinitialize relay channel tilt controls.
*
* Cleans up tilt resources including timers and event handlers.
* Should be called when tilt functionality is no longer needed.
*/
void relay_chn_tilt_deinit(void);
/**
* @brief Dispatch a tilt command to a relay channel.
*
* Queues a tilt command for execution on the specified channel.
*
* @param[in] tilt_ctl Pointer to tilt control structure.
* @param[in] cmd Tilt command to execute.
*
* @return ESP_OK on success, error code otherwise.
*/
esp_err_t relay_chn_tilt_dispatch_cmd(relay_chn_tilt_ctl_t *tilt_ctl, relay_chn_tilt_cmd_t cmd);
/**
* @brief Reset tilt counters for a relay channel.
*
* Resets both forward and reverse tilt counters to zero.
*
* @param[in] tilt_ctl Pointer to tilt control structure.
*/
void relay_chn_tilt_reset_count(relay_chn_tilt_ctl_t *tilt_ctl);
#ifdef __cplusplus
}
#endif

View File

@@ -1,4 +0,0 @@
idf_component_register(SRCS "src/relay_chn.c"
INCLUDE_DIRS include
REQUIRES driver
PRIV_REQUIRES esp_timer esp_event)

View File

@@ -1,29 +0,0 @@
menu "Relay Channel Driver Configuration"
config RELAY_CHN_OPPOSITE_INERTIA_MS
int "Inertia time before it runs opposite direction (ms)"
range 200 1500
default 800
help
Time to wait after changing the direction of the output before
starting the output. This is useful for the motors or some other
mechanical actuators to allow them to stop and settle before
changing the direction.
config RELAY_CHN_COUNT
int "Number of relay channels"
range 1 8
default 1
help
Number of relay channels between 1 and 8.
config RELAY_CHN_ENABLE_TILTING
bool "Enable tilting on relay channels"
default n
help
This option controls enabling tilting on channels. 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.
endmenu

View File

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

File diff suppressed because it is too large Load Diff

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" "nvs")
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|nvs|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

30
scripts/run_tests_all_cfgs.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -e
# Check tag argument
arg_tag=$1
if [[ -z "$arg_tag" ]]; then
arg_tag="all"
fi
# Resolve Paths and Switch to Working Directory
script_dir=$(dirname "$(readlink -f "$0")")
project_root=$(dirname "$script_dir")
echo "Script dir: ${script_dir}"
echo "Project root: ${project_root}"
echo "🔍 Searching for 'test_apps' directory in '$project_root'..."
test_apps_dir=$(find "$project_root" -type d -name "test_apps" | head -n 1)
echo "test_apps dir: ${test_apps_dir}"
# Execute tests for all configs
mapfile -t sdkcfg_files < <(find "$test_apps_dir" -maxdepth 1 -type f -name "sdkconfig.defaults*")
for sdkcfg_file in "${sdkcfg_files[@]}"; do
echo "🔧 Running tests with config: $sdkcfg_file"
"${script_dir}"/run_tests.sh -c -f "$sdkcfg_file" -t "$arg_tag" || {
echo "❌ Tests failed with config: $sdkcfg_file"
exit 1
}
done

589
src/relay_chn_core.c Normal file
View File

@@ -0,0 +1,589 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#include <stdio.h>
#include <stdlib.h>
#include "esp_check.h"
#include "esp_task.h"
#include "relay_chn_output.h"
#include "relay_chn_run_info.h"
#include "relay_chn_ctl.h"
#if RELAY_CHN_ENABLE_TILTING == 1
#include "relay_chn_tilt.h"
#endif
#if RELAY_CHN_ENABLE_NVS == 1
#include "relay_chn_nvs.h"
#endif
#include "relay_chn_core.h"
static const char *TAG = "RELAY_CHN_CORE";
ESP_EVENT_DEFINE_BASE(RELAY_CHN_CMD_EVENT);
// Structure to hold a listener entry in the linked list.
typedef struct relay_chn_listener_entry_type {
relay_chn_state_listener_t listener; /*!< The listener function pointer */
ListItem_t list_item; /*!< FreeRTOS list item */
} relay_chn_listener_entry_t;
// The list that holds references to the registered listeners.
static List_t relay_chn_listener_list;
// Define the event loop for global access both for this module and tilt module.
esp_event_loop_handle_t relay_chn_event_loop = NULL;
// Private function declarations
// Event handler for the relay channel command event
static void relay_chn_event_handler(void* handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data);
// Timer callback function for relay channel direction change inertia.
static void relay_chn_timer_cb(void* arg)
{
relay_chn_ctl_t* chn_ctl = (relay_chn_ctl_t*) arg;
// Does channel have a pending command?
if (chn_ctl->pending_cmd != RELAY_CHN_CMD_NONE) {
relay_chn_dispatch_cmd(chn_ctl, chn_ctl->pending_cmd);
chn_ctl->pending_cmd = RELAY_CHN_CMD_NONE;
}
else {
ESP_LOGE(TAG, "relay_chn_timer_cb: No pending cmd for relay channel %d!", chn_ctl->id);
}
}
esp_err_t relay_chn_init_timer(relay_chn_ctl_t *chn_ctl)
{
char timer_name[32];
snprintf(timer_name, sizeof(timer_name), "relay_chn_%d_timer", chn_ctl->id);
esp_timer_create_args_t timer_args = {
.callback = relay_chn_timer_cb,
.arg = chn_ctl,
.name = timer_name
};
return esp_timer_create(&timer_args, &chn_ctl->inertia_timer);
}
static esp_err_t relay_chn_create_event_loop()
{
esp_event_loop_args_t loop_args = {
.queue_size = RELAY_CHN_COUNT * 8,
.task_name = "relay_chn_event_loop",
.task_priority = ESP_TASKD_EVENT_PRIO - 1,
.task_stack_size = 2048,
.task_core_id = tskNO_AFFINITY
};
esp_err_t ret = esp_event_loop_create(&loop_args, &relay_chn_event_loop);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to create event loop for relay channel");
ret = esp_event_handler_register_with(relay_chn_event_loop,
RELAY_CHN_CMD_EVENT,
ESP_EVENT_ANY_ID,
relay_chn_event_handler, NULL);
return ret;
}
esp_err_t relay_chn_create(const uint8_t* gpio_map, uint8_t gpio_count)
{
ESP_RETURN_ON_FALSE(gpio_map != NULL, ESP_ERR_INVALID_ARG, TAG, "gpio_map cannot be NULL");
esp_err_t ret;
#if RELAY_CHN_ENABLE_NVS == 1
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 RELAY_CHN_COUNT > 1
relay_chn_output_t *outputs = relay_chn_output_get_all();
relay_chn_run_info_t *run_infos = relay_chn_run_info_get_all();
#else
relay_chn_output_t *outputs = relay_chn_output_get();
relay_chn_run_info_t *run_infos = relay_chn_run_info_get();
#endif
// Initialize the relay channel controls
ret = relay_chn_ctl_init(outputs, run_infos);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize relay channel control");
// Create relay channel command event loop
ret = relay_chn_create_event_loop();
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to create relay channel event loop");
#if RELAY_CHN_ENABLE_TILTING == 1
// Initialize the tilt feature
#if RELAY_CHN_COUNT > 1
relay_chn_ctl_t *chn_ctls = relay_chn_ctl_get_all();
#else
relay_chn_ctl_t *chn_ctls = relay_chn_ctl_get();
#endif // RELAY_CHN_COUNT > 1
ret = relay_chn_tilt_init(chn_ctls); // Initialize tilt feature
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize tilt feature");
#endif
// Init the state listener list
vListInitialise(&relay_chn_listener_list);
return ret;
}
void relay_chn_destroy(void)
{
#if RELAY_CHN_ENABLE_TILTING == 1
relay_chn_tilt_deinit();
#endif
relay_chn_ctl_deinit();
relay_chn_output_deinit();
#if RELAY_CHN_ENABLE_NVS == 1
relay_chn_nvs_deinit();
#endif
// Destroy the event loop
esp_event_loop_delete(relay_chn_event_loop);
relay_chn_event_loop = NULL;
// Free the listeners
while (listCURRENT_LIST_LENGTH(&relay_chn_listener_list) > 0) {
ListItem_t *pxItem = listGET_HEAD_ENTRY(&relay_chn_listener_list);
relay_chn_listener_entry_t *entry = listGET_LIST_ITEM_OWNER(pxItem);
uxListRemove(pxItem);
free(entry);
}
}
/**
* @brief Find a listener entry in the list by its function pointer.
*
* This function replaces the old index-based search and is used to check
* for the existence of a listener before registration or for finding it
* during unregistration.
*
* @param listener The listener function pointer to find.
* @return Pointer to the listener entry if found, otherwise NULL.
*/
static relay_chn_listener_entry_t* find_listener_entry(relay_chn_state_listener_t listener)
{
// Iterate through the linked list of listeners
for (ListItem_t *pxListItem = listGET_HEAD_ENTRY(&relay_chn_listener_list);
pxListItem != listGET_END_MARKER(&relay_chn_listener_list);
pxListItem = listGET_NEXT(pxListItem)) {
relay_chn_listener_entry_t *entry = (relay_chn_listener_entry_t *) listGET_LIST_ITEM_OWNER(pxListItem);
if (entry->listener == listener) {
// Found the listener, return the entry
return entry;
}
}
// Listener was not found in the list
return NULL;
}
esp_err_t relay_chn_register_listener(relay_chn_state_listener_t listener)
{
ESP_RETURN_ON_FALSE(listener, ESP_ERR_INVALID_ARG, TAG, "Listener cannot be NULL");
// Check for duplicates
if (find_listener_entry(listener) != NULL) {
ESP_LOGD(TAG, "Listener %p already registered", listener);
return ESP_OK;
}
// Allocate memory for the new listener entry
relay_chn_listener_entry_t *entry = malloc(sizeof(relay_chn_listener_entry_t));
ESP_RETURN_ON_FALSE(entry, ESP_ERR_NO_MEM, TAG, "Failed to allocate memory for listener");
// Initialize and insert the new listener
entry->listener = listener;
vListInitialiseItem(&(entry->list_item));
listSET_LIST_ITEM_OWNER(&(entry->list_item), (void *)entry);
vListInsertEnd(&relay_chn_listener_list, &(entry->list_item));
ESP_LOGD(TAG, "Registered listener %p", listener);
return ESP_OK;
}
void relay_chn_unregister_listener(relay_chn_state_listener_t listener)
{
if (listener == NULL)
{
ESP_LOGD(TAG, "Cannot unregister a NULL listener.");
return;
}
// Find the listener entry in the list
relay_chn_listener_entry_t *entry = find_listener_entry(listener);
if (entry != NULL) {
// Remove the item from the list and free the allocated memory
uxListRemove(&(entry->list_item));
free(entry);
ESP_LOGD(TAG, "Unregistered listener %p", listener);
} else {
ESP_LOGD(TAG, "Listener %p not found for unregistration.", listener);
}
}
// Dispatch relay channel command to its event loop
void relay_chn_dispatch_cmd(relay_chn_ctl_t *chn_ctl, relay_chn_cmd_t cmd) {
if (cmd == RELAY_CHN_CMD_NONE) {
return;
}
// Since the event_loop library creates a deep copy of the event data,
// and we need to pass the pointer of the relevant channel, here we need
// to pass the pointer to the pointer of the channel (&chn_ctl) so that
// the pointer value is preserved in the event data.
esp_event_post_to(relay_chn_event_loop,
RELAY_CHN_CMD_EVENT,
cmd,
&chn_ctl,
sizeof(chn_ctl),
portMAX_DELAY);
#if RELAY_CHN_ENABLE_TILTING == 1
// Reset the tilt counter when the command is either FORWARD or REVERSE
if (cmd == RELAY_CHN_CMD_FORWARD || cmd == RELAY_CHN_CMD_REVERSE) {
relay_chn_tilt_reset_count(chn_ctl->tilt_ctl);
}
#endif
}
esp_err_t relay_chn_start_esp_timer_once(esp_timer_handle_t esp_timer, uint32_t time_ms)
{
esp_err_t ret = esp_timer_start_once(esp_timer, time_ms * 1000);
if (ret == ESP_ERR_INVALID_STATE) {
// This timer is already running, stop the timer first
ret = esp_timer_stop(esp_timer);
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
return ret;
}
ret = esp_timer_start_once(esp_timer, time_ms * 1000);
}
return ret;
}
void relay_chn_update_state(relay_chn_ctl_t *chn_ctl, relay_chn_state_t new_state)
{
relay_chn_state_t old_state = chn_ctl->state;
// Only update and notify if the state has actually changed.
if (old_state == new_state) {
return;
}
chn_ctl->state = new_state;
// Iterate through the linked list of listeners and notify them.
for (ListItem_t *pxListItem = listGET_HEAD_ENTRY(&relay_chn_listener_list);
pxListItem != listGET_END_MARKER(&relay_chn_listener_list);
pxListItem = listGET_NEXT(pxListItem)) {
relay_chn_listener_entry_t *entry = (relay_chn_listener_entry_t *) listGET_LIST_ITEM_OWNER(pxListItem);
if (entry && entry->listener) {
// Emit the state change to the listeners
entry->listener(chn_ctl->id, old_state, new_state);
}
}
}
/**
* @brief The command issuer function.
*
* This function is the deciding logic for issuing a command to a relay channel. It evaluates
* the current state of the channel before issuing the command. Then it decides whether to run
* the command immediately or wait for the opposite inertia time.
*
* The STOP command is an exception, it is always run immediately since it is safe in any case.
*
* Another special consideration is the FLIP command. If the channel is running, the FLIP command
* is issued after the channel is stopped. If the channel is stopped, the FLIP command is issued
* immediately.
*
* @param chn_ctl The relay channel to issue the command to.
* @param cmd The command to issue.
*/
void relay_chn_issue_cmd(relay_chn_ctl_t* chn_ctl, relay_chn_cmd_t cmd)
{
if (cmd == RELAY_CHN_CMD_NONE) {
return;
}
if (cmd == RELAY_CHN_CMD_STOP) {
if (chn_ctl->state == RELAY_CHN_STATE_STOPPED) {
return; // Do nothing if already stopped
}
// If the command is STOP, issue it immediately
relay_chn_dispatch_cmd(chn_ctl, cmd);
return;
}
relay_chn_cmd_t last_run_cmd = relay_chn_run_info_get_last_run_cmd(chn_ctl->run_info);
// Evaluate the channel's next move depending on its status
switch (chn_ctl->state)
{
case RELAY_CHN_STATE_IDLE:
// If the channel is idle, run the command immediately
relay_chn_dispatch_cmd(chn_ctl, cmd);
break;
case RELAY_CHN_STATE_FORWARD_PENDING:
case RELAY_CHN_STATE_REVERSE_PENDING:
// The channel is already waiting for the opposite inertia time,
// so do nothing unless the command is STOP
if (cmd == RELAY_CHN_CMD_STOP) {
relay_chn_dispatch_cmd(chn_ctl, cmd);
}
break;
case RELAY_CHN_STATE_STOPPED:
if (last_run_cmd == cmd || last_run_cmd == RELAY_CHN_CMD_NONE) {
// Since the state is STOPPED, the inertia timer should be running and must be invalidated
// with the pending FREE command
esp_timer_stop(chn_ctl->inertia_timer);
chn_ctl->pending_cmd = RELAY_CHN_CMD_NONE;
// If this is the first run or the last run command is the same as the current command,
// run the command immediately
relay_chn_dispatch_cmd(chn_ctl, cmd);
}
else {
// If the last run command is different from the current command, calculate the time passed
// since the last run command stopped and decide whether to run the command immediately or wait
uint32_t last_run_cmd_time_ms = relay_chn_run_info_get_last_run_cmd_time_ms(chn_ctl->run_info);
uint32_t inertia_time_passed_ms = (uint32_t) (esp_timer_get_time() / 1000) - last_run_cmd_time_ms;
uint32_t inertia_time_ms = RELAY_CHN_OPPOSITE_INERTIA_MS - inertia_time_passed_ms;
if (inertia_time_ms > 0) {
chn_ctl->pending_cmd = cmd;
relay_chn_state_t new_state = cmd == RELAY_CHN_CMD_FORWARD
? RELAY_CHN_STATE_FORWARD_PENDING : RELAY_CHN_STATE_REVERSE_PENDING;
relay_chn_update_state(chn_ctl, new_state);
// If the time passed is less than the opposite inertia time, wait for the remaining time
relay_chn_start_esp_timer_once(chn_ctl->inertia_timer, inertia_time_ms);
}
else {
// If the time passed is more than the opposite inertia time, run the command immediately
relay_chn_dispatch_cmd(chn_ctl, cmd);
}
}
break;
case RELAY_CHN_STATE_FORWARD:
case RELAY_CHN_STATE_REVERSE:
if (cmd == RELAY_CHN_CMD_FLIP) {
// If the command is FLIP, stop the running channel first, then issue the FLIP command
relay_chn_dispatch_cmd(chn_ctl, RELAY_CHN_CMD_STOP);
relay_chn_dispatch_cmd(chn_ctl, cmd);
return;
}
if (last_run_cmd == cmd) {
// If the last run command is the same as the current command, do nothing
return;
}
// Stop the channel first before the schedule
relay_chn_dispatch_cmd(chn_ctl, RELAY_CHN_CMD_STOP);
// If the last run command is different from the current command, wait for the opposite inertia time
chn_ctl->pending_cmd = cmd;
relay_chn_state_t new_state = cmd == RELAY_CHN_CMD_FORWARD
? RELAY_CHN_STATE_FORWARD_PENDING : RELAY_CHN_STATE_REVERSE_PENDING;
relay_chn_update_state(chn_ctl, new_state);
relay_chn_start_esp_timer_once(chn_ctl->inertia_timer, RELAY_CHN_OPPOSITE_INERTIA_MS);
break;
#if RELAY_CHN_ENABLE_TILTING == 1
case RELAY_CHN_STATE_TILT_FORWARD:
// Terminate tilting first
relay_chn_tilt_dispatch_cmd(chn_ctl->tilt_ctl, RELAY_CHN_TILT_CMD_STOP);
if (cmd == RELAY_CHN_CMD_FORWARD) {
// Schedule for running forward
chn_ctl->pending_cmd = cmd;
relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_FORWARD_PENDING);
relay_chn_start_esp_timer_once(chn_ctl->inertia_timer, RELAY_CHN_OPPOSITE_INERTIA_MS);
} else if (cmd == RELAY_CHN_CMD_REVERSE) {
// Run directly since it is the same direction
relay_chn_dispatch_cmd(chn_ctl, cmd);
relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_REVERSE);
}
break;
case RELAY_CHN_STATE_TILT_REVERSE:
// Terminate tilting first
relay_chn_tilt_dispatch_cmd(chn_ctl->tilt_ctl, RELAY_CHN_TILT_CMD_STOP);
if (cmd == RELAY_CHN_CMD_FORWARD) {
// Run directly since it is the same direction
relay_chn_dispatch_cmd(chn_ctl, cmd);
relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_FORWARD);
} else if (cmd == RELAY_CHN_CMD_REVERSE) {
// Schedule for running reverse
chn_ctl->pending_cmd = cmd;
relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_REVERSE_PENDING);
relay_chn_start_esp_timer_once(chn_ctl->inertia_timer, RELAY_CHN_OPPOSITE_INERTIA_MS);
}
break;
#endif
default: ESP_LOGD(TAG, "relay_chn_evaluate: Unknown relay channel state!");
}
}
#if RELAY_CHN_COUNT > 1
bool relay_chn_is_channel_id_valid(uint8_t chn_id)
{
bool valid = (chn_id < RELAY_CHN_COUNT) || chn_id == RELAY_CHN_ID_ALL;
if (!valid) {
ESP_LOGE(TAG, "Invalid channel ID: %d", chn_id);
}
return valid;
}
#endif // RELAY_CHN_COUNT > 1
static void relay_chn_execute_stop(relay_chn_ctl_t *chn_ctl)
{
if (relay_chn_output_stop(chn_ctl->output) != ESP_OK) {
ESP_LOGE(TAG, "relay_chn_execute_stop: Failed to output stop for relay channel #%d!", chn_ctl->id);
}
relay_chn_state_t previous_state = chn_ctl->state;
relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_STOPPED);
// If there is any pending command, cancel it since the STOP command is issued right after it
chn_ctl->pending_cmd = RELAY_CHN_CMD_NONE;
// Invalidate the channel's timer if it is active
esp_timer_stop(chn_ctl->inertia_timer);
// Save the last run time only if the previous state was either STATE FORWARD
// or STATE_REVERSE. Then schedule a free command.
if (previous_state == RELAY_CHN_STATE_FORWARD || previous_state == RELAY_CHN_STATE_REVERSE) {
// Record the command's last run time
relay_chn_run_info_set_last_run_cmd_time_ms(chn_ctl->run_info, (uint32_t)(esp_timer_get_time() / 1000));
// Schedule a free command for the channel
chn_ctl->pending_cmd = RELAY_CHN_CMD_IDLE;
relay_chn_start_esp_timer_once(chn_ctl->inertia_timer, RELAY_CHN_OPPOSITE_INERTIA_MS);
} else {
// If the channel was not running one of the run or fwd, issue a free command immediately
relay_chn_dispatch_cmd(chn_ctl, RELAY_CHN_CMD_IDLE);
}
}
static void relay_chn_execute_forward(relay_chn_ctl_t *chn_ctl)
{
if (relay_chn_output_forward(chn_ctl->output) != ESP_OK) {
ESP_LOGE(TAG, "relay_chn_execute_forward: Failed to output forward for relay channel #%d!", chn_ctl->id);
return;
}
relay_chn_run_info_set_last_run_cmd(chn_ctl->run_info, RELAY_CHN_CMD_FORWARD);
relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_FORWARD);
}
static void relay_chn_execute_reverse(relay_chn_ctl_t *chn_ctl)
{
if (relay_chn_output_reverse(chn_ctl->output) != ESP_OK) {
ESP_LOGE(TAG, "relay_chn_execute_reverse: Failed to output reverse for relay channel #%d!", chn_ctl->id);
return;
}
relay_chn_run_info_set_last_run_cmd(chn_ctl->run_info, RELAY_CHN_CMD_REVERSE);
relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_REVERSE);
}
static void relay_chn_execute_flip(relay_chn_ctl_t *chn_ctl)
{
relay_chn_output_flip(chn_ctl->output);
// Set an inertia on the channel to prevent any immediate movement
chn_ctl->pending_cmd = RELAY_CHN_CMD_IDLE;
relay_chn_start_esp_timer_once(chn_ctl->inertia_timer, RELAY_CHN_OPPOSITE_INERTIA_MS);
}
void relay_chn_execute_idle(relay_chn_ctl_t *chn_ctl)
{
chn_ctl->pending_cmd = RELAY_CHN_CMD_NONE;
// Invalidate the channel's timer if it is active
esp_timer_stop(chn_ctl->inertia_timer);
relay_chn_update_state(chn_ctl, RELAY_CHN_STATE_IDLE);
}
static void relay_chn_event_handler(void* handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
relay_chn_ctl_t* chn_ctl = *(relay_chn_ctl_t**) event_data;
ESP_RETURN_VOID_ON_FALSE(chn_ctl != NULL, TAG, "event_data is NULL");
ESP_LOGD(TAG, "relay_chn_event_handler: Command: %s", relay_chn_cmd_str(event_id));
switch (event_id) {
case RELAY_CHN_CMD_STOP:
relay_chn_execute_stop(chn_ctl);
break;
case RELAY_CHN_CMD_FORWARD:
relay_chn_execute_forward(chn_ctl);
break;
case RELAY_CHN_CMD_REVERSE:
relay_chn_execute_reverse(chn_ctl);
break;
case RELAY_CHN_CMD_FLIP:
relay_chn_execute_flip(chn_ctl);
break;
case RELAY_CHN_CMD_IDLE:
relay_chn_execute_idle(chn_ctl);
break;
default:
ESP_LOGD(TAG, "Unknown relay channel command!");
}
}
char *relay_chn_cmd_str(relay_chn_cmd_t cmd)
{
switch (cmd) {
case RELAY_CHN_CMD_STOP:
return "STOP";
case RELAY_CHN_CMD_FORWARD:
return "FORWARD";
case RELAY_CHN_CMD_REVERSE:
return "REVERSE";
case RELAY_CHN_CMD_FLIP:
return "FLIP";
case RELAY_CHN_CMD_IDLE:
return "IDLE";
default:
return "UNKNOWN";
}
}
char *relay_chn_state_str(relay_chn_state_t state)
{
switch (state) {
case RELAY_CHN_STATE_IDLE:
return "IDLE";
case RELAY_CHN_STATE_STOPPED:
return "STOPPED";
case RELAY_CHN_STATE_FORWARD:
return "FORWARD";
case RELAY_CHN_STATE_REVERSE:
return "REVERSE";
case RELAY_CHN_STATE_FORWARD_PENDING:
return "FORWARD_PENDING";
case RELAY_CHN_STATE_REVERSE_PENDING:
return "REVERSE_PENDING";
#if RELAY_CHN_ENABLE_TILTING == 1
case RELAY_CHN_STATE_TILT_FORWARD:
return "TILT_FORWARD";
case RELAY_CHN_STATE_TILT_REVERSE:
return "TILT_REVERSE";
#endif
default:
return "UNKNOWN";
}
}

137
src/relay_chn_ctl_multi.c Normal file
View File

@@ -0,0 +1,137 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#include "esp_check.h"
#include "relay_chn_priv_types.h"
#include "relay_chn_core.h"
#include "relay_chn_ctl.h"
#include "relay_chn_output.h"
static const char *TAG = "RELAY_CHN_CTL";
static relay_chn_ctl_t chn_ctls[RELAY_CHN_COUNT];
esp_err_t relay_chn_ctl_init(relay_chn_output_t *outputs, relay_chn_run_info_t *run_infos)
{
// Initialize all relay channels
esp_err_t ret;
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
relay_chn_ctl_t* chn_ctl = &chn_ctls[i];
relay_chn_output_t* output = &outputs[i];
relay_chn_run_info_t* run_info = &run_infos[i];
chn_ctl->id = i;
chn_ctl->state = RELAY_CHN_STATE_IDLE;
chn_ctl->pending_cmd = RELAY_CHN_CMD_NONE;
chn_ctl->output = output;
chn_ctl->run_info = run_info;
ret = relay_chn_init_timer(chn_ctl); // Create direction change inertia timer
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to create relay channel timer for channel %d", i);
}
return ESP_OK;
}
void relay_chn_ctl_deinit()
{
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
relay_chn_ctl_t* chn_ctl = &chn_ctls[i];
if (chn_ctl->inertia_timer != NULL) {
esp_timer_delete(chn_ctl->inertia_timer);
chn_ctl->inertia_timer = NULL;
}
}
}
relay_chn_state_t relay_chn_ctl_get_state(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id) || chn_id == RELAY_CHN_ID_ALL) {
return RELAY_CHN_STATE_UNDEFINED;
}
return chn_ctls[chn_id].state;
}
char *relay_chn_ctl_get_state_str(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id) || chn_id == RELAY_CHN_ID_ALL) {
return relay_chn_state_str(RELAY_CHN_STATE_UNDEFINED);
}
return relay_chn_state_str(chn_ctls[chn_id].state);
}
static void relay_chn_ctl_issue_cmd_on_all_channels(relay_chn_cmd_t cmd)
{
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
relay_chn_issue_cmd(&chn_ctls[i], cmd);
}
}
void relay_chn_ctl_run_forward(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) return;
if (chn_id == RELAY_CHN_ID_ALL) {
relay_chn_ctl_issue_cmd_on_all_channels(RELAY_CHN_CMD_FORWARD);
return;
}
relay_chn_issue_cmd(&chn_ctls[chn_id], RELAY_CHN_CMD_FORWARD);
}
void relay_chn_ctl_run_reverse(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) return;
if (chn_id == RELAY_CHN_ID_ALL) {
relay_chn_ctl_issue_cmd_on_all_channels(RELAY_CHN_CMD_REVERSE);
return;
}
relay_chn_issue_cmd(&chn_ctls[chn_id], RELAY_CHN_CMD_REVERSE);
}
void relay_chn_ctl_stop(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) return;
if (chn_id == RELAY_CHN_ID_ALL) {
relay_chn_ctl_issue_cmd_on_all_channels(RELAY_CHN_CMD_STOP);
return;
}
relay_chn_issue_cmd(&chn_ctls[chn_id], RELAY_CHN_CMD_STOP);
}
void relay_chn_ctl_flip_direction(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) return;
if (chn_id == RELAY_CHN_ID_ALL) {
relay_chn_ctl_issue_cmd_on_all_channels(RELAY_CHN_CMD_FLIP);
return;
}
relay_chn_issue_cmd(&chn_ctls[chn_id], RELAY_CHN_CMD_FLIP);
}
relay_chn_direction_t relay_chn_ctl_get_direction(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) {
return RELAY_CHN_DIRECTION_DEFAULT;
}
relay_chn_ctl_t *chn_ctl = &chn_ctls[chn_id];
return relay_chn_output_get_direction(chn_ctl->output);
}
relay_chn_ctl_t *relay_chn_ctl_get(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) {
return NULL;
}
return &chn_ctls[chn_id];
}
relay_chn_ctl_t *relay_chn_ctl_get_all(void)
{
return chn_ctls;
}

View File

@@ -0,0 +1,76 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#include "relay_chn_priv_types.h"
#include "relay_chn_core.h"
#include "relay_chn_ctl.h"
#include "relay_chn_output.h"
static relay_chn_ctl_t chn_ctl;
esp_err_t relay_chn_ctl_init(relay_chn_output_t *output, relay_chn_run_info_t *run_info)
{
// Initialize the relay channel
chn_ctl.id = 0; // Single channel, so ID is 0
chn_ctl.state = RELAY_CHN_STATE_IDLE;
chn_ctl.pending_cmd = RELAY_CHN_CMD_NONE;
chn_ctl.output = output;
chn_ctl.run_info = run_info;
return relay_chn_init_timer(&chn_ctl); // Create direction change inertia timer
}
void relay_chn_ctl_deinit()
{
if (chn_ctl.inertia_timer != NULL) {
esp_timer_delete(chn_ctl.inertia_timer);
chn_ctl.inertia_timer = NULL;
}
}
/* relay_chn APIs */
relay_chn_state_t relay_chn_ctl_get_state()
{
return chn_ctl.state;
}
char *relay_chn_ctl_get_state_str()
{
return relay_chn_state_str(chn_ctl.state);
}
void relay_chn_ctl_run_forward()
{
relay_chn_issue_cmd(&chn_ctl, RELAY_CHN_CMD_FORWARD);
}
void relay_chn_ctl_run_reverse()
{
relay_chn_issue_cmd(&chn_ctl, RELAY_CHN_CMD_REVERSE);
}
void relay_chn_ctl_stop()
{
relay_chn_issue_cmd(&chn_ctl, RELAY_CHN_CMD_STOP);
}
void relay_chn_ctl_flip_direction()
{
relay_chn_issue_cmd(&chn_ctl, RELAY_CHN_CMD_FLIP);
}
relay_chn_direction_t relay_chn_ctl_get_direction()
{
return relay_chn_output_get_direction(chn_ctl.output);
}
/* relay_chn APIs */
relay_chn_ctl_t *relay_chn_ctl_get()
{
return &chn_ctl;
}

117
src/relay_chn_nvs.c Normal file
View File

@@ -0,0 +1,117 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#include "esp_check.h"
#include "relay_chn_nvs.h"
#define RELAY_CHN_KEY_DIR "dir" /*!< Direction key */
#ifdef RELAY_CHN_ENABLE_TILTING
#define RELAY_CHN_KEY_TSENS(ch) "tsens_%d" /*!< Tilt sensitivity key */
#define RELAY_CHN_KEY_TCNT(ch) "tcnt_%d" /*!< Tilt count key */
#endif
static const char *TAG = "RELAY_CHN_NVS";
static nvs_handle_t relay_chn_nvs;
esp_err_t relay_chn_nvs_init()
{
esp_err_t ret;
#if RELAY_CHN_NVS_CUSTOM_PARTITION == 1
ret = nvs_open_from_partition(RELAY_CHN_NVS_CUSTOM_PARTITION_NAME,
RELAY_CHN_NVS_NAMESPACE,
NVS_READWRITE,
&relay_chn_nvs);
ESP_RETURN_ON_ERROR(ret,
TAG,
"Failed to open NVS namespace '%s' from partition '%s' with error %s",
RELAY_CHN_NVS_NAMESPACE,
RELAY_CHN_NVS_CUSTOM_PARTITION_NAME,
esp_err_to_name(ret));
#else
ret = nvs_open(RELAY_CHN_NVS_NAMESPACE, NVS_READWRITE, &relay_chn_nvs);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to open NVS namespace '%s'", RELAY_CHN_NVS_NAMESPACE);
#endif // RELAY_CHN_NVS_CUSTOM_PARTITION
return ESP_OK;
}
esp_err_t relay_chn_nvs_set_direction(uint8_t ch, relay_chn_direction_t direction)
{
uint8_t direction_val;
esp_err_t ret = nvs_get_u8(relay_chn_nvs, RELAY_CHN_KEY_DIR, &direction_val);
if (ret == ESP_ERR_NVS_NOT_FOUND) {
// The key does not exist yet, set it to zero which is the default direction
direction_val = RELAY_CHN_DIRECTION_DEFAULT;
} else if (ret != ESP_OK) {
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to get direction from NVS with error: %s", esp_err_to_name(ret));
}
direction_val &= ~(1 << ch); // Clear the bit for the channel
direction_val |= (((uint8_t) direction) << ch); // Set the new direction bit
ret = nvs_set_u8(relay_chn_nvs, RELAY_CHN_KEY_DIR, direction_val);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set direction for channel %d", ch);
return nvs_commit(relay_chn_nvs);
}
esp_err_t relay_chn_nvs_get_direction(uint8_t ch, relay_chn_direction_t *direction)
{
ESP_RETURN_ON_FALSE(direction != NULL, ESP_ERR_INVALID_ARG, TAG, "Direction pointer is NULL");
uint8_t direction_val;
esp_err_t ret = nvs_get_u8(relay_chn_nvs, RELAY_CHN_KEY_DIR, &direction_val);
if (ret != ESP_OK) {
return ret; // Return error if the key does not exist
}
*direction = (relay_chn_direction_t)((direction_val >> ch) & 0x01);
return ESP_OK;
}
#ifdef RELAY_CHN_ENABLE_TILTING
esp_err_t relay_chn_nvs_set_tilt_sensitivity(uint8_t ch, uint8_t sensitivity)
{
esp_err_t ret = nvs_set_u8(relay_chn_nvs, RELAY_CHN_KEY_TSENS(ch), sensitivity);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set tilt sensitivity for channel %d", ch);
return nvs_commit(relay_chn_nvs);
}
esp_err_t relay_chn_nvs_get_tilt_sensitivity(uint8_t ch, uint8_t *sensitivity)
{
ESP_RETURN_ON_FALSE(sensitivity != NULL, ESP_ERR_INVALID_ARG, TAG, "Sensitivity pointer is NULL");
return nvs_get_u8(relay_chn_nvs, RELAY_CHN_KEY_TSENS(ch), sensitivity);
}
esp_err_t relay_chn_nvs_set_tilt_count(uint8_t ch, uint16_t tilt_count)
{
esp_err_t ret;
ret = nvs_set_u16(relay_chn_nvs, RELAY_CHN_KEY_TCNT(ch), tilt_count);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to save tilt_count tilt counter");
return nvs_commit(relay_chn_nvs);
}
esp_err_t relay_chn_nvs_get_tilt_count(uint8_t ch, uint16_t *tilt_count)
{
ESP_RETURN_ON_FALSE(tilt_count != NULL,
ESP_ERR_INVALID_ARG, TAG, "Counter pointers are NULL");
return nvs_get_u16(relay_chn_nvs, RELAY_CHN_KEY_TCNT(ch), tilt_count);
}
#endif // RELAY_CHN_ENABLE_TILTING
esp_err_t relay_chn_nvs_erase_all()
{
// Erase all key-value pairs in the relay_chn NVS namespace
esp_err_t ret = nvs_erase_all(relay_chn_nvs);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to erase all keys in NVS namespace '%s'", RELAY_CHN_NVS_NAMESPACE);
// Commit the changes
return nvs_commit(relay_chn_nvs);
}
esp_err_t relay_chn_nvs_deinit()
{
nvs_close(relay_chn_nvs);
return ESP_OK;
}

217
src/relay_chn_output.c Normal file
View File

@@ -0,0 +1,217 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#include "esp_check.h"
#include "esp_log.h"
#include "relay_chn_defs.h"
#include "relay_chn_output.h"
#include "relay_chn_core.h"
#if RELAY_CHN_ENABLE_NVS == 1
#include "relay_chn_nvs.h"
#endif
static const char *TAG = "RELAY_CHN_OUTPUT";
#if RELAY_CHN_COUNT > 1
static relay_chn_output_t outputs[RELAY_CHN_COUNT];
#else
static relay_chn_output_t output;
#endif
static esp_err_t relay_chn_output_check_gpio_capabilities(uint8_t gpio_count)
{
// Check if the device's GPIOs are enough for the number of channels
if (RELAY_CHN_COUNT > (GPIO_PIN_COUNT / 2)) {
ESP_LOGE(TAG, "Not enough GPIOs for the number of channels!");
ESP_LOGE(TAG, "Max available num of channels: %d, requested channels: %d", GPIO_PIN_COUNT / 2, RELAY_CHN_COUNT);
return ESP_ERR_INVALID_ARG;
}
// Check if the provided GPIOs correspond to the number of channels
if (gpio_count != RELAY_CHN_COUNT * 2) {
ESP_LOGE(TAG, "Invalid number of GPIOs provided: %d", gpio_count);
ESP_LOGE(TAG, "Expected number of GPIOs: %d", RELAY_CHN_COUNT * 2);
return ESP_ERR_INVALID_ARG;
}
return ESP_OK;
}
static esp_err_t relay_chn_output_ctl_init(relay_chn_output_t *output,
gpio_num_t forward_pin,
gpio_num_t reverse_pin,
relay_chn_direction_t direction)
{
ESP_RETURN_ON_FALSE(GPIO_IS_VALID_OUTPUT_GPIO(forward_pin), ESP_ERR_INVALID_ARG, TAG,
"Invalid GPIO pin number for forward_pin: %d", forward_pin);
ESP_RETURN_ON_FALSE(GPIO_IS_VALID_OUTPUT_GPIO(reverse_pin), ESP_ERR_INVALID_ARG, TAG,
"Invalid GPIO pin number for reverse_pin: %d", reverse_pin);
// Check if the GPIOs are valid
esp_err_t ret;
// Initialize the GPIOs
ret = gpio_reset_pin(forward_pin);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to reset GPIO forward pin: %d", forward_pin);
ret = gpio_set_direction(forward_pin, GPIO_MODE_OUTPUT);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set GPIO direction for forward pin: %d", forward_pin);
ret = gpio_reset_pin(reverse_pin);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to reset GPIO reverse pin: %d", reverse_pin);
ret = gpio_set_direction(reverse_pin, GPIO_MODE_OUTPUT);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set GPIO direction for reverse pin: %d", reverse_pin);
// Initialize the GPIOs
// Initialize the relay channel output
output->forward_pin = direction == RELAY_CHN_DIRECTION_DEFAULT ? forward_pin : reverse_pin;
output->reverse_pin = direction == RELAY_CHN_DIRECTION_DEFAULT ? reverse_pin : forward_pin;
output->direction = direction;
return ESP_OK;
}
#if RELAY_CHN_ENABLE_NVS == 1
static esp_err_t relay_chn_output_load_direction(uint8_t ch, relay_chn_direction_t *direction)
{
esp_err_t ret = relay_chn_nvs_get_direction(ch, direction);
if (ret == ESP_ERR_NVS_NOT_FOUND) {
// If the key does not exist, use the default direction
*direction = RELAY_CHN_DIRECTION_DEFAULT;
} else if (ret != ESP_OK) {
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to get direction from storage for channel %d: %s", ch, esp_err_to_name(ret));
}
return ESP_OK;
}
#endif
esp_err_t relay_chn_output_init(const uint8_t* gpio_map, uint8_t gpio_count)
{
esp_err_t ret;
ret = relay_chn_output_check_gpio_capabilities(gpio_count);
ESP_RETURN_ON_ERROR(ret, TAG, "Device does not support the provided GPIOs");
#if RELAY_CHN_COUNT > 1
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
relay_chn_output_t* output = &outputs[i];
int gpio_index = i << 1; // gpio_index = i * 2
gpio_num_t forward_pin = (gpio_num_t) gpio_map[gpio_index];
gpio_num_t reverse_pin = (gpio_num_t) gpio_map[gpio_index + 1];
relay_chn_direction_t direction = RELAY_CHN_DIRECTION_DEFAULT;
#if RELAY_CHN_ENABLE_NVS == 1
// If NVS storage is enabled, retrieve the direction from storage
ret = relay_chn_output_load_direction(i, &direction);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to load direction from storage for channel %d", i);
#endif
ret = relay_chn_output_ctl_init(output, forward_pin, reverse_pin, direction);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize relay channel %d", i);
}
#else
relay_chn_direction_t direction = RELAY_CHN_DIRECTION_DEFAULT;
#if RELAY_CHN_ENABLE_NVS == 1
// If NVS storage is enabled, retrieve the direction from storage
ret = relay_chn_output_load_direction(0, &direction);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to load direction from storage for channel %d", 0);
#endif
ret = relay_chn_output_ctl_init(&output, gpio_map[0], gpio_map[1], direction);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to initialize relay channel");
#endif
return ESP_OK;
}
static void relay_chn_output_ctl_deinit(relay_chn_output_t *output)
{
gpio_reset_pin(output->forward_pin);
gpio_reset_pin(output->reverse_pin);
}
void relay_chn_output_deinit()
{
#if RELAY_CHN_COUNT > 1
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
relay_chn_output_ctl_deinit(&outputs[i]);
}
#else
relay_chn_output_ctl_deinit(&output);
#endif // RELAY_CHN_COUNT > 1
}
#if RELAY_CHN_COUNT > 1
relay_chn_output_t *relay_chn_output_get(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) {
return NULL;
}
return &outputs[chn_id];
}
relay_chn_output_t *relay_chn_output_get_all(void)
{
return outputs;
}
#else
relay_chn_output_t *relay_chn_output_get(void)
{
return &output;
}
#endif // RELAY_CHN_COUNT > 1
esp_err_t relay_chn_output_stop(relay_chn_output_t *output)
{
esp_err_t ret;
ret = gpio_set_level(output->forward_pin, 0);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set forward pin to LOW");
return gpio_set_level(output->reverse_pin, 0);
}
esp_err_t relay_chn_output_forward(relay_chn_output_t *output)
{
esp_err_t ret;
ret = gpio_set_level(output->forward_pin, 1);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set forward pin to HIGH");
return gpio_set_level(output->reverse_pin, 0);
}
esp_err_t relay_chn_output_reverse(relay_chn_output_t *output)
{
esp_err_t ret;
ret = gpio_set_level(output->forward_pin, 0);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to set forward pin to LOW");
return gpio_set_level(output->reverse_pin, 1);
}
void relay_chn_output_flip(relay_chn_output_t *output)
{
// Flip the output GPIO pins
gpio_num_t temp = output->forward_pin;
output->forward_pin = output->reverse_pin;
output->reverse_pin = temp;
// Flip the direction
output->direction = (output->direction == RELAY_CHN_DIRECTION_DEFAULT)
? RELAY_CHN_DIRECTION_FLIPPED
: RELAY_CHN_DIRECTION_DEFAULT;
#if RELAY_CHN_ENABLE_NVS == 1
uint8_t ch = 0;
#if RELAY_CHN_COUNT > 1
for (uint8_t i = 0; i < RELAY_CHN_COUNT; i++) {
if (output == &outputs[i]) {
ch = i;
break;
}
}
#endif
esp_err_t ret = relay_chn_nvs_set_direction(ch, output->direction);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to save flipped direction for channel %d: %s", ch, esp_err_to_name(ret));
}
#endif
}
relay_chn_direction_t relay_chn_output_get_direction(relay_chn_output_t *output)
{
return output->direction;
}

74
src/relay_chn_run_info.c Normal file
View File

@@ -0,0 +1,74 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#include "relay_chn_core.h"
#include "relay_chn_run_info.h"
#if RELAY_CHN_COUNT > 1
static relay_chn_run_info_t run_infos[RELAY_CHN_COUNT];
#else
static relay_chn_run_info_t run_info;
#endif
void relay_chn_run_info_init()
{
#if RELAY_CHN_COUNT > 1
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
run_infos[i].last_run_cmd = RELAY_CHN_CMD_NONE;
run_infos[i].last_run_cmd_time_ms = 0;
}
#else
run_info.last_run_cmd = RELAY_CHN_CMD_NONE;
run_info.last_run_cmd_time_ms = 0;
#endif
}
#if RELAY_CHN_COUNT > 1
relay_chn_run_info_t *relay_chn_run_info_get(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) {
return NULL;
}
return &run_infos[chn_id];
}
relay_chn_run_info_t *relay_chn_run_info_get_all()
{
return run_infos;
}
#else
relay_chn_run_info_t *relay_chn_run_info_get()
{
return &run_info;
}
#endif // RELAY_CHN_COUNT > 1
relay_chn_cmd_t relay_chn_run_info_get_last_run_cmd(relay_chn_run_info_t *run_info)
{
return run_info == NULL ? RELAY_CHN_CMD_NONE : run_info->last_run_cmd;
}
void relay_chn_run_info_set_last_run_cmd(relay_chn_run_info_t *run_info, relay_chn_cmd_t cmd)
{
if (!run_info) {
return;
}
run_info->last_run_cmd = cmd;
}
uint32_t relay_chn_run_info_get_last_run_cmd_time_ms(relay_chn_run_info_t *run_info)
{
return run_info == NULL ? 0 : run_info->last_run_cmd_time_ms;
}
void relay_chn_run_info_set_last_run_cmd_time_ms(relay_chn_run_info_t *run_info, uint32_t time_ms)
{
if (!run_info) {
return;
}
run_info->last_run_cmd_time_ms = time_ms;
}

743
src/relay_chn_tilt.c Normal file
View File

@@ -0,0 +1,743 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#include "esp_check.h"
#include "relay_chn_core.h"
#include "relay_chn_output.h"
#include "relay_chn_run_info.h"
#include "relay_chn_tilt.h"
#if RELAY_CHN_ENABLE_NVS == 1
#include "relay_chn_nvs.h"
#define RELAY_CHN_TILT_FLUSH_DEBOUNCE_MS 3000
#endif
static const char *TAG = "RELAY_CHN_TILT";
/**@{*/
/*
* Tilt Pattern Timing Definitions
*
* The min and max timing definitions as well as the default timing definitions.
* These definitions are used to define and adjust the tilt sensitivity.
*/
#define RELAY_CHN_TILT_RUN_MIN_MS 50
#define RELAY_CHN_TILT_RUN_MAX_MS 10
#define RELAY_CHN_TILT_PAUSE_MIN_MS 450
#define RELAY_CHN_TILT_PAUSE_MAX_MS 90
#define RELAY_CHN_TILT_DEFAULT_RUN_MS 15
#define RELAY_CHN_TILT_DEFAULT_PAUSE_MS 150
#define RELAY_CHN_TILT_DEFAULT_SENSITIVITY \
( (RELAY_CHN_TILT_DEFAULT_RUN_MS - RELAY_CHN_TILT_RUN_MIN_MS) \
* 100 / (RELAY_CHN_TILT_RUN_MAX_MS - RELAY_CHN_TILT_RUN_MIN_MS) )
/**@}*/
ESP_EVENT_DEFINE_BASE(RELAY_CHN_TILT_CMD_EVENT_BASE);
/// @brief Tilt steps.
typedef enum {
RELAY_CHN_TILT_STEP_NONE, /*!< No step */
RELAY_CHN_TILT_STEP_PENDING, /*!< Pending step */
RELAY_CHN_TILT_STEP_MOVE, /*!< Move step. Tilt is driving either for forward or reverse */
RELAY_CHN_TILT_STEP_PAUSE /*!< Pause step. Tilt is paused */
} relay_chn_tilt_step_t;
/// @brief Tilt timing structure to manage tilt pattern timing.
typedef struct {
uint8_t sensitivity; /*!< Tilt sensitivity in percentage (%) */
uint32_t move_time_ms; /*!< Move time in milliseconds */
uint32_t pause_time_ms; /*!< Pause time in milliseconds */
} relay_chn_tilt_timing_t;
/// @brief Tilt control structure to manage tilt operations.
typedef struct relay_chn_tilt_ctl {
relay_chn_ctl_t *chn_ctl; /*!< The relay channel control structure */
relay_chn_tilt_cmd_t cmd; /*!< The tilt command in process */
relay_chn_tilt_step_t step; /*!< Current tilt step */
relay_chn_tilt_timing_t tilt_timing; /*!< Tilt timing structure */
uint16_t tilt_count; /*!< Tilt count to manage forward and reverse tilts */
esp_timer_handle_t tilt_timer; /*!< Tilt timer handle */
#if RELAY_CHN_ENABLE_NVS == 1
esp_timer_handle_t flush_timer; /*!< Flush timer to avoid frequent write of tilt counters */
#endif
} relay_chn_tilt_ctl_t;
#if RELAY_CHN_COUNT > 1
static relay_chn_tilt_ctl_t tilt_ctls[RELAY_CHN_COUNT];
#else
static relay_chn_tilt_ctl_t tilt_ctl;
#endif
esp_err_t relay_chn_tilt_dispatch_cmd(relay_chn_tilt_ctl_t *tilt_ctl, relay_chn_tilt_cmd_t cmd)
{
if (cmd == RELAY_CHN_TILT_CMD_NONE) return ESP_ERR_INVALID_ARG;
// Since the event_loop library creates a deep copy of the event data,
// and we need to pass the pointer of the relevant tilt control, here we need
// to pass the pointer to the pointer of the tilt_control (&tilt_ctl) so that
// the pointer value is preserved in the event data.
return esp_event_post_to(relay_chn_event_loop,
RELAY_CHN_TILT_CMD_EVENT_BASE,
cmd,
&tilt_ctl,
sizeof(tilt_ctl), portMAX_DELAY);
}
// Returns the required timing before tilting depending on the last run.
static uint32_t relay_chn_tilt_get_required_timing_before_tilting(relay_chn_tilt_ctl_t *tilt_ctl, relay_chn_tilt_cmd_t cmd)
{
relay_chn_cmd_t last_run_cmd = relay_chn_run_info_get_last_run_cmd(tilt_ctl->chn_ctl->run_info);
if (cmd == RELAY_CHN_TILT_CMD_FORWARD && last_run_cmd == RELAY_CHN_CMD_REVERSE)
return 0;
else if (cmd == RELAY_CHN_TILT_CMD_REVERSE && last_run_cmd == RELAY_CHN_CMD_FORWARD)
return 0;
uint32_t last_run_cmd_time_ms = relay_chn_run_info_get_last_run_cmd_time_ms(tilt_ctl->chn_ctl->run_info);
uint32_t inertia_time_passed_ms = (uint32_t) (esp_timer_get_time() / 1000) - last_run_cmd_time_ms;
return RELAY_CHN_OPPOSITE_INERTIA_MS - inertia_time_passed_ms;
}
// Issue a tilt command to a specific relay channel.
static void relay_chn_tilt_issue_cmd(relay_chn_tilt_ctl_t *tilt_ctl, relay_chn_tilt_cmd_t cmd)
{
if (relay_chn_run_info_get_last_run_cmd(tilt_ctl->chn_ctl->run_info) == RELAY_CHN_CMD_NONE) {
// Do not tilt if the channel hasn't been run before
ESP_LOGD(TAG, "relay_chn_tilt_issue_cmd: Tilt will not be executed since the channel hasn't been run yet");
return;
}
if (tilt_ctl->cmd == cmd) {
ESP_LOGD(TAG, "relay_chn_tilt_issue_cmd: There is already a tilt command in progress!");
return;
}
// Set the command that will be processed
tilt_ctl->cmd = cmd;
switch (tilt_ctl->chn_ctl->state) {
case RELAY_CHN_STATE_IDLE:
// Relay channel is free, tilt can be issued immediately
relay_chn_tilt_dispatch_cmd(tilt_ctl, cmd);
break;
case RELAY_CHN_STATE_FORWARD_PENDING:
case RELAY_CHN_STATE_REVERSE_PENDING:
// Issue a stop command first so that the timer and pending cmd get cleared
relay_chn_dispatch_cmd(tilt_ctl->chn_ctl, RELAY_CHN_CMD_STOP);
// break not put intentionally
case RELAY_CHN_STATE_STOPPED: {
// Check if channel needs timing before tilting
uint32_t req_timing_ms = relay_chn_tilt_get_required_timing_before_tilting(tilt_ctl, cmd);
if (req_timing_ms == 0) {
relay_chn_tilt_dispatch_cmd(tilt_ctl, cmd);
} else {
// Channel needs timing before running tilting action, schedule it
tilt_ctl->step = RELAY_CHN_TILT_STEP_PENDING;
relay_chn_start_esp_timer_once(tilt_ctl->tilt_timer, req_timing_ms);
}
break;
}
case RELAY_CHN_STATE_FORWARD:
if (cmd == RELAY_CHN_TILT_CMD_FORWARD) {
// Stop the running channel first
relay_chn_dispatch_cmd(tilt_ctl->chn_ctl, RELAY_CHN_CMD_STOP);
// Schedule for tilting
tilt_ctl->step = RELAY_CHN_TILT_STEP_PENDING;
relay_chn_start_esp_timer_once(tilt_ctl->tilt_timer, RELAY_CHN_OPPOSITE_INERTIA_MS);
} else if (cmd == RELAY_CHN_TILT_CMD_REVERSE) {
// Stop the running channel first
relay_chn_dispatch_cmd(tilt_ctl->chn_ctl, RELAY_CHN_CMD_STOP);
// If the tilt cmd is TILT_REVERSE then dispatch it immediately
relay_chn_tilt_dispatch_cmd(tilt_ctl, cmd);
}
break;
case RELAY_CHN_STATE_REVERSE:
if (cmd == RELAY_CHN_TILT_CMD_REVERSE) {
// Stop the running channel first
relay_chn_dispatch_cmd(tilt_ctl->chn_ctl, RELAY_CHN_CMD_STOP);
// Schedule for tilting
tilt_ctl->step = RELAY_CHN_TILT_STEP_PENDING;
relay_chn_start_esp_timer_once(tilt_ctl->tilt_timer, RELAY_CHN_OPPOSITE_INERTIA_MS);
} else if (cmd == RELAY_CHN_TILT_CMD_FORWARD) {
// Stop the running channel first
relay_chn_dispatch_cmd(tilt_ctl->chn_ctl, RELAY_CHN_CMD_STOP);
// If the tilt cmd is TILT_FORWARD then dispatch it immediately
relay_chn_tilt_dispatch_cmd(tilt_ctl, cmd);
}
break;
default:
ESP_LOGD(TAG, "relay_chn_tilt_issue_cmd: Unexpected relay channel state: %s!", relay_chn_state_str(tilt_ctl->chn_ctl->state));
}
}
static void relay_chn_tilt_issue_auto(relay_chn_tilt_ctl_t *tilt_ctl)
{
relay_chn_cmd_t last_run_cmd = relay_chn_run_info_get_last_run_cmd(tilt_ctl->chn_ctl->run_info);
if (last_run_cmd == RELAY_CHN_CMD_FORWARD || tilt_ctl->chn_ctl->state == RELAY_CHN_STATE_FORWARD) {
relay_chn_tilt_issue_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_FORWARD);
}
else if (last_run_cmd == RELAY_CHN_CMD_REVERSE || tilt_ctl->chn_ctl->state == RELAY_CHN_STATE_REVERSE) {
relay_chn_tilt_issue_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_REVERSE);
}
}
#if RELAY_CHN_COUNT > 1
void relay_chn_tilt_auto(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) {
return;
}
// Execute for all channels
if (chn_id == RELAY_CHN_ID_ALL) {
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
relay_chn_tilt_issue_auto(&tilt_ctls[i]);
}
}
// Execute for a single channel
else {
relay_chn_tilt_ctl_t* tilt_ctl = &tilt_ctls[chn_id];
relay_chn_tilt_issue_auto(tilt_ctl);
}
}
static void relay_chn_tilt_issue_cmd_on_all_channels(relay_chn_tilt_cmd_t cmd)
{
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
relay_chn_tilt_ctl_t* tilt_ctl = &tilt_ctls[i];
relay_chn_tilt_issue_cmd(tilt_ctl, cmd);
}
}
void relay_chn_tilt_forward(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) {
return;
}
if (chn_id == RELAY_CHN_ID_ALL)
relay_chn_tilt_issue_cmd_on_all_channels(RELAY_CHN_TILT_CMD_FORWARD);
else {
relay_chn_tilt_ctl_t* tilt_ctl = &tilt_ctls[chn_id];
relay_chn_tilt_issue_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_FORWARD);
}
}
void relay_chn_tilt_reverse(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) {
return;
}
if (chn_id == RELAY_CHN_ID_ALL)
relay_chn_tilt_issue_cmd_on_all_channels(RELAY_CHN_TILT_CMD_REVERSE);
else {
relay_chn_tilt_ctl_t* tilt_ctl = &tilt_ctls[chn_id];
relay_chn_tilt_issue_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_REVERSE);
}
}
void relay_chn_tilt_stop(uint8_t chn_id)
{
if (!relay_chn_is_channel_id_valid(chn_id)) {
return;
}
if (chn_id == RELAY_CHN_ID_ALL) {
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
relay_chn_tilt_dispatch_cmd(&tilt_ctls[i], RELAY_CHN_TILT_CMD_STOP);
}
}
else {
relay_chn_tilt_dispatch_cmd(&tilt_ctls[chn_id], RELAY_CHN_TILT_CMD_STOP);
}
}
#else // RELAY_CHN_COUNT > 1
void relay_chn_tilt_auto()
{
relay_chn_tilt_issue_auto(&tilt_ctl);
}
void relay_chn_tilt_forward()
{
relay_chn_tilt_issue_cmd(&tilt_ctl, RELAY_CHN_TILT_CMD_FORWARD);
}
void relay_chn_tilt_reverse()
{
relay_chn_tilt_issue_cmd(&tilt_ctl, RELAY_CHN_TILT_CMD_REVERSE);
}
void relay_chn_tilt_stop()
{
relay_chn_tilt_dispatch_cmd(&tilt_ctl, RELAY_CHN_TILT_CMD_STOP);
}
#endif // RELAY_CHN_COUNT > 1
static void relay_chn_tilt_set_timing_values(relay_chn_tilt_timing_t *tilt_timing,
uint8_t sensitivity,
uint32_t run_time_ms,
uint32_t pause_time_ms)
{
tilt_timing->sensitivity = sensitivity;
tilt_timing->move_time_ms = run_time_ms;
tilt_timing->pause_time_ms = pause_time_ms;
}
static void relay_chn_tilt_compute_set_sensitivity(relay_chn_tilt_ctl_t *tilt_ctl, uint8_t sensitivity)
{
if (sensitivity >= 100) {
relay_chn_tilt_set_timing_values(&tilt_ctl->tilt_timing,
100,
RELAY_CHN_TILT_RUN_MAX_MS,
RELAY_CHN_TILT_PAUSE_MAX_MS);
}
else if (sensitivity == 0) {
relay_chn_tilt_set_timing_values(&tilt_ctl->tilt_timing,
0,
RELAY_CHN_TILT_RUN_MIN_MS,
RELAY_CHN_TILT_PAUSE_MIN_MS);
}
else if (sensitivity == RELAY_CHN_TILT_DEFAULT_SENSITIVITY) {
relay_chn_tilt_set_timing_values(&tilt_ctl->tilt_timing,
sensitivity,
RELAY_CHN_TILT_DEFAULT_RUN_MS,
RELAY_CHN_TILT_DEFAULT_PAUSE_MS);
}
else {
// Compute the new timing values from the sensitivity percent value by using linear interpolation
uint32_t tilt_run_time_ms = 0, tilt_pause_time_ms = 0;
tilt_run_time_ms = RELAY_CHN_TILT_RUN_MIN_MS + (sensitivity * (RELAY_CHN_TILT_RUN_MAX_MS - RELAY_CHN_TILT_RUN_MIN_MS) / 100);
tilt_pause_time_ms = RELAY_CHN_TILT_PAUSE_MIN_MS + (sensitivity * (RELAY_CHN_TILT_PAUSE_MAX_MS - RELAY_CHN_TILT_PAUSE_MIN_MS) / 100);
relay_chn_tilt_set_timing_values(&tilt_ctl->tilt_timing,
sensitivity,
tilt_run_time_ms,
tilt_pause_time_ms);
}
}
#if RELAY_CHN_COUNT > 1
void relay_chn_tilt_set_sensitivity(uint8_t chn_id, uint8_t sensitivity)
{
if (!relay_chn_is_channel_id_valid(chn_id)) {
return;
}
if (chn_id == RELAY_CHN_ID_ALL) {
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
relay_chn_tilt_compute_set_sensitivity(&tilt_ctls[i], sensitivity);
}
}
else {
relay_chn_tilt_compute_set_sensitivity(&tilt_ctls[chn_id], sensitivity);
}
#if RELAY_CHN_ENABLE_NVS == 1
relay_chn_nvs_set_tilt_sensitivity(chn_id, sensitivity);
#endif // RELAY_CHN_ENABLE_NVS
}
esp_err_t relay_chn_tilt_get_sensitivity(uint8_t chn_id, uint8_t *sensitivity, size_t length)
{
if (!relay_chn_is_channel_id_valid(chn_id)) {
return ESP_ERR_INVALID_ARG;
}
if (sensitivity == NULL) {
ESP_LOGD(TAG, "relay_chn_tilt_get_sensitivity: sensitivity is NULL");
return ESP_ERR_INVALID_ARG;
}
if (chn_id == RELAY_CHN_ID_ALL) {
if (length < RELAY_CHN_COUNT) {
ESP_LOGD(TAG, "relay_chn_tilt_get_sensitivity: length is too short to store all sensitivity values");
return ESP_ERR_INVALID_ARG;
}
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
sensitivity[i] = tilt_ctls[i].tilt_timing.sensitivity;
}
return ESP_OK;
}
*sensitivity = tilt_ctls[chn_id].tilt_timing.sensitivity;
return ESP_OK;
}
#else
void relay_chn_tilt_set_sensitivity(uint8_t sensitivity)
{
relay_chn_tilt_compute_set_sensitivity(&tilt_ctl, sensitivity);
#if RELAY_CHN_ENABLE_NVS == 1
relay_chn_nvs_set_tilt_sensitivity(0, sensitivity);
#endif // RELAY_CHN_ENABLE_NVS
}
uint8_t relay_chn_tilt_get_sensitivity()
{
return tilt_ctl.tilt_timing.sensitivity;
}
#endif // RELAY_CHN_COUNT > 1
void relay_chn_tilt_reset_count(relay_chn_tilt_ctl_t *tilt_ctl)
{
tilt_ctl->tilt_count = 0;
#if RELAY_CHN_ENABLE_NVS == 1
esp_timer_stop(tilt_ctl->flush_timer);
#endif
}
/**
* @brief Update tilt count automatically and return the current value.
*
* This helper function updates the relevant tilt count depending on the
* last run info and helps the tilt module in deciding whether the requested
* tilt should execute or not.
*
* This is useful to control reverse tilting for the same direction particularly.
* For example:
* - If the channel's last run was FORWARD and a TILT_FORWARD is requested,
* then the tilt count will count up on the relay_chn_tilt_ctl_t::tilt_count
* and the function will return the actual count.
* - If the channel's last run was FORWARD and a TILT_REVERSE is requested,
* then the relay_chn_tilt_ctl_t::tilt_count will be checked against zero first,
* and then it will count down and return the actual count if it is greater
* than 0, else the function will return 0.
* - If the tilt command is irrelevant then the function will return 0.
* - If the last run is irrelevant then the function will return 0.
*
* @param tilt_ctl The relay channel handle.
*
* @return The actual value of the relevant count.
* @return 1 if the last tilt_count was 1 and decremented to 0.
* @return 0 if:
* - related count is already 0.
* - tilt command is irrelevant.
* - last run info is irrelevant.
*/
static uint16_t relay_chn_tilt_count_update(relay_chn_tilt_ctl_t *tilt_ctl)
{
relay_chn_cmd_t last_run_cmd = relay_chn_run_info_get_last_run_cmd(tilt_ctl->chn_ctl->run_info);
if (last_run_cmd == RELAY_CHN_CMD_FORWARD) {
if (tilt_ctl->cmd == RELAY_CHN_TILT_CMD_FORWARD) {
return ++tilt_ctl->tilt_count;
}
else if (tilt_ctl->cmd == RELAY_CHN_TILT_CMD_REVERSE) {
if (tilt_ctl->tilt_count > 0) {
--tilt_ctl->tilt_count;
// Still should do one more move, return non-zero value
return 1;
}
else
return 0;
}
}
else if (last_run_cmd == RELAY_CHN_CMD_REVERSE) {
if (tilt_ctl->cmd == RELAY_CHN_TILT_CMD_REVERSE) {
return ++tilt_ctl->tilt_count;
}
else if (tilt_ctl->cmd == RELAY_CHN_TILT_CMD_FORWARD) {
if (tilt_ctl->tilt_count > 0) {
--tilt_ctl->tilt_count;
// Still should do one more move, return non-zero value
return 1;
}
else
return 0;
}
}
// Irrelevant case -> reset
tilt_ctl->tilt_count = 0;
return 0;
}
#if RELAY_CHN_ENABLE_NVS == 1
static esp_err_t relay_chn_tilt_save_tilt_count(relay_chn_tilt_ctl_t *tilt_ctl)
{
// Save the tilt count to NVS storage
esp_err_t ret = relay_chn_nvs_set_tilt_count(tilt_ctl->chn_ctl->id, tilt_ctl->tilt_count);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "relay_chn_tilt_execute_stop: Failed to save tilt count for channel #%d: %s", tilt_ctl->chn_ctl->id, esp_err_to_name(ret));
}
return ESP_OK;
}
static void relay_chn_tilt_flush_timer_cb(void *arg)
{
relay_chn_tilt_ctl_t* tilt_ctl = (relay_chn_tilt_ctl_t*) arg;
ESP_RETURN_VOID_ON_FALSE(tilt_ctl != NULL, TAG, "relay_chn_tilt_flush_timer_cb: timer arg is NULL");
// Save the tilt count to storage
esp_err_t ret = relay_chn_tilt_save_tilt_count(tilt_ctl);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "relay_chn_tilt_execute_stop: Failed to save tilt count for channel #%d: %s", tilt_ctl->chn_ctl->id, esp_err_to_name(ret));
}
}
#endif
static void relay_chn_tilt_execute_stop(relay_chn_tilt_ctl_t *tilt_ctl)
{
// Stop the channel's timer if active
esp_timer_stop(tilt_ctl->tilt_timer);
// Invalidate tilt cmd and step
tilt_ctl->cmd = RELAY_CHN_TILT_CMD_NONE;
tilt_ctl->step = RELAY_CHN_TILT_STEP_NONE;
// Stop the channel
if (relay_chn_output_stop(tilt_ctl->chn_ctl->output) != ESP_OK) {
ESP_LOGE(TAG, "relay_chn_tilt_execute_stop: Failed to output stop for relay channel #%d!", tilt_ctl->chn_ctl->id);
}
relay_chn_dispatch_cmd(tilt_ctl->chn_ctl, RELAY_CHN_CMD_STOP);
#if RELAY_CHN_ENABLE_NVS == 1
// Start the flush debounce timer
relay_chn_start_esp_timer_once(tilt_ctl->flush_timer, RELAY_CHN_TILT_FLUSH_DEBOUNCE_MS);
#endif
}
static void relay_chn_tilt_execute_forward(relay_chn_tilt_ctl_t *tilt_ctl)
{
if (relay_chn_output_reverse(tilt_ctl->chn_ctl->output) != ESP_OK) {
ESP_LOGE(TAG, "relay_chn_tilt_execute_forward: Failed to output reverse for relay channel #%d!", tilt_ctl->chn_ctl->id);
// Stop tilting because of the error
relay_chn_tilt_dispatch_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_STOP);
return;
}
// Set the move time timer
relay_chn_start_esp_timer_once(tilt_ctl->tilt_timer, tilt_ctl->tilt_timing.move_time_ms);
// Set to pause step
tilt_ctl->step = RELAY_CHN_TILT_STEP_PAUSE;
}
static void relay_chn_tilt_execute_reverse(relay_chn_tilt_ctl_t *tilt_ctl)
{
if (relay_chn_output_forward(tilt_ctl->chn_ctl->output) != ESP_OK) {
ESP_LOGE(TAG, "relay_chn_tilt_execute_reverse: Failed to output forward for relay channel #%d!", tilt_ctl->chn_ctl->id);
// Stop tilting because of the error
relay_chn_tilt_dispatch_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_STOP);
return;
}
// Set the move time timer
relay_chn_start_esp_timer_once(tilt_ctl->tilt_timer, tilt_ctl->tilt_timing.move_time_ms);
// Set to pause step
tilt_ctl->step = RELAY_CHN_TILT_STEP_PAUSE;
}
static void relay_chn_tilt_execute_pause(relay_chn_tilt_ctl_t *tilt_ctl)
{
// Pause the channel
if (relay_chn_output_stop(tilt_ctl->chn_ctl->output) != ESP_OK) {
ESP_LOGE(TAG, "relay_chn_tilt_execute_pause: Failed to output stop for relay channel #%d!", tilt_ctl->chn_ctl->id);
// Stop tilting because of the error
relay_chn_tilt_dispatch_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_STOP);
return;
}
// Update the tilt count before the next move and expect the return value to be greater than 0
if (relay_chn_tilt_count_update(tilt_ctl) == 0) {
ESP_LOGD(TAG, "relay_chn_tilt_execute_pause: Relay channel cannot tilt anymore");
// Stop tilting since the tilting limit has been reached
relay_chn_tilt_dispatch_cmd(tilt_ctl, RELAY_CHN_TILT_CMD_STOP);
return;
}
// Set the pause time timer
relay_chn_start_esp_timer_once(tilt_ctl->tilt_timer, tilt_ctl->tilt_timing.pause_time_ms);
// Set to move step
tilt_ctl->step = RELAY_CHN_TILT_STEP_MOVE;
}
static void relay_chn_tilt_event_handler(void *handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
relay_chn_tilt_ctl_t* tilt_ctl = *(relay_chn_tilt_ctl_t**) event_data;
ESP_RETURN_VOID_ON_FALSE(tilt_ctl != NULL, TAG, "event_data is NULL");
ESP_LOGD(TAG, "relay_chn_event_handler: Command: %s", relay_chn_cmd_str(event_id));
switch(event_id) {
case RELAY_CHN_TILT_CMD_STOP:
relay_chn_tilt_execute_stop(tilt_ctl);
break;
case RELAY_CHN_TILT_CMD_FORWARD:
relay_chn_tilt_execute_forward(tilt_ctl);
// Update channel state
relay_chn_update_state(tilt_ctl->chn_ctl, RELAY_CHN_STATE_TILT_FORWARD);
break;
case RELAY_CHN_TILT_CMD_REVERSE:
relay_chn_tilt_execute_reverse(tilt_ctl);
// Update channel state
relay_chn_update_state(tilt_ctl->chn_ctl, RELAY_CHN_STATE_TILT_REVERSE);
break;
default:
ESP_LOGW(TAG, "Unexpected relay channel tilt command: %ld!", event_id);
}
}
// Timer callback for the relay_chn_tilt_control_t::tilt_timer
static void relay_chn_tilt_timer_cb(void *arg)
{
relay_chn_tilt_ctl_t* tilt_ctl = (relay_chn_tilt_ctl_t*) arg;
ESP_RETURN_VOID_ON_FALSE(tilt_ctl != NULL, TAG, "relay_chn_tilt_timer_cb: timer arg is NULL");
switch (tilt_ctl->step)
{
case RELAY_CHN_TILT_STEP_MOVE:
if (tilt_ctl->cmd == RELAY_CHN_TILT_CMD_FORWARD) {
relay_chn_tilt_execute_forward(tilt_ctl);
}
else if (tilt_ctl->cmd == RELAY_CHN_TILT_CMD_REVERSE) {
relay_chn_tilt_execute_reverse(tilt_ctl);
}
break;
case RELAY_CHN_TILT_STEP_PAUSE:
relay_chn_tilt_execute_pause(tilt_ctl);
break;
case RELAY_CHN_TILT_STEP_PENDING:
// Just dispatch the pending tilt command
relay_chn_tilt_dispatch_cmd(tilt_ctl, tilt_ctl->cmd);
break;
default:
break;
}
}
#if RELAY_CHN_ENABLE_NVS == 1
static esp_err_t relay_chn_tilt_load_sensitivity(uint8_t ch, uint8_t *sensitivity)
{
esp_err_t ret = relay_chn_nvs_get_tilt_sensitivity(ch, sensitivity);
if (ret == ESP_ERR_NVS_NOT_FOUND) {
*sensitivity = RELAY_CHN_TILT_DEFAULT_SENSITIVITY;
return ESP_OK;
}
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to load tilt sensitivity for channel %d", ch);
return ESP_OK;
}
static esp_err_t relay_chn_tilt_load_tilt_count(uint8_t ch, uint16_t *tilt_count)
{
esp_err_t ret = relay_chn_nvs_get_tilt_count(ch, tilt_count);
if (ret == ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGD(TAG, "relay_chn_tilt_load_tilt_count: No tilt count found in NVS for channel %d, initializing to zero", ch);
tilt_count = 0;
return ESP_OK;
}
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to load tilt counters for channel %d", ch);
return ESP_OK;
}
#endif // RELAY_CHN_ENABLE_NVS
static esp_err_t relay_chn_tilt_ctl_init(relay_chn_tilt_ctl_t *tilt_ctl,
relay_chn_ctl_t *chn_ctl,
uint16_t tilt_count ,
uint8_t sensitivity)
{
tilt_ctl->cmd = RELAY_CHN_TILT_CMD_NONE;
tilt_ctl->step = RELAY_CHN_TILT_STEP_NONE;
relay_chn_tilt_compute_set_sensitivity(tilt_ctl, sensitivity);
tilt_ctl->tilt_count = tilt_count;
tilt_ctl->chn_ctl = chn_ctl;
tilt_ctl->chn_ctl->tilt_ctl = tilt_ctl;
// Create tilt timer for the channel
char timer_name[32];
snprintf(timer_name, sizeof(timer_name), "relay_chn_%2d_tilt_timer", chn_ctl->id);
esp_timer_create_args_t timer_args = {
.callback = relay_chn_tilt_timer_cb,
.arg = tilt_ctl,
.name = timer_name
};
esp_err_t ret = esp_timer_create(&timer_args, &tilt_ctl->tilt_timer);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to create tilt timer for channel %d", chn_ctl->id);
#if RELAY_CHN_ENABLE_NVS == 1
// Create flush timer for the tilt counters
snprintf(timer_name, sizeof(timer_name), "relay_chn_%2d_tilt_flush_timer", chn_ctl->id);
timer_args.callback = relay_chn_tilt_flush_timer_cb;
timer_args.name = timer_name;
ret = esp_timer_create(&timer_args, &tilt_ctl->flush_timer);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to create tilt flush timer for channel %d", chn_ctl->id);
#endif
return ESP_OK;
}
esp_err_t relay_chn_tilt_init(relay_chn_ctl_t *chn_ctls)
{
uint8_t sensitivity;
uint16_t tilt_count;
#if RELAY_CHN_COUNT > 1
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
#if RELAY_CHN_ENABLE_NVS == 1
esp_err_t ret = relay_chn_tilt_load_sensitivity(i, &sensitivity);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to load tilt sensitivity for channel %d", i);
ret = relay_chn_tilt_load_tilt_count(i, &tilt_count);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to load tilt count for channel %d", i);
#else
sensitivity = RELAY_CHN_TILT_DEFAULT_SENSITIVITY;
tilt_count = 0;
#endif // RELAY_CHN_ENABLE_NVS == 1
relay_chn_tilt_ctl_init(&tilt_ctls[i], &chn_ctls[i], tilt_count, sensitivity);
}
#else
sensitivity = RELAY_CHN_TILT_DEFAULT_SENSITIVITY;
tilt_count = 0;
#if RELAY_CHN_ENABLE_NVS == 1
esp_err_t ret = relay_chn_tilt_load_sensitivity(0, &sensitivity);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to load tilt sensitivity for channel %d", 0);
ret = relay_chn_tilt_load_tilt_count(0, &tilt_count);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to load tilt count for channel %d", 0);
#endif // RELAY_CHN_ENABLE_NVS == 1
relay_chn_tilt_ctl_init(&tilt_ctl, chn_ctls, tilt_count, sensitivity);
#endif // RELAY_CHN_COUNT > 1
return esp_event_handler_register_with(relay_chn_event_loop,
RELAY_CHN_TILT_CMD_EVENT_BASE,
ESP_EVENT_ANY_ID,
relay_chn_tilt_event_handler, NULL);
}
void relay_chn_tilt_ctl_deinit(relay_chn_tilt_ctl_t *tilt_ctl)
{
if (tilt_ctl->tilt_timer != NULL) {
esp_timer_delete(tilt_ctl->tilt_timer);
tilt_ctl->tilt_timer = NULL;
}
#if RELAY_CHN_ENABLE_NVS == 1
if (tilt_ctl->flush_timer != NULL) {
esp_timer_delete(tilt_ctl->flush_timer);
tilt_ctl->flush_timer = NULL;
}
#endif // RELAY_CHN_ENABLE_NVS == 1
}
void relay_chn_tilt_deinit()
{
#if RELAY_CHN_COUNT > 1
for (int i = 0; i < RELAY_CHN_COUNT; i++) {
relay_chn_tilt_ctl_deinit(&tilt_ctls[i]);
}
#else
relay_chn_tilt_ctl_deinit(&tilt_ctl);
#endif // RELAY_CHN_COUNT > 1
esp_event_handler_unregister_with(relay_chn_event_loop,
RELAY_CHN_TILT_CMD_EVENT_BASE,
ESP_EVENT_ANY_ID,
relay_chn_tilt_event_handler);
}

3
test_apps/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"C_Cpp.intelliSenseEngine": "default"
}

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,42 @@
# === These files must be included in any case ===
set(srcs "test_common.c"
"test_app_main.c")
set(incdirs ".")
# === Selective compilation based on channel count ===
if(CONFIG_RELAY_CHN_COUNT GREATER 1)
list(APPEND srcs "test_relay_chn_core_multi.c"
"test_relay_chn_listener_multi.c")
else()
list(APPEND srcs "test_relay_chn_core_single.c"
"test_relay_chn_listener_single.c")
endif()
if(CONFIG_RELAY_CHN_ENABLE_TILTING)
if(CONFIG_RELAY_CHN_COUNT GREATER 1)
list(APPEND srcs "test_relay_chn_tilt_multi.c")
else()
list(APPEND srcs "test_relay_chn_tilt_single.c")
endif()
endif()
if(CONFIG_RELAY_CHN_ENABLE_NVS)
list(APPEND incdirs "../../private_include")
list(APPEND srcs "../../src/relay_chn_nvs.c")
if(CONFIG_RELAY_CHN_COUNT GREATER 1)
list(APPEND srcs "test_relay_chn_nvs_multi.c")
else()
list(APPEND srcs "test_relay_chn_nvs_single.c")
endif()
endif()
# 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 ${incdirs}
REQUIRES unity relay_chn
WHOLE_ARCHIVE
)

View File

@@ -0,0 +1,101 @@
#include <stdbool.h>
#include "esp_log.h"
#include "esp_system.h"
#include "unity.h"
#include "unity_internals.h"
#include "unity_test_runner.h"
#include "test_common.h"
#if RELAY_CHN_ENABLE_NVS == 1
#include "nvs_flash.h"
#include "relay_chn_nvs.h"
#endif
#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()
{
}
void tearDown()
{
reset_channels_to_idle_state();
}
static void test_nvs_flash_init(void)
{
esp_err_t ret;
#if RELAY_CHN_NVS_CUSTOM_PARTITION == 1
ret = nvs_flash_init_partition(RELAY_CHN_NVS_CUSTOM_PARTITION_NAME);
ESP_LOGI(TEST_TAG, "test_nvs_flash_init: NVS flash init partition return: %s", esp_err_to_name(ret));
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS partition is truncated and needs to be erased
ret = nvs_flash_erase_partition(RELAY_CHN_NVS_CUSTOM_PARTITION_NAME);
if (ret == ESP_OK) {
ret = nvs_flash_init_partition(RELAY_CHN_NVS_CUSTOM_PARTITION_NAME);
}
}
#else
ret = nvs_flash_init();
ESP_LOGI(TEST_TAG, "test_nvs_flash_init: NVS flash init return: %s", esp_err_to_name(ret));
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS partition is truncated and needs to be erased
ret = nvs_flash_erase();
if (ret == ESP_OK) {
ret = nvs_flash_init();
}
}
#endif
TEST_ESP_OK(ret);
}
static void test_nvs_flash_deinit(void)
{
esp_err_t ret;
#if RELAY_CHN_NVS_CUSTOM_PARTITION == 1
ret = nvs_flash_deinit_partition(RELAY_CHN_NVS_CUSTOM_PARTITION_NAME);
#else
ret = nvs_flash_deinit();
#endif
TEST_ESP_OK(ret);
}
void app_main(void)
{
// Init NVS once for all tests
test_nvs_flash_init();
// Create relay_chn once for all tests
TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count));
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();
// Destroy relay_chn
relay_chn_destroy();
// Deinit NVS
test_nvs_flash_deinit();
ESP_LOGI(TEST_TAG, "All tests complete.");
esp_restart(); // Restart to invoke qemu exit
}

View File

@@ -0,0 +1,52 @@
#include "test_common.h"
const char *TEST_TAG = "RELAY_CHN_TEST";
const uint8_t relay_chn_count = CONFIG_RELAY_CHN_COUNT;
const uint32_t opposite_inertia_ms = CONFIG_RELAY_CHN_OPPOSITE_INERTIA_MS;
const uint32_t test_delay_margin_ms = 50; // ms tolerance
// Test-wide GPIO map
#if CONFIG_RELAY_CHN_COUNT > 1
const uint8_t gpio_map[] = {
0, 1,
2, 3
#if CONFIG_RELAY_CHN_COUNT > 2
, 4, 5
#if CONFIG_RELAY_CHN_COUNT > 3
, 6, 7
#if CONFIG_RELAY_CHN_COUNT > 4
, 8, 9
#if CONFIG_RELAY_CHN_COUNT > 5
, 10, 11
#if CONFIG_RELAY_CHN_COUNT > 6
, 12, 13
#if CONFIG_RELAY_CHN_COUNT > 7
, 14, 15
#endif
#endif
#endif
#endif
#endif
#endif
};
#else
const uint8_t gpio_map[] = {4, 5};
#endif
const uint8_t gpio_count = sizeof(gpio_map) / sizeof(gpio_map[0]);
void reset_channels_to_idle_state()
{
#if CONFIG_RELAY_CHN_COUNT > 1
relay_chn_stop(RELAY_CHN_ID_ALL);
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
for (int i = 0; i < CONFIG_RELAY_CHN_COUNT; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, relay_chn_get_state(i));
}
#else
relay_chn_stop();
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, relay_chn_get_state());
#endif
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include <string.h> // For memset
#include "unity.h"
#include "relay_chn.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// Test log tag
extern const char *TEST_TAG;
// GPIO configurations
extern const uint8_t gpio_map[];
extern const uint8_t gpio_count;
extern const uint8_t relay_chn_count;
// Config variables for tests
extern const uint32_t opposite_inertia_ms;
extern const uint32_t test_delay_margin_ms;
// Init state
extern bool g_is_component_initialized;
// Reset channels to Idle state
void reset_channels_to_idle_state(void);

View File

@@ -0,0 +1,347 @@
#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 (127 is an invalid GPIO for output)
uint8_t invalid_gpio_map[] = {4, 127, 18, 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_IDLE
TEST_CASE("Relay channels initialize correctly to FREE state", "[relay_chn][core]") {
for (uint8_t i = 0; i < relay_chn_count; i++) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, 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]") {
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_IDLE, 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]") {
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]") {
// 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_IDLE, 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]") {
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]")
{
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]")
{
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]")
{
// 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_IDLE, relay_chn_get_state(i));
}
}
// TEST_CASE: Test that relays stop and transition to RELAY_CHN_STATE_IDLE
// 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]") {
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_IDLE
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, 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]") {
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]") {
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]") {
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_IDLE, 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_IDLE, 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_IDLE, relay_chn_get_state(0));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, 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
// 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;
// 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;
// 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_IDLE -> (relay_chn_run_forward) -> RELAY_CHN_STATE_FORWARD
TEST_CASE("FREE to Running transition without inertia", "[relay_chn][core][inertia]") {
uint8_t ch = 0;
// setUp() should have already brought the channel to FREE state
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, 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]")
{
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]")
{
// 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]")
{
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_IDLE, 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]")
{
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,174 @@
#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)
uint8_t invalid_gpio_map[] = {4, 127};
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_IDLE
TEST_CASE("Relay channels initialize correctly to IDLE state", "[relay_chn][core]") {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, relay_chn_get_state());
}
// 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]") {
relay_chn_run_forward();
// 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());
}
// 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]") {
relay_chn_run_reverse(); // 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());
}
// TEST_CASE: Test that relays stop and transition to RELAY_CHN_STATE_IDLE
// This test also verifies the transition to IDLE state after a STOP command.
TEST_CASE("Relay channels stop and update to IDLE state", "[relay_chn][core]") {
// First, run forward to test stopping and transitioning to IDLE state
relay_chn_run_forward();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state());
// Now, issue the stop command
relay_chn_stop(); // 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());
// Then, wait for the inertia period for it to transition to RELAY_CHN_STATE_IDLE
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, relay_chn_get_state());
}
// ### 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]") {
// 1. Start in forward direction
relay_chn_run_forward();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Short delay for state stabilization
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state());
// 2. Issue reverse command
relay_chn_run_reverse(); // 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());
// 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()); // 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]") {
// 1. Start in reverse direction
relay_chn_run_reverse(); // 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());
// 2. Issue forward command
relay_chn_run_forward();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD_PENDING, relay_chn_get_state());
// 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());
}
// 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]") {
// 1. Start in forward direction
relay_chn_run_forward();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state());
// 2. Issue the same forward command again
relay_chn_run_forward();
// 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());
}
// TEST_CASE: Test transition from IDLE state to running (no inertia expected)
// Scenario: RELAY_CHN_STATE_IDLE -> (relay_chn_run_forward) -> RELAY_CHN_STATE_FORWARD
TEST_CASE("IDLE to Running transition without inertia", "[relay_chn][core][inertia]") {
// setUp() should have already brought the channel to IDLE state
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, relay_chn_get_state());
// Start in forward direction
relay_chn_run_forward();
// No inertia is expected when starting from IDLE state.
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state());
}
// ### Direction Flipping Tests
TEST_CASE("Single channel direction can be flipped", "[relay_chn][core][direction]")
{
// 1. Initial direction should be default
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_DEFAULT, relay_chn_get_direction());
// 2. Flip the direction
relay_chn_flip_direction();
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());
// 4. Flip back
relay_chn_flip_direction();
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());
}
TEST_CASE("Flipping a running channel stops it and flips direction", "[relay_chn][core][direction]")
{
// 1. Start channel running and verify state
relay_chn_run_forward();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state());
// 2. Flip the direction while running
relay_chn_flip_direction();
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());
// 4. Wait for the flip inertia to pass, after which it should be IDLE and FLIPPED
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, relay_chn_get_state());
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_FLIPPED, relay_chn_get_direction());
}

View File

@@ -0,0 +1,123 @@
#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;
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_IDLE, 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;
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;
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_IDLE, 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_IDLE, 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]") {
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,118 @@
#include "test_common.h"
// --- Listener Test Globals ---
typedef struct {
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) {
/* Just ignore the channel 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) {
/* Just ignore the channel 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]") {
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();
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(RELAY_CHN_STATE_IDLE, 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]") {
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();
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]") {
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();
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_IDLE, 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_IDLE, 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]") {
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();
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,135 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#include <string.h>
#include "unity.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "relay_chn_nvs.h"
TEST_CASE("Test relay storage init/deinit", "[relay_chn][nvs]")
{
TEST_ESP_OK(relay_chn_nvs_init());
TEST_ESP_OK(relay_chn_nvs_deinit());
}
TEST_CASE("Test direction setting and getting", "[relay_chn][nvs]")
{
TEST_ESP_OK(relay_chn_nvs_init());
// Test all channels
relay_chn_direction_t dir;
relay_chn_direction_t test_directions[] = {
RELAY_CHN_DIRECTION_DEFAULT,
RELAY_CHN_DIRECTION_FLIPPED
};
for (int channel = 0; channel < 2; channel++) {
TEST_ESP_OK(relay_chn_nvs_set_direction(channel, test_directions[channel]));
TEST_ESP_OK(relay_chn_nvs_get_direction(channel, &dir));
TEST_ASSERT_EQUAL(test_directions[channel], dir);
}
TEST_ESP_OK(relay_chn_nvs_deinit());
}
TEST_CASE("Test invalid parameters", "[relay_chn][nvs]")
{
TEST_ESP_OK(relay_chn_nvs_init());
// Test NULL pointer for all channels
for (int channel = 0; channel < RELAY_CHN_COUNT; channel++) {
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_nvs_get_direction(channel, NULL));
}
TEST_ESP_OK(relay_chn_nvs_deinit());
}
TEST_CASE("Test relay_chn_nvs_erase_all", "[relay_chn][nvs]")
{
TEST_ESP_OK(relay_chn_nvs_init());
// Store some test data first
relay_chn_direction_t direction = RELAY_CHN_DIRECTION_FLIPPED;
for (int channel = 0; channel < RELAY_CHN_COUNT; channel++) {
TEST_ESP_OK(relay_chn_nvs_set_direction(0, direction));
}
#ifdef RELAY_CHN_ENABLE_TILTING
uint8_t sensitivity = 50;
for (int channel = 0; channel < RELAY_CHN_COUNT; channel++) {
TEST_ESP_OK(relay_chn_nvs_set_tilt_sensitivity(0, sensitivity));
TEST_ESP_OK(relay_chn_nvs_set_tilt_count(0, 100));
}
#endif
// Test erase all
TEST_ESP_OK(relay_chn_nvs_erase_all());
// Verify data was erased by trying to read it back
relay_chn_direction_t read_direction;
TEST_ASSERT_EQUAL(ESP_ERR_NVS_NOT_FOUND, relay_chn_nvs_get_direction(0, &read_direction));
#ifdef RELAY_CHN_ENABLE_TILTING
uint8_t read_sensitivity;
TEST_ASSERT_EQUAL(ESP_ERR_NVS_NOT_FOUND, relay_chn_nvs_get_tilt_sensitivity(0, &read_sensitivity));
uint16_t tilt_count;
TEST_ASSERT_EQUAL(ESP_ERR_NVS_NOT_FOUND, relay_chn_nvs_get_tilt_count(0, &tilt_count));
#endif
TEST_ESP_OK(relay_chn_nvs_deinit());
}
#ifdef RELAY_CHN_ENABLE_TILTING
TEST_CASE("Test sensitivity setting and getting", "[relay_chn][nvs][tilt]")
{
TEST_ESP_OK(relay_chn_nvs_init());
const uint8_t test_sensitivity = 75;
uint8_t sensitivity;
// Test all channels
for (int channel = 0; channel < RELAY_CHN_COUNT; channel++) {
TEST_ESP_OK(relay_chn_nvs_set_tilt_sensitivity(channel, test_sensitivity));
TEST_ESP_OK(relay_chn_nvs_get_tilt_sensitivity(channel, &sensitivity));
TEST_ASSERT_EQUAL(test_sensitivity, sensitivity);
}
TEST_ESP_OK(relay_chn_nvs_deinit());
}
TEST_CASE("Test tilt counter operations", "[relay_chn][nvs][tilt]")
{
TEST_ESP_OK(relay_chn_nvs_init());
const uint16_t tilt_count = 100;
uint16_t tilt_count_read;
// Test all channels
for (int channel = 0; channel < RELAY_CHN_COUNT; channel++) {
// Test setting counters
TEST_ESP_OK(relay_chn_nvs_set_tilt_count(channel, tilt_count));
TEST_ESP_OK(relay_chn_nvs_get_tilt_count(channel, &tilt_count_read));
TEST_ASSERT_EQUAL(tilt_count, tilt_count_read);
}
TEST_ESP_OK(relay_chn_nvs_deinit());
}
TEST_CASE("Test tilting invalid parameters", "[relay_chn][nvs][tilt]")
{
TEST_ESP_OK(relay_chn_nvs_init());
// Test NULL pointers for all channels
for (int channel = 0; channel < RELAY_CHN_COUNT; channel++) {
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_nvs_get_tilt_sensitivity(channel, NULL));
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_nvs_get_tilt_count(channel, NULL));
}
TEST_ESP_OK(relay_chn_nvs_deinit());
}
#endif // RELAY_CHN_ENABLE_TILTING

View File

@@ -0,0 +1,121 @@
/*
* SPDX-FileCopyrightText: 2025 Kozmotronik Tech
*
* SPDX-License-Identifier: MIT
*/
#include <string.h>
#include "unity.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "relay_chn_nvs.h"
TEST_CASE("Test relay storage init/deinit", "[relay_chn][nvs]")
{
TEST_ESP_OK(relay_chn_nvs_init());
TEST_ESP_OK(relay_chn_nvs_deinit());
}
TEST_CASE("Test direction setting and getting", "[relay_chn][nvs]")
{
TEST_ESP_OK(relay_chn_nvs_init());
// Test channel 0
TEST_ESP_OK(relay_chn_nvs_set_direction(0, RELAY_CHN_DIRECTION_DEFAULT));
relay_chn_direction_t dir;
TEST_ESP_OK(relay_chn_nvs_get_direction(0, &dir));
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_DEFAULT, dir);
// Test channel 1
TEST_ESP_OK(relay_chn_nvs_set_direction(0, RELAY_CHN_DIRECTION_FLIPPED));
TEST_ESP_OK(relay_chn_nvs_get_direction(0, &dir));
TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_FLIPPED, dir);
TEST_ESP_OK(relay_chn_nvs_deinit());
}
TEST_CASE("Test invalid parameters", "[relay_chn][nvs]")
{
TEST_ESP_OK(relay_chn_nvs_init());
// Test NULL pointer
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_nvs_get_direction(0, NULL));
TEST_ESP_OK(relay_chn_nvs_deinit());
}
TEST_CASE("Test relay_chn_nvs_erase_all", "[relay_chn][nvs]")
{
TEST_ESP_OK(relay_chn_nvs_init());
// Store some test data first
relay_chn_direction_t direction = RELAY_CHN_DIRECTION_FLIPPED;
TEST_ESP_OK(relay_chn_nvs_set_direction(0, direction));
#ifdef RELAY_CHN_ENABLE_TILTING
uint8_t sensitivity = 50;
TEST_ESP_OK(relay_chn_nvs_set_tilt_sensitivity(0, sensitivity));
TEST_ESP_OK(relay_chn_nvs_set_tilt_count(0, 100));
#endif
// Test erase all
TEST_ESP_OK(relay_chn_nvs_erase_all());
// Verify data was erased by trying to read it back
relay_chn_direction_t read_direction;
TEST_ASSERT_EQUAL(ESP_ERR_NVS_NOT_FOUND, relay_chn_nvs_get_direction(0, &read_direction));
#ifdef RELAY_CHN_ENABLE_TILTING
uint8_t read_sensitivity;
TEST_ASSERT_EQUAL(ESP_ERR_NVS_NOT_FOUND, relay_chn_nvs_get_tilt_sensitivity(0, &read_sensitivity));
uint16_t tilt_count;
TEST_ASSERT_EQUAL(ESP_ERR_NVS_NOT_FOUND, relay_chn_nvs_get_tilt_count(0, &tilt_count));
#endif
TEST_ESP_OK(relay_chn_nvs_deinit());
}
#ifdef RELAY_CHN_ENABLE_TILTING
TEST_CASE("Test sensitivity setting and getting", "[relay_chn][nvs][tilt]")
{
TEST_ESP_OK(relay_chn_nvs_init());
const uint8_t test_sensitivity = 75;
TEST_ESP_OK(relay_chn_nvs_set_tilt_sensitivity(0, test_sensitivity));
uint8_t sensitivity;
TEST_ESP_OK(relay_chn_nvs_get_tilt_sensitivity(0, &sensitivity));
TEST_ASSERT_EQUAL(test_sensitivity, sensitivity);
TEST_ESP_OK(relay_chn_nvs_deinit());
}
TEST_CASE("Test tilt counter operations", "[relay_chn][nvs][tilt]")
{
TEST_ESP_OK(relay_chn_nvs_init());
const uint16_t tilt_count = 100;
// Test setting counters
TEST_ESP_OK(relay_chn_nvs_set_tilt_count(0, tilt_count));
uint16_t tilt_count_read;
TEST_ESP_OK(relay_chn_nvs_get_tilt_count(0, &tilt_count_read));
TEST_ASSERT_EQUAL(tilt_count, tilt_count_read);
TEST_ESP_OK(relay_chn_nvs_deinit());
}
TEST_CASE("Test tilting invalid parameters", "[relay_chn][nvs][tilt]")
{
TEST_ESP_OK(relay_chn_nvs_init());
// Test NULL pointers
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_nvs_get_tilt_sensitivity(0, NULL));
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_nvs_get_tilt_count(0, NULL));
TEST_ESP_OK(relay_chn_nvs_deinit());
}
#endif // RELAY_CHN_ENABLE_TILTING

View File

@@ -0,0 +1,345 @@
#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 reset tilt control
relay_chn_tilt_stop(chn_id);
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
// 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_IDLE, 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;
// 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;
// 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_IDLE -> (prepare) -> RELAY_CHN_STATE_IDLE -> (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;
// 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_IDLE, 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_IDLE -> (prepare) -> RELAY_CHN_STATE_IDLE -> (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;
// 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_IDLE, 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;
// 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;
// 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;
// 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_IDLE
TEST_CASE("Tilt to Stop transition without immediate inertia for stop", "[relay_chn][tilt][inertia]") {
uint8_t ch = 0;
// 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_IDLE, 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]")
{
// 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]")
{
// 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]")
{
// 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_IDLE, relay_chn_get_state(i));
}
}
TEST_CASE("tilt_auto with ID_ALL tilts channels based on last run direction", "[relay_chn][tilt][id_all]")
{
// 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;
// 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_set_sensitivity and get", "[relay_chn][tilt][sensitivity]") {
uint8_t ch = 0;
uint8_t val = 0;
relay_chn_tilt_set_sensitivity(ch, 0);
TEST_ESP_OK(relay_chn_tilt_get_sensitivity(ch, &val, 1));
TEST_ASSERT_EQUAL_UINT8(0, val);
relay_chn_tilt_set_sensitivity(ch, 50);
TEST_ESP_OK(relay_chn_tilt_get_sensitivity(ch, &val, 1));
TEST_ASSERT_EQUAL_UINT8(50, val);
relay_chn_tilt_set_sensitivity(ch, 100);
TEST_ESP_OK(relay_chn_tilt_get_sensitivity(ch, &val, 1));
TEST_ASSERT_EQUAL_UINT8(100, val);
// Set all channels
relay_chn_tilt_set_sensitivity(RELAY_CHN_ID_ALL, 42);
uint8_t vals[CONFIG_RELAY_CHN_COUNT] = {0};
TEST_ESP_OK(relay_chn_tilt_get_sensitivity(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;
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;
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);
}

View File

@@ -0,0 +1,243 @@
#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(int initial_cmd) {
// Ensure the channel reset tilt control
relay_chn_tilt_stop();
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
// Ensure the channel has had a 'last_run_cmd'
if (initial_cmd == RELAY_CHN_CMD_FORWARD) {
relay_chn_run_forward();
} else { // Assuming initial_cmd is RELAY_CHN_CMD_REVERSE
relay_chn_run_reverse();
}
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Allow command to process
relay_chn_stop(); // 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_IDLE, relay_chn_get_state());
}
// 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]") {
// Prepare channel by running forward first to set last_run_cmd
prepare_channel_for_tilt(RELAY_CHN_CMD_FORWARD);
// 1. Start in forward direction
relay_chn_run_forward();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state());
// 2. Issue tilt forward command
relay_chn_tilt_forward();
// 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());
// 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());
}
// 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]") {
// Prepare channel by running reverse first to set last_run_cmd
prepare_channel_for_tilt(RELAY_CHN_CMD_REVERSE);
// 1. Start in reverse direction
relay_chn_run_reverse();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state());
// 2. Issue tilt reverse command
relay_chn_tilt_reverse();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state());
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state());
}
// TEST_CASE: Test transition from FREE state to tilt forward (now with preparation)
// Scenario: RELAY_CHN_STATE_IDLE -> (prepare) -> RELAY_CHN_STATE_IDLE -> (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]") {
// Prepare channel by running forward first to set last_run_cmd
prepare_channel_for_tilt(RELAY_CHN_CMD_FORWARD);
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, relay_chn_get_state()); // Ensure we are back to FREE
// Issue tilt forward command
relay_chn_tilt_forward();
// 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());
}
// TEST_CASE: Test transition from FREE state to tilt reverse (now with preparation)
// Scenario: RELAY_CHN_STATE_IDLE -> (prepare) -> RELAY_CHN_STATE_IDLE -> (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]") {
// Prepare channel by running reverse first to set last_run_cmd
prepare_channel_for_tilt(RELAY_CHN_CMD_REVERSE);
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, relay_chn_get_state()); // Ensure we are back to FREE
// Issue tilt reverse command
relay_chn_tilt_reverse();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state());
}
// 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]") {
// Prepare channel by running forward first to set last_run_cmd, then tilt
prepare_channel_for_tilt(RELAY_CHN_CMD_FORWARD);
relay_chn_tilt_forward(); // 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());
// 2. Issue run forward command
relay_chn_run_forward();
// 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());
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state());
}
// 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]") {
// Prepare channel by running reverse first to set last_run_cmd, then tilt
prepare_channel_for_tilt(RELAY_CHN_CMD_REVERSE);
relay_chn_tilt_reverse(); // 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());
// 2. Issue run reverse command
relay_chn_run_reverse();
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE_PENDING, relay_chn_get_state());
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state());
}
// 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]") {
// Prepare channel by running forward first to set last_run_cmd, then tilt
prepare_channel_for_tilt(RELAY_CHN_CMD_FORWARD);
relay_chn_tilt_forward(); // 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());
// 2. Issue run reverse command (opposite direction)
relay_chn_run_reverse();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state());
}
// 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_IDLE
TEST_CASE("Tilt to Stop transition without immediate inertia for stop", "[relay_chn][tilt][inertia]") {
// Prepare channel by running forward first to set last_run_cmd, then tilt
prepare_channel_for_tilt(RELAY_CHN_CMD_FORWARD);
relay_chn_tilt_forward(); // 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());
// 2. Issue stop command
relay_chn_stop();
// 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_IDLE, relay_chn_get_state());
}
// Test relay_chn_tilt_auto() chooses correct tilt direction
TEST_CASE("relay_chn_tilt_auto chooses correct direction", "[relay_chn][tilt][auto]") {
// Prepare FORWARD
prepare_channel_for_tilt(RELAY_CHN_CMD_FORWARD);
relay_chn_tilt_auto();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state());
relay_chn_tilt_stop();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
// Prepare REVERSE
prepare_channel_for_tilt(RELAY_CHN_CMD_REVERSE);
relay_chn_tilt_auto();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state());
}
// Test sensitivity set/get
TEST_CASE("relay_chn_tilt_set_sensitivity and get", "[relay_chn][tilt][sensitivity]") {
relay_chn_tilt_set_sensitivity(0);
TEST_ASSERT_EQUAL_UINT8(0, relay_chn_tilt_get_sensitivity());
relay_chn_tilt_set_sensitivity(50);
TEST_ASSERT_EQUAL_UINT8(50, relay_chn_tilt_get_sensitivity());
relay_chn_tilt_set_sensitivity(100);
TEST_ASSERT_EQUAL_UINT8(100, relay_chn_tilt_get_sensitivity());
relay_chn_tilt_set_sensitivity(42);
TEST_ASSERT_EQUAL_UINT8(42, relay_chn_tilt_get_sensitivity());
}
// Test tilt counter logic: forward x3, reverse x3, extra reverse fails
TEST_CASE("tilt counter logic: forward and reverse consumption", "[relay_chn][tilt][counter]") {
prepare_channel_for_tilt(RELAY_CHN_CMD_FORWARD);
// Tilt forward 3 times
for (int i = 0; i < 3; ++i) {
relay_chn_tilt_forward();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state());
relay_chn_tilt_stop();
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();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
if (i < 3) {
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state());
relay_chn_tilt_stop();
vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms));
}
}
// Extra reverse tilt should fail (counter exhausted)
relay_chn_tilt_reverse();
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();
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]") {
prepare_channel_for_tilt(RELAY_CHN_CMD_FORWARD);
relay_chn_tilt_forward();
vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms));
TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state());
// Issue run reverse while in TILT_FORWARD
relay_chn_run_reverse();
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();
TEST_ASSERT(state == RELAY_CHN_STATE_REVERSE || state == RELAY_CHN_STATE_REVERSE_PENDING);
}

View File

@@ -0,0 +1,6 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs,data,nvs,0xa000,24K,,
phy_init,data,phy,0x10000,4K,,
factory,app,factory,0x20000,1M,,
app_data,data,nvs,,8K,,
1 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs,data,nvs,0xa000,24K,,
4 phy_init,data,phy,0x10000,4K,,
5 factory,app,factory,0x20000,1M,,
6 app_data,data,nvs,,8K,,

1415
test_apps/sdkconfig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
# 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
CONFIG_RELAY_CHN_ENABLE_NVS=y

View File

@@ -0,0 +1,15 @@
# Disable task WDT for tests
CONFIG_ESP_TASK_WDT_INIT=n
# Partition configuration
CONFIG_PARTITION_TABLE_SINGLE_APP=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/part_nvs.csv"
# 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
CONFIG_RELAY_CHN_ENABLE_NVS=y
CONFIG_RELAY_CHN_NVS_CUSTOM_PARTITION=y

View File

@@ -0,0 +1,9 @@
# 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=1
CONFIG_RELAY_CHN_ENABLE_TILTING=y
CONFIG_RELAY_CHN_ENABLE_NVS=y

View File

@@ -0,0 +1,15 @@
# Disable task WDT for tests
CONFIG_ESP_TASK_WDT_INIT=n
# Partition configuration
CONFIG_PARTITION_TABLE_SINGLE_APP=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/part_nvs.csv"
# 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=1
CONFIG_RELAY_CHN_ENABLE_TILTING=y
CONFIG_RELAY_CHN_ENABLE_NVS=y
CONFIG_RELAY_CHN_NVS_CUSTOM_PARTITION=y

1380
test_apps/sdkconfig.old Normal file

File diff suppressed because it is too large Load Diff