#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 #include #include #include "sdkconfig.h" // For accessing CONFIG_* values #include const char *TAG = "RELAY_CHN_TEST"; // 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; 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; static bool g_is_component_initialized = false; // --- Test Setup/Teardown Functions --- void setUp(void) { // Reset state before each test. Initialization is now done inside each test case // to allow for testing of initialization failures. g_is_component_initialized = false; } void tearDown(void) { // Conditionally destroy the component to avoid crashing if creation failed. if (g_is_component_initialized) { relay_chn_destroy(); } } // --- Initialization Tests --- TEST_CASE("relay_chn_create handles invalid arguments", "[relay_chn][init]") { // 1. Test with NULL gpio_map TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_create(NULL, gpio_count)); // 2. Test with incorrect gpio_count (must be RELAY_CHN_COUNT * 2) TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_create(gpio_map, gpio_count - 1)); TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_create(gpio_map, 1)); TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_create(gpio_map, 0)); // 3. Test with invalid GPIO numbers (GPIO_NUM_MAX is an invalid GPIO for output) gpio_num_t invalid_gpio_map[] = {GPIO_NUM_4, GPIO_NUM_MAX, GPIO_NUM_18, GPIO_NUM_19}; TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_create(invalid_gpio_map, gpio_count)); } // --- Basic Functionality Tests --- // TEST_CASE: Test that relay channels initialize correctly to RELAY_CHN_STATE_FREE TEST_CASE("Relay channels initialize correctly to FREE state", "[relay_chn]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; for (uint8_t i = 0; i < relay_chn_count; i++) { TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(i)); } } // TEST_CASE: Test that relays do nothing when an invlid channel id given TEST_CASE("Run forward does nothing if channel id is invalid", "[relay_chn]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; for (uint8_t i = 0; i < relay_chn_count; i++) { int invalid_id = relay_chn_count * 2 + i; relay_chn_run_forward(invalid_id); // relay_chn_run_forward returns void // Short delay for state to update vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(i)); } } // TEST_CASE: Test that relays run in the forward direction and update their state TEST_CASE("Relay channels run forward and update state", "[relay_chn]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; for (uint8_t i = 0; i < relay_chn_count; i++) { relay_chn_run_forward(i); // relay_chn_run_forward returns void // Short delay for state to update vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(i)); } } // TEST_CASE: Test that relays do nothing when an invlid channel id given TEST_CASE("Run reverse does nothing if channel id is invalid", "[relay_chn]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // Verify that no valid channels were affected for (uint8_t i = 0; i < relay_chn_count; i++) { int invalid_id = relay_chn_count * 2 + i; // Call run_reverse with an invalid ID relay_chn_run_reverse(invalid_id); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(i)); } } // TEST_CASE: Test that relays run in the reverse direction and update their state TEST_CASE("Relay channels run reverse and update state", "[relay_chn]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; for (uint8_t i = 0; i < relay_chn_count; i++) { relay_chn_run_reverse(i); // relay_chn_run_reverse returns void vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(i)); } } // ### Broadcast Command (RELAY_CHN_ID_ALL) Tests TEST_CASE("run_forward with ID_ALL sets all channels to FORWARD", "[relay_chn][all]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; relay_chn_run_forward(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); for (uint8_t i = 0; i < relay_chn_count; i++) { TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(i)); } } TEST_CASE("run_reverse with ID_ALL sets all channels to REVERSE", "[relay_chn][all]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; relay_chn_run_reverse(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); for (uint8_t i = 0; i < relay_chn_count; i++) { TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(i)); } } TEST_CASE("stop with ID_ALL stops all running channels", "[relay_chn][all]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // 1. Start all channels forward to ensure they are in a known running state relay_chn_run_forward(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // 2. Stop all channels using the broadcast command relay_chn_stop(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); // 3. Verify all channels have transitioned to the FREE state for (uint8_t i = 0; i < relay_chn_count; i++) { TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(i)); } } // TEST_CASE: Test that relays stop and transition to RELAY_CHN_STATE_FREE // This test also verifies the transition to FREE state after a STOP command. TEST_CASE("Relay channels stop and update to FREE state", "[relay_chn]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; for (uint8_t i = 0; i < relay_chn_count; i++) { // First, run forward to test stopping and transitioning to FREE state relay_chn_run_forward(i); // relay_chn_run_forward returns void vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(i)); // Now, issue the stop command relay_chn_stop(i); // relay_chn_stop returns void // Immediately after stop, state should be STOPPED vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(i)); // Then, wait for the inertia period for it to transition to RELAY_CHN_STATE_FREE vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(i)); } } // TEST_CASE: Get state should return UNDEFINED when id is not valid TEST_CASE("Get state returns UNDEFINED when id is invalid", "[relay_chn]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; for (uint8_t i = 0; i < relay_chn_count; i++) { int invalid_id = relay_chn_count * 2 + i; TEST_ASSERT_EQUAL(RELAY_CHN_STATE_UNDEFINED, relay_chn_get_state(invalid_id)); } // Test for running states also relay_chn_run_forward(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); for (uint8_t i = 0; i < relay_chn_count; i++) { int invalid_id = relay_chn_count * 2 + i; TEST_ASSERT_EQUAL(RELAY_CHN_STATE_UNDEFINED, relay_chn_get_state(invalid_id)); } } // TEST_CASE: Get state string should return "UNKNOWN" when id is not valid TEST_CASE("Get state string returns UNKNOWN when id is invalid", "[relay_chn]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; for (uint8_t i = 0; i < relay_chn_count; i++) { int invalid_id = relay_chn_count * 2 + i; TEST_ASSERT_EQUAL_STRING("UNKNOWN", relay_chn_get_state_str(invalid_id)); } // Test for running states also relay_chn_run_forward(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); for (uint8_t i = 0; i < relay_chn_count; i++) { int invalid_id = relay_chn_count * 2 + i; TEST_ASSERT_EQUAL_STRING("UNKNOWN", relay_chn_get_state_str(invalid_id)); } } // TEST_CASE: Test independent operation of multiple relay channels TEST_CASE("Multiple channels can operate independently", "[relay_chn]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; if (relay_chn_count >= 2) { // Start Channel 0 in forward direction relay_chn_run_forward(0); // relay_chn_run_forward returns void vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(0)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(1)); // Other channel should not be affected // Start Channel 1 in reverse direction relay_chn_run_reverse(1); // relay_chn_run_reverse returns void vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(0)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(1)); // Stop Channel 0 and wait for it to become FREE relay_chn_stop(0); // relay_chn_stop returns void vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(0)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(1)); // Other channel should continue running // Stop Channel 1 and wait for it to become FREE relay_chn_stop(1); // relay_chn_stop returns void vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(0)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(1)); } else { ESP_LOGW("TEST", "Skipping 'Multiple channels can operate independently' test: Not enough channels available."); } } // ### Inertia and State Transition Tests // This section specifically targets the inertia periods and complex state transitions as per the component's logic. // TEST_CASE: Test transition from forward to reverse with inertia and state checks // Scenario: RELAY_CHN_STATE_FORWARD -> (relay_chn_run_reverse) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_REVERSE TEST_CASE("Forward to Reverse transition with opposite inertia", "[relay_chn][inertia]") { uint8_t ch = 0; // Channel to test TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // 1. Start in forward direction relay_chn_run_forward(ch); // relay_chn_run_forward returns void vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Short delay for state stabilization TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch)); // 2. Issue reverse command relay_chn_run_reverse(ch); // relay_chn_run_reverse returns void // Immediately after the command, the motor should be stopped vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE_PENDING, relay_chn_get_state(ch)); // Wait for the inertia period (after which the reverse command will be dispatched) vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(ch)); // Should now be in reverse state } // TEST_CASE: Test transition from reverse to forward with inertia and state checks // Scenario: RELAY_CHN_STATE_REVERSE -> (relay_chn_run_forward) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_FORWARD TEST_CASE("Reverse to Forward transition with opposite inertia", "[relay_chn][inertia]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // 1. Start in reverse direction relay_chn_run_reverse(ch); // relay_chn_run_reverse returns void vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(ch)); // 2. Issue forward command relay_chn_run_forward(ch); // relay_chn_run_forward returns void vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD_PENDING, relay_chn_get_state(ch)); // Wait for inertia vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch)); } // TEST_CASE: Test issuing the same run command while already running (no inertia expected) // Scenario: RELAY_CHN_STATE_FORWARD -> (relay_chn_run_forward) -> RELAY_CHN_STATE_FORWARD TEST_CASE("Running in same direction does not incur inertia", "[relay_chn][inertia]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // 1. Start in forward direction relay_chn_run_forward(ch); // relay_chn_run_forward returns void vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch)); // 2. Issue the same forward command again relay_chn_run_forward(ch); // relay_chn_run_forward returns void // As per the code, is_direction_opposite_to_current_motion should return false, so no inertia. // Just a short delay to check state remains the same. vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch)); } // TEST_CASE: Test transition from FREE state to running (no inertia expected) // Scenario: RELAY_CHN_STATE_FREE -> (relay_chn_run_forward) -> RELAY_CHN_STATE_FORWARD TEST_CASE("FREE to Running transition without inertia", "[relay_chn][inertia]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // setUp() should have already brought the channel to FREE state TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(ch)); // Start in forward direction relay_chn_run_forward(ch); // relay_chn_run_forward returns void // No inertia is expected when starting from FREE state. vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch)); } // --- Listener Test Globals --- typedef struct { uint8_t chn_id; relay_chn_state_t old_state; relay_chn_state_t new_state; int call_count; } listener_callback_info_t; static listener_callback_info_t listener1_info; static listener_callback_info_t listener2_info; // --- Listener Test Helper Functions --- // Clear the memory from possible garbage values static void reset_listener_info(listener_callback_info_t* info) { memset(info, 0, sizeof(listener_callback_info_t)); } static void test_listener_1(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) { listener1_info.chn_id = chn_id; listener1_info.old_state = old_state; listener1_info.new_state = new_state; listener1_info.call_count++; } static void test_listener_2(uint8_t chn_id, relay_chn_state_t old_state, relay_chn_state_t new_state) { listener2_info.chn_id = chn_id; listener2_info.old_state = old_state; listener2_info.new_state = new_state; listener2_info.call_count++; } // ### Listener Functionality Tests TEST_CASE("Listener is called on state change", "[relay_chn][listener]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; reset_listener_info(&listener1_info); // 1. Register the listener TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); // 2. Trigger a state change relay_chn_run_forward(ch); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Allow event to be processed // 3. Verify the listener was called with correct parameters TEST_ASSERT_EQUAL(1, listener1_info.call_count); TEST_ASSERT_EQUAL(ch, listener1_info.chn_id); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, listener1_info.old_state); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener1_info.new_state); // 4. Unregister to clean up relay_chn_unregister_listener(test_listener_1); } TEST_CASE("Unregistered listener is not called", "[relay_chn][listener]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; reset_listener_info(&listener1_info); // 1. Register and then immediately unregister the listener TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); relay_chn_unregister_listener(test_listener_1); // 2. Trigger a state change relay_chn_run_forward(ch); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // 3. Verify the listener was NOT called TEST_ASSERT_EQUAL(0, listener1_info.call_count); } TEST_CASE("Multiple listeners are called on state change", "[relay_chn][listener]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; reset_listener_info(&listener1_info); reset_listener_info(&listener2_info); // 1. Register two different listeners TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); TEST_ESP_OK(relay_chn_register_listener(test_listener_2)); // 2. Trigger a state change relay_chn_run_forward(ch); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // 3. Verify listener 1 was called correctly TEST_ASSERT_EQUAL(1, listener1_info.call_count); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, listener1_info.old_state); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener1_info.new_state); // 4. Verify listener 2 was also called correctly TEST_ASSERT_EQUAL(1, listener2_info.call_count); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, listener2_info.old_state); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener2_info.new_state); // 5. Clean up relay_chn_unregister_listener(test_listener_1); relay_chn_unregister_listener(test_listener_2); } TEST_CASE("Listener registration handles invalid arguments and duplicates", "[relay_chn][listener]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; reset_listener_info(&listener1_info); // 1. Registering a NULL listener should fail TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_register_listener(NULL)); // 2. Unregistering a NULL listener should not crash relay_chn_unregister_listener(NULL); // 3. Registering the same listener twice should be handled gracefully TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); // Second call should be a no-op // 4. Trigger a state change and verify the listener is only called ONCE relay_chn_run_forward(0); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(1, listener1_info.call_count); // 5. Clean up relay_chn_unregister_listener(test_listener_1); } // ### 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: Test transition from running forward to tilt forward // Scenario: RELAY_CHN_STATE_FORWARD -> (relay_chn_tilt_forward) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_TILT_FORWARD TEST_CASE("Run Forward to Tilt Forward transition with inertia", "[relay_chn][tilt][inertia]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // Prepare channel by running forward first to set last_run_cmd prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD); // 1. Start in forward direction relay_chn_run_forward(ch); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch)); // 2. Issue tilt forward command relay_chn_tilt_forward(ch); // After tilt command, it should immediately stop and then trigger inertia. vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(ch)); // Wait for the inertia period (after which the tilt command will be dispatched) vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch)); } // TEST_CASE: Test transition from running reverse to tilt reverse // Scenario: RELAY_CHN_STATE_REVERSE -> (relay_chn_tilt_reverse) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_TILT_REVERSE TEST_CASE("Run Reverse to Tilt Reverse transition with inertia", "[relay_chn][tilt][inertia]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // Prepare channel by running reverse first to set last_run_cmd prepare_channel_for_tilt(ch, RELAY_CHN_CMD_REVERSE); // 1. Start in reverse direction relay_chn_run_reverse(ch); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(ch)); // 2. Issue tilt reverse command relay_chn_tilt_reverse(ch); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(ch)); vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state(ch)); } // TEST_CASE: Test transition from FREE state to tilt forward (now with preparation) // Scenario: RELAY_CHN_STATE_FREE -> (prepare) -> RELAY_CHN_STATE_FREE -> (relay_chn_tilt_forward) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_TILT_FORWARD TEST_CASE("FREE to Tilt Forward transition with inertia (prepared)", "[relay_chn][tilt][inertia]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // Prepare channel by running forward first to set last_run_cmd prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(ch)); // Ensure we are back to FREE // Issue tilt forward command relay_chn_tilt_forward(ch); // From FREE state, tilt command should still incur the inertia due to the internal timer logic vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch)); } // TEST_CASE: Test transition from FREE state to tilt reverse (now with preparation) // Scenario: RELAY_CHN_STATE_FREE -> (prepare) -> RELAY_CHN_STATE_FREE -> (relay_chn_tilt_reverse) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_TILT_REVERSE TEST_CASE("FREE to Tilt Reverse transition with inertia (prepared)", "[relay_chn][tilt][inertia]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // Prepare channel by running reverse first to set last_run_cmd prepare_channel_for_tilt(ch, RELAY_CHN_CMD_REVERSE); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(ch)); // Ensure we are back to FREE // Issue tilt reverse command relay_chn_tilt_reverse(ch); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state(ch)); } // TEST_CASE: Test transition from tilt forward to run forward (inertia expected for run) // Scenario: RELAY_CHN_STATE_TILT_FORWARD -> (relay_chn_run_forward) -> RELAY_CHN_STATE_FORWARD TEST_CASE("Tilt Forward to Run Forward transition with inertia", "[relay_chn][tilt][inertia]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // Prepare channel by running forward first to set last_run_cmd, then tilt prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD); relay_chn_tilt_forward(ch); // Go to tilt state vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch)); // 2. Issue run forward command relay_chn_run_forward(ch); // From Tilt to Run in the same logical name but in the opposite direction, inertia is expected. TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD_PENDING, relay_chn_get_state(ch)); vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch)); } // TEST_CASE: Test transition from tilt reverse to run reverse (no inertia expected for run) // Scenario: RELAY_CHN_STATE_TILT_REVERSE -> (relay_chn_run_reverse) -> RELAY_CHN_STATE_REVERSE TEST_CASE("Tilt Reverse to Run Reverse transition with inertia", "[relay_chn][tilt][inertia]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // Prepare channel by running reverse first to set last_run_cmd, then tilt prepare_channel_for_tilt(ch, RELAY_CHN_CMD_REVERSE); relay_chn_tilt_reverse(ch); // Go to tilt state vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state(ch)); // 2. Issue run reverse command relay_chn_run_reverse(ch); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE_PENDING, relay_chn_get_state(ch)); vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(ch)); } // TEST_CASE: Test transition from tilt forward to run reverse (without inertia) // Scenario: RELAY_CHN_STATE_TILT_FORWARD -> (relay_chn_run_reverse) -> RELAY_CHN_STATE_REVERSE TEST_CASE("Tilt Forward to Run Reverse transition without inertia", "[relay_chn][tilt][inertia]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // Prepare channel by running forward first to set last_run_cmd, then tilt prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD); relay_chn_tilt_forward(ch); // Go to tilt state vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch)); // 2. Issue run reverse command (opposite direction) relay_chn_run_reverse(ch); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_REVERSE, relay_chn_get_state(ch)); } // TEST_CASE: Test stopping from a tilt state (no inertia for stop command itself) // Scenario: RELAY_CHN_STATE_TILT_FORWARD -> (relay_chn_stop) -> RELAY_CHN_STATE_STOPPED -> (inertia) -> RELAY_CHN_STATE_FREE TEST_CASE("Tilt to Stop transition without immediate inertia for stop", "[relay_chn][tilt][inertia]") { uint8_t ch = 0; TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // Prepare channel by running forward first to set last_run_cmd, then tilt prepare_channel_for_tilt(ch, RELAY_CHN_CMD_FORWARD); relay_chn_tilt_forward(ch); // Go to tilt state vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(ch)); // 2. Issue stop command relay_chn_stop(ch); // Stop command should apply immediately, setting state to FREE since last state was tilt. vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(ch)); } // ### Tilt Broadcast Command (RELAY_CHN_ID_ALL) Tests TEST_CASE("tilt_forward with ID_ALL sets all channels to TILT_FORWARD", "[relay_chn][tilt][all]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // 1. Prepare all channels. for (uint8_t i = 0; i < relay_chn_count; i++) { prepare_channel_for_tilt(i, RELAY_CHN_CMD_FORWARD); } // 2. Issue tilt forward to all channels relay_chn_tilt_forward(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Tilt from FREE doesn't have stop-inertia // 3. Verify all channels are tilting forward for (uint8_t i = 0; i < relay_chn_count; i++) { TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(i)); } } TEST_CASE("tilt_reverse with ID_ALL sets all channels to TILT_REVERSE", "[relay_chn][tilt][all]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // 1. Prepare all channels. for (uint8_t i = 0; i < relay_chn_count; i++) { prepare_channel_for_tilt(i, RELAY_CHN_CMD_REVERSE); } // 2. Issue tilt reverse to all channels relay_chn_tilt_reverse(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // 3. Verify all channels are tilting reverse for (uint8_t i = 0; i < relay_chn_count; i++) { TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state(i)); } } TEST_CASE("tilt_stop with ID_ALL stops all tilting channels", "[relay_chn][tilt][all]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // 1. Prepare and start all channels tilting forward for (uint8_t i = 0; i < relay_chn_count; i++) { prepare_channel_for_tilt(i, RELAY_CHN_CMD_REVERSE); } relay_chn_tilt_forward(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // 2. Stop tilting on all channels relay_chn_tilt_stop(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); // 3. Verify all channels are free for (uint8_t i = 0; i < relay_chn_count; i++) { TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(i)); } } TEST_CASE("tilt_auto with ID_ALL tilts channels based on last run direction", "[relay_chn][tilt][all]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // This test requires at least 2 channels to demonstrate different behaviors TEST_ASSERT_GREATER_OR_EQUAL_MESSAGE(2, relay_chn_count, "Test requires at least 2 channels"); // 1. Prepare channel 0 with last run FORWARD and channel 1 with last run REVERSE prepare_channel_for_tilt(0, RELAY_CHN_CMD_FORWARD); prepare_channel_for_tilt(1, RELAY_CHN_CMD_REVERSE); // 2. Issue auto tilt command to all channels relay_chn_tilt_auto(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Tilt from FREE state is dispatched immediately // 3. Verify channel 0 tilts forward (last run was forward) and channel 1 tilts reverse (last run was reverse) TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_FORWARD, relay_chn_get_state(0)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_TILT_REVERSE, relay_chn_get_state(1)); } #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]") { TEST_ASSERT_TRUE(true); // Just to ensure at least one test passes for visibility } #endif // CONFIG_RELAY_CHN_ENABLE_TILTING // ### Direction Flipping Tests TEST_CASE("Single channel direction can be flipped", "[relay_chn][direction]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; const uint8_t ch = 0; // 1. Initial direction should be default TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_DEFAULT, relay_chn_get_direction(ch)); // 2. Flip the direction relay_chn_flip_direction(ch); vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); // Wait for flip inertia // 3. Verify direction is flipped TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_FLIPPED, relay_chn_get_direction(ch)); // 4. Flip back relay_chn_flip_direction(ch); vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); // Wait for flip inertia // 5. Verify direction is back to default TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_DEFAULT, relay_chn_get_direction(ch)); } TEST_CASE("All channels direction can be flipped simultaneously", "[relay_chn][direction][all]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; // 1. Flip all channels relay_chn_flip_direction(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); // 2. Verify all channels are flipped for (uint8_t i = 0; i < relay_chn_count; i++) { TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_FLIPPED, relay_chn_get_direction(i)); } // 3. Flip all back relay_chn_flip_direction(RELAY_CHN_ID_ALL); vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); // 4. Verify all channels are back to default for (uint8_t i = 0; i < relay_chn_count; i++) { TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_DEFAULT, relay_chn_get_direction(i)); } } TEST_CASE("Flipping a running channel stops it and flips direction", "[relay_chn][direction]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; const uint8_t ch = 0; // 1. Start channel running and verify state relay_chn_run_forward(ch); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, relay_chn_get_state(ch)); // 2. Flip the direction while running relay_chn_flip_direction(ch); vTaskDelay(pdMS_TO_TICKS(test_delay_margin_ms)); // Give time for events to process // 3. The channel should stop as part of the flip process TEST_ASSERT_EQUAL(RELAY_CHN_STATE_STOPPED, relay_chn_get_state(ch)); // 4. Wait for the flip inertia to pass, after which it should be FREE and FLIPPED vTaskDelay(pdMS_TO_TICKS(opposite_inertia_ms + test_delay_margin_ms)); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FREE, relay_chn_get_state(ch)); TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_FLIPPED, relay_chn_get_direction(ch)); } TEST_CASE("Direction flip handles invalid channel ID gracefully", "[relay_chn][direction]") { TEST_ESP_OK(relay_chn_create(gpio_map, gpio_count)); g_is_component_initialized = true; const uint8_t invalid_ch = relay_chn_count + 5; relay_chn_flip_direction(invalid_ch); // Call with an invalid ID TEST_ASSERT_EQUAL(RELAY_CHN_DIRECTION_DEFAULT, relay_chn_get_direction(invalid_ch)); } // ### `app_main` Function // --- app_main function --- void app_main(void) { // Run the Unity test runner unity_run_all_tests(); ESP_LOGI(TAG, "============================== END OF TESTS =============================="); // After tests complete, instead of restarting, the device will halt. while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); // Wait with low power consumption } }