diff --git a/components/openthread/CMakeLists.txt b/components/openthread/CMakeLists.txt index 1da3be68b3..a3a2f4a87c 100644 --- a/components/openthread/CMakeLists.txt +++ b/components/openthread/CMakeLists.txt @@ -196,6 +196,11 @@ if(CONFIG_OPENTHREAD_ENABLED) "src/port/esp_openthread_messagepool.c") endif() + if(NOT CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE) + list(APPEND exclude_srcs + "src/ncp/esp_openthread_ncp_console.cpp") + endif() + if(CONFIG_OPENTHREAD_FTD) set(device_type "OPENTHREAD_FTD=1") elseif(CONFIG_OPENTHREAD_MTD) diff --git a/components/openthread/Kconfig b/components/openthread/Kconfig index 9ccc77f5fe..b8fac33744 100644 --- a/components/openthread/Kconfig +++ b/components/openthread/Kconfig @@ -233,6 +233,12 @@ menu "OpenThread" default y help Select this to enable OpenThread NCP vendor commands. + + config OPENTHREAD_RCP_SPINEL_CONSOLE + bool "Enable RCP console via Spinel" + default n + help + Select this to enable sending console commands to OpenThread RCP via Spinel. endmenu config OPENTHREAD_BORDER_ROUTER diff --git a/components/openthread/include/esp_openthread_spinel.h b/components/openthread/include/esp_openthread_spinel.h index 2900095420..09f7dfe99e 100644 --- a/components/openthread/include/esp_openthread_spinel.h +++ b/components/openthread/include/esp_openthread_spinel.h @@ -76,6 +76,14 @@ esp_err_t esp_openthread_rcp_init(void); */ esp_err_t esp_openthread_rcp_version_set(const char *version_str); +/** + * @brief Sends a console command to RCP. + * + * @param[in] input The pointer to a command string to be run on RCP. + * + */ +void esp_openthread_rcp_send_command(const char *input); + #ifdef __cplusplus } #endif diff --git a/components/openthread/private_include/esp_openthread_ncp.h b/components/openthread/private_include/esp_openthread_ncp.h index 45b585cf33..2d529dd770 100644 --- a/components/openthread/private_include/esp_openthread_ncp.h +++ b/components/openthread/private_include/esp_openthread_ncp.h @@ -12,6 +12,10 @@ extern "C" { void otAppNcpInit(otInstance *aInstance); +#if CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE +esp_err_t esp_console_redirect_to_otlog(void); +#endif // CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE + #ifdef __cplusplus } #endif diff --git a/components/openthread/private_include/esp_spinel_ncp_vendor_macro.h b/components/openthread/private_include/esp_spinel_ncp_vendor_macro.h index 3bd9f4fb85..6d572f45b3 100644 --- a/components/openthread/private_include/esp_spinel_ncp_vendor_macro.h +++ b/components/openthread/private_include/esp_spinel_ncp_vendor_macro.h @@ -11,3 +11,5 @@ #define SPINEL_PROP_VENDOR_ESP_SET_PENDINGMODE (SPINEL_PROP_VENDOR_ESP__BEGIN + 2) /* Vendor command for pending mode.*/ #define SPINEL_PROP_VENDOR_ESP_COEX_EVENT (SPINEL_PROP_VENDOR_ESP__BEGIN + 3) + +#define SPINEL_PROP_VENDOR_ESP_SET_CONSOLE_CMD (SPINEL_PROP_VENDOR_ESP__BEGIN + 4) diff --git a/components/openthread/private_include/openthread-core-esp32x-radio-config.h b/components/openthread/private_include/openthread-core-esp32x-radio-config.h index 009635bac7..7e30ea2e5c 100644 --- a/components/openthread/private_include/openthread-core-esp32x-radio-config.h +++ b/components/openthread/private_include/openthread-core-esp32x-radio-config.h @@ -192,10 +192,10 @@ /** * @def OPENTHREAD_CONFIG_LOG_OUTPUT * - * The ESP-IDF platform provides an otPlatLog() function. + * Use app log for RCP to transmit logs to host via Spinel. */ #ifndef OPENTHREAD_CONFIG_LOG_OUTPUT -#define OPENTHREAD_CONFIG_LOG_OUTPUT OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED +#define OPENTHREAD_CONFIG_LOG_OUTPUT OPENTHREAD_CONFIG_LOG_OUTPUT_APP #endif /** diff --git a/components/openthread/src/ncp/esp_openthread_ncp.cpp b/components/openthread/src/ncp/esp_openthread_ncp.cpp index becdd132ac..11cb31e274 100644 --- a/components/openthread/src/ncp/esp_openthread_ncp.cpp +++ b/components/openthread/src/ncp/esp_openthread_ncp.cpp @@ -5,6 +5,7 @@ */ #include "sdkconfig.h" +#include "esp_check.h" #include "esp_ieee802154.h" #include "esp_openthread_ncp.h" #include "esp_spinel_ncp_vendor_macro.h" @@ -18,6 +19,24 @@ #include "utils/uart.h" #endif +#if CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE +#include "esp_console.h" +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "esp_openthread_common_macro.h" + +static constexpr size_t s_console_command_max_length = 256; +static constexpr size_t s_console_command_queue_length = 8; +static QueueHandle_t s_console_command_queue = nullptr; + +#define NO_LOG_TAG "" // don't use a tag to reduce log lengths from RCP + +struct ConsoleCmdMsg +{ + char cmd[s_console_command_max_length]; +}; +#endif // CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE + #if CONFIG_OPENTHREAD_RCP_UART extern "C" { static int NcpSend(const uint8_t *aBuf, uint16_t aBufLength) @@ -28,15 +47,69 @@ extern "C" { } #endif +#if CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE +static void console_command_worker_task(void *arg) +{ + ConsoleCmdMsg msg; + + for (;;) { + if (xQueueReceive(s_console_command_queue, &msg, portMAX_DELAY) != pdTRUE) { + continue; + } + + int ret = 0; + esp_err_t err = esp_console_run(msg.cmd, &ret); + + if (err == ESP_ERR_NOT_FOUND){ + ESP_LOGI(NO_LOG_TAG, "Unrecognized command: %s", msg.cmd); + } else if (err == ESP_ERR_INVALID_ARG) { + ESP_LOGI(NO_LOG_TAG, "Command is empty"); + } else if (err == ESP_OK && ret != ESP_OK) { + ESP_LOGI(NO_LOG_TAG, "Command returned non-zero error code: 0x%x (%s)", ret, esp_err_to_name(ret)); + } else if (err != ESP_OK) { + ESP_LOGI(NO_LOG_TAG, "Internal error running '%s': %s", msg.cmd, esp_err_to_name(err)); + } + } +} + +static esp_err_t init_console_command_worker() +{ + if (s_console_command_queue) { + return ESP_OK; + } + + s_console_command_queue = xQueueCreate(s_console_command_queue_length, sizeof(ConsoleCmdMsg)); + ESP_RETURN_ON_FALSE(s_console_command_queue, ESP_ERR_NO_MEM, OT_PLAT_LOG_TAG, "Failed to create s_console_command_queue"); + + BaseType_t ret = xTaskCreatePinnedToCore(console_command_worker_task, "ot_console", 3072, nullptr, 3, nullptr, tskNO_AFFINITY); + + if (ret != pdPASS) { + vQueueDelete(s_console_command_queue); + s_console_command_queue = nullptr; + return ESP_FAIL; + } + + return ESP_OK; +} +#endif // CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE + extern "C" void otAppNcpInit(otInstance *aInstance) { -#if CONFIG_OPENTHREAD_RCP_SPI - otNcpSpiInit(aInstance); -#else +#if CONFIG_OPENTHREAD_RCP_UART IgnoreError(otPlatUartEnable()); - otNcpHdlcInit(aInstance, NcpSend); +#else + otNcpSpiInit(aInstance); #endif + +#if CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE + esp_err_t err = esp_console_redirect_to_otlog(); + if (err != ESP_OK) { + ESP_LOGE(NO_LOG_TAG, "Failed to redirect console to otPlatLog: %s", esp_err_to_name(err)); + } + + init_console_command_worker(); +#endif // CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE } namespace ot { @@ -127,6 +200,30 @@ otError NcpBase::VendorSetPropertyHandler(spinel_prop_key_t aPropKey) #endif break; } +#if CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE + case SPINEL_PROP_VENDOR_ESP_SET_CONSOLE_CMD: { + const uint8_t *data = nullptr; + uint16_t len = 0; + mDecoder.ReadDataWithLen(data, len); + + if (len >= s_console_command_max_length) { + ESP_LOGW(NO_LOG_TAG, "Console command too long (%u bytes, max %zu), dropping", len, s_console_command_max_length - 1); + break; + } + + ConsoleCmdMsg msg{}; + memcpy(msg.cmd, data, len); + msg.cmd[len] = '\0'; + + // Use a separate task to run the command so that RCP does not time out + if (s_console_command_queue == nullptr) { + ESP_LOGW(NO_LOG_TAG, "Console worker not initialized, dropping cmd: %s", msg.cmd); + } else if (xQueueSend(s_console_command_queue, &msg, 0) != pdTRUE) { + ESP_LOGW(NO_LOG_TAG, "Console cmd queue full, dropping cmd: %s", msg.cmd); + } + break; + } +#endif // CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE default: error = OT_ERROR_NOT_FOUND; diff --git a/components/openthread/src/ncp/esp_openthread_ncp_console.cpp b/components/openthread/src/ncp/esp_openthread_ncp_console.cpp new file mode 100644 index 0000000000..e70c247f55 --- /dev/null +++ b/components/openthread/src/ncp/esp_openthread_ncp_console.cpp @@ -0,0 +1,170 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_check.h" +#include "esp_err.h" +#include "esp_vfs.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/stream_buffer.h" +#include "openthread/platform/logging.h" +#include "esp_openthread_common_macro.h" +#include "esp_openthread_ncp.h" + +#define OTLOG_LINE_MAX 256 +#define ROM_TAP_SB_SIZE 1024 +#define ROM_TAP_TRIGLVL 1 + +typedef struct { + char line[OTLOG_LINE_MAX]; + size_t len; +} line_buf_t; + +static SemaphoreHandle_t s_mutex; +static line_buf_t s_stdout_buf; +static line_buf_t s_stderr_buf; +static StreamBufferHandle_t s_rom_tap_sb; +static line_buf_t s_rom_line; + +static inline line_buf_t *get_buf_for_fd(int fd) +{ + if (fd == STDERR_FILENO) return &s_stderr_buf; + // default: treat as stdout + return &s_stdout_buf; +} + +// Flush one assembled line to otPlatLog (strip trailing CR/LF) +static void flush_line_to_otlog(line_buf_t *b, otLogLevel level, bool allow_empty) +{ + while (b->len > 0 && (b->line[b->len - 1] == '\n' || b->line[b->len - 1] == '\r')) { + b->len--; + } + b->line[b->len] = '\0'; + + if (b->len == 0 && allow_empty) { + otPlatLog(level, OT_LOG_REGION_NCP, "%s", " "); + return; + } + + otPlatLog(level, OT_LOG_REGION_NCP, "%s", b->line); + b->len = 0; +} + +// Accumulate bytes; emit at newline or buffer full. +static void buffer_and_maybe_flush(int fd, const char *data, size_t size) +{ + line_buf_t *b = get_buf_for_fd(fd); + otLogLevel lvl = (fd == STDERR_FILENO) ? OT_LOG_LEVEL_WARN : OT_LOG_LEVEL_NOTE; + + for (size_t i = 0; i < size; i++) { + char c = data[i]; + if (b->len < OTLOG_LINE_MAX - 1) { + b->line[b->len++] = c; + } + + if (c == '\n') { + flush_line_to_otlog(b, lvl, /*allow_empty=*/true); + } else if (b->len == OTLOG_LINE_MAX - 1) { + flush_line_to_otlog(b, lvl, /*allow_empty=*/false); + } + } +} + +static ssize_t otlog_write(int fd, const void *data, size_t size) +{ + if (!data || size == 0) return 0; + + if (s_mutex) xSemaphoreTake(s_mutex, portMAX_DELAY); + buffer_and_maybe_flush(fd, (const char *)data, size); + if (s_mutex) xSemaphoreGive(s_mutex); + + return (ssize_t)size; +} + +static ssize_t otlog_read(int fd, void *dst, size_t size) +{ + (void)fd; (void)dst; (void)size; + return 0; +} + +static int otlog_open(const char *path, int flags, int mode) +{ + (void)path; (void)flags; (void)mode; + return 0; +} + +static int otlog_close(int fd) +{ + (void)fd; + return 0; +} + +static IRAM_ATTR void rom_putc_tap(char c) +{ + BaseType_t woken = pdFALSE; + + if (!xPortCanYield()) { + xStreamBufferSendFromISR(s_rom_tap_sb, &c, 1, &woken); + if (woken) portYIELD_FROM_ISR(); + } else { + xStreamBufferSend(s_rom_tap_sb, &c, 1, 0); + } +} + +static esp_err_t esp_rom_printf_mirror_install(void) +{ + s_rom_tap_sb = xStreamBufferCreate(ROM_TAP_SB_SIZE, ROM_TAP_TRIGLVL); + ESP_RETURN_ON_FALSE(s_rom_tap_sb, ESP_ERR_NO_MEM, OT_PLAT_LOG_TAG, "Failed to create s_rom_tap_sb buffer"); + + esp_rom_install_channel_putc(1, rom_putc_tap); + return ESP_OK; +} + +static void rom_tap_consumer_task(void *arg) +{ + const otLogLevel lvl = OT_LOG_LEVEL_NOTE; + uint8_t ch; + + for (;;) { + if (xStreamBufferReceive(s_rom_tap_sb, &ch, 1, portMAX_DELAY) != 1) continue; + + if (s_rom_line.len < OTLOG_LINE_MAX - 1) { + s_rom_line.line[s_rom_line.len++] = (char)ch; + } + if (ch == '\n' || s_rom_line.len == OTLOG_LINE_MAX - 1) { + flush_line_to_otlog(&s_rom_line, lvl, /*allow_empty=*/true); + s_rom_line.len = 0; + } + } +} + +esp_err_t esp_console_redirect_to_otlog(void) +{ + if (!s_mutex) { + s_mutex = xSemaphoreCreateMutex(); + ESP_RETURN_ON_FALSE(s_mutex, ESP_ERR_NO_MEM, OT_PLAT_LOG_TAG, "Failed to create s_mutex"); + } + + esp_vfs_t vfs = {}; + vfs.flags = ESP_VFS_FLAG_DEFAULT; + vfs.write = &otlog_write; + vfs.read = &otlog_read; + vfs.open = &otlog_open; + vfs.close = &otlog_close; + + ESP_RETURN_ON_ERROR(esp_vfs_register("/dev/otlog", &vfs, NULL), OT_PLAT_LOG_TAG, "vfs register failed"); + + FILE *fout = freopen("/dev/otlog", "w", stdout); + FILE *ferr = freopen("/dev/otlog", "w", stderr); + ESP_RETURN_ON_FALSE(fout && ferr, ESP_FAIL, OT_PLAT_LOG_TAG, "freopen failed (stdout=%p, stderr=%p)", fout, ferr); + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + + ESP_RETURN_ON_ERROR(esp_rom_printf_mirror_install(), OT_PLAT_LOG_TAG, "esp_rom_printf mirror install failed"); + xTaskCreatePinnedToCore(rom_tap_consumer_task, "ot_rom_tap", 2048, NULL, 4, NULL, tskNO_AFFINITY); + + return ESP_OK; +} diff --git a/components/openthread/src/port/esp_openthread_radio_spinel.cpp b/components/openthread/src/port/esp_openthread_radio_spinel.cpp index 846544b98a..c5ff44923d 100644 --- a/components/openthread/src/port/esp_openthread_radio_spinel.cpp +++ b/components/openthread/src/port/esp_openthread_radio_spinel.cpp @@ -11,10 +11,12 @@ #include "esp_err.h" #include "esp_openthread_border_router.h" #include "esp_openthread_common_macro.h" +#include "esp_openthread_ncp.h" #include "esp_openthread_platform.h" #include "esp_openthread_types.h" #include "esp_system.h" #include "esp_spinel_interface.hpp" +#include "esp_spinel_ncp_vendor_macro.h" #include "esp_spi_spinel_interface.hpp" #include "esp_uart_spinel_interface.hpp" #include "openthread-core-config.h" @@ -210,6 +212,11 @@ void esp_openthread_handle_netif_state_change(bool state) s_radio.SetTimeSyncState(state); } +void esp_openthread_rcp_send_command(const char *input) +{ + s_radio.Set(SPINEL_PROP_VENDOR_ESP_SET_CONSOLE_CMD, SPINEL_DATATYPE_DATA_WLEN_S, input, strlen(input)); +} + void otPlatRadioGetIeeeEui64(otInstance *instance, uint8_t *ieee_eui64) { SuccessOrDie(s_radio.GetIeeeEui64(ieee_eui64)); diff --git a/examples/openthread/ot_rcp/main/esp_ot_rcp.c b/examples/openthread/ot_rcp/main/esp_ot_rcp.c index fddd776684..b3c4e6c26f 100644 --- a/examples/openthread/ot_rcp/main/esp_ot_rcp.c +++ b/examples/openthread/ot_rcp/main/esp_ot_rcp.c @@ -29,6 +29,10 @@ #error "RCP is only supported for the SoCs which have IEEE 802.15.4 module" #endif +#if CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE +#include "esp_console.h" +#endif + #define TAG "ot_esp_rcp" extern void otAppNcpInit(otInstance *instance); @@ -59,5 +63,11 @@ void app_main(void) }, }; +#if CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE + esp_console_config_t console_config = ESP_CONSOLE_CONFIG_DEFAULT(); + esp_console_init(&console_config); + esp_console_register_help_command(); +#endif + ESP_ERROR_CHECK(esp_openthread_start(&config)); } diff --git a/examples/openthread/ot_rcp/sdkconfig.defaults b/examples/openthread/ot_rcp/sdkconfig.defaults index 7a624549d1..7a6672629e 100644 --- a/examples/openthread/ot_rcp/sdkconfig.defaults +++ b/examples/openthread/ot_rcp/sdkconfig.defaults @@ -52,3 +52,9 @@ CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC=n CONFIG_OPENTHREAD_LOG_LEVEL_NONE=y CONFIG_OPENTHREAD_TIMING_OPTIMIZATION=y CONFIG_FREERTOS_HZ=1000 + +# +# Turn on RCP console by default, overriding default log level from above +# +CONFIG_OPENTHREAD_RCP_SPINEL_CONSOLE=y +CONFIG_LOG_DEFAULT_LEVEL_INFO=y