diff --git a/device/esp_tinyusb/test_apps/teardown_device/CMakeLists.txt b/device/esp_tinyusb/test_apps/teardown_device/CMakeLists.txt new file mode 100644 index 0000000..d63d14e --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/CMakeLists.txt @@ -0,0 +1,9 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +set(COMPONENTS main) + +project(test_app_teardown_device) diff --git a/device/esp_tinyusb/test_apps/teardown_device/main/CMakeLists.txt b/device/esp_tinyusb/test_apps/teardown_device/main/CMakeLists.txt new file mode 100644 index 0000000..e81e627 --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRC_DIRS . + INCLUDE_DIRS . + REQUIRES unity + WHOLE_ARCHIVE) diff --git a/device/esp_tinyusb/test_apps/teardown_device/main/idf_component.yml b/device/esp_tinyusb/test_apps/teardown_device/main/idf_component.yml new file mode 100644 index 0000000..b1cb5b5 --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/main/idf_component.yml @@ -0,0 +1,5 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/esp_tinyusb: + version: "*" + override_path: "../../../" diff --git a/device/esp_tinyusb/test_apps/teardown_device/main/test_app_main.c b/device/esp_tinyusb/test_apps/teardown_device/main/test_app_main.c new file mode 100644 index 0000000..b1321ed --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/main/test_app_main.c @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_test_utils_memory.h" + +void app_main(void) +{ + /* + _ _ _ + | | (_) | | + ___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__ + / _ \/ __| '_ \| __| | '_ \| | | | | | / __| '_ \ + | __/\__ \ |_) | |_| | | | | |_| | |_| \__ \ |_) | + \___||___/ .__/ \__|_|_| |_|\__, |\__,_|___/_.__/ + | |______ __/ | + |_|______| |___/ + _____ _____ _____ _____ + |_ _| ___/ ___|_ _| + | | | |__ \ `--. | | + | | | __| `--. \ | | + | | | |___/\__/ / | | + \_/ \____/\____/ \_/ + */ + + printf(" _ _ _ \n"); + printf(" | | (_) | | \n"); + printf(" ___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__ \n"); + printf(" / _ \\/ __| '_ \\| __| | '_ \\| | | | | | / __| '_ \\ \n"); + printf("| __/\\__ \\ |_) | |_| | | | | |_| | |_| \\__ \\ |_) |\n"); + printf(" \\___||___/ .__/ \\__|_|_| |_|\\__, |\\__,_|___/_.__/ \n"); + printf(" | |______ __/ | \n"); + printf(" |_|______| |___/ \n"); + printf(" _____ _____ _____ _____ \n"); + printf("|_ _| ___/ ___|_ _| \n"); + printf(" | | | |__ \\ `--. | | \n"); + printf(" | | | __| `--. \\ | | \n"); + printf(" | | | |___/\\__/ / | | \n"); + printf(" \\_/ \\____/\\____/ \\_/ \n"); + + unity_utils_setup_heap_record(80); + unity_utils_set_leak_level(128); + unity_run_menu(); +} + +/* setUp runs before every test */ +void setUp(void) +{ + unity_utils_record_free_mem(); +} + +/* tearDown runs after every test */ +void tearDown(void) +{ + unity_utils_evaluate_leaks(); +} diff --git a/device/esp_tinyusb/test_apps/teardown_device/main/test_teardown.c b/device/esp_tinyusb/test_apps/teardown_device/main/test_teardown.c new file mode 100644 index 0000000..fd27616 --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/main/test_teardown.c @@ -0,0 +1,213 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "soc/soc_caps.h" +#if SOC_USB_OTG_SUPPORTED + +// +#include +#include +// +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +// +#include "esp_system.h" +#include "esp_log.h" +#include "esp_err.h" +// +#include "unity.h" +#include "tinyusb.h" +#include "tusb_cdc_acm.h" + +static const char *TAG = "teardown"; + +SemaphoreHandle_t wait_mount = NULL; +SemaphoreHandle_t wait_terminal = NULL; +SemaphoreHandle_t wait_command = NULL; + +static uint8_t rx_buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE + 1]; +static uint8_t tx_buf[CONFIG_TINYUSB_CDC_TX_BUFSIZE + 1] = { 0 }; + +#define TEARDOWN_CMD_KEY 0xAA +#define TEARDOWN_RPL_KEY 0x55 +#define TEARDOWN_CMD_RPL_SIZE ((TUD_OPT_HIGH_SPEED ? 512 : 64)) +#define TEARDOWN_ATTACH_TIMEOUT_MS 2000 +#define TEARDOWN_COMMAND_TIMEOUT_MS 3000 +#define TEARDOWN_AMOUNT 4 + +static const tusb_desc_device_t cdc_device_descriptor = { + .bLength = sizeof(cdc_device_descriptor), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = USB_ESPRESSIF_VID, + .idProduct = 0x4002, + .bcdDevice = 0x0100, + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + .bNumConfigurations = 0x01 +}; + +static const uint16_t cdc_desc_config_len = TUD_CONFIG_DESC_LEN + CFG_TUD_CDC * TUD_CDC_DESC_LEN; +static const uint8_t cdc_desc_configuration[] = { + TUD_CONFIG_DESCRIPTOR(1, 2, 0, cdc_desc_config_len, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), + TUD_CDC_DESCRIPTOR(0, 4, 0x81, 8, 0x02, 0x82, (TUD_OPT_HIGH_SPEED ? 512 : 64)), +}; + +#if (TUD_OPT_HIGH_SPEED) +static const tusb_desc_device_qualifier_t device_qualifier = { + .bLength = sizeof(tusb_desc_device_qualifier_t), + .bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .bNumConfigurations = 0x01, + .bReserved = 0 +}; +#endif // TUD_OPT_HIGH_SPEED + +static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) +{ + // Something was received + xSemaphoreGive(wait_command); +} + +/** + * @brief CDC device line change callback + * + * CDC device signals, that the DTR, RTS states changed + * + * @param[in] itf CDC device index + * @param[in] event CDC event type + */ +void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) +{ + int dtr = event->line_state_changed_data.dtr; + int rts = event->line_state_changed_data.rts; + ESP_LOGD(TAG, "Line state changed on channel %d: DTR:%d, RTS:%d", itf, dtr, rts); + + // Terminal: + // dtr==1 && rts==1 - connected + // dtr==0 && rts==0 - disconnected + xSemaphoreGive(wait_terminal); +} + +// Invoked when device is mounted +void tud_mount_cb(void) +{ + xSemaphoreGive(wait_mount); +} + +/** + * @brief TinyUSB Teardown specific testcase + * + * Scenario: + * - Installs the tinyUSB driver via esp-tinyusb wrapper with 1xCDC class device + * - Awaits device configuration be the Host (TEARDOWN_ATTACH_TIMEOUT_MS) + * - Awaits the terminal connection (TEARDOWN_ATTACH_TIMEOUT_MS) + * - Expects the command sequence from the Host (TEARDOWN_COMMAND_TIMEOUT_MS) + * - Replies with the response sequence to the Host + * - Awaits terminal disconnection (TEARDOWN_ATTACH_TIMEOUT_MS) + * - Teardowns the tinyUSB driver via esp-tinyusb wrapper + * - Repeats the steps from the step.1 N times (where N = TEARDOWN_AMOUNT) + * - Verifies amount of attempts and memory leakage (attempts should be 0) + * + * command sequence[] = ep_size * 0xAA + * response sequence[] = ep_size * 0x55 + * + * Hint: Values 0xAA and 0x55 were selected to verify the buffer memory integrity, + * as the 0xAA and 0x55 are the inversion of each other and the data bits in the same position changes from 1 to 0 in every transaction. + */ +TEST_CASE("tinyusb_teardown", "[esp_tinyusb][teardown]") +{ + size_t rx_size = 0; + + wait_mount = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_EQUAL(NULL, wait_mount); + wait_command = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_EQUAL(NULL, wait_command); + wait_terminal = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_EQUAL(NULL, wait_terminal); + + // Prep reply + for (int i = 0; i < TEARDOWN_CMD_RPL_SIZE; i++) { + tx_buf[i] = TEARDOWN_RPL_KEY; + } + + // TinyUSB driver configuration + const tinyusb_config_t tusb_cfg = { + .device_descriptor = &cdc_device_descriptor, + .string_descriptor = NULL, + .string_descriptor_count = 0, + .external_phy = false, +#if (TUD_OPT_HIGH_SPEED) + .fs_configuration_descriptor = cdc_desc_configuration, + .hs_configuration_descriptor = cdc_desc_configuration, + .qualifier_descriptor = &device_qualifier, +#else + .configuration_descriptor = cdc_desc_configuration, +#endif // TUD_OPT_HIGH_SPEED + }; + + // TinyUSB ACM Driver configuration + const tinyusb_config_cdcacm_t acm_cfg = { + .usb_dev = TINYUSB_USBDEV_0, + .cdc_port = TINYUSB_CDC_ACM_0, + .rx_unread_buf_sz = 64, + .callback_rx = &tinyusb_cdc_rx_callback, + .callback_rx_wanted_char = NULL, + .callback_line_state_changed = &tinyusb_cdc_line_state_changed_callback, + .callback_line_coding_changed = NULL + }; + + int attempts = TEARDOWN_AMOUNT; + while (attempts) { + TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_install(&tusb_cfg)); + // Init CDC 0 + TEST_ASSERT_FALSE(tusb_cdc_acm_initialized(TINYUSB_CDC_ACM_0)); + TEST_ASSERT_EQUAL(ESP_OK, tusb_cdc_acm_init(&acm_cfg)); + TEST_ASSERT_TRUE(tusb_cdc_acm_initialized(TINYUSB_CDC_ACM_0)); + // Wait for the usb event + ESP_LOGD(TAG, "wait dev mounted..."); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(wait_mount, pdMS_TO_TICKS(TEARDOWN_ATTACH_TIMEOUT_MS))); + ESP_LOGD(TAG, "wait terminal connection..."); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(wait_terminal, pdMS_TO_TICKS(TEARDOWN_ATTACH_TIMEOUT_MS))); + // Wait for the command + ESP_LOGD(TAG, "wait command..."); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(wait_command, pdMS_TO_TICKS(TEARDOWN_COMMAND_TIMEOUT_MS))); + TEST_ASSERT_EQUAL(ESP_OK, tinyusb_cdcacm_read(TINYUSB_CDC_ACM_0, rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size)); + for (int i = 0; i < TEARDOWN_CMD_RPL_SIZE; i++) { + TEST_ASSERT_EQUAL(TEARDOWN_CMD_KEY, rx_buf[i]); + } + ESP_LOGD(TAG, "command received"); + // Reply the response sequence + ESP_LOGD(TAG, "send response..."); + TEST_ASSERT_EQUAL(TEARDOWN_CMD_RPL_SIZE, tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0, tx_buf, TEARDOWN_CMD_RPL_SIZE)); + TEST_ASSERT_EQUAL(ESP_OK, tinyusb_cdcacm_write_flush(TINYUSB_CDC_ACM_0, pdMS_TO_TICKS(1000))); + ESP_LOGD(TAG, "wait for terminal disconnection"); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(wait_terminal, pdMS_TO_TICKS(TEARDOWN_ATTACH_TIMEOUT_MS))); + // Teardown + attempts--; + TEST_ASSERT_EQUAL(ESP_OK, tinyusb_cdcacm_unregister_callback(TINYUSB_CDC_ACM_0, CDC_EVENT_RX)); + TEST_ASSERT_EQUAL(ESP_OK, tinyusb_cdcacm_unregister_callback(TINYUSB_CDC_ACM_0, CDC_EVENT_LINE_STATE_CHANGED)); + TEST_ASSERT_EQUAL(ESP_OK, tusb_cdc_acm_deinit(TINYUSB_CDC_ACM_0)); + TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall()); + } + // Remove primitives + vSemaphoreDelete(wait_mount); + vSemaphoreDelete(wait_command); + // All attempts should be completed + TEST_ASSERT_EQUAL(0, attempts); +} + +#endif diff --git a/device/esp_tinyusb/test_apps/teardown_device/pytest_teardown_device.py b/device/esp_tinyusb/test_apps/teardown_device/pytest_teardown_device.py new file mode 100644 index 0000000..4d28658 --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/pytest_teardown_device.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from pytest_embedded_idf.dut import IdfDut +import serial +from serial.tools.list_ports import comports +from time import sleep, time + +def get_tusb_cdc_device(vid, pid): + ports = comports() + for cdc in ports: + if cdc.vid == vid and cdc.pid == pid: + return cdc.device + return None + +def wait_for_tusb_cdc(vid, pid, timeout=30): + start_time = time() + while time() - start_time < timeout: + sleep(0.5) # Check every 0.5 seconds + tusb_cdc = get_tusb_cdc_device(vid, pid) + if tusb_cdc: + return tusb_cdc + return None + +def teardown_device(key_len, amount): + TUSB_VID = 0x303A # Espressif TinyUSB VID + TUSB_PID = 0x4002 # Espressif TinyUSB VID + + # Command to send and expected response + COMMAND = b'\xAA' * key_len + EXPECTED_RESPONSE = b'\x55' * key_len + + # Number of iterations, must be equal to ITERATIONS in the test application + ITERATIONS = amount + + for i in range(ITERATIONS): + print(f"Iteration {i+1} of {ITERATIONS}") + + # Wait for the device to appear + print("Waiting for the device to connect...") + tusb_cdc = wait_for_tusb_cdc(TUSB_VID, TUSB_PID) + if not tusb_cdc: + print("Error: Device did not appear within the timeout period.") + assert True + + try: + # Open the serial port + with serial.Serial(port=tusb_cdc, baudrate=9600, timeout=1) as cdc: + print(f"Opened port: {tusb_cdc}") + # Send the key command + res = cdc.write(COMMAND) + assert res == key_len + # Get the response + res = cdc.readline() + assert len(res) == key_len + # Explicitly close the cdc + cdc.close() + # Check if the response matches the expected response + if res == EXPECTED_RESPONSE: + print("Response matches expected value.") + else: + print(f"Sent {len(COMMAND)}: {COMMAND.hex().upper()}") + print(f"Received {len(res)}: {res.hex().upper()}") + raise Exception("Error: Response does not match expected value.") + + except serial.SerialException as e: + print(f"Error communicating with the serial port: {e}") + raise + + # Wait for the device to disconnect + print("Waiting for the device to disconnect...") + while get_tusb_cdc_device(TUSB_VID, TUSB_PID): + sleep(0.1) # Poll every 0.1 second while tinyusb cdc dev still in list + + print("Finished all iterations.") + +@pytest.mark.esp32s2 +@pytest.mark.esp32s3 +@pytest.mark.esp32p4 +@pytest.mark.usb_device +def test_usb_teardown_device(dut) -> None: + dut.expect_exact('Press ENTER to see the list of tests.') + dut.write('[teardown]') + dut.expect_exact('TinyUSB: TinyUSB Driver installed') + sleep(2) # Some time for the OS to enumerate our USB device + if dut.target == 'esp32p4': + MPS = 512 + else: + MPS = 64 + # On Linux, the serial port kept opened, while len==MPS, https://github.com/pyserial/pyserial/issues/753 + # that can be seen via Beagle: CDC OUT transaction appears only when terminal is closed. + teardown_device(MPS, 4) # Teardown tusb device + diff --git a/device/esp_tinyusb/test_apps/teardown_device/sdkconfig.defaults b/device/esp_tinyusb/test_apps/teardown_device/sdkconfig.defaults new file mode 100644 index 0000000..e437709 --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/sdkconfig.defaults @@ -0,0 +1,16 @@ +# Configure TinyUSB, it will be used to mock USB devices +CONFIG_TINYUSB_CDC_ENABLED=y +CONFIG_TINYUSB_CDC_COUNT=1 + +# Disable watchdogs, they'd get triggered during unity interactive menu +CONFIG_ESP_INT_WDT=n +CONFIG_ESP_TASK_WDT=n + +# Run-time checks of Heap and Stack +CONFIG_HEAP_POISONING_COMPREHENSIVE=y +CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y +CONFIG_COMPILER_STACK_CHECK=y + +CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL=y + +CONFIG_COMPILER_CXX_EXCEPTIONS=y