[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:
Esko Dijk
2025-10-29 16:29:09 +01:00
committed by GitHub
parent 13e7c4e702
commit e4479fb6b1
11 changed files with 301 additions and 34 deletions
+1
View File
@@ -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
+1 -1
View File
@@ -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
+1
View File
@@ -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.
+6
View File
@@ -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.
*/
+98 -19
View File
@@ -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,
+18 -11
View File
@@ -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
+3
View File
@@ -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:
+4
View File
@@ -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"; }
+2
View File
@@ -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);
+28 -3
View File
@@ -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
View File
@@ -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()