From 56d0e7212b5aaedaa6c605bbad05fd15ea3dff35 Mon Sep 17 00:00:00 2001 From: Yakun Xu Date: Tue, 10 Mar 2020 00:08:46 +0800 Subject: [PATCH] [mac] add enhanced frame pending (#4334) This commit adds enhanced frame pending feature of Thread 1.2. This requires the platform radio driver to correctly set frame pending bit for Data frames and MAC Data Request frames. --- examples/platforms/simulation/radio.c | 11 +- examples/platforms/utils/mac_frame.cpp | 5 + examples/platforms/utils/mac_frame.h | 11 ++ src/core/mac/data_poll_sender.cpp | 8 +- src/core/mac/data_poll_sender.hpp | 12 +- src/core/mac/mac.cpp | 4 +- src/core/mac/mac_frame.hpp | 9 ++ tests/scripts/thread-cert/common.py | 2 + tests/scripts/thread-cert/ipv6.py | 7 +- tests/scripts/thread-cert/node.py | 30 +++-- tests/scripts/thread-cert/simulator.py | 6 + .../thread-cert/test_ipv6_fragmentation.py | 12 +- .../v1_2_test_enhanced_frame_pending.py | 112 ++++++++++++++++++ .../v1_2_test_enhanced_keep_alive.py | 2 +- 14 files changed, 201 insertions(+), 30 deletions(-) create mode 100755 tests/scripts/thread-cert/v1_2_test_enhanced_frame_pending.py diff --git a/examples/platforms/simulation/radio.c b/examples/platforms/simulation/radio.c index 4147bfc57..8fe4c8d08 100644 --- a/examples/platforms/simulation/radio.c +++ b/examples/platforms/simulation/radio.c @@ -125,12 +125,11 @@ static void ReverseExtAddress(otExtAddress *aReversed, const otExtAddress *aOrig } } -static bool isDataRequestAndHasFramePending(const otRadioFrame *aFrame) +static bool HasFramePending(const otRadioFrame *aFrame) { bool rval = false; otMacAddress src; - otEXPECT(otMacFrameIsDataRequest(aFrame)); otEXPECT_ACTION(sSrcMatchEnabled, rval = true); otEXPECT(otMacFrameGetSrcAddr(aFrame, &src) == OT_ERROR_NONE); @@ -715,7 +714,13 @@ void radioSendAck(void) sAckFrame.mLength = IEEE802154_ACK_LENGTH; sAckMessage.mPsdu[0] = IEEE802154_FRAME_TYPE_ACK; - if (isDataRequestAndHasFramePending(&sReceiveFrame)) + if ( +#if OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2 + (otMacFrameIsData(&sReceiveFrame) || otMacFrameIsDataRequest(&sReceiveFrame)) +#else + otMacFrameIsDataRequest(&sReceiveFrame) +#endif + && HasFramePending(&sReceiveFrame)) { sAckMessage.mPsdu[0] |= IEEE802154_FRAME_PENDING; sReceiveFrame.mInfo.mRxInfo.mAckedWithFramePending = true; diff --git a/examples/platforms/utils/mac_frame.cpp b/examples/platforms/utils/mac_frame.cpp index 276e38bf5..f782143c7 100644 --- a/examples/platforms/utils/mac_frame.cpp +++ b/examples/platforms/utils/mac_frame.cpp @@ -72,6 +72,11 @@ bool otMacFrameIsAck(const otRadioFrame *aFrame) return static_cast(aFrame)->GetType() == Mac::Frame::kFcfFrameAck; } +bool otMacFrameIsData(const otRadioFrame *aFrame) +{ + return static_cast(aFrame)->GetType() == Mac::Frame::kFcfFrameData; +} + bool otMacFrameIsDataRequest(const otRadioFrame *aFrame) { return static_cast(aFrame)->IsDataRequestCommand(); diff --git a/examples/platforms/utils/mac_frame.h b/examples/platforms/utils/mac_frame.h index 429a75339..2f2145ea0 100644 --- a/examples/platforms/utils/mac_frame.h +++ b/examples/platforms/utils/mac_frame.h @@ -79,6 +79,17 @@ typedef struct otMacAddress */ bool otMacFrameIsAck(const otRadioFrame *aFrame); +/** + * Check if @p aFrame is a Data frame. + * + * @param[in] aFrame A pointer to the frame. + * + * @retval true It is a Data frame. + * @retval false It is not a Data frame. + * + */ +bool otMacFrameIsData(const otRadioFrame *aFrame); + /** * Check if @p aFrame is an Data Request Command. * diff --git a/src/core/mac/data_poll_sender.cpp b/src/core/mac/data_poll_sender.cpp index 710032c01..0a79beac2 100644 --- a/src/core/mac/data_poll_sender.cpp +++ b/src/core/mac/data_poll_sender.cpp @@ -307,7 +307,7 @@ exit: return; } -void DataPollSender::CheckFramePending(Mac::RxFrame &aFrame) +void DataPollSender::ProcessFrame(const Mac::RxFrame &aFrame) { VerifyOrExit(mEnabled); @@ -317,6 +317,12 @@ void DataPollSender::CheckFramePending(Mac::RxFrame &aFrame) { SendDataPoll(); } +#if OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2 + else if (aFrame.IsAck()) + { + ResetKeepAliveTimer(); + } +#endif exit: return; diff --git a/src/core/mac/data_poll_sender.hpp b/src/core/mac/data_poll_sender.hpp index d24dbd010..675accf45 100644 --- a/src/core/mac/data_poll_sender.hpp +++ b/src/core/mac/data_poll_sender.hpp @@ -167,12 +167,16 @@ public: void HandlePollTimeout(void); /** - * This method informs the data poll sender that a mac frame has been received. It checks the "frame pending" in - * the received frame header and if it is set, data poll sender will send an immediate data poll to retrieve the - * pending frame. + * This method informs the data poll sender to process a MAC frame. + * + * 1. Data Frame: send an immediate data poll if "frame pending" is set. + * 2. Ack Frame for a secured data frame in version 1.2 or newer: send an immediate data poll if + * "frame pending" is set, otherwise reset the keep-alive timer for sending next poll. + * + * @param[in] aFrame The frame to process. * */ - void CheckFramePending(Mac::RxFrame &aFrame); + void ProcessFrame(const Mac::RxFrame &aFrame); /** * This method asks the data poll sender to recalculate the poll period. diff --git a/src/core/mac/mac.cpp b/src/core/mac/mac.cpp index 95258fe4c..398a3faa9 100644 --- a/src/core/mac/mac.cpp +++ b/src/core/mac/mac.cpp @@ -1307,7 +1307,7 @@ void Mac::HandleTransmitDone(TxFrame &aFrame, RxFrame *aAckFrame, otError aError if (aError == OT_ERROR_NONE && Get().GetParent().IsEnhancedKeepAliveSupported() && aFrame.GetSecurityEnabled() && aAckFrame != NULL) { - Get().ResetKeepAliveTimer(); + Get().ProcessFrame(*aAckFrame); } #endif PerformNextOperation(); @@ -1651,7 +1651,7 @@ void Mac::HandleReceivedFrame(RxFrame *aFrame, otError aError) ExitNow(); } - Get().CheckFramePending(*aFrame); + Get().ProcessFrame(*aFrame); if (neighbor != NULL) { diff --git a/src/core/mac/mac_frame.hpp b/src/core/mac/mac_frame.hpp index aac2fad5b..40513b946 100644 --- a/src/core/mac/mac_frame.hpp +++ b/src/core/mac/mac_frame.hpp @@ -360,6 +360,15 @@ public: */ uint8_t GetType(void) const { return GetPsdu()[0] & kFcfFrameTypeMask; } + /** + * This method returns whether the frame is an Ack frame. + * + * @retval TRUE If this is an Ack. + * @retval FALSE If this is not an Ack. + * + */ + bool IsAck(void) const { return GetType() == kFcfFrameAck; } + /** * This method returns the IEEE 802.15.4 Frame Version. * diff --git a/tests/scripts/thread-cert/common.py b/tests/scripts/thread-cert/common.py index c82d6e706..e42a70e21 100644 --- a/tests/scripts/thread-cert/common.py +++ b/tests/scripts/thread-cert/common.py @@ -37,6 +37,8 @@ import ipaddress # Map of 2 bits of parent priority. pp_map = {1: 1, 0: 0, 3: -1, 2: -2} +UDP_TEST_PORT = 12345 + # Get the signed parent priority from the byte that parent priority is in. def map_pp(pp_byte): diff --git a/tests/scripts/thread-cert/ipv6.py b/tests/scripts/thread-cert/ipv6.py index 3068ab9c6..fd7bb5667 100644 --- a/tests/scripts/thread-cert/ipv6.py +++ b/tests/scripts/thread-cert/ipv6.py @@ -34,6 +34,8 @@ import struct from binascii import hexlify from ipaddress import ip_address +import common + try: from itertools import izip_longest as zip_longest except ImportError: @@ -1079,7 +1081,10 @@ class UdpBasedOnSrcDstPortsPayloadFactory: if message_info.src_port in self._factories: factory = self._factories[message_info.src_port] - if factory is None: + if message_info.dst_port == common.UDP_TEST_PORT: + # Ignore traffic targeted to test port + return None + elif factory is None: raise RuntimeError("Could not find factory to build UDP payload.") return factory.parse(data, message_info) diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index 0a8637230..bc79d09ca 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -784,25 +784,29 @@ class Node: self.send_command(cmd) - if isinstance(self.simulator, simulator.VirtualTime): - self.simulator.go(timeout) + end = self.simulator.now() + timeout + + responders = {} result = True - try: - responders = {} - # ncp-sim doesn't print Done - done = (self.node_type == 'ncp-sim') - while len(responders) < num_responses or not done: - i = self._expect([r'from (\S+):', 'Done']) + # ncp-sim doesn't print Done + done = (self.node_type == 'ncp-sim') + while len(responders) < num_responses or not done: + self.simulator.go(1) + try: + i = self._expect([r'from (\S+):', r'Done'], timeout=0.1) + except (pexpect.TIMEOUT, socket.timeout): + if self.simulator.now() < end: + continue + result = False + if isinstance(self.simulator, simulator.VirtualTime): + self.simulator.sync_devices() + break + else: if i == 0: responders[self.pexpect.match.groups()[0]] = 1 elif i == 1: done = True - self._expect('\n') - except (pexpect.TIMEOUT, socket.timeout): - result = False - if isinstance(self.simulator, simulator.VirtualTime): - self.simulator.sync_devices() return result diff --git a/tests/scripts/thread-cert/simulator.py b/tests/scripts/thread-cert/simulator.py index 449a3abad..a7b856f78 100644 --- a/tests/scripts/thread-cert/simulator.py +++ b/tests/scripts/thread-cert/simulator.py @@ -107,6 +107,9 @@ class RealTime(BaseSimulator): self.commissioning_messages[nodeid] = [] return ret + def now(self): + return time.time() + def go(self, duration, nodeid=None): time.sleep(duration) @@ -456,6 +459,9 @@ class VirtualTime(BaseSimulator): self.receive_events() self.awake_devices.clear() + def now(self): + return self.current_time / 1000000 + def go(self, duration, nodeid=None): assert self.current_time == self._pause_time duration = int(duration) * 1000000 diff --git a/tests/scripts/thread-cert/test_ipv6_fragmentation.py b/tests/scripts/thread-cert/test_ipv6_fragmentation.py index 545848353..d4eae301e 100755 --- a/tests/scripts/thread-cert/test_ipv6_fragmentation.py +++ b/tests/scripts/thread-cert/test_ipv6_fragmentation.py @@ -29,6 +29,7 @@ import unittest +import common import config import node @@ -76,18 +77,19 @@ class TestIPv6Fragmentation(unittest.TestCase): mleid_router = self.nodes[ROUTER].get_ip6_address( config.ADDRESS_TYPE.ML_EID) - self.nodes[LEADER].udp_start("::", 12345) - self.nodes[ROUTER].udp_start("::", 12345) + self.nodes[LEADER].udp_start("::", common.UDP_TEST_PORT) + self.nodes[ROUTER].udp_start("::", common.UDP_TEST_PORT) - self.nodes[LEADER].udp_send(1952, mleid_router, 12345) + self.nodes[LEADER].udp_send(1952, mleid_router, common.UDP_TEST_PORT) self.simulator.go(5) self.nodes[ROUTER].udp_check_rx(1952) - self.nodes[ROUTER].udp_send(1831, mleid_leader, 12345) + self.nodes[ROUTER].udp_send(1831, mleid_leader, common.UDP_TEST_PORT) self.simulator.go(5) self.nodes[LEADER].udp_check_rx(1831) - self.nodes[ROUTER].udp_send(1953, mleid_leader, 12345, False) + self.nodes[ROUTER].udp_send(1953, mleid_leader, common.UDP_TEST_PORT, + False) self.nodes[ROUTER].udp_stop() self.nodes[LEADER].udp_stop() diff --git a/tests/scripts/thread-cert/v1_2_test_enhanced_frame_pending.py b/tests/scripts/thread-cert/v1_2_test_enhanced_frame_pending.py new file mode 100755 index 000000000..4bbbf19f9 --- /dev/null +++ b/tests/scripts/thread-cert/v1_2_test_enhanced_frame_pending.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2020, 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. +# +"""This test case verifies Thread 1.2 enhanced frame pending feature. + +Topology + + Leader ----- SED_1 + +1. Set up topology a Leader and a SED. +2. SED pings leader so a data poll is just sent. +3. Leader sends a UDP packet to SED to put a pending frame in queue. +4. Wait for half polling period, verify no packet received by SED. +5. SED sends a UDP packet to Leader to solicit an ACK with frame pending set. + * Verify Leader receives a UDP packet + * Verify SED sends a data poll + * Verify SED receives a UDP packet +""" + +import unittest +import pexpect + +import thread_cert +import common + +LEADER = 1 +SED_1 = 2 + +CHILD_TIMEOUT = 30 +UDP_BYTES_COUNT = 73 + +DEFAULT_POLL_PERIOD = CHILD_TIMEOUT - 4 +"""The default poll period calculated from child timeout.""" + + +class SED_EnhancedFramePending(thread_cert.TestCase): + topology = { + LEADER: None, + SED_1: { + 'mode': 's', + }, + } + + def test(self): + self.nodes[SED_1].set_timeout(CHILD_TIMEOUT) + + # 1 - Set up topology + self.nodes[LEADER].start() + self.simulator.go(5) + self.assertEqual(self.nodes[LEADER].get_state(), 'leader') + + self.nodes[SED_1].start() + self.simulator.go(7) + self.assertEqual(self.nodes[SED_1].get_state(), 'child') + + self.nodes[LEADER].udp_start('::', common.UDP_TEST_PORT) + self.nodes[SED_1].udp_start('::', common.UDP_TEST_PORT) + + # 2 - Ping Leader + self.assertTrue(self.nodes[SED_1].ping(self.nodes[LEADER].get_rloc(), + timeout=CHILD_TIMEOUT)) + + self.flush_all() + + # 3 - Send to SED + self.nodes[LEADER].udp_send(UDP_BYTES_COUNT, + self.nodes[SED_1].get_rloc(), + common.UDP_TEST_PORT) + + # 4 - Wait for half polling period + self.simulator.go(DEFAULT_POLL_PERIOD // 2) + with self.assertRaises(pexpect.TIMEOUT): + self.nodes[SED_1].udp_check_rx(UDP_BYTES_COUNT) + + # 5 - Send to Leader + self.nodes[SED_1].udp_send(UDP_BYTES_COUNT, + self.nodes[LEADER].get_rloc(), + common.UDP_TEST_PORT) + self.simulator.go(1) + self.nodes[LEADER].udp_check_rx(UDP_BYTES_COUNT) + sed_messages = self.simulator.get_messages_sent_by(SED_1) + self.assertNotEqual(sed_messages.next_data_poll(), None) + self.nodes[SED_1].udp_check_rx(UDP_BYTES_COUNT) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/scripts/thread-cert/v1_2_test_enhanced_keep_alive.py b/tests/scripts/thread-cert/v1_2_test_enhanced_keep_alive.py index 6bfd8bf4e..eee5e64da 100755 --- a/tests/scripts/thread-cert/v1_2_test_enhanced_keep_alive.py +++ b/tests/scripts/thread-cert/v1_2_test_enhanced_keep_alive.py @@ -135,7 +135,7 @@ class SED_EnhancedKeepAlive(thread_cert.TestCase): self.nodes[LEADER].enable_whitelist() self.nodes[SED_1].enable_whitelist() self.nodes[SED_1].set_pollperiod(CHILD_TIMEOUT * 1000 * 2) - self.simulator.go(CHILD_TIMEOUT) + self.simulator.go(CHILD_TIMEOUT + 1) self.assertEqual(self.nodes[SED_1].get_state(), 'child') self.nodes[SED_1].set_pollperiod(USER_POLL_PERIOD * 1000) self.nodes[LEADER].disable_whitelist()