mirror of
https://github.com/espressif/openthread.git
synced 2026-06-05 21:14:49 +00:00
[tcat] add TCAT_ENABLE.req TMF command (#12013)
This adds support for the TMF command to enable TCAT remotely. A test is added that uses the 'UDP send' mechanism to send the new TMF command to a target node. Some fixes/additions to the test framework are made to support the new test, including a new argument for udp_send() to send a specific byte array and udp_rx() to receive data by a UDP client on a node.
This commit is contained in:
@@ -286,6 +286,7 @@ typedef enum otMeshcopTlvType
|
||||
OT_MESHCOP_TLV_JOINER_IID = 19, ///< meshcop Joiner IID TLV
|
||||
OT_MESHCOP_TLV_JOINER_RLOC = 20, ///< meshcop Joiner Router Locator TLV
|
||||
OT_MESHCOP_TLV_JOINER_ROUTER_KEK = 21, ///< meshcop Joiner Router KEK TLV
|
||||
OT_MESHCOP_TLV_DURATION = 23, ///< meshcop Duration TLV
|
||||
OT_MESHCOP_TLV_PROVISIONING_URL = 32, ///< meshcop Provisioning URL TLV
|
||||
OT_MESHCOP_TLV_VENDOR_NAME_TLV = 33, ///< meshcop Vendor Name TLV
|
||||
OT_MESHCOP_TLV_VENDOR_MODEL_TLV = 34, ///< meshcop Vendor Model TLV
|
||||
|
||||
@@ -52,7 +52,7 @@ extern "C" {
|
||||
*
|
||||
* @note This number versions both OpenThread platform and user APIs.
|
||||
*/
|
||||
#define OPENTHREAD_API_VERSION (546)
|
||||
#define OPENTHREAD_API_VERSION (547)
|
||||
|
||||
/**
|
||||
* @addtogroup api-instance
|
||||
|
||||
@@ -73,6 +73,7 @@ extern "C" {
|
||||
#define OT_TCAT_OPCODE 0x2 ///< TCAT Advertisement Operation Code.
|
||||
#define OT_TCAT_MAX_ADVERTISED_DEVICEID_SIZE 5 ///< TCAT max size of any type of advertised Device ID.
|
||||
#define OT_TCAT_MAX_DEVICEID_SIZE 64 ///< TCAT max size of device ID.
|
||||
#define OT_TCAT_ENABLE_MAX 600 ///< TCAT_ENABLE_MAX, default max TMF TCAT enable time, in seconds.
|
||||
|
||||
/**
|
||||
* Represents TCAT status code.
|
||||
|
||||
@@ -94,6 +94,7 @@ public:
|
||||
kJoinerIid = OT_MESHCOP_TLV_JOINER_IID, ///< Joiner IID TLV
|
||||
kJoinerRouterLocator = OT_MESHCOP_TLV_JOINER_RLOC, ///< Joiner Router Locator TLV
|
||||
kJoinerRouterKek = OT_MESHCOP_TLV_JOINER_ROUTER_KEK, ///< Joiner Router KEK TLV
|
||||
kDuration = OT_MESHCOP_TLV_DURATION, ///< Duration TLV
|
||||
kProvisioningUrl = OT_MESHCOP_TLV_PROVISIONING_URL, ///< Provisioning URL TLV
|
||||
kVendorName = OT_MESHCOP_TLV_VENDOR_NAME_TLV, ///< meshcop Vendor Name TLV
|
||||
kVendorModel = OT_MESHCOP_TLV_VENDOR_MODEL_TLV, ///< meshcop Vendor Model TLV
|
||||
@@ -205,6 +206,11 @@ typedef UintTlvInfo<Tlv::kJoinerRouterLocator, uint16_t> JoinerRouterLocatorTlv;
|
||||
*/
|
||||
typedef SimpleTlvInfo<Tlv::kJoinerRouterKek, Kek> JoinerRouterKekTlv;
|
||||
|
||||
/**
|
||||
* Defines Duration TLV constants and types.
|
||||
*/
|
||||
typedef UintTlvInfo<Tlv::kDuration, uint16_t> DurationTlv;
|
||||
|
||||
/**
|
||||
* Defines Count TLV constants and types.
|
||||
*/
|
||||
|
||||
@@ -60,6 +60,7 @@ TcatAgent::TcatAgent(Instance &aInstance)
|
||||
, mVendorInfo(nullptr)
|
||||
, mState(kStateDisabled)
|
||||
, mNextState(kStateDisabled)
|
||||
, mTimerSetsToActive(false)
|
||||
, mActiveOrStandbyTimer(aInstance)
|
||||
, mTcatActiveDurationMs(0)
|
||||
{
|
||||
@@ -95,7 +96,6 @@ Error TcatAgent::Start(AppDataReceiveCallback aAppDataReceiveCallback, JoinCallb
|
||||
mState = kStateActive;
|
||||
mNextState = kStateActive;
|
||||
mTcatActiveDurationMs = 0;
|
||||
mActiveOrStandbyTimer.Stop();
|
||||
LogInfo("Start");
|
||||
|
||||
exit:
|
||||
@@ -109,6 +109,7 @@ void TcatAgent::Stop(void)
|
||||
mAppDataReceiveCallback.Clear();
|
||||
mJoinCallback.Clear();
|
||||
mState = kStateDisabled;
|
||||
mActiveOrStandbyTimer.Stop();
|
||||
ClearCommissionerState();
|
||||
LogInfo("Stop");
|
||||
}
|
||||
@@ -122,7 +123,7 @@ Error TcatAgent::Standby(void)
|
||||
mTcatActiveDurationMs = 0;
|
||||
mActiveOrStandbyTimer.Stop();
|
||||
mNextState = kStateStandby;
|
||||
if (!IsConnected())
|
||||
if (!IsConnected() && mState != kStateStandby)
|
||||
{
|
||||
// if already TLS-connected, only move to 'standby' once the connection is done.
|
||||
// if not yet fully connected, go to 'standby' immediately (ignoring a TLS handshake that may be ongoing)
|
||||
@@ -141,14 +142,17 @@ Error TcatAgent::Activate(const uint32_t aDelayMs, const uint32_t aDurationMs)
|
||||
Error error = kErrorNone;
|
||||
|
||||
VerifyOrExit(IsStarted(), error = kErrorInvalidState);
|
||||
VerifyOrExit(mState != kStateActive);
|
||||
|
||||
mTcatActiveDurationMs = aDurationMs;
|
||||
mTimerSetsToActive = true;
|
||||
if (aDelayMs > 0)
|
||||
{
|
||||
mActiveOrStandbyTimer.Start(aDelayMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
mActiveOrStandbyTimer.Stop();
|
||||
HandleTimer();
|
||||
}
|
||||
|
||||
@@ -219,8 +223,7 @@ Error TcatAgent::Connected(MeshCoP::Tls::Extension &aTls)
|
||||
}
|
||||
|
||||
// A temporary enablement stops after disconnect: to standby.
|
||||
// For others, return to prior state, upon disconnect.
|
||||
mNextState = (mState == kStateActiveTemporary) ? kStateStandby : mState;
|
||||
mNextState = (mState == kStateActiveTemporary) ? kStateStandby : kStateActive;
|
||||
mState = kStateConnected;
|
||||
NotifyStateChange();
|
||||
LogInfo("Connected");
|
||||
@@ -237,8 +240,8 @@ void TcatAgent::Disconnected(void)
|
||||
if (mState != kStateDisabled)
|
||||
{
|
||||
mState = mNextState;
|
||||
NotifyStateChange();
|
||||
LogInfo("Disconnected");
|
||||
NotifyStateChange();
|
||||
ClearCommissionerState();
|
||||
}
|
||||
}
|
||||
@@ -1071,28 +1074,30 @@ void TcatAgent::HandleTimer(void)
|
||||
{
|
||||
case kStateStandby:
|
||||
case kStateStandbyTemporary:
|
||||
if (mTcatActiveDurationMs > 0)
|
||||
case kStateActiveTemporary:
|
||||
if (mTimerSetsToActive)
|
||||
{
|
||||
mActiveOrStandbyTimer.Start(mTcatActiveDurationMs);
|
||||
mState = kStateActiveTemporary;
|
||||
mTimerSetsToActive = false;
|
||||
if (mTcatActiveDurationMs > 0)
|
||||
{
|
||||
mState = kStateActiveTemporary;
|
||||
mActiveOrStandbyTimer.Start(mTcatActiveDurationMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
mState = kStateActive;
|
||||
}
|
||||
NotifyStateChange();
|
||||
LogInfo("Active");
|
||||
}
|
||||
else
|
||||
{
|
||||
mState = kStateActive;
|
||||
IgnoreError(Standby());
|
||||
}
|
||||
NotifyStateChange();
|
||||
LogInfo("Active");
|
||||
break;
|
||||
|
||||
case kStateActiveTemporary:
|
||||
IgnoreError(Standby());
|
||||
break;
|
||||
|
||||
case kStateConnected:
|
||||
mNextState = (mTcatActiveDurationMs > 0) ? kStateStandby : kStateActive;
|
||||
break;
|
||||
|
||||
// kStateActive: will not go to standby, based on timer. Application has forced it to 'active'.
|
||||
// kStateConnected: no change here, mNextState already set.
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -1105,6 +1110,80 @@ void TcatAgent::NotifyStateChange(void)
|
||||
mState == kStateConnected);
|
||||
}
|
||||
|
||||
template <> void TcatAgent::HandleTmf<kUriTcatEnable>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
|
||||
{
|
||||
Error error = kErrorNone;
|
||||
Coap::Message *message = nullptr;
|
||||
uint32_t delayTimerMs = 0;
|
||||
uint16_t durationSec = 0;
|
||||
uint32_t durationMs;
|
||||
|
||||
VerifyOrExit(aMessage.IsConfirmablePostRequest());
|
||||
LogInfo("Received %s from %s", UriToString<kUriTcatEnable>(), aMessageInfo.GetPeerAddr().ToString().AsCString());
|
||||
message = Get<Tmf::Agent>().NewResponseMessage(aMessage);
|
||||
VerifyOrExit(message != nullptr, error = kErrorNoBufs);
|
||||
|
||||
SuccessOrExit(error = Tlv::Find<DelayTimerTlv>(aMessage, delayTimerMs));
|
||||
switch (Tlv::Find<DurationTlv>(aMessage, durationSec))
|
||||
{
|
||||
case kErrorNone:
|
||||
break;
|
||||
case kErrorNotFound:
|
||||
durationSec = kTcatTmfEnableDefaultSec; // If Duration TLV absent: use default duration
|
||||
break;
|
||||
default:
|
||||
ExitNow(error = kErrorParse);
|
||||
}
|
||||
durationMs = Time::SecToMsec(durationSec);
|
||||
|
||||
// if an existing activation is ongoing, adapt the requested one to be compatible.
|
||||
AdaptToExistingActivePeriod(delayTimerMs, durationMs);
|
||||
|
||||
error = Activate(delayTimerMs, durationMs);
|
||||
|
||||
exit:
|
||||
if (message != nullptr)
|
||||
{
|
||||
error =
|
||||
Tlv::Append<StateTlv>(*message, error == kErrorNone ? StateTlv::State::kAccept : StateTlv::State::kReject);
|
||||
if (error == kErrorNone)
|
||||
{
|
||||
error = Get<Tmf::Agent>().SendMessage(*message, aMessageInfo);
|
||||
}
|
||||
FreeMessageOnError(message, error);
|
||||
}
|
||||
LogWarnOnError(error, "send TCAT_ENABLE.rsp");
|
||||
}
|
||||
|
||||
// Adapts delay/duration parameters of the given TCAT temporary activation period to the
|
||||
// already-ongoing temporary TCAT activation (if any). The goals of the adaptation are:
|
||||
// - not shorten the duration of an existing (scheduled/ongoing) activation
|
||||
// - not set the start of activation later than the existing scheduled activation time
|
||||
void TcatAgent::AdaptToExistingActivePeriod(uint32_t &aPeriodDelayMs, uint32_t &aPeriodDurationMs)
|
||||
{
|
||||
VerifyOrExit(mState != kStateActive);
|
||||
|
||||
if (mActiveOrStandbyTimer.IsRunning())
|
||||
{
|
||||
TimeMilli now = TimerMilli::GetNow();
|
||||
uint32_t remainingMs;
|
||||
remainingMs = (mActiveOrStandbyTimer.GetFireTime() > now) ? mActiveOrStandbyTimer.GetFireTime() - now : 0;
|
||||
if (mTimerSetsToActive)
|
||||
{
|
||||
aPeriodDelayMs = Min(aPeriodDelayMs, remainingMs);
|
||||
aPeriodDurationMs = Max(aPeriodDurationMs, remainingMs + mTcatActiveDurationMs - aPeriodDelayMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
aPeriodDelayMs = 0;
|
||||
aPeriodDurationMs = Max(aPeriodDurationMs, remainingMs);
|
||||
}
|
||||
}
|
||||
|
||||
exit:
|
||||
return;
|
||||
}
|
||||
|
||||
void SerializeTcatAdvertisementTlv(uint8_t *aBuffer,
|
||||
uint16_t &aOffset,
|
||||
TcatAdvertisementTlvType aType,
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
#include "meshcop/meshcop.hpp"
|
||||
#include "meshcop/meshcop_tlvs.hpp"
|
||||
#include "meshcop/secure_transport.hpp"
|
||||
#include "thread/tmf.hpp"
|
||||
|
||||
namespace ot {
|
||||
|
||||
@@ -330,7 +331,7 @@ public:
|
||||
* Activate TCAT functions of the TCAT agent.
|
||||
*
|
||||
* This requires the TCAT agent to be already started.
|
||||
* The state transitions to kStateActive of kStateActiveTemporary. In these states, TCAT Advertisements
|
||||
* The state transitions to kStateActive or kStateActiveTemporary. In these states, TCAT Advertisements
|
||||
* are actively sent and TCAT Commissioners are able to connect. From here, TCAT can be set to standby
|
||||
* again using Standby().
|
||||
* If a connection is ongoing and aDurationMs==0, this call will ensure that kStateActive will
|
||||
@@ -338,7 +339,7 @@ public:
|
||||
* This function will override any ongoing temporary activation of TCAT, or any
|
||||
* previously scheduled activation for a future time.
|
||||
*
|
||||
* @param[in] aDelayMs Delay in ms before activating. If 0, activate immediately.
|
||||
* @param[in] aDelayMs Delay in ms before activating. If 0, activate immediately.
|
||||
* @param[in] aDurationMs Duration in ms of the activation. If 0, activate indefinitely.
|
||||
*
|
||||
* @retval kErrorNone Successfully set the TCAT agent to kStateActive now, OR scheduled
|
||||
@@ -413,6 +414,8 @@ public:
|
||||
*/
|
||||
bool GetApplicationResponsePending(void) const { return mApplicationResponsePending; }
|
||||
|
||||
template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
|
||||
|
||||
private:
|
||||
void NotifyApplicationResponseSent(void) { mApplicationResponsePending = false; }
|
||||
void NotifyStateChange(void);
|
||||
@@ -456,7 +459,7 @@ private:
|
||||
TcatApplicationProtocol aApplicationProtocol,
|
||||
bool &aResponse);
|
||||
void HandleTimer(void);
|
||||
|
||||
void AdaptToExistingActivePeriod(uint32_t &aPeriodDelayMs, uint32_t &aPeriodDurationMs);
|
||||
Error VerifyHash(const Message &aIncomingMessage,
|
||||
uint16_t aOffset,
|
||||
uint16_t aLength,
|
||||
@@ -478,6 +481,7 @@ private:
|
||||
static constexpr uint16_t kBufferReserve = 2048 / (Buffer::kSize - sizeof(otMessageBuffer)) + 1;
|
||||
static constexpr uint8_t kServiceNameMaxLength = OT_TCAT_SERVICE_NAME_MAX_LENGTH;
|
||||
static constexpr uint8_t kApplicationLayerMaxCount = OT_TCAT_APPLICATION_LAYER_MAX_COUNT;
|
||||
static constexpr uint16_t kTcatTmfEnableDefaultSec = OT_TCAT_ENABLE_MAX;
|
||||
|
||||
const VendorInfo *mVendorInfo;
|
||||
Callback<JoinCallback> mJoinCallback;
|
||||
@@ -488,7 +492,8 @@ private:
|
||||
NetworkName mCommissionerDomainName;
|
||||
ExtendedPanId mCommissionerExtendedPanId;
|
||||
State mState;
|
||||
State mNextState;
|
||||
State mNextState; //< desired state after client disconnects
|
||||
bool mTimerSetsToActive : 1;
|
||||
bool mCommissionerHasNetworkName : 1;
|
||||
bool mCommissionerHasDomainName : 1;
|
||||
bool mCommissionerHasExtendedPanId : 1;
|
||||
@@ -503,15 +508,10 @@ private:
|
||||
uint32_t mTcatActiveDurationMs;
|
||||
};
|
||||
|
||||
} // namespace MeshCoP
|
||||
|
||||
DefineCoreType(otTcatVendorInfo, MeshCoP::TcatAgent::VendorInfo);
|
||||
|
||||
DefineMapEnum(otTcatApplicationProtocol, MeshCoP::TcatAgent::TcatApplicationProtocol);
|
||||
DefineMapEnum(otTcatAdvertisedDeviceIdType, MeshCoP::TcatAgent::TcatDeviceIdType);
|
||||
DeclareTmfHandler(TcatAgent, kUriTcatEnable);
|
||||
|
||||
// Command class TLVs
|
||||
typedef UintTlvInfo<MeshCoP::TcatAgent::kTlvResponseWithStatus, uint8_t> ResponseWithStatusTlv;
|
||||
typedef UintTlvInfo<TcatAgent::kTlvResponseWithStatus, uint8_t> ResponseWithStatusTlv;
|
||||
|
||||
/**
|
||||
* Represent TCAT Device Type and Status
|
||||
@@ -545,6 +545,13 @@ enum TcatAdvertisementTlvType : uint8_t
|
||||
kTlvVendorIanaPen = 6, ///< TCAT Vendor IANA PEN
|
||||
};
|
||||
|
||||
} // namespace MeshCoP
|
||||
|
||||
DefineCoreType(otTcatVendorInfo, MeshCoP::TcatAgent::VendorInfo);
|
||||
|
||||
DefineMapEnum(otTcatApplicationProtocol, MeshCoP::TcatAgent::TcatApplicationProtocol);
|
||||
DefineMapEnum(otTcatAdvertisedDeviceIdType, MeshCoP::TcatAgent::TcatDeviceIdType);
|
||||
|
||||
} // namespace ot
|
||||
|
||||
#endif // OPENTHREAD_CONFIG_BLE_TCAT_ENABLE
|
||||
|
||||
@@ -179,6 +179,9 @@ bool Agent::HandleResource(const char *aUriPath, Message &aMessage, const Ip6::M
|
||||
#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
|
||||
Case(kUriDuaRegistrationRequest, BackboneRouter::Manager);
|
||||
#endif
|
||||
#endif
|
||||
#if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE
|
||||
Case(kUriTcatEnable, MeshCoP::TcatAgent);
|
||||
#endif
|
||||
|
||||
default:
|
||||
|
||||
@@ -83,6 +83,7 @@ static constexpr Entry kEntries[] = {
|
||||
{"c/pq"}, // kUriPanIdQuery
|
||||
{"c/ps"}, // kUriPendingSet
|
||||
{"c/rx"}, // kUriRelayRx
|
||||
{"c/te"}, // kUriTcatEnable
|
||||
{"c/tx"}, // kUriRelayTx
|
||||
{"c/ur"}, // kUriProxyRx
|
||||
{"c/ut"}, // kUriProxyTx
|
||||
@@ -128,6 +129,7 @@ static_assert(AreConstStringsEqual(kEntries[kUriPendingGet].mPath, "c/pg"), "kEn
|
||||
static_assert(AreConstStringsEqual(kEntries[kUriPanIdQuery].mPath, "c/pq"), "kEntries is invalid");
|
||||
static_assert(AreConstStringsEqual(kEntries[kUriPendingSet].mPath, "c/ps"), "kEntries is invalid");
|
||||
static_assert(AreConstStringsEqual(kEntries[kUriRelayRx].mPath, "c/rx"), "kEntries is invalid");
|
||||
static_assert(AreConstStringsEqual(kEntries[kUriTcatEnable].mPath, "c/te"), "kEntries is invalid");
|
||||
static_assert(AreConstStringsEqual(kEntries[kUriRelayTx].mPath, "c/tx"), "kEntries is invalid");
|
||||
static_assert(AreConstStringsEqual(kEntries[kUriProxyRx].mPath, "c/ur"), "kEntries is invalid");
|
||||
static_assert(AreConstStringsEqual(kEntries[kUriProxyTx].mPath, "c/ut"), "kEntries is invalid");
|
||||
@@ -172,6 +174,7 @@ struct UriEnumCheck
|
||||
ValidateNextEnum(kUriPanIdQuery);
|
||||
ValidateNextEnum(kUriPendingSet);
|
||||
ValidateNextEnum(kUriRelayRx);
|
||||
ValidateNextEnum(kUriTcatEnable);
|
||||
ValidateNextEnum(kUriRelayTx);
|
||||
ValidateNextEnum(kUriProxyRx);
|
||||
ValidateNextEnum(kUriProxyTx);
|
||||
@@ -235,6 +238,7 @@ template <> const char *UriToString<kUriPendingGet>(void) { return "PendingGet";
|
||||
template <> const char *UriToString<kUriPanIdQuery>(void) { return "PanIdQuery"; }
|
||||
template <> const char *UriToString<kUriPendingSet>(void) { return "PendingSet"; }
|
||||
template <> const char *UriToString<kUriRelayRx>(void) { return "RelayRx"; }
|
||||
template <> const char *UriToString<kUriTcatEnable>(void) { return "TcatEnable"; }
|
||||
template <> const char *UriToString<kUriRelayTx>(void) { return "RelayTx"; }
|
||||
template <> const char *UriToString<kUriProxyRx>(void) { return "ProxyRx"; }
|
||||
template <> const char *UriToString<kUriProxyTx>(void) { return "ProxyTx"; }
|
||||
|
||||
@@ -75,6 +75,7 @@ enum Uri : uint8_t
|
||||
kUriPanIdQuery, ///< PAN ID Query ("c/pq")
|
||||
kUriPendingSet, ///< MGMT_PENDING_SET ("c/ps")
|
||||
kUriRelayRx, ///< Relay RX ("c/rx")
|
||||
kUriTcatEnable, ///< TCAT Enable ("c/te")
|
||||
kUriRelayTx, ///< Relay TX ("c/tx")
|
||||
kUriProxyRx, ///< Proxy RX ("c/ur")
|
||||
kUriProxyTx, ///< Proxy TX ("c/ut")
|
||||
@@ -146,6 +147,7 @@ template <> const char *UriToString<kUriPendingGet>(void);
|
||||
template <> const char *UriToString<kUriPanIdQuery>(void);
|
||||
template <> const char *UriToString<kUriPendingSet>(void);
|
||||
template <> const char *UriToString<kUriRelayRx>(void);
|
||||
template <> const char *UriToString<kUriTcatEnable>(void);
|
||||
template <> const char *UriToString<kUriRelayTx>(void);
|
||||
template <> const char *UriToString<kUriProxyRx>(void);
|
||||
template <> const char *UriToString<kUriProxyTx>(void);
|
||||
|
||||
@@ -951,7 +951,7 @@ class NodeImpl:
|
||||
PROMPT = 'spinel-cli > ' if self.node_type == 'ncp-sim' else '> '
|
||||
while True:
|
||||
self._expect(r"[^\n]+\n")
|
||||
line = self.pexpect.match.group(0).decode('utf8').strip()
|
||||
line = self.pexpect.match.group(0).decode('utf-8', errors='backslashreplace').strip()
|
||||
while line.startswith(PROMPT):
|
||||
line = line[len(PROMPT):]
|
||||
|
||||
@@ -3268,6 +3268,14 @@ class NodeImpl:
|
||||
payload += tlv.to_hex()
|
||||
self.commissioner_mgmtset(self.bytes_to_hex_str(payload))
|
||||
|
||||
def tcat(self, cmd):
|
||||
self.send_command(f'tcat {cmd}')
|
||||
self._expect_done()
|
||||
|
||||
def udp_start_client(self):
|
||||
self.send_command('udp open')
|
||||
self._expect_done()
|
||||
|
||||
def udp_start(self, local_ipaddr, local_port, bind_unspecified=False):
|
||||
cmd = 'udp open'
|
||||
self.send_command(cmd)
|
||||
@@ -3282,8 +3290,11 @@ class NodeImpl:
|
||||
self.send_command(cmd)
|
||||
self._expect_done()
|
||||
|
||||
def udp_send(self, bytes, ipaddr, port, success=True):
|
||||
cmd = 'udp send %s %d -s %d ' % (ipaddr, port, bytes)
|
||||
def udp_send(self, bytes_count, ipaddr, port, success=True, data_bytes: bytes = None):
|
||||
if data_bytes is None:
|
||||
cmd = 'udp send %s %d -s %d ' % (ipaddr, port, bytes_count)
|
||||
else:
|
||||
cmd = 'udp send %s %d -x %s ' % (ipaddr, port, data_bytes.hex())
|
||||
self.send_command(cmd)
|
||||
if success:
|
||||
self._expect_done()
|
||||
@@ -3293,6 +3304,20 @@ class NodeImpl:
|
||||
def udp_check_rx(self, bytes_should_rx):
|
||||
self._expect('%d bytes' % bytes_should_rx)
|
||||
|
||||
def udp_rx(self) -> bytes:
|
||||
PROMPT = 'spinel-cli > ' if self.node_type == 'ncp-sim' else '> '
|
||||
while True:
|
||||
# match non-newline chars until EOL, such as prompts, whitespace, or UDP results '\d+ bytes from'
|
||||
self._expect(r"[^\n]+$")
|
||||
line = self.pexpect.match.group(0)
|
||||
line_utf = line.decode('utf-8', errors='backslashreplace').lstrip()
|
||||
if line_utf.startswith(PROMPT) or len(line_utf.rstrip()) == 0 or self.__is_logging_line(line_utf):
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
return line.strip()
|
||||
|
||||
def set_routereligible(self, enable: bool):
|
||||
cmd = f'routereligible {"enable" if enable else "disable"}'
|
||||
self.send_command(cmd)
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import config
|
||||
import unittest
|
||||
|
||||
import thread_cert
|
||||
|
||||
# Test description:
|
||||
# This test verifies that a TCAT-enabled device can successfully respond to
|
||||
# a TMF request to enable TCAT (TCAT_ENABLE.req) from another node.
|
||||
#
|
||||
# Topology:
|
||||
# ROUTER_1 (TCAT agent)
|
||||
# |
|
||||
# |
|
||||
# ROUTER_2 (TMF client)
|
||||
#
|
||||
|
||||
ROUTER_1 = 1
|
||||
ROUTER_2 = 2
|
||||
|
||||
|
||||
class TcatTmfEnableReq(thread_cert.TestCase):
|
||||
USE_MESSAGE_FACTORY = False
|
||||
SUPPORT_NCP = False
|
||||
|
||||
TOPOLOGY = {
|
||||
ROUTER_1: {
|
||||
'name': 'ROUTER_1_TCAT',
|
||||
'network_key': '00112233445566778899aabbccddeeff',
|
||||
'mode': 'rdn',
|
||||
},
|
||||
ROUTER_2: {
|
||||
'name': 'ROUTER_2_CLIENT',
|
||||
'network_key': '00112233445566778899aabbccddeeff',
|
||||
'mode': 'rdn',
|
||||
},
|
||||
}
|
||||
|
||||
def test(self):
|
||||
router1 = self.nodes[ROUTER_1]
|
||||
router2 = self.nodes[ROUTER_2]
|
||||
|
||||
#
|
||||
# 0. Start the nodes and form a network.
|
||||
#
|
||||
router1.start()
|
||||
self.simulator.go(config.LEADER_STARTUP_DELAY)
|
||||
self.assertEqual(router1.get_state(), 'leader')
|
||||
|
||||
router2.start()
|
||||
self.simulator.go(config.ROUTER_STARTUP_DELAY)
|
||||
self.assertEqual(router2.get_state(), 'router')
|
||||
|
||||
# Allow some time for the network to stabilize and routes to propagate.
|
||||
self.simulator.go(5)
|
||||
|
||||
# Enable the TCAT agent on Router 1.
|
||||
router1.tcat('start')
|
||||
router1.tcat('standby')
|
||||
self.simulator.go(1)
|
||||
print("TCAT agent started and in standby on Router 1.")
|
||||
|
||||
# Router 2 sends a TMF request to Router 1.
|
||||
rloc1 = router1.get_rloc()
|
||||
router2.udp_start_client()
|
||||
print(f"Router 2 sending TCAT_ENABLE.req to Router 1 at RLOC {rloc1}...")
|
||||
# CoAP header: Ver=1, Type=CON(0), Code=POST(0.02), MID=0x1234
|
||||
# URI-Option: 'c' (0x63)
|
||||
# URI-Option: 'te' (0x7465) (TCAT_ENABLE.req)
|
||||
# Payload Marker (0xff)
|
||||
# DelayTimer TLV: type=52/0x34, len=4, val=1024ms
|
||||
tmf_payload = bytes.fromhex('40021234b163027465ff340400000400')
|
||||
router2.udp_send(0, rloc1, 61631, data_bytes=tmf_payload)
|
||||
|
||||
# Allow time for the request to be sent and the response to be received.
|
||||
self.simulator.go(5)
|
||||
|
||||
# Read the UDP command result (comes later).
|
||||
tmf_rsp = router2.udp_rx()
|
||||
print(f'Received CoAP TMF response: {tmf_rsp}')
|
||||
|
||||
# Verify OK response
|
||||
expected_state_tlv = bytes.fromhex('100101') # Accept status 0x01
|
||||
tmf_rsp_state_tlv = tmf_rsp[-len(expected_state_tlv):]
|
||||
self.assertEqual(tmf_rsp_state_tlv, expected_state_tlv,
|
||||
f"Expected State TLV {expected_state_tlv} but got {tmf_rsp_state_tlv}")
|
||||
|
||||
# TODO: no way yet to verify actual tcat state on Router 1 via CLI.
|
||||
|
||||
# Router 2 sends invalid TCAT_ENABLE.req request.
|
||||
print(f"Router 2 sending invalid TCAT_ENABLE.req to Router 1 at RLOC {rloc1}...")
|
||||
# CoAP header: Ver=1, Type=CON(0), Code=POST(0.02), MID=0x5678
|
||||
# URI-Path: 'c/te' (TCAT_ENABLE.req)
|
||||
# Payload Marker (0xff)
|
||||
# Incomplete payload: contains only a Duration TLV (type 23/0x17) with uint16 value 240 (0xf0),
|
||||
# but misses a DelayTimer TLV.
|
||||
tmf_payload = bytes.fromhex('40025678b163027465ff170200f0')
|
||||
router2.udp_send(0, rloc1, 61631, data_bytes=tmf_payload)
|
||||
|
||||
# And remaining verification steps.
|
||||
self.simulator.go(5)
|
||||
tmf_rsp2 = router2.udp_rx()
|
||||
print(f'Received CoAP TMF response: {tmf_rsp2}')
|
||||
expected_state_tlv = bytes.fromhex('1001ff') # Reject status 0xff
|
||||
tmf_rsp_state_tlv = tmf_rsp2[-len(expected_state_tlv):]
|
||||
self.assertEqual(tmf_rsp_state_tlv, expected_state_tlv,
|
||||
f"Expected State TLV {expected_state_tlv} but got {tmf_rsp_state_tlv}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user