diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 677649af1..cc927f105 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 (94) +#define OPENTHREAD_API_VERSION (95) /** * @addtogroup api-instance diff --git a/include/openthread/ping_sender.h b/include/openthread/ping_sender.h index 52200987b..9de484ccb 100644 --- a/include/openthread/ping_sender.h +++ b/include/openthread/ping_sender.h @@ -115,7 +115,7 @@ typedef struct otPingSenderConfig uint16_t mSize; ///< Data size (# of bytes) excludes IPv6/ICMPv6 header. Zero for default. uint16_t mCount; ///< Number of ping messages to send. Zero to use default. uint32_t mInterval; ///< Ping tx interval in milliseconds. Zero to use default. - uint16_t mTimeout; ///< Time in milliseconds to wait for a reply after sending out the request. + uint16_t mTimeout; ///< Time in milliseconds to wait for final reply after sending final request. ///< Zero to use default. uint8_t mHopLimit; ///< Hop limit (used if `mAllowZeroHopLimit` is false). Zero for default. bool mAllowZeroHopLimit; ///< Indicates whether hop limit is zero. diff --git a/src/cli/README.md b/src/cli/README.md index 1c0b4e706..bfb93a68d 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -1882,7 +1882,7 @@ Set the preferred Thread Leader Partition ID. Done ``` -### ping \ [size][count] [interval][hoplimit] +### ping \ \[size\] \[count\] \[interval\] \[hoplimit\] \[timeout\] Send an ICMPv6 Echo Request. @@ -1890,6 +1890,7 @@ Send an ICMPv6 Echo Request. - count: The number of ICMPv6 Echo Requests to be sent. - interval: The interval between two consecutive ICMPv6 Echo Requests in seconds. The value may have fractional form, for example `0.5`. - hoplimit: The hoplimit of ICMPv6 Echo Request to be sent. +- timeout: Time in seconds to wait for the final ICMPv6 Echo Reply after sending out the request. The value may have fractional form, for example `3.5`. ```bash > ping fdde:ad00:beef:0:558:f56b:d688:799 diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index d17b32650..1452458c2 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -3202,7 +3202,8 @@ void Interpreter::HandlePingStatistics(const otPingSenderStatistics *aStatistics { OutputFormat("%u packets transmitted, %u packets received.", aStatistics->mSentCount, aStatistics->mReceivedCount); - if ((aStatistics->mSentCount != 0) && !aStatistics->mIsMulticast) + if ((aStatistics->mSentCount != 0) && !aStatistics->mIsMulticast && + aStatistics->mReceivedCount <= aStatistics->mSentCount) { uint32_t packetLossRate = 1000 * (aStatistics->mSentCount - aStatistics->mReceivedCount) / aStatistics->mSentCount; @@ -3217,6 +3218,7 @@ void Interpreter::HandlePingStatistics(const otPingSenderStatistics *aStatistics } OutputLine(""); + OutputResult(OT_ERROR_NONE); } otError Interpreter::ProcessPing(uint8_t aArgsLength, char *aArgs[]) @@ -3257,7 +3259,15 @@ otError Interpreter::ProcessPing(uint8_t aArgsLength, char *aArgs[]) config.mAllowZeroHopLimit = (config.mHopLimit == 0); } - VerifyOrExit(aArgsLength <= 5, error = OT_ERROR_INVALID_ARGS); + if (aArgsLength > 5) + { + uint32_t timeout; + SuccessOrExit(error = ParsePingInterval(aArgs[5], timeout)); + VerifyOrExit(timeout <= NumericLimits::Max(), error = OT_ERROR_INVALID_ARGS); + config.mTimeout = static_cast(timeout); + } + + VerifyOrExit(aArgsLength <= 6, error = OT_ERROR_INVALID_ARGS); config.mReplyCallback = Interpreter::HandlePingReply; config.mStatisticsCallback = Interpreter::HandlePingStatistics; diff --git a/src/core/utils/ping_sender.cpp b/src/core/utils/ping_sender.cpp index 80bfb9ba6..48d04b754 100644 --- a/src/core/utils/ping_sender.cpp +++ b/src/core/utils/ping_sender.cpp @@ -97,7 +97,7 @@ PingSender::PingSender(Instance &aInstance) Error PingSender::Ping(const Config &aConfig) { - Error error = kErrorNone; + Error error = kErrorPending; VerifyOrExit(!mTimer.IsRunning(), error = kErrorBusy); @@ -160,7 +160,7 @@ exit: { mTimer.Start(mConfig.mInterval); } - else if (!mStatistics.mIsMulticast) + else { mTimer.Start(mConfig.mTimeout); } @@ -200,6 +200,7 @@ void PingSender::HandleIcmpReceive(const Message & aMessage, Reply reply; uint32_t timestamp; + VerifyOrExit(mTimer.IsRunning()); VerifyOrExit(aIcmpHeader.GetType() == Ip6::Icmp::Header::kTypeEchoReply); VerifyOrExit(aIcmpHeader.GetId() == mIdentifier); diff --git a/tests/scripts/thread-cert/Cert_5_6_09_NetworkDataForwarding.py b/tests/scripts/thread-cert/Cert_5_6_09_NetworkDataForwarding.py index d3e834f0f..86646e1fe 100755 --- a/tests/scripts/thread-cert/Cert_5_6_09_NetworkDataForwarding.py +++ b/tests/scripts/thread-cert/Cert_5_6_09_NetworkDataForwarding.py @@ -120,23 +120,23 @@ class Cert_5_6_9_NetworkDataForwarding(thread_cert.TestCase): self.nodes[ROUTER2].register_netdata() self.simulator.go(15) - self.assertFalse(self.nodes[SED].ping('2001:2:0:2::1')) + self.assertFalse(self.nodes[SED].ping('2001:2:0:2::1', timeout=10)) - self.assertFalse(self.nodes[SED].ping('2007::1')) + self.assertFalse(self.nodes[SED].ping('2007::1', timeout=10)) self.nodes[ROUTER2].remove_prefix('2001:2:0:1::/64') self.nodes[ROUTER2].add_prefix('2001:2:0:1::/64', 'paros', 'high') self.nodes[ROUTER2].register_netdata() self.simulator.go(15) - self.assertFalse(self.nodes[SED].ping('2007::1')) + self.assertFalse(self.nodes[SED].ping('2007::1', timeout=10)) self.nodes[ROUTER2].remove_prefix('2001:2:0:1::/64') self.nodes[ROUTER2].add_prefix('2001:2:0:1::/64', 'paros', 'med') self.nodes[ROUTER2].register_netdata() self.simulator.go(15) - self.assertFalse(self.nodes[SED].ping('2007::1')) + self.assertFalse(self.nodes[SED].ping('2007::1', timeout=10)) def verify(self, pv): pkts = pv.pkts diff --git a/tests/scripts/thread-cert/Cert_9_2_09_PendingPartition.py b/tests/scripts/thread-cert/Cert_9_2_09_PendingPartition.py index 937fbb117..9ca0a1065 100755 --- a/tests/scripts/thread-cert/Cert_9_2_09_PendingPartition.py +++ b/tests/scripts/thread-cert/Cert_9_2_09_PendingPartition.py @@ -208,8 +208,8 @@ class Cert_9_2_09_PendingPartition(thread_cert.TestCase): leader_addr = self.nodes[LEADER].get_ip6_address(config.ADDRESS_TYPE.ML_EID) router1_addr = self.nodes[ROUTER1].get_ip6_address(config.ADDRESS_TYPE.ML_EID) - self.assertTrue(self.nodes[ROUTER2].ping(leader_addr)) - self.assertTrue(self.nodes[COMMISSIONER].ping(router1_addr)) + self.assertTrue(self.nodes[ROUTER2].ping(leader_addr, timeout=10)) + self.assertTrue(self.nodes[COMMISSIONER].ping(router1_addr, timeout=10)) def verify(self, pv): pkts = pv.pkts diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index 48c8cced0..afe0de5a9 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -1751,28 +1751,23 @@ class NodeImpl: return self._expect_results( r'\|\s(\S+)\s+\|\s(\S+)\s+\|\s([0-9a-fA-F]{4})\s\|\s([0-9a-fA-F]{16})\s\|\s(\d+)') - def ping(self, ipaddr, num_responses=1, size=None, timeout=5): - cmd = 'ping %s' % ipaddr - if size is not None: - cmd += ' %d' % size + def ping(self, ipaddr, num_responses=1, size=8, timeout=5, count=1, interval=1, hoplimit=64): + cmd = f'ping {ipaddr} {size} {count} {interval} {hoplimit} {timeout}' self.send_command(cmd) - end = self.simulator.now() + timeout + wait_allowance = 3 + end = self.simulator.now() + timeout + wait_allowance responders = {} result = True # ncp-sim doesn't print Done done = (self.node_type == 'ncp-sim') - - # ncp-sim doesn't print statistics - received_statistics = (self.node_type == 'ncp-sim') - is_multicast = ipaddress.IPv6Address(ipaddr).is_multicast - while len(responders) < num_responses or not done or (not is_multicast and not received_statistics): + while len(responders) < num_responses or not done: self.simulator.go(1) try: - i = self._expect([r'from (\S+):', r'Done', r'packets transmitted'], timeout=0.1) + i = self._expect([r'from (\S+):', r'Done'], timeout=0.1) except (pexpect.TIMEOUT, socket.timeout): if self.simulator.now() < end: continue @@ -1785,8 +1780,6 @@ class NodeImpl: responders[self.pexpect.match.groups()[0]] = 1 elif i == 1: done = True - elif i == 2: - received_statistics = True return result def reset(self): diff --git a/tools/otci/otci/command_handlers.py b/tools/otci/otci/command_handlers.py index c3b648460..aaca3b327 100644 --- a/tools/otci/otci/command_handlers.py +++ b/tools/otci/otci/command_handlers.py @@ -76,9 +76,7 @@ class OtCliCommandRunner(OTCommandHandler): r')') """regex used to filter logs""" - __ASYNC_COMMANDS = { - 'scan', - } + __ASYNC_COMMANDS = {'scan', 'ping'} def __init__(self, otcli: OtCliHandler, is_spinel_cli=False): self.__otcli: OtCliHandler = otcli @@ -94,7 +92,7 @@ class OtCliCommandRunner(OTCommandHandler): def __repr__(self): return repr(self.__otcli) - def execute_command(self, cmd, timeout=10) -> None: + def execute_command(self, cmd, timeout=10) -> List[str]: self.__otcli.writeline(cmd) if cmd in {'reset', 'factoryreset'}: diff --git a/tools/otci/otci/otci.py b/tools/otci/otci/otci.py index 84c143bf7..a03b8dbe8 100644 --- a/tools/otci/otci/otci.py +++ b/tools/otci/otci/otci.py @@ -174,31 +174,48 @@ class OTCI(object): # # Network Operations # + _PING_STATISTICS_PATTERN = re.compile( + r'^(?P\d+) packets transmitted, (?P\d+) packets received.(?: Packet loss = (?P\d+\.\d+)%.)?(?: Round-trip min/avg/max = (?P\d+)/(?P\d+\.\d+)/(?P\d+) ms.)?$' + ) - def ping(self, ip: str, size: int = None, count: int = None, interval: int = None, hoplimit: int = None): - """Send an ICMPv6 Echo Request. + def ping(self, + ip: str, + size: int = 8, + count: int = 1, + interval: float = 1, + hoplimit: int = 64, + timeout: float = 3) -> Dict: + """Send an ICMPv6 Echo Request. + The default arguments are consistent with https://github.com/openthread/openthread/blob/main/src/core/utils/ping_sender.hpp. :param ip: The target IPv6 address to ping. - :param size: The number of data bytes in the payload. - :param count: The number of ICMPv6 Echo Requests to be sent. - :param interval: The interval between two consecutive ICMPv6 Echo Requests in seconds. The value may have fractional form, for example 0.5. - :param hoplimit: The hoplimit of ICMPv6 Echo Request to be sent. + :param size: The number of data bytes in the payload. Default is 8. + :param count: The number of ICMPv6 Echo Requests to be sent. Default is 1. + :param interval: The interval between two consecutive ICMPv6 Echo Requests in seconds. The value may have fractional form, for example 0.5. Default is 1. + :param hoplimit: The hoplimit of ICMPv6 Echo Request to be sent. Default is 64. See OPENTHREAD_CONFIG_IP6_HOP_LIMIT_DEFAULT in src/core/config/ip6.h. + :param timeout: The maximum duration in seconds for the ping command to wait after the final echo request is sent. Default is 3. """ - cmd = f'ping {ip}' + cmd = f'ping {ip} {size} {count} {interval} {hoplimit} {timeout}' - if size is not None: - cmd += f' {size}' + timeout_allowance = 3 + lines = self.execute_command(cmd, timeout=(count - 1) * interval + timeout + timeout_allowance) - if count is not None: - cmd += f' {count}' - - if interval is not None: - cmd += f' {interval}' - - if hoplimit is not None: - cmd += f' {hoplimit}' - - self.execute_command(cmd) + statistics = {} + for line in lines: + m = OTCI._PING_STATISTICS_PATTERN.match(line) + if m is not None: + if m.group('transmitted') is not None: + statistics['transmitted_packets'] = int(m.group('transmitted')) + statistics['received_packets'] = int(m.group('received')) + if m.group('loss') is not None: + statistics['packet_loss'] = float(m.group('loss')) / 100 + if m.group('min') is not None: + statistics['round_trip_time'] = { + 'min': int(m.group('min')), + 'avg': float(m.group('avg')), + 'max': int(m.group('max')) + } + return statistics def ping_stop(self): """Stop sending ICMPv6 Echo Requests.""" diff --git a/tools/otci/tests/test_otci.py b/tools/otci/tests/test_otci.py index dfdfb2673..8d12082b9 100644 --- a/tools/otci/tests/test_otci.py +++ b/tools/otci/tests/test_otci.py @@ -402,7 +402,12 @@ class TestOTCI(unittest.TestCase): self.assertEqual('router', commissioner.get_state()) for dst_ip in leader.get_ipaddrs(): - commissioner.ping(dst_ip, size=10, count=1, interval=2, hoplimit=3) + statistics = commissioner.ping(dst_ip, size=10, count=10, interval=2, hoplimit=3) + self.assertEqual(statistics['transmitted_packets'], 10) + self.assertEqual(statistics['received_packets'], 10) + self.assertAlmostEqual(statistics['packet_loss'], 0.0, delta=1e-9) + rtt = statistics['round_trip_time'] + self.assertTrue(rtt['min'] - 1e-9 <= rtt['avg'] <= rtt['max'] + 1e-9) commissioner.wait(1) self.assertEqual('disabled', commissioner.get_commissioiner_state()) @@ -513,6 +518,12 @@ class TestOTCI(unittest.TestCase): self.assertFalse(leader.is_singleton()) + statistics = commissioner.ping("ff02::1", size=1, count=10, interval=1, hoplimit=255) + self.assertEqual(statistics['transmitted_packets'], 10) + self.assertEqual(statistics['received_packets'], 20) + rtt = statistics['round_trip_time'] + self.assertTrue(rtt['min'] - 1e-9 <= rtt['avg'] <= rtt['max'] + 1e-9) + # Shutdown leader.thread_stop() logging.info("node state: %s", leader.get_state())