From 9bb09e74e2bfddb8e5272cc8ade6e0fa4f8125f0 Mon Sep 17 00:00:00 2001 From: jinran-google Date: Fri, 3 Jun 2022 00:04:14 +0800 Subject: [PATCH] [mle] add API to detach gracefully (#7666) This commit adds an API `otThreadDetachGracefully` to notify other nodes in the network (if any) and then stop Thread protocol operation. It sends an Address Release if it's a router, or sets its child timeout to 0 if it's a child. --- include/openthread/instance.h | 2 +- include/openthread/thread.h | 26 ++++ src/cli/cli.cpp | 31 ++++ src/cli/cli.hpp | 3 + src/core/api/thread_api.cpp | 5 + src/core/thread/mle.cpp | 101 ++++++++++++- src/core/thread/mle.hpp | 40 +++++ src/core/thread/mle_router.cpp | 5 +- src/core/thread/mle_router.hpp | 10 +- tests/scripts/thread-cert/Makefile.am | 2 + tests/scripts/thread-cert/node.py | 24 +++ tests/scripts/thread-cert/test_detach.py | 181 +++++++++++++++++++++++ 12 files changed, 423 insertions(+), 7 deletions(-) create mode 100755 tests/scripts/thread-cert/test_detach.py diff --git a/include/openthread/instance.h b/include/openthread/instance.h index ae935f154..02e5b89b7 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -53,7 +53,7 @@ extern "C" { * @note This number versions both OpenThread platform and user APIs. * */ -#define OPENTHREAD_API_VERSION (216) +#define OPENTHREAD_API_VERSION (217) /** * @addtogroup api-instance diff --git a/include/openthread/thread.h b/include/openthread/thread.h index fe829003c..04cb747f1 100644 --- a/include/openthread/thread.h +++ b/include/openthread/thread.h @@ -194,11 +194,22 @@ typedef struct otThreadParentResponseInfo bool mIsAttached; ///< Is the node receiving parent response attached } otThreadParentResponseInfo; +/** + * This callback informs the application that the detaching process has finished. + * + * @param[in] aContext A pointer to application-specific context. + * + */ +typedef void (*otDetachGracefullyCallback)(void *aContext); + /** * This function starts Thread protocol operation. * * The interface must be up when calling this function. * + * Calling this function with @p aEnabled set to FALSE stops any ongoing processes of detaching started by + * otThreadDetachGracefully(). Its callback will be called. + * * @param[in] aInstance A pointer to an OpenThread instance. * @param[in] aEnabled TRUE if Thread is enabled, FALSE otherwise. * @@ -1009,6 +1020,21 @@ otError otThreadSendProactiveBackboneNotification(otInstance * aIns otIp6InterfaceIdentifier *aMlIid, uint32_t aTimeSinceLastTransaction); +/** + * This function notifies other nodes in the network (if any) and then stops Thread protocol operation. + * + * It sends an Address Release if it's a router, or sets its child timeout to 0 if it's a child. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aCallback A pointer to a function that is called upon finishing detaching. + * @param[in] aContext A pointer to callback application-specific context. + * + * @retval OT_ERROR_NONE Successfully started detaching. + * @retval OT_ERROR_BUSY Detaching is already in progress. + * + */ +otError otThreadDetachGracefully(otInstance *aInstance, otDetachGracefullyCallback aCallback, void *aContext); + /** * @} * diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index ef5376ff3..b1f250ffd 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -1453,6 +1453,25 @@ exit: } #endif +template <> otError Interpreter::Process(Arg aArgs[]) +{ + otError error = OT_ERROR_NONE; + + if (aArgs[0] == "async") + { + SuccessOrExit(error = otThreadDetachGracefully(GetInstancePtr(), nullptr, nullptr)); + } + else + { + SuccessOrExit(error = + otThreadDetachGracefully(GetInstancePtr(), &Interpreter::HandleDetachGracefullyResult, this)); + error = OT_ERROR_PENDING; + } + +exit: + return error; +} + template <> otError Interpreter::Process(Arg aArgs[]) { otError error = OT_ERROR_NONE; @@ -4903,6 +4922,17 @@ void Interpreter::OutputChildTableEntry(uint8_t aIndentSize, const otNetworkDiag } #endif // OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE +void Interpreter::HandleDetachGracefullyResult(void *aContext) +{ + static_cast(aContext)->HandleDetachGracefullyResult(); +} + +void Interpreter::HandleDetachGracefullyResult(void) +{ + OutputLine("Finished detaching"); + OutputResult(OT_ERROR_NONE); +} + void Interpreter::HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo &aInfo) { OutputFormat("~ Discovery Request from "); @@ -5017,6 +5047,7 @@ otError Interpreter::ProcessCommand(Arg aArgs[]) #if OPENTHREAD_FTD CmdEntry("delaytimermin"), #endif + CmdEntry("detach"), #endif // OPENTHREAD_FTD || OPENTHREAD_MTD #if OPENTHREAD_CONFIG_DIAG_ENABLE CmdEntry("diag"), diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp index 90144d196..196703a9e 100644 --- a/src/cli/cli.hpp +++ b/src/cli/cli.hpp @@ -452,6 +452,9 @@ private: const char *LinkMetricsStatusToStr(uint8_t aStatus); #endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE + static void HandleDetachGracefullyResult(void *aContext); + void HandleDetachGracefullyResult(void); + static void HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo *aInfo, void *aContext) { static_cast(aContext)->HandleDiscoveryRequest(*aInfo); diff --git a/src/core/api/thread_api.cpp b/src/core/api/thread_api.cpp index ca38757e8..bec8c0633 100644 --- a/src/core/api/thread_api.cpp +++ b/src/core/api/thread_api.cpp @@ -497,4 +497,9 @@ bool otThreadIsAnycastLocateInProgress(otInstance *aInstance) } #endif +otError otThreadDetachGracefully(otInstance *aInstance, otDetachGracefullyCallback aCallback, void *aContext) +{ + return AsCoreType(aInstance).Get().DetachGracefully(aCallback, aContext); +} + #endif // OPENTHREAD_FTD || OPENTHREAD_MTD diff --git a/src/core/thread/mle.cpp b/src/core/thread/mle.cpp index 8e3a646cb..99b93ef05 100644 --- a/src/core/thread/mle.cpp +++ b/src/core/thread/mle.cpp @@ -81,7 +81,10 @@ Mle::Mle(Instance &aInstance) , mAttachTimer(aInstance, Mle::HandleAttachTimer) , mDelayedResponseTimer(aInstance, Mle::HandleDelayedResponseTimer) , mMessageTransmissionTimer(aInstance, Mle::HandleMessageTransmissionTimer) + , mDetachGracefullyTimer(aInstance, Mle::HandleDetachGracefullyTimer) , mParentLeaderCost(0) + , mDetachGracefullyCallback(nullptr) + , mDetachGracefullyContext(nullptr) , mAttachMode(kAnyPartition) , mParentPriority(0) , mParentLinkQuality3(0) @@ -259,7 +262,18 @@ void Mle::Stop(StopMode aMode) SetRole(kRoleDisabled); exit: - return; + mDetachGracefullyTimer.Stop(); + + if (mDetachGracefullyCallback != nullptr) + { + otDetachGracefullyCallback callback = mDetachGracefullyCallback; + void * context = mDetachGracefullyContext; + + mDetachGracefullyCallback = nullptr; + mDetachGracefullyContext = nullptr; + + callback(context); + } } void Mle::SetRole(DeviceRole aRole) @@ -2422,6 +2436,11 @@ exit: } Error Mle::SendChildUpdateRequest(void) +{ + return SendChildUpdateRequest(mTimeout); +} + +Error Mle::SendChildUpdateRequest(uint32_t aTimeout) { Error error = kErrorNone; Ip6::Address destination; @@ -2452,7 +2471,7 @@ Error Mle::SendChildUpdateRequest(void) case kRoleChild: SuccessOrExit(error = message->AppendSourceAddressTlv()); SuccessOrExit(error = message->AppendLeaderDataTlv()); - SuccessOrExit(error = message->AppendTimeoutTlv(mTimeout)); + SuccessOrExit(error = message->AppendTimeoutTlv(aTimeout)); #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE if (Get().IsCslEnabled()) { @@ -4017,7 +4036,14 @@ void Mle::HandleChildUpdateResponse(RxInfo &aRxInfo) switch (Tlv::Find(aRxInfo.mMessage, timeout)) { case kErrorNone: - mTimeout = timeout; + if (timeout == 0 && IsDetachingGracefully()) + { + Stop(); + } + else + { + mTimeout = timeout; + } break; case kErrorNotFound: break; @@ -4743,5 +4769,74 @@ void Mle::DelayedResponseMetadata::RemoveFrom(Message &aMessage) const SuccessOrAssert(aMessage.SetLength(aMessage.GetLength() - sizeof(*this))); } +Error Mle::DetachGracefully(otDetachGracefullyCallback aCallback, void *aContext) +{ + Error error = kErrorNone; + + VerifyOrExit(!IsDetachingGracefully(), error = kErrorBusy); + + OT_ASSERT(mDetachGracefullyCallback == nullptr); + + mDetachGracefullyCallback = aCallback; + mDetachGracefullyContext = aContext; + + if (IsChild() || IsRouter()) + { + mDetachGracefullyTimer.Start(kDetachGracefullyTimeout); + } + else + { + // If the device is a leader, or it's already detached or disabled, we start the timer with zero duration to + // stop and invoke the callback when the timer fires, so the operation finishes immediately and asynchronously. + mDetachGracefullyTimer.Start(0); + } + + if (IsChild()) + { + IgnoreError(SendChildUpdateRequest(/*aTimeout=*/0)); + } +#if OPENTHREAD_FTD + else if (IsRouter()) + { + Get().SendAddressRelease(&Mle::HandleDetachGracefullyAddressReleaseResponse, this); + } +#endif + +exit: + return error; +} + +void Mle::HandleDetachGracefullyTimer(Timer &aTimer) +{ + aTimer.Get().HandleDetachGracefullyTimer(); +} + +void Mle::HandleDetachGracefullyTimer(void) +{ + Stop(); +} + +#if OPENTHREAD_FTD +void Mle::HandleDetachGracefullyAddressReleaseResponse(void * aContext, + otMessage * aMessage, + const otMessageInfo *aMessageInfo, + Error aResult) +{ + OT_UNUSED_VARIABLE(aMessage); + OT_UNUSED_VARIABLE(aMessageInfo); + OT_UNUSED_VARIABLE(aResult); + + static_cast(aContext)->HandleDetachGracefullyAddressReleaseResponse(); +} + +void Mle::HandleDetachGracefullyAddressReleaseResponse(void) +{ + if (IsDetachingGracefully()) + { + Stop(); + } +} +#endif // OPENTHREAD_FTD + } // namespace Mle } // namespace ot diff --git a/src/core/thread/mle.hpp b/src/core/thread/mle.hpp index ff3c173e8..ddd8b91cd 100644 --- a/src/core/thread/mle.hpp +++ b/src/core/thread/mle.hpp @@ -186,6 +186,20 @@ public: */ Error BecomeChild(void); + /** + * This function notifies other nodes in the network (if any) and then stops Thread protocol operation. + * + * It sends an Address Release if it's a router, or sets its child timeout to 0 if it's a child. + * + * @param[in] aCallback A pointer to a function that is called upon finishing detaching. + * @param[in] aContext A pointer to callback application-specific context. + * + * @retval OT_ERROR_NONE Successfully started detaching. + * @retval OT_ERROR_BUSY Detaching is already in progress. + * + */ + Error DetachGracefully(otDetachGracefullyCallback aCallback, void *aContext); + /** * This method indicates whether or not the Thread device is attached to a Thread network. * @@ -1651,6 +1665,15 @@ protected: #endif + /** + * This method indicates whether the device is detaching gracefully. + * + * @retval TRUE Detaching is in progress. + * @retval FALSE Not detaching. + * + */ + bool IsDetachingGracefully(void) { return mDetachGracefullyTimer.IsRunning(); } + Ip6::Netif::UnicastAddress mLeaderAloc; ///< Leader anycast locator LeaderData mLeaderData; ///< Last received Leader Data TLV. @@ -1667,8 +1690,14 @@ protected: TimerMilli mAttachTimer; ///< The timer for driving the attach process. TimerMilli mDelayedResponseTimer; ///< The timer to delay MLE responses. TimerMilli mMessageTransmissionTimer; ///< The timer for (re-)sending of MLE messages (e.g. Child Update). + TimerMilli mDetachGracefullyTimer; uint8_t mParentLeaderCost; + otDetachGracefullyCallback mDetachGracefullyCallback; + void * mDetachGracefullyContext; + + static constexpr uint32_t kDetachGracefullyTimeout = 1000; + private: static constexpr uint8_t kMleHopLimit = 255; static constexpr uint8_t kMleSecurityTagSize = 4; // Security tag size in bytes. @@ -1795,6 +1824,17 @@ private: static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo); void HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo); void ScheduleMessageTransmissionTimer(void); + static void HandleDetachGracefullyTimer(Timer &aTimer); + void HandleDetachGracefullyTimer(void); + Error SendChildUpdateRequest(uint32_t aTimeout); + +#if OPENTHREAD_FTD + static void HandleDetachGracefullyAddressReleaseResponse(void * aContext, + otMessage * aMessage, + const otMessageInfo *aMessageInfo, + Error aResult); + void HandleDetachGracefullyAddressReleaseResponse(void); +#endif void HandleAdvertisement(RxInfo &aRxInfo); void HandleChildIdResponse(RxInfo &aRxInfo); diff --git a/src/core/thread/mle_router.cpp b/src/core/thread/mle_router.cpp index 813f1a2a1..3f5016fd7 100644 --- a/src/core/thread/mle_router.cpp +++ b/src/core/thread/mle_router.cpp @@ -3730,7 +3730,7 @@ exit: return error; } -void MleRouter::SendAddressRelease(void) +void MleRouter::SendAddressRelease(Coap::ResponseHandler aResponseHandler, void *aResponseHandlerContext) { Error error = kErrorNone; Tmf::MessageInfo messageInfo(GetInstance()); @@ -3744,7 +3744,8 @@ void MleRouter::SendAddressRelease(void) SuccessOrExit(error = messageInfo.SetSockAddrToRlocPeerAddrToLeaderRloc()); - SuccessOrExit(error = Get().SendMessage(*message, messageInfo)); + SuccessOrExit(error = + Get().SendMessage(*message, messageInfo, aResponseHandler, aResponseHandlerContext)); Log(kMessageSend, kTypeAddressRelease, messageInfo.GetPeerAddr()); diff --git a/src/core/thread/mle_router.hpp b/src/core/thread/mle_router.hpp index 3ccd231fb..e84899f35 100644 --- a/src/core/thread/mle_router.hpp +++ b/src/core/thread/mle_router.hpp @@ -570,6 +570,15 @@ public: void SetThreadVersionCheckEnabled(bool aEnabled) { mThreadVersionCheckEnabled = aEnabled; } #endif + /** + * This function sends an Address Release. + * + * @param[in] aResponseHandler A pointer to a function that is called upon response reception or time-out. + * @param[in] aResponseHandlerContext A pointer to callback application-specific context. + * + */ + void SendAddressRelease(Coap::ResponseHandler aResponseHandler = nullptr, void *aResponseHandlerContext = nullptr); + private: static constexpr uint16_t kDiscoveryMaxJitter = 250; // Max jitter delay Discovery Responses (in msec). static constexpr uint32_t kStateUpdatePeriod = 1000; // State update period (in msec). @@ -602,7 +611,6 @@ private: Error ProcessRouteTlv(RxInfo &aRxInfo, RouteTlv &aRouteTlv); void StopAdvertiseTrickleTimer(void); Error SendAddressSolicit(ThreadStatusTlv::Status aStatus); - void SendAddressRelease(void); void SendAddressSolicitResponse(const Coap::Message & aRequest, ThreadStatusTlv::Status aResponseStatus, const Router * aRouter, diff --git a/tests/scripts/thread-cert/Makefile.am b/tests/scripts/thread-cert/Makefile.am index 326869093..72906e699 100644 --- a/tests/scripts/thread-cert/Makefile.am +++ b/tests/scripts/thread-cert/Makefile.am @@ -160,6 +160,7 @@ EXTRA_DIST = \ test_common.py \ test_crypto.py \ test_dataset_updater.py \ + test_detach.py \ test_diag.py \ test_dns_client_config_auto_start.py \ test_dnssd.py \ @@ -236,6 +237,7 @@ check_SCRIPTS = \ test_common.py \ test_crypto.py \ test_dataset_updater.py \ + test_detach.py \ test_diag.py \ test_dns_client_config_auto_start.py \ test_dnssd.py \ diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index f2cf62b98..9b317bac5 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -815,6 +815,30 @@ class NodeImpl: self.send_command('thread stop') self._expect_done() + def detach(self, is_async=False): + cmd = 'detach' + if is_async: + cmd += ' async' + + self.send_command(cmd) + + if is_async: + self._expect_done() + return + + end = self.simulator.now() + 4 + while True: + self.simulator.go(1) + try: + self._expect_done(timeout=0.1) + return + except (pexpect.TIMEOUT, socket.timeout): + if self.simulator.now() > end: + raise + + def expect_finished_detaching(self): + self._expect('Finished detaching') + def commissioner_start(self): cmd = 'commissioner start' self.send_command(cmd) diff --git a/tests/scripts/thread-cert/test_detach.py b/tests/scripts/thread-cert/test_detach.py new file mode 100755 index 000000000..e9f79fa7b --- /dev/null +++ b/tests/scripts/thread-cert/test_detach.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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 unittest + +import thread_cert +import config +from pktverify.consts import MLE_CHILD_UPDATE_REQUEST, TIMEOUT_TLV, ADDR_REL_URI +from pktverify.packet_verifier import PacketVerifier + +# Test description: +# This test verifies that detaching function can send correct "goodbye" messages. +# +# Topology: +# +# CHILD_1 ----- ROUTER_1 ----- LEADER +# +# + +LEADER = 1 +ROUTER_1 = 2 +CHILD_1 = 3 + + +class TestDetach(thread_cert.TestCase): + USE_MESSAGE_FACTORY = False + SUPPORT_NCP = False + + TOPOLOGY = { + LEADER: { + 'name': 'Leader', + 'allowlist': [ROUTER_1], + 'mode': 'rdn', + }, + ROUTER_1: { + 'name': 'Router_1', + 'allowlist': [LEADER, CHILD_1], + 'mode': 'rdn', + }, + CHILD_1: { + 'name': 'Child_1', + 'is_mtd': True, + 'allowlist': [ROUTER_1], + 'mode': '-', + 'timeout': 10, + }, + } + + def test(self): + leader = self.nodes[LEADER] + router1 = self.nodes[ROUTER_1] + child1 = self.nodes[CHILD_1] + + leader.start() + self.simulator.go(5) + self.assertEqual(leader.get_state(), 'leader') + + router1.start() + self.simulator.go(5) + self.assertEqual(router1.get_state(), 'router') + router1_rloc16 = router1.get_addr16() + self.assertTrue(list(filter(lambda x: x[1]['rloc16'] == router1_rloc16, leader.router_table().items()))) + + self.collect_rloc16s() + + child1.start() + self.simulator.go(7) + self.assertEqual(child1.get_state(), 'child') + child_table = router1.get_child_table() + self.assertEqual(len(child_table), 1) + self.assertEqual(child_table[1]['timeout'], 10) + + child1.detach() + self.assertEqual(child1.get_state(), 'disabled') + self.assertFalse(router1.get_child_table()) + + router1.detach() + self.assertEqual(router1.get_state(), 'disabled') + self.assertFalse(list(filter(lambda x: x[1]['rloc16'] == router1_rloc16, leader.router_table().items()))) + + router1.start() + self.simulator.go(5) + self.assertEqual(router1.get_state(), 'router') + + child1.start() + self.simulator.go(7) + self.assertEqual(child1.get_state(), 'child') + child_table = router1.get_child_table() + self.assertEqual(len(child_table), 1) + self.assertEqual(child_table[2]['timeout'], 10) + + router1.thread_stop() + self.assertEqual(router1.get_state(), 'disabled') + child1.detach() + self.assertEqual(child1.get_state(), 'disabled') + + router1.start() + self.simulator.go(5) + self.assertEqual(router1.get_state(), 'router') + + child1.start() + self.simulator.go(7) + self.assertEqual(child1.get_state(), 'child') + + leader.detach() + self.assertEqual(leader.get_state(), 'disabled') + + self.assertTrue(child1.ping(router1.get_mleid(), timeout=20)) + + router1.detach() + self.assertEqual(router1.get_state(), 'disabled') + + leader.detach() + self.assertEqual(leader.get_state(), 'disabled') + + leader.start() + self.assertEqual(leader.get_state(), 'detached') + leader.detach() + self.assertEqual(leader.get_state(), 'disabled') + + leader.start() + self.simulator.go(5) + self.assertEqual(leader.get_state(), 'leader') + router1.start() + self.simulator.go(5) + self.assertEqual(router1.get_state(), 'router') + + leader.thread_stop() + router1.detach(is_async=True) + self.assertEqual(router1.get_state(), 'router') + router1.thread_stop() + self.assertEqual(router1.get_state(), 'disabled') + router1.detach() + self.assertEqual(router1.get_state(), 'disabled') + + def verify(self, pv: PacketVerifier): + pkts = pv.pkts + pv.summary.show() + + leader = pv.vars['Leader'] + router1 = pv.vars['Router_1'] + child1 = pv.vars['Child_1'] + leader_rloc16 = pv.vars['Leader_RLOC16'] + + pkts.filter_wpan_src64(child1).filter_mle_cmd(MLE_CHILD_UPDATE_REQUEST).filter_wpan_dst64( + router1).must_next().must_verify(lambda p: TIMEOUT_TLV in set(p.mle.tlv.type) and p.mle.tlv.timeout == 0) + pkts.filter_wpan_src64(router1).filter_coap_request(ADDR_REL_URI).filter_wpan_dst16(leader_rloc16).must_next() + pkts.filter_wpan_src64(child1).filter_mle_cmd(MLE_CHILD_UPDATE_REQUEST).filter_wpan_dst64( + router1).must_next().must_verify(lambda p: TIMEOUT_TLV in set(p.mle.tlv.type) and p.mle.tlv.timeout == 0) + pkts.filter_wpan_src64(leader).filter_coap_request(ADDR_REL_URI).must_not_next() + pkts.filter_wpan_src64(router1).filter_coap_request(ADDR_REL_URI).filter_wpan_dst16(leader_rloc16).must_next() + + +if __name__ == '__main__': + unittest.main()