diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 31f1aa351..a640e3430 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -88,7 +88,7 @@ jobs: run: cd build/simulation && ninja test - name: Build NCP Simulation run: ./script/cmake-build simulation -DOT_BUILD_GTEST=OFF -DOT_MTD=OFF -DOT_RCP=OFF -DOT_APP_CLI=OFF -DOT_APP_RCP=OFF \ - -DOT_BORDER_ROUTING=ON -DOT_NCP_INFRA_IF=ON -DOT_SRP_SERVER=ON -DOT_NCP_DNSSD=ON -DOT_PLATFORM_DNSSD=ON + -DOT_BORDER_ROUTING=ON -DOT_NCP_INFRA_IF=ON -DOT_SRP_SERVER=ON -DOT_NCP_DNSSD=ON -DOT_PLATFORM_DNSSD=ON -DOT_NCP_CLI_STREAM=ON - name: Test NCP Simulation run: cd build/simulation && ninja test - name: Build POSIX diff --git a/src/lib/spinel/spinel.c b/src/lib/spinel/spinel.c index ba23b8947..8c4f5f4e2 100644 --- a/src/lib/spinel/spinel.c +++ b/src/lib/spinel/spinel.c @@ -1381,6 +1381,7 @@ const char *spinel_prop_key_to_cstr(spinel_prop_key_t prop_key) {SPINEL_PROP_STREAM_NET, "STREAM_NET"}, {SPINEL_PROP_STREAM_NET_INSECURE, "STREAM_NET_INSECURE"}, {SPINEL_PROP_STREAM_LOG, "STREAM_LOG"}, + {SPINEL_PROP_STREAM_CLI, "STREAM_CLI"}, {SPINEL_PROP_MESHCOP_COMMISSIONER_STATE, "MESHCOP_COMMISSIONER_STATE"}, {SPINEL_PROP_MESHCOP_COMMISSIONER_JOINERS, "MESHCOP_COMMISSIONER_JOINERS"}, {SPINEL_PROP_MESHCOP_COMMISSIONER_PROVISIONING_URL, "MESHCOP_COMMISSIONER_PROVISIONING_URL"}, diff --git a/src/lib/spinel/spinel.h b/src/lib/spinel/spinel.h index 355eb627f..869bc78c1 100644 --- a/src/lib/spinel/spinel.h +++ b/src/lib/spinel/spinel.h @@ -3561,6 +3561,21 @@ enum */ SPINEL_PROP_STREAM_LOG = SPINEL_PROP_STREAM__BEGIN + 4, + /// CLI Stream + /** Format: `U` - Set (Host to NCP) and Unsolicited Notifications (NCP to Host) + * + * This streaming property provides a bidirectional channel for interacting with the NCP's + * command-line interface (CLI). + * + * The host can send CLI commands to the NCP by setting this property. The NCP will then + * execute the commands. The NCP will send the output of the executed command (if any) back to + * the host via unsolicited notifications of this same property. + * + * Both the input command string (from host) and the output string (from NCP) are + * zero-terminated UTF-8 encoded strings. + */ + SPINEL_PROP_STREAM_CLI = SPINEL_PROP_STREAM__BEGIN + 5, + SPINEL_PROP_STREAM__END = 0x80, SPINEL_PROP_STREAM_EXT__BEGIN = 0x1700, diff --git a/src/ncp/CMakeLists.txt b/src/ncp/CMakeLists.txt index 2f0a1f8e8..d88fbe293 100644 --- a/src/ncp/CMakeLists.txt +++ b/src/ncp/CMakeLists.txt @@ -63,6 +63,13 @@ else() target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_NCP_DNSSD_ENABLE=0") endif() +option(OT_NCP_CLI_STREAM "enable NCP CLI stream ") +if (OT_NCP_CLI_STREAM) + target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE=1") +else() + target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE=0") +endif() + set(COMMON_NCP_SOURCES ${COMMON_SOURCES} ncp_base_ftd.cpp diff --git a/src/ncp/ftd.cmake b/src/ncp/ftd.cmake index fbf230a4f..7255815f8 100644 --- a/src/ncp/ftd.cmake +++ b/src/ncp/ftd.cmake @@ -49,6 +49,7 @@ target_link_libraries(openthread-ncp-ftd openthread-ftd PRIVATE ${OT_MBEDTLS} + openthread-cli-ftd openthread-hdlc openthread-spinel-ncp ot-config-ftd diff --git a/src/ncp/mtd.cmake b/src/ncp/mtd.cmake index 5bcf3783e..b30a5759b 100644 --- a/src/ncp/mtd.cmake +++ b/src/ncp/mtd.cmake @@ -49,6 +49,7 @@ target_link_libraries(openthread-ncp-mtd openthread-mtd PRIVATE ${OT_MBEDTLS} + openthread-cli-mtd openthread-hdlc openthread-spinel-ncp ot-config-mtd diff --git a/src/ncp/ncp_base.cpp b/src/ncp/ncp_base.cpp index e16b1db4c..7b7fc1658 100644 --- a/src/ncp/ncp_base.cpp +++ b/src/ncp/ncp_base.cpp @@ -36,6 +36,7 @@ #include #include +#include #include #include #include @@ -353,6 +354,9 @@ NcpBase::NcpBase(Instance *aInstance) IgnoreError(otSetStateChangedCallback(mInstance, &NcpBase::HandleStateChanged, this)); otIp6SetReceiveCallback(mInstance, &NcpBase::HandleDatagramFromStack, this); otIp6SetReceiveFilterEnabled(mInstance, true); +#if OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE + otCliInit(mInstance, &NcpBase::HandleCliOutput, this); +#endif #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE otNetworkTimeSyncSetCallback(mInstance, &NcpBase::HandleTimeSyncUpdate, this); #endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE diff --git a/src/ncp/ncp_base.hpp b/src/ncp/ncp_base.hpp index 8da1bfb42..7f72639fc 100644 --- a/src/ncp/ncp_base.hpp +++ b/src/ncp/ncp_base.hpp @@ -654,6 +654,11 @@ protected: void HandleDiagOutput(const char *aFormat, va_list aArguments); #endif +#if OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE + static int HandleCliOutput(void *aContext, const char *aFormat, va_list aArguments); + int HandleCliOutput(const char *aFormat, va_list aArguments); +#endif + #if OPENTHREAD_ENABLE_NCP_VENDOR_HOOK /** * Defines a vendor "command handler" hook to process vendor-specific spinel commands. diff --git a/src/ncp/ncp_base_dispatcher.cpp b/src/ncp/ncp_base_dispatcher.cpp index 1cadfb6f5..ef3a0b8f9 100644 --- a/src/ncp/ncp_base_dispatcher.cpp +++ b/src/ncp/ncp_base_dispatcher.cpp @@ -490,6 +490,9 @@ NcpBase::PropertyHandler NcpBase::FindSetPropertyHandler(spinel_prop_key_t aKey) OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_IPV6_ICMP_PING_OFFLOAD_MODE), OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_STREAM_NET), OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_STREAM_NET_INSECURE), +#if OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE + OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_STREAM_CLI), +#endif #if OPENTHREAD_CONFIG_JOINER_ENABLE OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_MESHCOP_JOINER_COMMISSIONING), #endif diff --git a/src/ncp/ncp_base_mtd.cpp b/src/ncp/ncp_base_mtd.cpp index f2ae0f1f9..728604251 100644 --- a/src/ncp/ncp_base_mtd.cpp +++ b/src/ncp/ncp_base_mtd.cpp @@ -41,6 +41,7 @@ #include #endif #include +#include #include #include #if OPENTHREAD_CONFIG_JAM_DETECTION_ENABLE @@ -68,6 +69,7 @@ #include #endif +#include "cli/cli_config.h" #include "common/code_utils.hpp" #include "common/debug.hpp" #include "common/string.hpp" @@ -2975,6 +2977,21 @@ template <> otError NcpBase::HandlePropertySet return OT_ERROR_NONE; } +#if OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE +template <> otError NcpBase::HandlePropertySet(void) +{ + otError error = OT_ERROR_NONE; + const char *string = nullptr; + + SuccessOrExit(error = mDecoder.ReadUtf8(string)); + + otCliInputLine(const_cast(string)); + +exit: + return error; +} +#endif + #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE template <> otError NcpBase::HandlePropertyGet(void) @@ -4571,6 +4588,29 @@ exit: } #endif // OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE +#if OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE +int NcpBase::HandleCliOutput(void *aContext, const char *aFormat, va_list aArguments) +{ + return static_cast(aContext)->HandleCliOutput(aFormat, aArguments); +} + +int NcpBase::HandleCliOutput(const char *aFormat, va_list aArguments) +{ + uint8_t header = SPINEL_HEADER_FLAG | SPINEL_HEADER_IID_0; + char output[OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH]; + int rval; + + VerifyOrExit((rval = vsnprintf(output, sizeof(output), aFormat, aArguments)) > 0); + + SuccessOrExit(mEncoder.BeginFrame(header, SPINEL_CMD_PROP_VALUE_IS, SPINEL_PROP_STREAM_CLI)); + SuccessOrExit(mEncoder.WriteUtf8(output)); + SuccessOrExit(mEncoder.EndFrame()); + +exit: + return rval; +} +#endif + // ---------------------------------------------------------------------------- // MARK: Pcap frame handling // ---------------------------------------------------------------------------- diff --git a/src/ncp/ncp_config.h b/src/ncp/ncp_config.h index 191858627..8fd34e07d 100644 --- a/src/ncp/ncp_config.h +++ b/src/ncp/ncp_config.h @@ -185,6 +185,19 @@ #define OPENTHREAD_CONFIG_NCP_DNSSD_ENABLE 0 #endif +/** + * @def OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE + * + * Define to 1 to enable the NCP CLI Stream feature. + * + * When enabled, the NCP will support sending and receiving CLI input/output + * data over a dedicated Spinel stream (using `SPINEL_PROP_STREAM_CLI`). This + * allows the host to interact with the OpenThread CLI running on the NCP. + */ +#ifndef OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE +#define OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE 0 +#endif + /** * @def OPENTHREAD_ENABLE_NCP_VENDOR_HOOK * diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index af89d17d7..e59a16377 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -264,6 +264,7 @@ ot_unit_test(toolchain test_toolchain_c.c) ot_unit_test(trickle_timer) ot_unit_test(url) +ot_unit_ncp_test(cli) ot_unit_ncp_test(dnssd) ot_unit_ncp_test(infra_if) ot_unit_ncp_test(srp_server) diff --git a/tests/unit/test_ncp_cli.cpp b/tests/unit/test_ncp_cli.cpp new file mode 100644 index 000000000..a2ca06d45 --- /dev/null +++ b/tests/unit/test_ncp_cli.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "test_platform.h" +#include "test_util.h" +#include "common/code_utils.hpp" +#include "lib/spinel/spinel_buffer.hpp" +#include "lib/spinel/spinel_encoder.hpp" +#include "ncp/ncp_base.hpp" + +#if OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE + +namespace ot { + +constexpr uint16_t kMaxSpinelBufferSize = 2048; + +static otError GenerateSpinelCliCommandFrame(const char *aCommand, uint8_t *aBuf, uint16_t &aLen) +{ + otError error = OT_ERROR_NONE; + uint8_t buf[kMaxSpinelBufferSize]; + Spinel::Buffer ncpBuffer(buf, kMaxSpinelBufferSize); + Spinel::Encoder encoder(ncpBuffer); + + uint8_t header = SPINEL_HEADER_FLAG | 0 /* Iid */ | 1 /* Tid */; + SuccessOrExit(error = encoder.BeginFrame(header, SPINEL_CMD_PROP_VALUE_SET, SPINEL_PROP_STREAM_CLI)); + SuccessOrExit(error = encoder.WriteUtf8(aCommand)); + SuccessOrExit(error = encoder.EndFrame()); + + SuccessOrExit(ncpBuffer.OutFrameBegin()); + aLen = ncpBuffer.OutFrameGetLength(); + VerifyOrExit(ncpBuffer.OutFrameRead(aLen, aBuf) == aLen, error = OT_ERROR_FAILED); + +exit: + return error; +} + +void TestNcpCliCommand(void) +{ + Instance *instance = static_cast(testInitInstance()); + Ncp::NcpBase ncpBase(instance); + + uint8_t recvBuf[kMaxSpinelBufferSize]; + uint16_t recvLen; + + { + // Test IPv6 interface bring up + constexpr char cliCommand[] = "ifconfig up"; + + SuccessOrQuit(GenerateSpinelCliCommandFrame(cliCommand, recvBuf, recvLen)); + ncpBase.HandleReceive(recvBuf, recvLen); + VerifyOrQuit(otIp6IsEnabled(instance) == true); + } + + { + // Test Thread network name set + constexpr char cliCommand[] = "networkname Test"; + + SuccessOrQuit(GenerateSpinelCliCommandFrame(cliCommand, recvBuf, recvLen)); + ncpBase.HandleReceive(recvBuf, recvLen); + VerifyOrQuit(strcmp(otThreadGetNetworkName(instance), "Test") == 0); + } + + printf("Test Ncp Cli Command passed.\n"); +} + +} // namespace ot + +#endif // OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE + +int main(void) +{ +#if OPENTHREAD_CONFIG_NCP_CLI_STREAM_ENABLE + ot::TestNcpCliCommand(); +#endif + printf("All tests passed\n"); + return 0; +}