[cli] add CLI commands for testing otPlatTcp APIs (#13204)

This commit introduces a new `plattcp` CLI command designed primarily
for testing and evaluating `otPlatTcp` platform implementations.

The new CLI provides commands to manually drive the platform TCP API.

These commands act as a thin wrapper around the `Ip6::PlatTcp`
classes, allowing to manually invoke and observe connection events,
states, and data reception from a console. The feature is guarded by
`OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE`.
This commit is contained in:
Abtin Keshavarzian
2026-06-23 10:22:30 -07:00
committed by GitHub
parent 7874555efb
commit 9b8d5b7471
9 changed files with 479 additions and 0 deletions
+2
View File
@@ -63,6 +63,8 @@ openthread_cli_sources = [
"cli_network_data.hpp",
"cli_ping.cpp",
"cli_ping.hpp",
"cli_plat_tcp.cpp",
"cli_plat_tcp.hpp",
"cli_srp_client.cpp",
"cli_srp_client.hpp",
"cli_srp_server.cpp",
+1
View File
@@ -51,6 +51,7 @@ set(COMMON_SOURCES
cli_mesh_diag.cpp
cli_network_data.cpp
cli_ping.cpp
cli_plat_tcp.cpp
cli_srp_client.cpp
cli_srp_server.cpp
cli_tcat.cpp
+10
View File
@@ -108,6 +108,9 @@ Interpreter::Interpreter(Instance *aInstance, otCliOutputCallback aCallback, voi
#if OPENTHREAD_CONFIG_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_TCP_ENABLE
, mTcp(aInstance, *this)
#endif
#if OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE
, mPlatTcp(aInstance, *this)
#endif
#if OPENTHREAD_CONFIG_COAP_API_ENABLE
, mCoap(aInstance, *this)
#endif
@@ -6935,6 +6938,10 @@ template <> otError Interpreter::Process<Cmd("tcat")>(Arg aArgs[]) { return mTca
template <> otError Interpreter::Process<Cmd("tcp")>(Arg aArgs[]) { return mTcp.Process(aArgs); }
#endif
#if OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE
template <> otError Interpreter::Process<Cmd("plattcp")>(Arg aArgs[]) { return mPlatTcp.Process(aArgs); }
#endif
template <> otError Interpreter::Process<Cmd("udp")>(Arg aArgs[]) { return mUdp.Process(aArgs); }
template <> otError Interpreter::Process<Cmd("unsecureport")>(Arg aArgs[])
@@ -8695,6 +8702,9 @@ otError Interpreter::ProcessCommand(Arg aArgs[])
CmdEntry("ping"),
#endif
CmdEntry("platform"),
#if OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE
CmdEntry("plattcp"),
#endif
CmdEntry("pollperiod"),
#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
CmdEntry("preferrouterid"),
+5
View File
@@ -73,6 +73,7 @@
#include "cli/cli_mesh_diag.hpp"
#include "cli/cli_network_data.hpp"
#include "cli/cli_ping.hpp"
#include "cli/cli_plat_tcp.hpp"
#include "cli/cli_srp_client.hpp"
#include "cli/cli_srp_server.hpp"
#include "cli/cli_tcat.hpp"
@@ -445,6 +446,10 @@ private:
TcpExample mTcp;
#endif
#if OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE
PlatTcp mPlatTcp;
#endif
#if OPENTHREAD_CONFIG_COAP_API_ENABLE
Coap mCoap;
#endif
+12
View File
@@ -97,6 +97,18 @@
#define OPENTHREAD_CONFIG_CLI_IFCONFIG_INIT_ENABLE 0
#endif
/**
* @def OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE
*
* Indicates whether the plat-tcp CLI commands are enabled.
*
* These CLI commands are intended primarily for testing and evaluation of the `otPlatTcp` API implementations.
*
*/
#ifndef OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE
#define OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE 1
#endif
/**
* @def OPENTHREAD_CONFIG_CLI_TCP_ENABLE
*
+302
View File
@@ -0,0 +1,302 @@
/*
* Copyright (c) 2026, 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.
*/
/**
* @file
* This file implements a TCP CLI tool.
*/
#include "openthread-core-config.h"
#include "cli_config.h"
#if OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE
#include "cli_plat_tcp.hpp"
#include "instance/instance.hpp"
namespace ot {
namespace Cli {
//---------------------------------------------------------------------------------------------------------------------
// PlatTcp::Listener
PlatTcp::Listener::Listener(Instance &aInstance, PlatTcp &aOwner)
: Ip6::PlatTcp::Listener(aInstance, Accept, nullptr)
, mOwner(aOwner)
{
}
Ip6::PlatTcp::Connection *PlatTcp::Listener::Accept(Ip6::PlatTcp::Listener &aListener, const SockAddr &aPeerSockAddr)
{
return static_cast<Listener &>(aListener).mOwner.Accept(aPeerSockAddr);
}
//---------------------------------------------------------------------------------------------------------------------
// PlatTcp::Connection
PlatTcp::Connection::Connection(Instance &aInstance, PlatTcp &aOwner)
: Ip6::PlatTcp::Connection(aInstance, HandleEvent, nullptr)
, mOwner(aOwner)
{
}
void PlatTcp::Connection::HandleEvent(Ip6::PlatTcp::Connection &aConnection, const Event aEvent)
{
return static_cast<Connection &>(aConnection).mOwner.HandleEvent(aEvent);
}
//---------------------------------------------------------------------------------------------------------------------
// PlatTcp
PlatTcp::PlatTcp(otInstance *aInstance, OutputImplementer &aOutputImplementer)
: Utils(aInstance, aOutputImplementer)
, mListener(AsCoreType(aInstance), *this)
, mConnection(AsCoreType(aInstance), *this)
, mIsBound(false)
{
}
PlatTcp::Connection *PlatTcp::Accept(const SockAddr &aPeerSockAddr)
{
OutputLine("PlatTcp: Accept %s", aPeerSockAddr.ToString().AsCString());
return &mConnection;
}
void PlatTcp::HandleEvent(Connection::Event aEvent)
{
OutputLine("PlatTcp:: HandleEvent %s", Connection::EventToString(aEvent));
if (aEvent == Connection::kEventDisconnected)
{
OutputLine("PlatTcp:: DisconnectReason: %s",
Connection::DisconnectReasonToString(mConnection.GetDisconnectReason()));
}
if (aEvent == Connection::kEventReceive)
{
const Message *rxMessage = mConnection.GetRxMessage();
VerifyOrExit(rxMessage != nullptr);
OutputLine("PlatTcp: Received %u bytes", rxMessage->GetLength());
for (uint16_t offset = 0; offset < rxMessage->GetLength();)
{
uint8_t buffer[32];
uint16_t readLength = rxMessage->ReadBytes(offset, buffer, sizeof(buffer));
OutputSpaces(4);
OutputBytesLine(buffer, readLength);
offset += readLength;
}
mConnection.FreeRxMessage();
}
exit:
return;
}
otError PlatTcp::GetSockAddr(Arg aArgs[], SockAddr &aSockAddr)
{
otError error = OT_ERROR_NONE;
uint16_t port;
uint32_t ifIndex;
aSockAddr.Clear();
VerifyOrExit(!aArgs[0].IsEmpty());
SuccessOrExit(error = aArgs[0].ParseAsIp6Address(aSockAddr.GetAddress()));
VerifyOrExit(!aArgs[1].IsEmpty());
SuccessOrExit(error = aArgs[1].ParseAsUint16(port));
aSockAddr.SetPort(port);
VerifyOrExit(!aArgs[2].IsEmpty());
SuccessOrExit(error = aArgs[2].ParseAsUint32(ifIndex));
aSockAddr.SetIfIndex(ifIndex);
exit:
return error;
}
template <> otError PlatTcp::Process<Cmd("listener")>(Arg aArgs[])
{
otError error = OT_ERROR_NONE;
if (aArgs[0].IsEmpty())
{
OutputEnabledDisabledStatus(mListener.GetState() == Listener::kStateEnabled);
}
else if (aArgs[0] == "enable")
{
SockAddr sockAddr;
SuccessOrExit(error = GetSockAddr(&aArgs[1], sockAddr));
OutputLine("Enabling listener on %s", sockAddr.ToString().AsCString());
error = mListener.Enable(sockAddr);
}
else if (aArgs[0] == "disable")
{
VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
mListener.Disable();
}
else
{
error = OT_ERROR_INVALID_COMMAND;
}
exit:
return error;
}
template <> otError PlatTcp::Process<Cmd("bind")>(Arg aArgs[])
{
otError error = OT_ERROR_NONE;
SockAddr newSockAddr;
VerifyOrExit(mConnection.GetState() == Connection::kStateUnused, error = OT_ERROR_INVALID_STATE);
if (aArgs[0].IsEmpty())
{
mIsBound = false;
mLocalSockAddr.Clear();
ExitNow();
}
SuccessOrExit(error = GetSockAddr(aArgs, newSockAddr));
mLocalSockAddr = newSockAddr;
mIsBound = true;
OutputLine("LocalSockAddr: %s", mLocalSockAddr.ToString().AsCString());
exit:
return error;
}
template <> otError PlatTcp::Process<Cmd("connect")>(Arg aArgs[])
{
otError error = OT_ERROR_NONE;
SockAddr peerSockAddr;
SuccessOrExit(error = GetSockAddr(aArgs, peerSockAddr));
if (mIsBound)
{
OutputLine("Binding to %s, connecting to %s", mLocalSockAddr.ToString().AsCString(),
peerSockAddr.ToString().AsCString());
error = mConnection.BindAndConnect(mLocalSockAddr, peerSockAddr);
}
else
{
OutputLine("Connecting to %s", peerSockAddr.ToString().AsCString());
error = mConnection.Connect(peerSockAddr);
}
exit:
return error;
}
template <> otError PlatTcp::Process<Cmd("send")>(Arg aArgs[])
{
otError error = OT_ERROR_NONE;
OwnedPtr<Message> message;
uint16_t length;
SuccessOrExit(error = aArgs[0].ParseAsUint16(length));
VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
message.Reset(AsCoreType(GetInstancePtr()).Get<MessagePool>().Allocate(Message::kTypeOther));
VerifyOrExit(message != nullptr, error = OT_ERROR_NO_BUFS);
for (uint16_t i = 0; i < length; i++)
{
SuccessOrExit(error = message->Append<uint8_t>(static_cast<uint8_t>(i & 0xff) ^ (i >> 8)));
}
error = mConnection.Send(message.PassOwnership());
exit:
return error;
}
template <> otError PlatTcp::Process<Cmd("close")>(Arg aArgs[])
{
otError error = OT_ERROR_NONE;
VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
mConnection.Close();
exit:
return error;
}
template <> otError PlatTcp::Process<Cmd("abort")>(Arg aArgs[])
{
otError error = OT_ERROR_NONE;
VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
mConnection.Abort();
exit:
return error;
}
otError PlatTcp::Process(Arg aArgs[])
{
#define CmdEntry(aCommandString) {aCommandString, &PlatTcp::Process<Cmd(aCommandString)>}
static constexpr Command kCommands[] = {CmdEntry("abort"), CmdEntry("bind"), CmdEntry("close"),
CmdEntry("connect"), CmdEntry("listener"), CmdEntry("send")};
static_assert(BinarySearch::IsSorted(kCommands), "kCommands is not sorted");
otError error = OT_ERROR_INVALID_COMMAND;
const Command *command;
if (aArgs[0].IsEmpty() || (aArgs[0] == "help"))
{
OutputCommandTable(kCommands);
ExitNow(error = aArgs[0].IsEmpty() ? error : OT_ERROR_NONE);
}
command = BinarySearch::Find(aArgs[0].GetCString(), kCommands);
VerifyOrExit(command != nullptr);
error = (this->*command->mHandler)(aArgs + 1);
exit:
return error;
}
} // namespace Cli
} // namespace ot
#endif // OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE
+122
View File
@@ -0,0 +1,122 @@
/*
* Copyright (c) 2026, 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.
*/
/**
* @file
* This file contains definitions for a plat-TCP CLI tool.
*/
#ifndef OT_CLI_CLI_PLAT_TCP_HPP_
#define OT_CLI_CLI_PLAT_TCP_HPP_
#include "openthread-core-config.h"
#if OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE
#include "cli/cli_config.h"
#include "cli/cli_utils.hpp"
#include "net/plat_tcp.hpp"
namespace ot {
namespace Cli {
/**
* Implements a CLI-based plat-TCP example.
*
* These CLI commands are intended primarily for testing and evaluation of the `otPlatTcp` API implementations.
*/
class PlatTcp : private Utils
{
public:
/**
* Constructor
*
* @param[in] aInstance The OpenThread Instance.
* @param[in] aOutputImplementer An `OutputImplementer`.
*/
PlatTcp(otInstance *aInstance, OutputImplementer &aOutputImplementer);
/**
* Processes a CLI sub-command.
*
* @param[in] aArgs An array of command line arguments.
*
* @retval OT_ERROR_NONE Successfully executed the CLI command.
* @retval OT_ERROR_PENDING The CLI command was successfully started but final result is pending.
* @retval OT_ERROR_INVALID_COMMAND Invalid or unknown CLI command.
* @retval OT_ERROR_INVALID_ARGS Invalid arguments.
* @retval ... Error during execution of the CLI command.
*/
otError Process(Arg aArgs[]);
private:
using Command = CommandEntry<PlatTcp>;
using SockAddr = Ip6::PlatTcp::SockAddr;
class Listener : public Ip6::PlatTcp::Listener
{
public:
Listener(Instance &aInstance, PlatTcp &aOwner);
private:
static Ip6::PlatTcp::Connection *Accept(Ip6::PlatTcp::Listener &aListener, const SockAddr &aPeerSockAddr);
PlatTcp &mOwner;
};
class Connection : public Ip6::PlatTcp::Connection
{
public:
Connection(Instance &aInstance, PlatTcp &aOwner);
private:
static void HandleEvent(Ip6::PlatTcp::Connection &aConnection, Event aEvent);
PlatTcp &mOwner;
};
template <CommandId kCommandId> otError Process(Arg aArgs[]);
otError GetSockAddr(Arg aArgs[], SockAddr &aSockAddr);
Connection *Accept(const SockAddr &aPeerSockAddr);
void HandleEvent(Connection::Event aEvent);
Listener mListener;
Connection mConnection;
SockAddr mLocalSockAddr;
bool mIsBound;
};
} // namespace Cli
} // namespace ot
#endif // OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE
#endif // OT_CLI_CLI_PLAT_TCP_HPP_
+16
View File
@@ -567,6 +567,22 @@ const char *PlatTcp::Connection::EventToString(Event aEvent)
return kStrings[aEvent];
}
const char *PlatTcp::Connection::DisconnectReasonToString(DisconnectReason aDisconnectReason)
{
#define ConnDisconnectReasonMapList(_) \
_(kDisconnectReasonClosed, "Closed") \
_(kDisconnectReasonTimeout, "Timeout") \
_(kDisconnectReasonRefused, "Refused") \
_(kDisconnectReasonReset, "Reset") \
_(kDisconnectReasonError, "Error") \
_(kDisconnectReasonAbort, "Abort") \
_(kDisconnectReasonNoBufs, "NoBufs")
DefineEnumStringArray(ConnDisconnectReasonMapList);
return kStrings[aDisconnectReason];
}
//---------------------------------------------------------------------------------------------------------------------
// PlatTcp
+9
View File
@@ -509,6 +509,15 @@ public:
*/
static const char *EventToString(Event aEvent);
/**
* Converts a `DisconnectReason` to a human-readable string.
*
* @param[in] aReason The disconnect reason.
*
* @returns A string representation of @p aReason.
*/
static const char *DisconnectReasonToString(DisconnectReason aReason);
private:
bool Matches(State aState) const { return mState == aState; }
void SetState(State aState);