• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#  Copyright (c) 2016, The OpenThread Authors.
4#  All rights reserved.
5#
6#  Redistribution and use in source and binary forms, with or without
7#  modification, are permitted provided that the following conditions are met:
8#  1. Redistributions of source code must retain the above copyright
9#     notice, this list of conditions and the following disclaimer.
10#  2. Redistributions in binary form must reproduce the above copyright
11#     notice, this list of conditions and the following disclaimer in the
12#     documentation and/or other materials provided with the distribution.
13#  3. Neither the name of the copyright holder nor the
14#     names of its contributors may be used to endorse or promote products
15#     derived from this software without specific prior written permission.
16#
17#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27#  POSSIBILITY OF SUCH DAMAGE.
28#
29
30import json
31import binascii
32import ipaddress
33import logging
34import os
35import re
36import shlex
37import socket
38import subprocess
39import sys
40import time
41import traceback
42import typing
43import unittest
44from ipaddress import IPv6Address, IPv6Network
45from typing import Union, Dict, Optional, List, Any
46
47import pexpect
48import pexpect.popen_spawn
49
50import config
51import simulator
52import thread_cert
53
54PORT_OFFSET = int(os.getenv('PORT_OFFSET', "0"))
55
56INFRA_DNS64 = int(os.getenv('NAT64', 0))
57
58
59class OtbrDocker:
60    RESET_DELAY = 3
61
62    _socat_proc = None
63    _ot_rcp_proc = None
64    _docker_proc = None
65    _border_routing_counters = None
66
67    def __init__(self, nodeid: int, **kwargs):
68        self.verbose = int(float(os.getenv('VERBOSE', 0)))
69        try:
70            self._docker_name = config.OTBR_DOCKER_NAME_PREFIX + str(nodeid)
71            self._prepare_ot_rcp_sim(nodeid)
72            self._launch_docker()
73        except Exception:
74            traceback.print_exc()
75            self.destroy()
76            raise
77
78    def _prepare_ot_rcp_sim(self, nodeid: int):
79        self._socat_proc = subprocess.Popen(['socat', '-d', '-d', 'pty,raw,echo=0', 'pty,raw,echo=0'],
80                                            stderr=subprocess.PIPE,
81                                            stdin=subprocess.DEVNULL,
82                                            stdout=subprocess.DEVNULL)
83
84        line = self._socat_proc.stderr.readline().decode('ascii').strip()
85        self._rcp_device_pty = rcp_device_pty = line[line.index('PTY is /dev') + 7:]
86        line = self._socat_proc.stderr.readline().decode('ascii').strip()
87        self._rcp_device = rcp_device = line[line.index('PTY is /dev') + 7:]
88        logging.info(f"socat running: device PTY: {rcp_device_pty}, device: {rcp_device}")
89
90        ot_rcp_path = self._get_ot_rcp_path()
91        self._ot_rcp_proc = subprocess.Popen(f"{ot_rcp_path} {nodeid} > {rcp_device_pty} < {rcp_device_pty}",
92                                             shell=True,
93                                             stdin=subprocess.DEVNULL,
94                                             stdout=subprocess.DEVNULL,
95                                             stderr=subprocess.DEVNULL)
96
97        try:
98            self._ot_rcp_proc.wait(1)
99        except subprocess.TimeoutExpired:
100            # We expect ot-rcp not to quit in 1 second.
101            pass
102        else:
103            raise Exception(f"ot-rcp {nodeid} exited unexpectedly!")
104
105    def _get_ot_rcp_path(self) -> str:
106        srcdir = os.environ['top_builddir']
107        path = '%s/examples/apps/ncp/ot-rcp' % srcdir
108        logging.info("ot-rcp path: %s", path)
109        return path
110
111    def _launch_docker(self):
112        logging.info(f'Docker image: {config.OTBR_DOCKER_IMAGE}')
113        subprocess.check_call(f"docker rm -f {self._docker_name} || true", shell=True)
114        CI_ENV = os.getenv('CI_ENV', '').split()
115        dns = ['--dns=127.0.0.1'] if INFRA_DNS64 == 1 else ['--dns=8.8.8.8']
116        nat64_prefix = ['--nat64-prefix', '2001:db8:1:ffff::/96'] if INFRA_DNS64 == 1 else []
117        os.makedirs('/tmp/coverage/', exist_ok=True)
118
119        cmd = ['docker', 'run'] + CI_ENV + [
120            '--rm',
121            '--name',
122            self._docker_name,
123            '--network',
124            config.BACKBONE_DOCKER_NETWORK_NAME,
125        ] + dns + [
126            '-i',
127            '--sysctl',
128            'net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1',
129            '--privileged',
130            '--cap-add=NET_ADMIN',
131            '--volume',
132            f'{self._rcp_device}:/dev/ttyUSB0',
133            '-v',
134            '/tmp/coverage/:/tmp/coverage/',
135            config.OTBR_DOCKER_IMAGE,
136            '-B',
137            config.BACKBONE_IFNAME,
138            '--trel-url',
139            f'trel://{config.BACKBONE_IFNAME}',
140        ] + nat64_prefix
141        logging.info(' '.join(cmd))
142        self._docker_proc = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=sys.stdout, stderr=sys.stderr)
143
144        launch_docker_deadline = time.time() + 300
145        launch_ok = False
146
147        while time.time() < launch_docker_deadline:
148            try:
149                subprocess.check_call(f'docker exec -i {self._docker_name} ot-ctl state', shell=True)
150                launch_ok = True
151                logging.info("OTBR Docker %s Is Ready!", self._docker_name)
152                break
153            except subprocess.CalledProcessError:
154                time.sleep(5)
155                continue
156
157        assert launch_ok
158
159        self.start_ot_ctl()
160
161    def __repr__(self):
162        return f'OtbrDocker<{self.nodeid}>'
163
164    def start_otbr_service(self):
165        self.bash('service otbr-agent start')
166        self.simulator.go(3)
167        self.start_ot_ctl()
168
169    def stop_otbr_service(self):
170        self.stop_ot_ctl()
171        self.bash('service otbr-agent stop')
172
173    def stop_mdns_service(self):
174        self.bash('service avahi-daemon stop; service mdns stop; !(cat /proc/net/udp | grep -i :14E9)')
175
176    def start_mdns_service(self):
177        self.bash('service avahi-daemon start; service mdns start; cat /proc/net/udp | grep -i :14E9')
178
179    def start_ot_ctl(self):
180        cmd = f'docker exec -i {self._docker_name} ot-ctl'
181        self.pexpect = pexpect.popen_spawn.PopenSpawn(cmd, timeout=30)
182        if self.verbose:
183            self.pexpect.logfile_read = sys.stdout.buffer
184
185        # Add delay to ensure that the process is ready to receive commands.
186        timeout = 0.4
187        while timeout > 0:
188            self.pexpect.send('\r\n')
189            try:
190                self.pexpect.expect('> ', timeout=0.1)
191                break
192            except pexpect.TIMEOUT:
193                timeout -= 0.1
194
195    def stop_ot_ctl(self):
196        self.pexpect.sendeof()
197        self.pexpect.wait()
198        self.pexpect.proc.kill()
199
200    def reserve_udp_port(self, port):
201        self.bash(f'socat -u UDP6-LISTEN:{port},bindtodevice=wpan0 - &')
202
203    def destroy(self):
204        logging.info("Destroying %s", self)
205        self._shutdown_docker()
206        self._shutdown_ot_rcp()
207        self._shutdown_socat()
208
209    def _shutdown_docker(self):
210        if self._docker_proc is None:
211            return
212
213        try:
214            COVERAGE = int(os.getenv('COVERAGE', '0'))
215            OTBR_COVERAGE = int(os.getenv('OTBR_COVERAGE', '0'))
216            test_name = os.getenv('TEST_NAME')
217            unique_node_id = f'{test_name}-{PORT_OFFSET}-{self.nodeid}'
218
219            if COVERAGE or OTBR_COVERAGE:
220                self.bash('service otbr-agent stop')
221
222                cov_file_path = f'/tmp/coverage/coverage-{unique_node_id}.info'
223                # Upload OTBR code coverage if OTBR_COVERAGE=1, otherwise OpenThread code coverage.
224                if OTBR_COVERAGE:
225                    codecov_cmd = f'lcov --directory . --capture --output-file {cov_file_path}'
226                else:
227                    codecov_cmd = ('lcov --directory build/otbr/third_party/openthread/repo --capture '
228                                   f'--output-file {cov_file_path}')
229
230                self.bash(codecov_cmd)
231
232            copyCore = subprocess.run(f'docker cp {self._docker_name}:/core ./coredump_{unique_node_id}', shell=True)
233            if copyCore.returncode == 0:
234                subprocess.check_call(
235                    f'docker cp {self._docker_name}:/usr/sbin/otbr-agent ./otbr-agent_{unique_node_id}', shell=True)
236
237        finally:
238            subprocess.check_call(f"docker rm -f {self._docker_name}", shell=True)
239            self._docker_proc.wait()
240            del self._docker_proc
241
242    def _shutdown_ot_rcp(self):
243        if self._ot_rcp_proc is not None:
244            self._ot_rcp_proc.kill()
245            self._ot_rcp_proc.wait()
246            del self._ot_rcp_proc
247
248    def _shutdown_socat(self):
249        if self._socat_proc is not None:
250            self._socat_proc.stderr.close()
251            self._socat_proc.kill()
252            self._socat_proc.wait()
253            del self._socat_proc
254
255    def bash(self, cmd: str, encoding='ascii') -> List[str]:
256        logging.info("%s $ %s", self, cmd)
257        proc = subprocess.Popen(['docker', 'exec', '-i', self._docker_name, 'bash', '-c', cmd],
258                                stdin=subprocess.DEVNULL,
259                                stdout=subprocess.PIPE,
260                                stderr=sys.stderr,
261                                encoding=encoding)
262
263        with proc:
264
265            lines = []
266
267            while True:
268                line = proc.stdout.readline()
269
270                if not line:
271                    break
272
273                lines.append(line)
274                logging.info("%s $ %r", self, line.rstrip('\r\n'))
275
276            proc.wait()
277
278            if proc.returncode != 0:
279                raise subprocess.CalledProcessError(proc.returncode, cmd, ''.join(lines))
280            else:
281                return lines
282
283    def dns_dig(self, server: str, name: str, qtype: str):
284        """
285        Run dig command to query a DNS server.
286
287        Args:
288            server: the server address.
289            name: the name to query.
290            qtype: the query type (e.g. AAAA, PTR, TXT, SRV).
291
292        Returns:
293            The dig result similar as below:
294            {
295                "opcode": "QUERY",
296                "status": "NOERROR",
297                "id": "64144",
298                "QUESTION": [
299                    ('google.com.', 'IN', 'AAAA')
300                ],
301                "ANSWER": [
302                    ('google.com.', 107,	'IN', 'AAAA', '2404:6800:4008:c00::71'),
303                    ('google.com.', 107,	'IN', 'AAAA', '2404:6800:4008:c00::8a'),
304                    ('google.com.', 107,	'IN', 'AAAA', '2404:6800:4008:c00::66'),
305                    ('google.com.', 107,	'IN', 'AAAA', '2404:6800:4008:c00::8b'),
306                ],
307                "ADDITIONAL": [
308                ],
309            }
310        """
311        output = self.bash(f'dig -6 @{server} \'{name}\' {qtype}', encoding='raw_unicode_escape')
312
313        section = None
314        dig_result = {
315            'QUESTION': [],
316            'ANSWER': [],
317            'ADDITIONAL': [],
318        }
319
320        for line in output:
321            line = line.strip()
322
323            if line.startswith(';; ->>HEADER<<- '):
324                headers = line[len(';; ->>HEADER<<- '):].split(', ')
325                for header in headers:
326                    key, val = header.split(': ')
327                    dig_result[key] = val
328
329                continue
330
331            if line == ';; QUESTION SECTION:':
332                section = 'QUESTION'
333                continue
334            elif line == ';; ANSWER SECTION:':
335                section = 'ANSWER'
336                continue
337            elif line == ';; ADDITIONAL SECTION:':
338                section = 'ADDITIONAL'
339                continue
340            elif section and not line:
341                section = None
342                continue
343
344            if section:
345                assert line
346
347                if section == 'QUESTION':
348                    assert line.startswith(';')
349                    line = line[1:]
350                record = list(line.split())
351
352                if section == 'QUESTION':
353                    if record[2] in ('SRV', 'TXT'):
354                        record[0] = self.__unescape_dns_instance_name(record[0])
355                else:
356                    record[1] = int(record[1])
357                    if record[3] == 'SRV':
358                        record[0] = self.__unescape_dns_instance_name(record[0])
359                        record[4], record[5], record[6] = map(int, [record[4], record[5], record[6]])
360                    elif record[3] == 'TXT':
361                        record[0] = self.__unescape_dns_instance_name(record[0])
362                        record[4:] = [self.__parse_dns_dig_txt(line)]
363                    elif record[3] == 'PTR':
364                        record[4] = self.__unescape_dns_instance_name(record[4])
365
366                dig_result[section].append(tuple(record))
367
368        return dig_result
369
370    def call_dbus_method(self, *args):
371        args = shlex.join([args[0], args[1], json.dumps(args[2:])])
372        return json.loads(
373            self.bash(f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/call_dbus_method.py {args}')
374            [0])
375
376    def get_dbus_property(self, property_name):
377        return self.call_dbus_method('org.freedesktop.DBus.Properties', 'Get', 'io.openthread.BorderRouter',
378                                     property_name)
379
380    def set_dbus_property(self, property_name, property_value):
381        return self.call_dbus_method('org.freedesktop.DBus.Properties', 'Set', 'io.openthread.BorderRouter',
382                                     property_name, property_value)
383
384    def get_border_routing_counters(self):
385        counters = self.get_dbus_property('BorderRoutingCounters')
386        counters = {
387            'inbound_unicast': counters[0],
388            'inbound_multicast': counters[1],
389            'outbound_unicast': counters[2],
390            'outbound_multicast': counters[3],
391            'ra_rx': counters[4],
392            'ra_tx_success': counters[5],
393            'ra_tx_failure': counters[6],
394            'rs_rx': counters[7],
395            'rs_tx_success': counters[8],
396            'rs_tx_failure': counters[9],
397        }
398        logging.info(f'border routing counters: {counters}')
399        return counters
400
401    def _process_traffic_counters(self, counter):
402        return {
403            '4to6': {
404                'packets': counter[0],
405                'bytes': counter[1],
406            },
407            '6to4': {
408                'packets': counter[2],
409                'bytes': counter[3],
410            }
411        }
412
413    def _process_packet_counters(self, counter):
414        return {'4to6': {'packets': counter[0]}, '6to4': {'packets': counter[1]}}
415
416    def nat64_set_enabled(self, enable):
417        return self.call_dbus_method('io.openthread.BorderRouter', 'SetNat64Enabled', enable)
418
419    @property
420    def nat64_cidr(self):
421        self.send_command('nat64 cidr')
422        cidr = self._expect_command_output()[0].strip()
423        return ipaddress.IPv4Network(cidr, strict=False)
424
425    @nat64_cidr.setter
426    def nat64_cidr(self, cidr: ipaddress.IPv4Network):
427        if not isinstance(cidr, ipaddress.IPv4Network):
428            raise ValueError("cidr is expected to be an instance of ipaddress.IPv4Network")
429        self.send_command(f'nat64 cidr {cidr}')
430        self._expect_done()
431
432    @property
433    def nat64_state(self):
434        state = self.get_dbus_property('Nat64State')
435        return {'PrefixManager': state[0], 'Translator': state[1]}
436
437    @property
438    def nat64_mappings(self):
439        return [{
440            'id': row[0],
441            'ip4': row[1],
442            'ip6': row[2],
443            'expiry': row[3],
444            'counters': {
445                'total': self._process_traffic_counters(row[4][0]),
446                'ICMP': self._process_traffic_counters(row[4][1]),
447                'UDP': self._process_traffic_counters(row[4][2]),
448                'TCP': self._process_traffic_counters(row[4][3]),
449            }
450        } for row in self.get_dbus_property('Nat64Mappings')]
451
452    @property
453    def nat64_counters(self):
454        res_error = self.get_dbus_property('Nat64ErrorCounters')
455        res_proto = self.get_dbus_property('Nat64ProtocolCounters')
456        return {
457            'protocol': {
458                'Total': self._process_traffic_counters(res_proto[0]),
459                'ICMP': self._process_traffic_counters(res_proto[1]),
460                'UDP': self._process_traffic_counters(res_proto[2]),
461                'TCP': self._process_traffic_counters(res_proto[3]),
462            },
463            'errors': {
464                'Unknown': self._process_packet_counters(res_error[0]),
465                'Illegal Pkt': self._process_packet_counters(res_error[1]),
466                'Unsup Proto': self._process_packet_counters(res_error[2]),
467                'No Mapping': self._process_packet_counters(res_error[3]),
468            }
469        }
470
471    @property
472    def nat64_traffic_counters(self):
473        res = self.get_dbus_property('Nat64TrafficCounters')
474        return {
475            'Total': self._process_traffic_counters(res[0]),
476            'ICMP': self._process_traffic_counters(res[1]),
477            'UDP': self._process_traffic_counters(res[2]),
478            'TCP': self._process_traffic_counters(res[3]),
479        }
480
481    @property
482    def dns_upstream_query_state(self):
483        return bool(self.get_dbus_property('DnsUpstreamQueryState'))
484
485    @dns_upstream_query_state.setter
486    def dns_upstream_query_state(self, value):
487        if type(value) is not bool:
488            raise ValueError("dns_upstream_query_state must be a bool")
489        return self.set_dbus_property('DnsUpstreamQueryState', value)
490
491    def read_border_routing_counters_delta(self):
492        old_counters = self._border_routing_counters
493        new_counters = self.get_border_routing_counters()
494        self._border_routing_counters = new_counters
495        delta_counters = {}
496        if old_counters is None:
497            delta_counters = new_counters
498        else:
499            for i in ('inbound', 'outbound'):
500                for j in ('unicast', 'multicast'):
501                    key = f'{i}_{j}'
502                    assert (key in old_counters)
503                    assert (key in new_counters)
504                    value = [new_counters[key][0] - old_counters[key][0], new_counters[key][1] - old_counters[key][1]]
505                    delta_counters[key] = value
506        delta_counters = {
507            key: value for key, value in delta_counters.items() if not isinstance(value, int) and value[0] and value[1]
508        }
509
510        return delta_counters
511
512    @staticmethod
513    def __unescape_dns_instance_name(name: str) -> str:
514        new_name = []
515        i = 0
516        while i < len(name):
517            c = name[i]
518
519            if c == '\\':
520                assert i + 1 < len(name), name
521                if name[i + 1].isdigit():
522                    assert i + 3 < len(name) and name[i + 2].isdigit() and name[i + 3].isdigit(), name
523                    new_name.append(chr(int(name[i + 1:i + 4])))
524                    i += 3
525                else:
526                    new_name.append(name[i + 1])
527                    i += 1
528            else:
529                new_name.append(c)
530
531            i += 1
532
533        return ''.join(new_name)
534
535    def __parse_dns_dig_txt(self, line: str):
536        # Example TXT entry:
537        # "xp=\\000\\013\\184\\000\\000\\000\\000\\000"
538        txt = {}
539        for entry in re.findall(r'"((?:[^\\]|\\.)*?)"', line):
540            if entry == "":
541                continue
542
543            k, v = entry.split('=', 1)
544            txt[k] = v
545
546        return txt
547
548    def _setup_sysctl(self):
549        self.bash(f'sysctl net.ipv6.conf.{self.ETH_DEV}.accept_ra=2')
550        self.bash(f'sysctl net.ipv6.conf.{self.ETH_DEV}.accept_ra_rt_info_max_plen=64')
551
552
553class OtCli:
554    RESET_DELAY = 0.1
555
556    def __init__(self, nodeid, is_mtd=False, version=None, is_bbr=False, **kwargs):
557        self.verbose = int(float(os.getenv('VERBOSE', 0)))
558        self.node_type = os.getenv('NODE_TYPE', 'sim')
559        self.env_version = os.getenv('THREAD_VERSION', '1.1')
560        self.is_bbr = is_bbr
561        self._initialized = False
562        if os.getenv('COVERAGE', 0) and os.getenv('CC', 'gcc') == 'gcc':
563            self._cmd_prefix = '/usr/bin/env GCOV_PREFIX=%s/ot-run/%s/ot-gcda.%d ' % (os.getenv(
564                'top_srcdir', '.'), sys.argv[0], nodeid)
565        else:
566            self._cmd_prefix = ''
567
568        if version is not None:
569            self.version = version
570        else:
571            self.version = self.env_version
572
573        mode = os.environ.get('USE_MTD') == '1' and is_mtd and 'mtd' or 'ftd'
574
575        if self.node_type == 'soc':
576            self.__init_soc(nodeid)
577        elif self.node_type == 'ncp-sim':
578            # TODO use mode after ncp-mtd is available.
579            self.__init_ncp_sim(nodeid, 'ftd')
580        else:
581            self.__init_sim(nodeid, mode)
582
583        if self.verbose:
584            self.pexpect.logfile_read = sys.stdout.buffer
585
586        self._initialized = True
587
588    def __init_sim(self, nodeid, mode):
589        """ Initialize a simulation node. """
590
591        # Default command if no match below, will be overridden if below conditions are met.
592        cmd = './ot-cli-%s' % (mode)
593
594        # For Thread 1.2 MTD node, use ot-cli-mtd build regardless of OT_CLI_PATH
595        if self.version != '1.1' and mode == 'mtd' and 'top_builddir' in os.environ:
596            srcdir = os.environ['top_builddir']
597            cmd = '%s/examples/apps/cli/ot-cli-%s %d' % (srcdir, mode, nodeid)
598
599        # If Thread version of node matches the testing environment version.
600        elif self.version == self.env_version:
601            # Load Thread 1.2 BBR device when testing Thread 1.2 scenarios
602            # which requires device with Backbone functionality.
603            if self.version != '1.1' and self.is_bbr:
604                if 'OT_CLI_PATH_BBR' in os.environ:
605                    cmd = os.environ['OT_CLI_PATH_BBR']
606                elif 'top_builddir_1_3_bbr' in os.environ:
607                    srcdir = os.environ['top_builddir_1_3_bbr']
608                    cmd = '%s/examples/apps/cli/ot-cli-%s' % (srcdir, mode)
609
610            # Load Thread device of the testing environment version (may be 1.1 or 1.2)
611            else:
612                if 'OT_CLI_PATH' in os.environ:
613                    cmd = os.environ['OT_CLI_PATH']
614                elif 'top_builddir' in os.environ:
615                    srcdir = os.environ['top_builddir']
616                    cmd = '%s/examples/apps/cli/ot-cli-%s' % (srcdir, mode)
617
618            if 'RADIO_DEVICE' in os.environ:
619                cmd += ' --real-time-signal=+1 -v spinel+hdlc+uart://%s?forkpty-arg=%d' % (os.environ['RADIO_DEVICE'],
620                                                                                           nodeid)
621                self.is_posix = True
622            else:
623                cmd += ' %d' % nodeid
624
625        # Load Thread 1.1 node when testing Thread 1.2 scenarios for interoperability
626        elif self.version == '1.1':
627            # Posix app
628            if 'OT_CLI_PATH_1_1' in os.environ:
629                cmd = os.environ['OT_CLI_PATH_1_1']
630            elif 'top_builddir_1_1' in os.environ:
631                srcdir = os.environ['top_builddir_1_1']
632                cmd = '%s/examples/apps/cli/ot-cli-%s' % (srcdir, mode)
633
634            if 'RADIO_DEVICE_1_1' in os.environ:
635                cmd += ' --real-time-signal=+1 -v spinel+hdlc+uart://%s?forkpty-arg=%d' % (
636                    os.environ['RADIO_DEVICE_1_1'], nodeid)
637                self.is_posix = True
638            else:
639                cmd += ' %d' % nodeid
640
641        print("%s" % cmd)
642
643        self.pexpect = pexpect.popen_spawn.PopenSpawn(self._cmd_prefix + cmd, timeout=10)
644
645        # Add delay to ensure that the process is ready to receive commands.
646        timeout = 0.4
647        while timeout > 0:
648            self.pexpect.send('\r\n')
649            try:
650                self.pexpect.expect('> ', timeout=0.1)
651                break
652            except pexpect.TIMEOUT:
653                timeout -= 0.1
654
655    def __init_ncp_sim(self, nodeid, mode):
656        """ Initialize an NCP simulation node. """
657
658        # Default command if no match below, will be overridden if below conditions are met.
659        cmd = 'spinel-cli.py -p ./ot-ncp-%s -n' % mode
660
661        # If Thread version of node matches the testing environment version.
662        if self.version == self.env_version:
663            if 'RADIO_DEVICE' in os.environ:
664                args = ' --real-time-signal=+1 spinel+hdlc+uart://%s?forkpty-arg=%d' % (os.environ['RADIO_DEVICE'],
665                                                                                        nodeid)
666                self.is_posix = True
667            else:
668                args = ''
669
670            # Load Thread 1.2 BBR device when testing Thread 1.2 scenarios
671            # which requires device with Backbone functionality.
672            if self.version != '1.1' and self.is_bbr:
673                if 'OT_NCP_PATH_1_3_BBR' in os.environ:
674                    cmd = 'spinel-cli.py -p "%s%s" -n' % (
675                        os.environ['OT_NCP_PATH_1_3_BBR'],
676                        args,
677                    )
678                elif 'top_builddir_1_3_bbr' in os.environ:
679                    srcdir = os.environ['top_builddir_1_3_bbr']
680                    cmd = '%s/examples/apps/ncp/ot-ncp-%s' % (srcdir, mode)
681                    cmd = 'spinel-cli.py -p "%s%s" -n' % (
682                        cmd,
683                        args,
684                    )
685
686            # Load Thread device of the testing environment version (may be 1.1 or 1.2).
687            else:
688                if 'OT_NCP_PATH' in os.environ:
689                    cmd = 'spinel-cli.py -p "%s%s" -n' % (
690                        os.environ['OT_NCP_PATH'],
691                        args,
692                    )
693                elif 'top_builddir' in os.environ:
694                    srcdir = os.environ['top_builddir']
695                    cmd = '%s/examples/apps/ncp/ot-ncp-%s' % (srcdir, mode)
696                    cmd = 'spinel-cli.py -p "%s%s" -n' % (
697                        cmd,
698                        args,
699                    )
700
701        # Load Thread 1.1 node when testing Thread 1.2 scenarios for interoperability.
702        elif self.version == '1.1':
703            if 'RADIO_DEVICE_1_1' in os.environ:
704                args = ' --real-time-signal=+1 spinel+hdlc+uart://%s?forkpty-arg=%d' % (os.environ['RADIO_DEVICE_1_1'],
705                                                                                        nodeid)
706                self.is_posix = True
707            else:
708                args = ''
709
710            if 'OT_NCP_PATH_1_1' in os.environ:
711                cmd = 'spinel-cli.py -p "%s%s" -n' % (
712                    os.environ['OT_NCP_PATH_1_1'],
713                    args,
714                )
715            elif 'top_builddir_1_1' in os.environ:
716                srcdir = os.environ['top_builddir_1_1']
717                cmd = '%s/examples/apps/ncp/ot-ncp-%s' % (srcdir, mode)
718                cmd = 'spinel-cli.py -p "%s%s" -n' % (
719                    cmd,
720                    args,
721                )
722
723        cmd += ' %d' % nodeid
724        print("%s" % cmd)
725
726        self.pexpect = pexpect.spawn(self._cmd_prefix + cmd, timeout=10)
727
728        # Add delay to ensure that the process is ready to receive commands.
729        time.sleep(0.2)
730        self._expect('spinel-cli >')
731        self.debug(int(os.getenv('DEBUG', '0')))
732
733    def __init_soc(self, nodeid):
734        """ Initialize a System-on-a-chip node connected via UART. """
735        import fdpexpect
736
737        serialPort = '/dev/ttyUSB%d' % ((nodeid - 1) * 2)
738        self.pexpect = fdpexpect.fdspawn(os.open(serialPort, os.O_RDWR | os.O_NONBLOCK | os.O_NOCTTY))
739
740    def destroy(self):
741        if not self._initialized:
742            return
743
744        if (hasattr(self.pexpect, 'proc') and self.pexpect.proc.poll() is None or
745                not hasattr(self.pexpect, 'proc') and self.pexpect.isalive()):
746            print("%d: exit" % self.nodeid)
747            self.pexpect.send('exit\n')
748            self.pexpect.expect(pexpect.EOF)
749            self.pexpect.wait()
750            self._initialized = False
751
752
753class NodeImpl:
754    is_host = False
755    is_otbr = False
756
757    def __init__(self, nodeid, name=None, simulator=None, **kwargs):
758        self.nodeid = nodeid
759        self.name = name or ('Node%d' % nodeid)
760        self.is_posix = False
761
762        self.simulator = simulator
763        if self.simulator:
764            self.simulator.add_node(self)
765
766        super().__init__(nodeid, **kwargs)
767
768        self.set_addr64('%016x' % (thread_cert.EXTENDED_ADDRESS_BASE + nodeid))
769
770    def _expect(self, pattern, timeout=-1, *args, **kwargs):
771        """ Process simulator events until expected the pattern. """
772        if timeout == -1:
773            timeout = self.pexpect.timeout
774
775        assert timeout > 0
776
777        while timeout > 0:
778            try:
779                return self.pexpect.expect(pattern, 0.1, *args, **kwargs)
780            except pexpect.TIMEOUT:
781                timeout -= 0.1
782                self.simulator.go(0)
783                if timeout <= 0:
784                    raise
785
786    def _expect_done(self, timeout=-1):
787        self._expect('Done', timeout)
788
789    def _expect_result(self, pattern, *args, **kwargs):
790        """Expect a single matching result.
791
792        The arguments are identical to pexpect.expect().
793
794        Returns:
795            The matched line.
796        """
797        results = self._expect_results(pattern, *args, **kwargs)
798        assert len(results) == 1, results
799        return results[0]
800
801    def _expect_results(self, pattern, *args, **kwargs):
802        """Expect multiple matching results.
803
804        The arguments are identical to pexpect.expect().
805
806        Returns:
807            The matched lines.
808        """
809        output = self._expect_command_output()
810        results = [line for line in output if self._match_pattern(line, pattern)]
811        return results
812
813    def _expect_key_value_pairs(self, pattern, separator=': '):
814        """Expect 'key: value' in multiple lines.
815
816        Returns:
817            Dictionary of the key:value pairs.
818        """
819        result = {}
820        for line in self._expect_results(pattern):
821            key, val = line.split(separator)
822            result.update({key: val})
823        return result
824
825    @staticmethod
826    def _match_pattern(line, pattern):
827        if isinstance(pattern, str):
828            pattern = re.compile(pattern)
829
830        if isinstance(pattern, typing.Pattern):
831            return pattern.match(line)
832        else:
833            return any(NodeImpl._match_pattern(line, p) for p in pattern)
834
835    def _expect_command_output(self, ignore_logs=True):
836        lines = []
837
838        while True:
839            line = self.__readline(ignore_logs=ignore_logs)
840
841            if line == 'Done':
842                break
843            elif line.startswith('Error '):
844                raise Exception(line)
845            else:
846                lines.append(line)
847
848        print(f'_expect_command_output() returns {lines!r}')
849        return lines
850
851    def __is_logging_line(self, line: str) -> bool:
852        return len(line) >= 3 and line[:3] in {'[D]', '[I]', '[N]', '[W]', '[C]', '[-]'}
853
854    def read_cert_messages_in_commissioning_log(self, timeout=-1):
855        """Get the log of the traffic after DTLS handshake.
856        """
857        format_str = br"=+?\[\[THCI\].*?type=%s.*?\].*?=+?[\s\S]+?-{40,}"
858        join_fin_req = format_str % br"JOIN_FIN\.req"
859        join_fin_rsp = format_str % br"JOIN_FIN\.rsp"
860        dummy_format_str = br"\[THCI\].*?type=%s.*?"
861        join_ent_ntf = dummy_format_str % br"JOIN_ENT\.ntf"
862        join_ent_rsp = dummy_format_str % br"JOIN_ENT\.rsp"
863        pattern = (b"(" + join_fin_req + b")|(" + join_fin_rsp + b")|(" + join_ent_ntf + b")|(" + join_ent_rsp + b")")
864
865        messages = []
866        # There are at most 4 cert messages both for joiner and commissioner
867        for _ in range(0, 4):
868            try:
869                self._expect(pattern, timeout=timeout)
870                log = self.pexpect.match.group(0)
871                messages.append(self._extract_cert_message(log))
872            except BaseException:
873                break
874        return messages
875
876    def _extract_cert_message(self, log):
877        res = re.search(br"direction=\w+", log)
878        assert res
879        direction = res.group(0).split(b'=')[1].strip()
880
881        res = re.search(br"type=\S+", log)
882        assert res
883        type = res.group(0).split(b'=')[1].strip()
884
885        payload = bytearray([])
886        payload_len = 0
887        if type in [b"JOIN_FIN.req", b"JOIN_FIN.rsp"]:
888            res = re.search(br"len=\d+", log)
889            assert res
890            payload_len = int(res.group(0).split(b'=')[1].strip())
891
892            hex_pattern = br"\|(\s([0-9a-fA-F]{2}|\.\.))+?\s+?\|"
893            while True:
894                res = re.search(hex_pattern, log)
895                if not res:
896                    break
897                data = [int(hex, 16) for hex in res.group(0)[1:-1].split(b' ') if hex and hex != b'..']
898                payload += bytearray(data)
899                log = log[res.end() - 1:]
900        assert len(payload) == payload_len
901        return (direction, type, payload)
902
903    def send_command(self, cmd, go=True, expect_command_echo=True):
904        print("%d: %s" % (self.nodeid, cmd))
905        self.pexpect.send(cmd + '\n')
906        if go:
907            self.simulator.go(0, nodeid=self.nodeid)
908        sys.stdout.flush()
909
910        if expect_command_echo:
911            self._expect_command_echo(cmd)
912
913    def _expect_command_echo(self, cmd):
914        cmd = cmd.strip()
915        while True:
916            line = self.__readline()
917            if line == cmd:
918                break
919
920            logging.warning("expecting echo %r, but read %r", cmd, line)
921
922    def __readline(self, ignore_logs=True):
923        PROMPT = 'spinel-cli > ' if self.node_type == 'ncp-sim' else '> '
924        while True:
925            self._expect(r"[^\n]+\n")
926            line = self.pexpect.match.group(0).decode('utf8').strip()
927            while line.startswith(PROMPT):
928                line = line[len(PROMPT):]
929
930            if line == '':
931                continue
932
933            if ignore_logs and self.__is_logging_line(line):
934                continue
935
936            return line
937
938    def get_commands(self):
939        self.send_command('?')
940        self._expect('Commands:')
941        return self._expect_results(r'\S+')
942
943    def set_mode(self, mode):
944        cmd = 'mode %s' % mode
945        self.send_command(cmd)
946        self._expect_done()
947
948    def debug(self, level):
949        # `debug` command will not trigger interaction with simulator
950        self.send_command('debug %d' % level, go=False)
951
952    def start(self):
953        self.interface_up()
954        self.thread_start()
955
956    def stop(self):
957        self.thread_stop()
958        self.interface_down()
959
960    def set_log_level(self, level: int):
961        self.send_command(f'log level {level}')
962        self._expect_done()
963
964    def interface_up(self):
965        self.send_command('ifconfig up')
966        self._expect_done()
967
968    def interface_down(self):
969        self.send_command('ifconfig down')
970        self._expect_done()
971
972    def thread_start(self):
973        self.send_command('thread start')
974        self._expect_done()
975
976    def thread_stop(self):
977        self.send_command('thread stop')
978        self._expect_done()
979
980    def detach(self, is_async=False):
981        cmd = 'detach'
982        if is_async:
983            cmd += ' async'
984
985        self.send_command(cmd)
986
987        if is_async:
988            self._expect_done()
989            return
990
991        end = self.simulator.now() + 4
992        while True:
993            self.simulator.go(1)
994            try:
995                self._expect_done(timeout=0.1)
996                return
997            except (pexpect.TIMEOUT, socket.timeout):
998                if self.simulator.now() > end:
999                    raise
1000
1001    def expect_finished_detaching(self):
1002        self._expect('Finished detaching')
1003
1004    def commissioner_start(self):
1005        cmd = 'commissioner start'
1006        self.send_command(cmd)
1007        self._expect_done()
1008
1009    def commissioner_stop(self):
1010        cmd = 'commissioner stop'
1011        self.send_command(cmd)
1012        self._expect_done()
1013
1014    def commissioner_state(self):
1015        states = [r'disabled', r'petitioning', r'active']
1016        self.send_command('commissioner state')
1017        return self._expect_result(states)
1018
1019    def commissioner_add_joiner(self, addr, psk):
1020        cmd = 'commissioner joiner add %s %s' % (addr, psk)
1021        self.send_command(cmd)
1022        self._expect_done()
1023
1024    def commissioner_set_provisioning_url(self, provisioning_url=''):
1025        cmd = 'commissioner provisioningurl %s' % provisioning_url
1026        self.send_command(cmd)
1027        self._expect_done()
1028
1029    def joiner_start(self, pskd='', provisioning_url=''):
1030        cmd = 'joiner start %s %s' % (pskd, provisioning_url)
1031        self.send_command(cmd)
1032        self._expect_done()
1033
1034    def clear_allowlist(self):
1035        cmd = 'macfilter addr clear'
1036        self.send_command(cmd)
1037        self._expect_done()
1038
1039    def enable_allowlist(self):
1040        cmd = 'macfilter addr allowlist'
1041        self.send_command(cmd)
1042        self._expect_done()
1043
1044    def disable_allowlist(self):
1045        cmd = 'macfilter addr disable'
1046        self.send_command(cmd)
1047        self._expect_done()
1048
1049    def add_allowlist(self, addr, rssi=None):
1050        cmd = 'macfilter addr add %s' % addr
1051
1052        if rssi is not None:
1053            cmd += ' %s' % rssi
1054
1055        self.send_command(cmd)
1056        self._expect_done()
1057
1058    def radiofilter_is_enabled(self) -> bool:
1059        states = [r'Disabled', r'Enabled']
1060        self.send_command('radiofilter')
1061        return self._expect_result(states) == 'Enabled'
1062
1063    def radiofilter_enable(self):
1064        cmd = 'radiofilter enable'
1065        self.send_command(cmd)
1066        self._expect_done()
1067
1068    def radiofilter_disable(self):
1069        cmd = 'radiofilter disable'
1070        self.send_command(cmd)
1071        self._expect_done()
1072
1073    def get_bbr_registration_jitter(self):
1074        self.send_command('bbr jitter')
1075        return int(self._expect_result(r'\d+'))
1076
1077    def set_bbr_registration_jitter(self, jitter):
1078        cmd = 'bbr jitter %d' % jitter
1079        self.send_command(cmd)
1080        self._expect_done()
1081
1082    def get_rcp_version(self) -> str:
1083        self.send_command('rcp version')
1084        rcp_version = self._expect_command_output()[0].strip()
1085        return rcp_version
1086
1087    def srp_server_get_state(self):
1088        states = ['disabled', 'running', 'stopped']
1089        self.send_command('srp server state')
1090        return self._expect_result(states)
1091
1092    def srp_server_get_addr_mode(self):
1093        modes = [r'unicast', r'anycast']
1094        self.send_command(f'srp server addrmode')
1095        return self._expect_result(modes)
1096
1097    def srp_server_set_addr_mode(self, mode):
1098        self.send_command(f'srp server addrmode {mode}')
1099        self._expect_done()
1100
1101    def srp_server_get_anycast_seq_num(self):
1102        self.send_command(f'srp server seqnum')
1103        return int(self._expect_result(r'\d+'))
1104
1105    def srp_server_set_anycast_seq_num(self, seqnum):
1106        self.send_command(f'srp server seqnum {seqnum}')
1107        self._expect_done()
1108
1109    def srp_server_set_enabled(self, enable):
1110        cmd = f'srp server {"enable" if enable else "disable"}'
1111        self.send_command(cmd)
1112        self._expect_done()
1113
1114    def srp_server_set_lease_range(self, min_lease, max_lease, min_key_lease, max_key_lease):
1115        self.send_command(f'srp server lease {min_lease} {max_lease} {min_key_lease} {max_key_lease}')
1116        self._expect_done()
1117
1118    def srp_server_set_ttl_range(self, min_ttl, max_ttl):
1119        self.send_command(f'srp server ttl {min_ttl} {max_ttl}')
1120        self._expect_done()
1121
1122    def srp_server_get_hosts(self):
1123        """Returns the host list on the SRP server as a list of property
1124           dictionary.
1125
1126           Example output:
1127           [{
1128               'fullname': 'my-host.default.service.arpa.',
1129               'name': 'my-host',
1130               'deleted': 'false',
1131               'addresses': ['2001::1', '2001::2']
1132           }]
1133        """
1134
1135        cmd = 'srp server host'
1136        self.send_command(cmd)
1137        lines = self._expect_command_output()
1138        host_list = []
1139        while lines:
1140            host = {}
1141
1142            host['fullname'] = lines.pop(0).strip()
1143            host['name'] = host['fullname'].split('.')[0]
1144
1145            host['deleted'] = lines.pop(0).strip().split(':')[1].strip()
1146            if host['deleted'] == 'true':
1147                host_list.append(host)
1148                continue
1149
1150            addresses = lines.pop(0).strip().split('[')[1].strip(' ]').split(',')
1151            map(str.strip, addresses)
1152            host['addresses'] = [addr.strip() for addr in addresses if addr]
1153
1154            host_list.append(host)
1155
1156        return host_list
1157
1158    def srp_server_get_host(self, host_name):
1159        """Returns host on the SRP server that matches given host name.
1160
1161           Example usage:
1162           self.srp_server_get_host("my-host")
1163        """
1164
1165        for host in self.srp_server_get_hosts():
1166            if host_name == host['name']:
1167                return host
1168
1169    def srp_server_get_services(self):
1170        """Returns the service list on the SRP server as a list of property
1171           dictionary.
1172
1173           Example output:
1174           [{
1175               'fullname': 'my-service._ipps._tcp.default.service.arpa.',
1176               'instance': 'my-service',
1177               'name': '_ipps._tcp',
1178               'deleted': 'false',
1179               'port': '12345',
1180               'priority': '0',
1181               'weight': '0',
1182               'ttl': '7200',
1183               'lease': '7200',
1184               'key-lease': '7200',
1185               'TXT': ['abc=010203'],
1186               'host_fullname': 'my-host.default.service.arpa.',
1187               'host': 'my-host',
1188               'addresses': ['2001::1', '2001::2']
1189           }]
1190
1191           Note that the TXT data is output as a HEX string.
1192        """
1193
1194        cmd = 'srp server service'
1195        self.send_command(cmd)
1196        lines = self._expect_command_output()
1197
1198        service_list = []
1199        while lines:
1200            service = {}
1201
1202            service['fullname'] = lines.pop(0).strip()
1203            name_labels = service['fullname'].split('.')
1204            service['instance'] = name_labels[0]
1205            service['name'] = '.'.join(name_labels[1:3])
1206
1207            service['deleted'] = lines.pop(0).strip().split(':')[1].strip()
1208            if service['deleted'] == 'true':
1209                service_list.append(service)
1210                continue
1211
1212            # 'subtypes', port', 'priority', 'weight', 'ttl', 'lease', and 'key-lease'
1213            for i in range(0, 7):
1214                key_value = lines.pop(0).strip().split(':')
1215                service[key_value[0].strip()] = key_value[1].strip()
1216
1217            txt_entries = lines.pop(0).strip().split('[')[1].strip(' ]').split(',')
1218            txt_entries = map(str.strip, txt_entries)
1219            service['TXT'] = [txt for txt in txt_entries if txt]
1220
1221            service['host_fullname'] = lines.pop(0).strip().split(':')[1].strip()
1222            service['host'] = service['host_fullname'].split('.')[0]
1223
1224            addresses = lines.pop(0).strip().split('[')[1].strip(' ]').split(',')
1225            addresses = map(str.strip, addresses)
1226            service['addresses'] = [addr for addr in addresses if addr]
1227
1228            service_list.append(service)
1229
1230        return service_list
1231
1232    def srp_server_get_service(self, instance_name, service_name):
1233        """Returns service on the SRP server that matches given instance
1234           name and service name.
1235
1236           Example usage:
1237           self.srp_server_get_service("my-service", "_ipps._tcp")
1238        """
1239
1240        for service in self.srp_server_get_services():
1241            if (instance_name == service['instance'] and service_name == service['name']):
1242                return service
1243
1244    def get_srp_server_port(self):
1245        """Returns the SRP server UDP port by parsing
1246           the SRP Server Data in Network Data.
1247        """
1248
1249        for service in self.get_services():
1250            # TODO: for now, we are using 0xfd as the SRP service data.
1251            #       May use a dedicated bit flag for SRP server.
1252            if int(service[1], 16) == 0x5d:
1253                # The SRP server data contains IPv6 address (16 bytes)
1254                # followed by UDP port number.
1255                return int(service[2][2 * 16:], 16)
1256
1257    def srp_client_start(self, server_address, server_port):
1258        self.send_command(f'srp client start {server_address} {server_port}')
1259        self._expect_done()
1260
1261    def srp_client_stop(self):
1262        self.send_command(f'srp client stop')
1263        self._expect_done()
1264
1265    def srp_client_get_state(self):
1266        cmd = 'srp client state'
1267        self.send_command(cmd)
1268        return self._expect_command_output()[0]
1269
1270    def srp_client_get_auto_start_mode(self):
1271        cmd = 'srp client autostart'
1272        self.send_command(cmd)
1273        return self._expect_command_output()[0]
1274
1275    def srp_client_enable_auto_start_mode(self):
1276        self.send_command(f'srp client autostart enable')
1277        self._expect_done()
1278
1279    def srp_client_disable_auto_start_mode(self):
1280        self.send_command(f'srp client autostart disable')
1281        self._expect_done()
1282
1283    def srp_client_get_server_address(self):
1284        cmd = 'srp client server address'
1285        self.send_command(cmd)
1286        return self._expect_command_output()[0]
1287
1288    def srp_client_get_server_port(self):
1289        cmd = 'srp client server port'
1290        self.send_command(cmd)
1291        return int(self._expect_command_output()[0])
1292
1293    def srp_client_get_host_state(self):
1294        cmd = 'srp client host state'
1295        self.send_command(cmd)
1296        return self._expect_command_output()[0]
1297
1298    def srp_client_set_host_name(self, name):
1299        self.send_command(f'srp client host name {name}')
1300        self._expect_done()
1301
1302    def srp_client_get_host_name(self):
1303        self.send_command(f'srp client host name')
1304        self._expect_done()
1305
1306    def srp_client_remove_host(self, remove_key=False, send_unreg_to_server=False):
1307        self.send_command(f'srp client host remove {int(remove_key)} {int(send_unreg_to_server)}')
1308        self._expect_done()
1309
1310    def srp_client_clear_host(self):
1311        self.send_command(f'srp client host clear')
1312        self._expect_done()
1313
1314    def srp_client_enable_auto_host_address(self):
1315        self.send_command(f'srp client host address auto')
1316        self._expect_done()
1317
1318    def srp_client_set_host_address(self, *addrs: str):
1319        self.send_command(f'srp client host address {" ".join(addrs)}')
1320        self._expect_done()
1321
1322    def srp_client_get_host_address(self):
1323        self.send_command(f'srp client host address')
1324        self._expect_done()
1325
1326    def srp_client_add_service(self,
1327                               instance_name,
1328                               service_name,
1329                               port,
1330                               priority=0,
1331                               weight=0,
1332                               txt_entries=[],
1333                               lease=0,
1334                               key_lease=0):
1335        txt_record = "".join(self._encode_txt_entry(entry) for entry in txt_entries)
1336        if txt_record == '':
1337            txt_record = '-'
1338        instance_name = self._escape_escapable(instance_name)
1339        self.send_command(
1340            f'srp client service add {instance_name} {service_name} {port} {priority} {weight} {txt_record} {lease} {key_lease}'
1341        )
1342        self._expect_done()
1343
1344    def srp_client_remove_service(self, instance_name, service_name):
1345        self.send_command(f'srp client service remove {instance_name} {service_name}')
1346        self._expect_done()
1347
1348    def srp_client_clear_service(self, instance_name, service_name):
1349        self.send_command(f'srp client service clear {instance_name} {service_name}')
1350        self._expect_done()
1351
1352    def srp_client_get_services(self):
1353        cmd = 'srp client service'
1354        self.send_command(cmd)
1355        service_lines = self._expect_command_output()
1356        return [self._parse_srp_client_service(line) for line in service_lines]
1357
1358    def srp_client_set_lease_interval(self, leaseinterval: int):
1359        cmd = f'srp client leaseinterval {leaseinterval}'
1360        self.send_command(cmd)
1361        self._expect_done()
1362
1363    def srp_client_get_lease_interval(self) -> int:
1364        cmd = 'srp client leaseinterval'
1365        self.send_command(cmd)
1366        return int(self._expect_result('\d+'))
1367
1368    def srp_client_set_key_lease_interval(self, leaseinterval: int):
1369        cmd = f'srp client keyleaseinterval {leaseinterval}'
1370        self.send_command(cmd)
1371        self._expect_done()
1372
1373    def srp_client_get_key_lease_interval(self) -> int:
1374        cmd = 'srp client keyleaseinterval'
1375        self.send_command(cmd)
1376        return int(self._expect_result('\d+'))
1377
1378    def srp_client_set_ttl(self, ttl: int):
1379        cmd = f'srp client ttl {ttl}'
1380        self.send_command(cmd)
1381        self._expect_done()
1382
1383    def srp_client_get_ttl(self) -> int:
1384        cmd = 'srp client ttl'
1385        self.send_command(cmd)
1386        return int(self._expect_result('\d+'))
1387
1388    #
1389    # TREL utilities
1390    #
1391
1392    def enable_trel(self):
1393        cmd = 'trel enable'
1394        self.send_command(cmd)
1395        self._expect_done()
1396
1397    def is_trel_enabled(self) -> Union[None, bool]:
1398        states = [r'Disabled', r'Enabled']
1399        self.send_command('trel')
1400        try:
1401            return self._expect_result(states) == 'Enabled'
1402        except Exception as ex:
1403            if 'InvalidCommand' in str(ex):
1404                return None
1405
1406            raise
1407
1408    def get_trel_counters(self):
1409        cmd = 'trel counters'
1410        self.send_command(cmd)
1411        result = self._expect_command_output()
1412
1413        counters = {}
1414        for line in result:
1415            m = re.match(r'(\w+)\:[^\d]+(\d+)[^\d]+(\d+)(?:[^\d]+(\d+))?', line)
1416            if m:
1417                groups = m.groups()
1418                sub_counters = {
1419                    'packets': int(groups[1]),
1420                    'bytes': int(groups[2]),
1421                }
1422                if groups[3]:
1423                    sub_counters['failures'] = int(groups[3])
1424                counters[groups[0]] = sub_counters
1425        return counters
1426
1427    def reset_trel_counters(self):
1428        cmd = 'trel counters reset'
1429        self.send_command(cmd)
1430        self._expect_done()
1431
1432    def _encode_txt_entry(self, entry):
1433        """Encodes the TXT entry to the DNS-SD TXT record format as a HEX string.
1434
1435           Example usage:
1436           self._encode_txt_entries(['abc'])     -> '03616263'
1437           self._encode_txt_entries(['def='])    -> '046465663d'
1438           self._encode_txt_entries(['xyz=XYZ']) -> '0778797a3d58595a'
1439        """
1440        return '{:02x}'.format(len(entry)) + "".join("{:02x}".format(ord(c)) for c in entry)
1441
1442    def _parse_srp_client_service(self, line: str):
1443        """Parse one line of srp service list into a dictionary which
1444           maps string keys to string values.
1445
1446           Example output for input
1447           'instance:\"%s\", name:\"%s\", state:%s, port:%d, priority:%d, weight:%d"'
1448           {
1449               'instance': 'my-service',
1450               'name': '_ipps._udp',
1451               'state': 'ToAdd',
1452               'port': '12345',
1453               'priority': '0',
1454               'weight': '0'
1455           }
1456
1457           Note that value of 'port', 'priority' and 'weight' are represented
1458           as strings but not integers.
1459        """
1460        key_values = [word.strip().split(':') for word in line.split(', ')]
1461        keys = [key_value[0] for key_value in key_values]
1462        values = [key_value[1].strip('"') for key_value in key_values]
1463        return dict(zip(keys, values))
1464
1465    def locate(self, anycast_addr):
1466        cmd = 'locate ' + anycast_addr
1467        self.send_command(cmd)
1468        self.simulator.go(5)
1469        return self._parse_locate_result(self._expect_command_output()[0])
1470
1471    def _parse_locate_result(self, line: str):
1472        """Parse anycast locate result as list of ml-eid and rloc16.
1473
1474           Example output for input
1475           'fd00:db8:0:0:acf9:9d0:7f3c:b06e 0xa800'
1476
1477           [ 'fd00:db8:0:0:acf9:9d0:7f3c:b06e', '0xa800' ]
1478        """
1479        return line.split(' ')
1480
1481    def enable_backbone_router(self):
1482        cmd = 'bbr enable'
1483        self.send_command(cmd)
1484        self._expect_done()
1485
1486    def disable_backbone_router(self):
1487        cmd = 'bbr disable'
1488        self.send_command(cmd)
1489        self._expect_done()
1490
1491    def register_backbone_router(self):
1492        cmd = 'bbr register'
1493        self.send_command(cmd)
1494        self._expect_done()
1495
1496    def get_backbone_router_state(self):
1497        states = [r'Disabled', r'Primary', r'Secondary']
1498        self.send_command('bbr state')
1499        return self._expect_result(states)
1500
1501    @property
1502    def is_primary_backbone_router(self) -> bool:
1503        return self.get_backbone_router_state() == 'Primary'
1504
1505    def get_backbone_router(self):
1506        cmd = 'bbr config'
1507        self.send_command(cmd)
1508        self._expect(r'(.*)Done')
1509        g = self.pexpect.match.groups()
1510        output = g[0].decode("utf-8")
1511        lines = output.strip().split('\n')
1512        lines = [l.strip() for l in lines]
1513        ret = {}
1514        for l in lines:
1515            z = re.search(r'seqno:\s+([0-9]+)', l)
1516            if z:
1517                ret['seqno'] = int(z.groups()[0])
1518
1519            z = re.search(r'delay:\s+([0-9]+)', l)
1520            if z:
1521                ret['delay'] = int(z.groups()[0])
1522
1523            z = re.search(r'timeout:\s+([0-9]+)', l)
1524            if z:
1525                ret['timeout'] = int(z.groups()[0])
1526
1527        return ret
1528
1529    def set_backbone_router(self, seqno=None, reg_delay=None, mlr_timeout=None):
1530        cmd = 'bbr config'
1531
1532        if seqno is not None:
1533            cmd += ' seqno %d' % seqno
1534
1535        if reg_delay is not None:
1536            cmd += ' delay %d' % reg_delay
1537
1538        if mlr_timeout is not None:
1539            cmd += ' timeout %d' % mlr_timeout
1540
1541        self.send_command(cmd)
1542        self._expect_done()
1543
1544    def set_domain_prefix(self, prefix, flags='prosD'):
1545        self.add_prefix(prefix, flags)
1546        self.register_netdata()
1547
1548    def remove_domain_prefix(self, prefix):
1549        self.remove_prefix(prefix)
1550        self.register_netdata()
1551
1552    def set_next_dua_response(self, status: Union[str, int], iid=None):
1553        # Convert 5.00 to COAP CODE 160
1554        if isinstance(status, str):
1555            assert '.' in status
1556            status = status.split('.')
1557            status = (int(status[0]) << 5) + int(status[1])
1558
1559        cmd = 'bbr mgmt dua {}'.format(status)
1560        if iid is not None:
1561            cmd += ' ' + str(iid)
1562        self.send_command(cmd)
1563        self._expect_done()
1564
1565    def set_dua_iid(self, iid: str):
1566        assert len(iid) == 16
1567        int(iid, 16)
1568
1569        cmd = 'dua iid {}'.format(iid)
1570        self.send_command(cmd)
1571        self._expect_done()
1572
1573    def clear_dua_iid(self):
1574        cmd = 'dua iid clear'
1575        self.send_command(cmd)
1576        self._expect_done()
1577
1578    def multicast_listener_list(self) -> Dict[IPv6Address, int]:
1579        cmd = 'bbr mgmt mlr listener'
1580        self.send_command(cmd)
1581
1582        table = {}
1583        for line in self._expect_results("\S+ \d+"):
1584            line = line.split()
1585            assert len(line) == 2, line
1586            ip = IPv6Address(line[0])
1587            timeout = int(line[1])
1588            assert ip not in table
1589
1590            table[ip] = timeout
1591
1592        return table
1593
1594    def multicast_listener_clear(self):
1595        cmd = f'bbr mgmt mlr listener clear'
1596        self.send_command(cmd)
1597        self._expect_done()
1598
1599    def multicast_listener_add(self, ip: Union[IPv6Address, str], timeout: int = 0):
1600        if not isinstance(ip, IPv6Address):
1601            ip = IPv6Address(ip)
1602
1603        cmd = f'bbr mgmt mlr listener add {ip.compressed} {timeout}'
1604        self.send_command(cmd)
1605        self._expect(r"(Done|Error .*)")
1606
1607    def set_next_mlr_response(self, status: int):
1608        cmd = 'bbr mgmt mlr response {}'.format(status)
1609        self.send_command(cmd)
1610        self._expect_done()
1611
1612    def register_multicast_listener(self, *ipaddrs: Union[IPv6Address, str], timeout=None):
1613        assert len(ipaddrs) > 0, ipaddrs
1614
1615        ipaddrs = map(str, ipaddrs)
1616        cmd = f'mlr reg {" ".join(ipaddrs)}'
1617        if timeout is not None:
1618            cmd += f' {int(timeout)}'
1619        self.send_command(cmd)
1620        self.simulator.go(3)
1621        lines = self._expect_command_output()
1622        m = re.match(r'status (\d+), (\d+) failed', lines[0])
1623        assert m is not None, lines
1624        status = int(m.group(1))
1625        failed_num = int(m.group(2))
1626        assert failed_num == len(lines) - 1
1627        failed_ips = list(map(IPv6Address, lines[1:]))
1628        print(f"register_multicast_listener {ipaddrs} => status: {status}, failed ips: {failed_ips}")
1629        return status, failed_ips
1630
1631    def set_link_quality(self, addr, lqi):
1632        cmd = 'macfilter rss add-lqi %s %s' % (addr, lqi)
1633        self.send_command(cmd)
1634        self._expect_done()
1635
1636    def set_outbound_link_quality(self, lqi):
1637        cmd = 'macfilter rss add-lqi * %s' % (lqi)
1638        self.send_command(cmd)
1639        self._expect_done()
1640
1641    def remove_allowlist(self, addr):
1642        cmd = 'macfilter addr remove %s' % addr
1643        self.send_command(cmd)
1644        self._expect_done()
1645
1646    def get_addr16(self):
1647        self.send_command('rloc16')
1648        rloc16 = self._expect_result(r'[0-9a-fA-F]{4}')
1649        return int(rloc16, 16)
1650
1651    def get_router_id(self):
1652        rloc16 = self.get_addr16()
1653        return rloc16 >> 10
1654
1655    def get_addr64(self):
1656        self.send_command('extaddr')
1657        return self._expect_result('[0-9a-fA-F]{16}')
1658
1659    def set_addr64(self, addr64: str):
1660        # Make sure `addr64` is a hex string of length 16
1661        assert len(addr64) == 16
1662        int(addr64, 16)
1663        self.send_command('extaddr %s' % addr64)
1664        self._expect_done()
1665
1666    def get_eui64(self):
1667        self.send_command('eui64')
1668        return self._expect_result('[0-9a-fA-F]{16}')
1669
1670    def set_extpanid(self, extpanid):
1671        self.send_command('extpanid %s' % extpanid)
1672        self._expect_done()
1673
1674    def get_extpanid(self):
1675        self.send_command('extpanid')
1676        return self._expect_result('[0-9a-fA-F]{16}')
1677
1678    def get_mesh_local_prefix(self):
1679        self.send_command('prefix meshlocal')
1680        return self._expect_command_output()[0]
1681
1682    def set_mesh_local_prefix(self, mesh_local_prefix):
1683        self.send_command('prefix meshlocal %s' % mesh_local_prefix)
1684        self._expect_done()
1685
1686    def get_joiner_id(self):
1687        self.send_command('joiner id')
1688        return self._expect_result('[0-9a-fA-F]{16}')
1689
1690    def get_channel(self):
1691        self.send_command('channel')
1692        return int(self._expect_result(r'\d+'))
1693
1694    def set_channel(self, channel):
1695        cmd = 'channel %d' % channel
1696        self.send_command(cmd)
1697        self._expect_done()
1698
1699    def get_networkkey(self):
1700        self.send_command('networkkey')
1701        return self._expect_result('[0-9a-fA-F]{32}')
1702
1703    def set_networkkey(self, networkkey):
1704        cmd = 'networkkey %s' % networkkey
1705        self.send_command(cmd)
1706        self._expect_done()
1707
1708    def get_key_sequence_counter(self):
1709        self.send_command('keysequence counter')
1710        result = self._expect_result(r'\d+')
1711        return int(result)
1712
1713    def set_key_sequence_counter(self, key_sequence_counter):
1714        cmd = 'keysequence counter %d' % key_sequence_counter
1715        self.send_command(cmd)
1716        self._expect_done()
1717
1718    def get_key_switch_guardtime(self):
1719        self.send_command('keysequence guardtime')
1720        return int(self._expect_result(r'\d+'))
1721
1722    def set_key_switch_guardtime(self, key_switch_guardtime):
1723        cmd = 'keysequence guardtime %d' % key_switch_guardtime
1724        self.send_command(cmd)
1725        self._expect_done()
1726
1727    def set_network_id_timeout(self, network_id_timeout):
1728        cmd = 'networkidtimeout %d' % network_id_timeout
1729        self.send_command(cmd)
1730        self._expect_done()
1731
1732    def _escape_escapable(self, string):
1733        """Escape CLI escapable characters in the given string.
1734
1735        Args:
1736            string (str): UTF-8 input string.
1737
1738        Returns:
1739            [str]: The modified string with escaped characters.
1740        """
1741        escapable_chars = '\\ \t\r\n'
1742        for char in escapable_chars:
1743            string = string.replace(char, '\\%s' % char)
1744        return string
1745
1746    def get_network_name(self):
1747        self.send_command('networkname')
1748        return self._expect_result([r'\S+'])
1749
1750    def set_network_name(self, network_name):
1751        cmd = 'networkname %s' % self._escape_escapable(network_name)
1752        self.send_command(cmd)
1753        self._expect_done()
1754
1755    def get_panid(self):
1756        self.send_command('panid')
1757        result = self._expect_result('0x[0-9a-fA-F]{4}')
1758        return int(result, 16)
1759
1760    def set_panid(self, panid=config.PANID):
1761        cmd = 'panid %d' % panid
1762        self.send_command(cmd)
1763        self._expect_done()
1764
1765    def set_parent_priority(self, priority):
1766        cmd = 'parentpriority %d' % priority
1767        self.send_command(cmd)
1768        self._expect_done()
1769
1770    def get_partition_id(self):
1771        self.send_command('partitionid')
1772        return self._expect_result(r'\d+')
1773
1774    def get_preferred_partition_id(self):
1775        self.send_command('partitionid preferred')
1776        return self._expect_result(r'\d+')
1777
1778    def set_preferred_partition_id(self, partition_id):
1779        cmd = 'partitionid preferred %d' % partition_id
1780        self.send_command(cmd)
1781        self._expect_done()
1782
1783    def get_pollperiod(self):
1784        self.send_command('pollperiod')
1785        return self._expect_result(r'\d+')
1786
1787    def set_pollperiod(self, pollperiod):
1788        self.send_command('pollperiod %d' % pollperiod)
1789        self._expect_done()
1790
1791    def get_child_supervision_interval(self):
1792        self.send_command('childsupervision interval')
1793        return self._expect_result(r'\d+')
1794
1795    def set_child_supervision_interval(self, interval):
1796        self.send_command('childsupervision interval %d' % interval)
1797        self._expect_done()
1798
1799    def get_child_supervision_check_timeout(self):
1800        self.send_command('childsupervision checktimeout')
1801        return self._expect_result(r'\d+')
1802
1803    def set_child_supervision_check_timeout(self, timeout):
1804        self.send_command('childsupervision checktimeout %d' % timeout)
1805        self._expect_done()
1806
1807    def get_child_supervision_check_failure_counter(self):
1808        self.send_command('childsupervision failcounter')
1809        return self._expect_result(r'\d+')
1810
1811    def reset_child_supervision_check_failure_counter(self):
1812        self.send_command('childsupervision failcounter reset')
1813        self._expect_done()
1814
1815    def get_csl_info(self):
1816        self.send_command('csl')
1817        return self._expect_key_value_pairs(r'\S+')
1818
1819    def set_csl_channel(self, csl_channel):
1820        self.send_command('csl channel %d' % csl_channel)
1821        self._expect_done()
1822
1823    def set_csl_period(self, csl_period):
1824        self.send_command('csl period %d' % csl_period)
1825        self._expect_done()
1826
1827    def set_csl_timeout(self, csl_timeout):
1828        self.send_command('csl timeout %d' % csl_timeout)
1829        self._expect_done()
1830
1831    def send_mac_emptydata(self):
1832        self.send_command('mac send emptydata')
1833        self._expect_done()
1834
1835    def send_mac_datarequest(self):
1836        self.send_command('mac send datarequest')
1837        self._expect_done()
1838
1839    def set_router_upgrade_threshold(self, threshold):
1840        cmd = 'routerupgradethreshold %d' % threshold
1841        self.send_command(cmd)
1842        self._expect_done()
1843
1844    def set_router_downgrade_threshold(self, threshold):
1845        cmd = 'routerdowngradethreshold %d' % threshold
1846        self.send_command(cmd)
1847        self._expect_done()
1848
1849    def get_router_downgrade_threshold(self) -> int:
1850        self.send_command('routerdowngradethreshold')
1851        return int(self._expect_result(r'\d+'))
1852
1853    def set_router_eligible(self, enable: bool):
1854        cmd = f'routereligible {"enable" if enable else "disable"}'
1855        self.send_command(cmd)
1856        self._expect_done()
1857
1858    def get_router_eligible(self) -> bool:
1859        states = [r'Disabled', r'Enabled']
1860        self.send_command('routereligible')
1861        return self._expect_result(states) == 'Enabled'
1862
1863    def prefer_router_id(self, router_id):
1864        cmd = 'preferrouterid %d' % router_id
1865        self.send_command(cmd)
1866        self._expect_done()
1867
1868    def release_router_id(self, router_id):
1869        cmd = 'releaserouterid %d' % router_id
1870        self.send_command(cmd)
1871        self._expect_done()
1872
1873    def get_state(self):
1874        states = [r'detached', r'child', r'router', r'leader', r'disabled']
1875        self.send_command('state')
1876        return self._expect_result(states)
1877
1878    def set_state(self, state):
1879        cmd = 'state %s' % state
1880        self.send_command(cmd)
1881        self._expect_done()
1882
1883    def get_timeout(self):
1884        self.send_command('childtimeout')
1885        return self._expect_result(r'\d+')
1886
1887    def set_timeout(self, timeout):
1888        cmd = 'childtimeout %d' % timeout
1889        self.send_command(cmd)
1890        self._expect_done()
1891
1892    def set_max_children(self, number):
1893        cmd = 'childmax %d' % number
1894        self.send_command(cmd)
1895        self._expect_done()
1896
1897    def get_weight(self):
1898        self.send_command('leaderweight')
1899        return self._expect_result(r'\d+')
1900
1901    def set_weight(self, weight):
1902        cmd = 'leaderweight %d' % weight
1903        self.send_command(cmd)
1904        self._expect_done()
1905
1906    def add_ipaddr(self, ipaddr):
1907        cmd = 'ipaddr add %s' % ipaddr
1908        self.send_command(cmd)
1909        self._expect_done()
1910
1911    def del_ipaddr(self, ipaddr):
1912        cmd = 'ipaddr del %s' % ipaddr
1913        self.send_command(cmd)
1914        self._expect_done()
1915
1916    def add_ipmaddr(self, ipmaddr):
1917        cmd = 'ipmaddr add %s' % ipmaddr
1918        self.send_command(cmd)
1919        self._expect_done()
1920
1921    def del_ipmaddr(self, ipmaddr):
1922        cmd = 'ipmaddr del %s' % ipmaddr
1923        self.send_command(cmd)
1924        self._expect_done()
1925
1926    def get_addrs(self, verbose=False):
1927        self.send_command('ipaddr' + (' -v' if verbose else ''))
1928
1929        return self._expect_results(r'\S+(:\S*)+')
1930
1931    def get_mleid(self):
1932        self.send_command('ipaddr mleid')
1933        return self._expect_result(r'\S+(:\S*)+')
1934
1935    def get_linklocal(self):
1936        self.send_command('ipaddr linklocal')
1937        return self._expect_result(r'\S+(:\S*)+')
1938
1939    def get_rloc(self):
1940        self.send_command('ipaddr rloc')
1941        return self._expect_result(r'\S+(:\S*)+')
1942
1943    def get_addr(self, prefix):
1944        network = ipaddress.ip_network(u'%s' % str(prefix))
1945        addrs = self.get_addrs()
1946
1947        for addr in addrs:
1948            if isinstance(addr, bytearray):
1949                addr = bytes(addr)
1950            ipv6_address = ipaddress.ip_address(addr)
1951            if ipv6_address in network:
1952                return ipv6_address.exploded
1953
1954        return None
1955
1956    def has_ipaddr(self, address):
1957        ipaddr = ipaddress.ip_address(address)
1958        ipaddrs = self.get_addrs()
1959        for addr in ipaddrs:
1960            if isinstance(addr, bytearray):
1961                addr = bytes(addr)
1962            if ipaddress.ip_address(addr) == ipaddr:
1963                return True
1964        return False
1965
1966    def get_ipmaddrs(self):
1967        self.send_command('ipmaddr')
1968        return self._expect_results(r'\S+(:\S*)+')
1969
1970    def has_ipmaddr(self, address):
1971        ipmaddr = ipaddress.ip_address(address)
1972        ipmaddrs = self.get_ipmaddrs()
1973        for addr in ipmaddrs:
1974            if isinstance(addr, bytearray):
1975                addr = bytes(addr)
1976            if ipaddress.ip_address(addr) == ipmaddr:
1977                return True
1978        return False
1979
1980    def get_addr_leader_aloc(self):
1981        addrs = self.get_addrs()
1982        for addr in addrs:
1983            segs = addr.split(':')
1984            if (segs[4] == '0' and segs[5] == 'ff' and segs[6] == 'fe00' and segs[7] == 'fc00'):
1985                return addr
1986        return None
1987
1988    def get_mleid_iid(self):
1989        ml_eid = IPv6Address(self.get_mleid())
1990        return ml_eid.packed[8:].hex()
1991
1992    def get_eidcaches(self):
1993        eidcaches = []
1994        self.send_command('eidcache')
1995        for line in self._expect_results(r'([a-fA-F0-9\:]+) ([a-fA-F0-9]+)'):
1996            eidcaches.append(line.split())
1997
1998        return eidcaches
1999
2000    def add_service(self, enterpriseNumber, serviceData, serverData):
2001        cmd = 'service add %s %s %s' % (
2002            enterpriseNumber,
2003            serviceData,
2004            serverData,
2005        )
2006        self.send_command(cmd)
2007        self._expect_done()
2008
2009    def remove_service(self, enterpriseNumber, serviceData):
2010        cmd = 'service remove %s %s' % (enterpriseNumber, serviceData)
2011        self.send_command(cmd)
2012        self._expect_done()
2013
2014    def get_child_table(self) -> Dict[int, Dict[str, Any]]:
2015        """Get the table of attached children."""
2016        cmd = 'child table'
2017        self.send_command(cmd)
2018        output = self._expect_command_output()
2019
2020        #
2021        # Example output:
2022        # | ID  | RLOC16 | Timeout    | Age        | LQ In | C_VN |R|D|N|Ver|CSL|QMsgCnt|Suprvsn| Extended MAC     |
2023        # +-----+--------+------------+------------+-------+------+-+-+-+---+---+-------+-------+------------------+
2024        # |   1 | 0xc801 |        240 |         24 |     3 |  131 |1|0|0|  3| 0 |     0 |   129 | 4ecede68435358ac |
2025        # |   2 | 0xc802 |        240 |          2 |     3 |  131 |0|0|0|  3| 1 |     0 |     0 | a672a601d2ce37d8 |
2026        # Done
2027        #
2028
2029        headers = self.__split_table_row(output[0])
2030
2031        table = {}
2032        for line in output[2:]:
2033            line = line.strip()
2034            if not line:
2035                continue
2036
2037            fields = self.__split_table_row(line)
2038            col = lambda colname: self.__get_table_col(colname, headers, fields)
2039
2040            id = int(col("ID"))
2041            r, d, n = int(col("R")), int(col("D")), int(col("N"))
2042            mode = f'{"r" if r else ""}{"d" if d else ""}{"n" if n else ""}'
2043
2044            table[int(id)] = {
2045                'id': int(id),
2046                'rloc16': int(col('RLOC16'), 16),
2047                'timeout': int(col('Timeout')),
2048                'age': int(col('Age')),
2049                'lq_in': int(col('LQ In')),
2050                'c_vn': int(col('C_VN')),
2051                'mode': mode,
2052                'extaddr': col('Extended MAC'),
2053                'ver': int(col('Ver')),
2054                'csl': bool(int(col('CSL'))),
2055                'qmsgcnt': int(col('QMsgCnt')),
2056                'suprvsn': int(col('Suprvsn'))
2057            }
2058
2059        return table
2060
2061    def __split_table_row(self, row: str) -> List[str]:
2062        if not (row.startswith('|') and row.endswith('|')):
2063            raise ValueError(row)
2064
2065        fields = row.split('|')
2066        fields = [x.strip() for x in fields[1:-1]]
2067        return fields
2068
2069    def __get_table_col(self, colname: str, headers: List[str], fields: List[str]) -> str:
2070        return fields[headers.index(colname)]
2071
2072    def __getOmrAddress(self):
2073        prefixes = [prefix.split('::')[0] for prefix in self.get_prefixes()]
2074        omr_addrs = []
2075        for addr in self.get_addrs():
2076            for prefix in prefixes:
2077                if (addr.startswith(prefix)) and (addr != self.__getDua()):
2078                    omr_addrs.append(addr)
2079                    break
2080
2081        return omr_addrs
2082
2083    def __getLinkLocalAddress(self):
2084        for ip6Addr in self.get_addrs():
2085            if re.match(config.LINK_LOCAL_REGEX_PATTERN, ip6Addr, re.I):
2086                return ip6Addr
2087
2088        return None
2089
2090    def __getGlobalAddress(self):
2091        global_address = []
2092        for ip6Addr in self.get_addrs():
2093            if ((not re.match(config.LINK_LOCAL_REGEX_PATTERN, ip6Addr, re.I)) and
2094                (not re.match(config.MESH_LOCAL_PREFIX_REGEX_PATTERN, ip6Addr, re.I)) and
2095                (not re.match(config.ROUTING_LOCATOR_REGEX_PATTERN, ip6Addr, re.I))):
2096                global_address.append(ip6Addr)
2097
2098        return global_address
2099
2100    def __getRloc(self):
2101        for ip6Addr in self.get_addrs():
2102            if (re.match(config.MESH_LOCAL_PREFIX_REGEX_PATTERN, ip6Addr, re.I) and
2103                    re.match(config.ROUTING_LOCATOR_REGEX_PATTERN, ip6Addr, re.I) and
2104                    not (re.match(config.ALOC_FLAG_REGEX_PATTERN, ip6Addr, re.I))):
2105                return ip6Addr
2106        return None
2107
2108    def __getAloc(self):
2109        aloc = []
2110        for ip6Addr in self.get_addrs():
2111            if (re.match(config.MESH_LOCAL_PREFIX_REGEX_PATTERN, ip6Addr, re.I) and
2112                    re.match(config.ROUTING_LOCATOR_REGEX_PATTERN, ip6Addr, re.I) and
2113                    re.match(config.ALOC_FLAG_REGEX_PATTERN, ip6Addr, re.I)):
2114                aloc.append(ip6Addr)
2115
2116        return aloc
2117
2118    def __getMleid(self):
2119        for ip6Addr in self.get_addrs():
2120            if re.match(config.MESH_LOCAL_PREFIX_REGEX_PATTERN, ip6Addr,
2121                        re.I) and not (re.match(config.ROUTING_LOCATOR_REGEX_PATTERN, ip6Addr, re.I)):
2122                return ip6Addr
2123
2124        return None
2125
2126    def __getDua(self) -> Optional[str]:
2127        for ip6Addr in self.get_addrs():
2128            if re.match(config.DOMAIN_PREFIX_REGEX_PATTERN, ip6Addr, re.I):
2129                return ip6Addr
2130
2131        return None
2132
2133    def get_ip6_address_by_prefix(self, prefix: Union[str, IPv6Network]) -> List[IPv6Address]:
2134        """Get addresses matched with given prefix.
2135
2136        Args:
2137            prefix: the prefix to match against.
2138                    Can be either a string or ipaddress.IPv6Network.
2139
2140        Returns:
2141            The IPv6 address list.
2142        """
2143        if isinstance(prefix, str):
2144            prefix = IPv6Network(prefix)
2145        addrs = map(IPv6Address, self.get_addrs())
2146
2147        return [addr for addr in addrs if addr in prefix]
2148
2149    def get_ip6_address(self, address_type):
2150        """Get specific type of IPv6 address configured on thread device.
2151
2152        Args:
2153            address_type: the config.ADDRESS_TYPE type of IPv6 address.
2154
2155        Returns:
2156            IPv6 address string.
2157        """
2158        if address_type == config.ADDRESS_TYPE.LINK_LOCAL:
2159            return self.__getLinkLocalAddress()
2160        elif address_type == config.ADDRESS_TYPE.GLOBAL:
2161            return self.__getGlobalAddress()
2162        elif address_type == config.ADDRESS_TYPE.RLOC:
2163            return self.__getRloc()
2164        elif address_type == config.ADDRESS_TYPE.ALOC:
2165            return self.__getAloc()
2166        elif address_type == config.ADDRESS_TYPE.ML_EID:
2167            return self.__getMleid()
2168        elif address_type == config.ADDRESS_TYPE.DUA:
2169            return self.__getDua()
2170        elif address_type == config.ADDRESS_TYPE.BACKBONE_GUA:
2171            return self._getBackboneGua()
2172        elif address_type == config.ADDRESS_TYPE.OMR:
2173            return self.__getOmrAddress()
2174        else:
2175            return None
2176
2177    def get_context_reuse_delay(self):
2178        self.send_command('contextreusedelay')
2179        return self._expect_result(r'\d+')
2180
2181    def set_context_reuse_delay(self, delay):
2182        cmd = 'contextreusedelay %d' % delay
2183        self.send_command(cmd)
2184        self._expect_done()
2185
2186    def add_prefix(self, prefix, flags='paosr', prf='med'):
2187        cmd = 'prefix add %s %s %s' % (prefix, flags, prf)
2188        self.send_command(cmd)
2189        self._expect_done()
2190
2191    def remove_prefix(self, prefix):
2192        cmd = 'prefix remove %s' % prefix
2193        self.send_command(cmd)
2194        self._expect_done()
2195
2196    def enable_br(self):
2197        self.send_command('br enable')
2198        self._expect_done()
2199
2200    def disable_br(self):
2201        self.send_command('br disable')
2202        self._expect_done()
2203
2204    def get_br_omr_prefix(self):
2205        cmd = 'br omrprefix local'
2206        self.send_command(cmd)
2207        return self._expect_command_output()[0]
2208
2209    def get_netdata_omr_prefixes(self):
2210        omr_prefixes = []
2211        for prefix in self.get_prefixes():
2212            prefix, flags = prefix.split()[:2]
2213            if 'a' in flags and 'o' in flags and 's' in flags and 'D' not in flags:
2214                omr_prefixes.append(prefix)
2215
2216        return omr_prefixes
2217
2218    def get_br_on_link_prefix(self):
2219        cmd = 'br onlinkprefix local'
2220        self.send_command(cmd)
2221        return self._expect_command_output()[0]
2222
2223    def get_netdata_non_nat64_routes(self):
2224        nat64_routes = []
2225        routes = self.get_routes()
2226        for route in routes:
2227            if 'n' not in route.split(' ')[1]:
2228                nat64_routes.append(route.split(' ')[0])
2229        return nat64_routes
2230
2231    def get_netdata_nat64_routes(self):
2232        nat64_routes = []
2233        routes = self.get_routes()
2234        for route in routes:
2235            if 'n' in route.split(' ')[1]:
2236                nat64_routes.append(route.split(' ')[0])
2237        return nat64_routes
2238
2239    def get_br_nat64_prefix(self):
2240        cmd = 'br nat64prefix local'
2241        self.send_command(cmd)
2242        return self._expect_command_output()[0]
2243
2244    def get_br_favored_nat64_prefix(self):
2245        cmd = 'br nat64prefix favored'
2246        self.send_command(cmd)
2247        return self._expect_command_output()[0].split(' ')[0]
2248
2249    def enable_nat64(self):
2250        self.send_command(f'nat64 enable')
2251        self._expect_done()
2252
2253    def disable_nat64(self):
2254        self.send_command(f'nat64 disable')
2255        self._expect_done()
2256
2257    def get_nat64_state(self):
2258        self.send_command('nat64 state')
2259        res = {}
2260        for line in self._expect_command_output():
2261            state = line.split(':')
2262            res[state[0].strip()] = state[1].strip()
2263        return res
2264
2265    def get_nat64_mappings(self):
2266        cmd = 'nat64 mappings'
2267        self.send_command(cmd)
2268        result = self._expect_command_output()
2269        session = None
2270        session_counters = None
2271        sessions = []
2272
2273        for line in result:
2274            m = re.match(
2275                r'\|\s+([a-f0-9]+)\s+\|\s+(.+)\s+\|\s+(.+)\s+\|\s+(\d+)s\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|',
2276                line)
2277            if m:
2278                groups = m.groups()
2279                if session:
2280                    session['counters'] = session_counters
2281                    sessions.append(session)
2282                session = {
2283                    'id': groups[0],
2284                    'ip6': groups[1],
2285                    'ip4': groups[2],
2286                    'expiry': int(groups[3]),
2287                }
2288                session_counters = {}
2289                session_counters['total'] = {
2290                    '4to6': {
2291                        'packets': int(groups[4]),
2292                        'bytes': int(groups[5]),
2293                    },
2294                    '6to4': {
2295                        'packets': int(groups[6]),
2296                        'bytes': int(groups[7]),
2297                    },
2298                }
2299                continue
2300            if not session:
2301                continue
2302            m = re.match(r'\|\s+\|\s+(.+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', line)
2303            if m:
2304                groups = m.groups()
2305                session_counters[groups[0]] = {
2306                    '4to6': {
2307                        'packets': int(groups[1]),
2308                        'bytes': int(groups[2]),
2309                    },
2310                    '6to4': {
2311                        'packets': int(groups[3]),
2312                        'bytes': int(groups[4]),
2313                    },
2314                }
2315        if session:
2316            session['counters'] = session_counters
2317            sessions.append(session)
2318        return sessions
2319
2320    def get_nat64_counters(self):
2321        cmd = 'nat64 counters'
2322        self.send_command(cmd)
2323        result = self._expect_command_output()
2324
2325        protocol_counters = {}
2326        error_counters = {}
2327        for line in result:
2328            m = re.match(r'\|\s+(.+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', line)
2329            if m:
2330                groups = m.groups()
2331                protocol_counters[groups[0]] = {
2332                    '4to6': {
2333                        'packets': int(groups[1]),
2334                        'bytes': int(groups[2]),
2335                    },
2336                    '6to4': {
2337                        'packets': int(groups[3]),
2338                        'bytes': int(groups[4]),
2339                    },
2340                }
2341                continue
2342            m = re.match(r'\|\s+(.+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', line)
2343            if m:
2344                groups = m.groups()
2345                error_counters[groups[0]] = {
2346                    '4to6': {
2347                        'packets': int(groups[1]),
2348                    },
2349                    '6to4': {
2350                        'packets': int(groups[2]),
2351                    },
2352                }
2353                continue
2354        return {'protocol': protocol_counters, 'errors': error_counters}
2355
2356    def get_prefixes(self):
2357        return self.get_netdata()['Prefixes']
2358
2359    def get_routes(self):
2360        return self.get_netdata()['Routes']
2361
2362    def get_services(self):
2363        netdata = self.netdata_show()
2364        services = []
2365        services_section = False
2366
2367        for line in netdata:
2368            if line.startswith('Services:'):
2369                services_section = True
2370            elif line.startswith('Contexts'):
2371                services_section = False
2372            elif services_section:
2373                services.append(line.strip().split(' '))
2374        return services
2375
2376    def netdata_show(self):
2377        self.send_command('netdata show')
2378        return self._expect_command_output()
2379
2380    def get_netdata(self):
2381        raw_netdata = self.netdata_show()
2382        netdata = {'Prefixes': [], 'Routes': [], 'Services': [], 'Contexts': [], 'Commissioning': []}
2383        key_list = ['Prefixes', 'Routes', 'Services', 'Contexts', 'Commissioning']
2384        key = None
2385
2386        for i in range(0, len(raw_netdata)):
2387            keys = list(filter(raw_netdata[i].startswith, key_list))
2388            if keys != []:
2389                key = keys[0]
2390            elif key is not None:
2391                netdata[key].append(raw_netdata[i])
2392
2393        return netdata
2394
2395    def add_route(self, prefix, stable=False, nat64=False, prf='med'):
2396        cmd = 'route add %s ' % prefix
2397        if stable:
2398            cmd += 's'
2399        if nat64:
2400            cmd += 'n'
2401        cmd += ' %s' % prf
2402        self.send_command(cmd)
2403        self._expect_done()
2404
2405    def remove_route(self, prefix):
2406        cmd = 'route remove %s' % prefix
2407        self.send_command(cmd)
2408        self._expect_done()
2409
2410    def register_netdata(self):
2411        self.send_command('netdata register')
2412        self._expect_done()
2413
2414    def netdata_publish_dnssrp_anycast(self, seqnum):
2415        self.send_command(f'netdata publish dnssrp anycast {seqnum}')
2416        self._expect_done()
2417
2418    def netdata_publish_dnssrp_unicast(self, address, port):
2419        self.send_command(f'netdata publish dnssrp unicast {address} {port}')
2420        self._expect_done()
2421
2422    def netdata_publish_dnssrp_unicast_mleid(self, port):
2423        self.send_command(f'netdata publish dnssrp unicast {port}')
2424        self._expect_done()
2425
2426    def netdata_unpublish_dnssrp(self):
2427        self.send_command('netdata unpublish dnssrp')
2428        self._expect_done()
2429
2430    def netdata_publish_prefix(self, prefix, flags='paosr', prf='med'):
2431        self.send_command(f'netdata publish prefix {prefix} {flags} {prf}')
2432        self._expect_done()
2433
2434    def netdata_publish_route(self, prefix, flags='s', prf='med'):
2435        self.send_command(f'netdata publish route {prefix} {flags} {prf}')
2436        self._expect_done()
2437
2438    def netdata_publish_replace(self, old_prefix, prefix, flags='s', prf='med'):
2439        self.send_command(f'netdata publish replace {old_prefix} {prefix} {flags} {prf}')
2440        self._expect_done()
2441
2442    def netdata_unpublish_prefix(self, prefix):
2443        self.send_command(f'netdata unpublish {prefix}')
2444        self._expect_done()
2445
2446    def send_network_diag_get(self, addr, tlv_types):
2447        self.send_command('networkdiagnostic get %s %s' % (addr, ' '.join([str(t.value) for t in tlv_types])))
2448
2449        if isinstance(self.simulator, simulator.VirtualTime):
2450            self.simulator.go(8)
2451            timeout = 1
2452        else:
2453            timeout = 8
2454
2455        self._expect_done(timeout=timeout)
2456
2457    def send_network_diag_reset(self, addr, tlv_types):
2458        self.send_command('networkdiagnostic reset %s %s' % (addr, ' '.join([str(t.value) for t in tlv_types])))
2459
2460        if isinstance(self.simulator, simulator.VirtualTime):
2461            self.simulator.go(8)
2462            timeout = 1
2463        else:
2464            timeout = 8
2465
2466        self._expect_done(timeout=timeout)
2467
2468    def energy_scan(self, mask, count, period, scan_duration, ipaddr):
2469        cmd = 'commissioner energy %d %d %d %d %s' % (
2470            mask,
2471            count,
2472            period,
2473            scan_duration,
2474            ipaddr,
2475        )
2476        self.send_command(cmd)
2477
2478        if isinstance(self.simulator, simulator.VirtualTime):
2479            self.simulator.go(8)
2480            timeout = 1
2481        else:
2482            timeout = 8
2483
2484        self._expect('Energy:', timeout=timeout)
2485
2486    def panid_query(self, panid, mask, ipaddr):
2487        cmd = 'commissioner panid %d %d %s' % (panid, mask, ipaddr)
2488        self.send_command(cmd)
2489
2490        if isinstance(self.simulator, simulator.VirtualTime):
2491            self.simulator.go(8)
2492            timeout = 1
2493        else:
2494            timeout = 8
2495
2496        self._expect('Conflict:', timeout=timeout)
2497
2498    def scan(self, result=1, timeout=10):
2499        self.send_command('scan')
2500
2501        self.simulator.go(timeout)
2502
2503        if result == 1:
2504            networks = []
2505            for line in self._expect_command_output()[2:]:
2506                _, panid, extaddr, channel, dbm, lqi, _ = map(str.strip, line.split('|'))
2507                panid = int(panid, 16)
2508                channel, dbm, lqi = map(int, (channel, dbm, lqi))
2509
2510                networks.append({
2511                    'panid': panid,
2512                    'extaddr': extaddr,
2513                    'channel': channel,
2514                    'dbm': dbm,
2515                    'lqi': lqi,
2516                })
2517            return networks
2518
2519    def scan_energy(self, timeout=10):
2520        self.send_command('scan energy')
2521        self.simulator.go(timeout)
2522        rssi_list = []
2523        for line in self._expect_command_output()[2:]:
2524            _, channel, rssi, _ = line.split('|')
2525            rssi_list.append({
2526                'channel': int(channel.strip()),
2527                'rssi': int(rssi.strip()),
2528            })
2529        return rssi_list
2530
2531    def ping(self, ipaddr, num_responses=1, size=8, timeout=5, count=1, interval=1, hoplimit=64, interface=None):
2532        args = f'{ipaddr} {size} {count} {interval} {hoplimit} {timeout}'
2533        if interface is not None:
2534            args = f'-I {interface} {args}'
2535        cmd = f'ping {args}'
2536
2537        self.send_command(cmd)
2538
2539        wait_allowance = 3
2540        end = self.simulator.now() + timeout + wait_allowance
2541
2542        responders = {}
2543
2544        result = True
2545        # ncp-sim doesn't print Done
2546        done = (self.node_type == 'ncp-sim')
2547        while len(responders) < num_responses or not done:
2548            self.simulator.go(1)
2549            try:
2550                i = self._expect([r'from (\S+):', r'Done'], timeout=0.1)
2551            except (pexpect.TIMEOUT, socket.timeout):
2552                if self.simulator.now() < end:
2553                    continue
2554                result = False
2555                if isinstance(self.simulator, simulator.VirtualTime):
2556                    self.simulator.sync_devices()
2557                break
2558            else:
2559                if i == 0:
2560                    responders[self.pexpect.match.groups()[0]] = 1
2561                elif i == 1:
2562                    done = True
2563        return result
2564
2565    def reset(self):
2566        self._reset('reset')
2567
2568    def factory_reset(self):
2569        self._reset('factoryreset')
2570
2571    def _reset(self, cmd):
2572        self.send_command(cmd, expect_command_echo=False)
2573        time.sleep(self.RESET_DELAY)
2574        # Send a "version" command and drain the CLI output after reset
2575        self.send_command('version', expect_command_echo=False)
2576        while True:
2577            try:
2578                self._expect(r"[^\n]+\n", timeout=0.1)
2579                continue
2580            except pexpect.TIMEOUT:
2581                break
2582
2583        if self.is_otbr:
2584            self.set_log_level(5)
2585
2586    def set_router_selection_jitter(self, jitter):
2587        cmd = 'routerselectionjitter %d' % jitter
2588        self.send_command(cmd)
2589        self._expect_done()
2590
2591    def set_active_dataset(
2592        self,
2593        timestamp=None,
2594        channel=None,
2595        channel_mask=None,
2596        extended_panid=None,
2597        mesh_local_prefix=None,
2598        network_key=None,
2599        network_name=None,
2600        panid=None,
2601        pskc=None,
2602        security_policy=[],
2603        updateExisting=False,
2604    ):
2605
2606        if updateExisting:
2607            self.send_command('dataset init active', go=False)
2608        else:
2609            self.send_command('dataset clear', go=False)
2610        self._expect_done()
2611
2612        if timestamp is not None:
2613            cmd = 'dataset activetimestamp %d' % timestamp
2614            self.send_command(cmd, go=False)
2615            self._expect_done()
2616
2617        if channel is not None:
2618            cmd = 'dataset channel %d' % channel
2619            self.send_command(cmd, go=False)
2620            self._expect_done()
2621
2622        if channel_mask is not None:
2623            cmd = 'dataset channelmask %d' % channel_mask
2624            self.send_command(cmd, go=False)
2625            self._expect_done()
2626
2627        if extended_panid is not None:
2628            cmd = 'dataset extpanid %s' % extended_panid
2629            self.send_command(cmd, go=False)
2630            self._expect_done()
2631
2632        if mesh_local_prefix is not None:
2633            cmd = 'dataset meshlocalprefix %s' % mesh_local_prefix
2634            self.send_command(cmd, go=False)
2635            self._expect_done()
2636
2637        if network_key is not None:
2638            cmd = 'dataset networkkey %s' % network_key
2639            self.send_command(cmd, go=False)
2640            self._expect_done()
2641
2642        if network_name is not None:
2643            cmd = 'dataset networkname %s' % network_name
2644            self.send_command(cmd, go=False)
2645            self._expect_done()
2646
2647        if panid is not None:
2648            cmd = 'dataset panid %d' % panid
2649            self.send_command(cmd, go=False)
2650            self._expect_done()
2651
2652        if pskc is not None:
2653            cmd = 'dataset pskc %s' % pskc
2654            self.send_command(cmd, go=False)
2655            self._expect_done()
2656
2657        if security_policy is not None:
2658            if len(security_policy) >= 2:
2659                cmd = 'dataset securitypolicy %s %s' % (
2660                    str(security_policy[0]),
2661                    security_policy[1],
2662                )
2663            if len(security_policy) >= 3:
2664                cmd += ' %s' % (str(security_policy[2]))
2665            self.send_command(cmd, go=False)
2666            self._expect_done()
2667
2668        self.send_command('dataset commit active', go=False)
2669        self._expect_done()
2670
2671    def set_pending_dataset(self, pendingtimestamp, activetimestamp, panid=None, channel=None, delay=None):
2672        self.send_command('dataset clear')
2673        self._expect_done()
2674
2675        cmd = 'dataset pendingtimestamp %d' % pendingtimestamp
2676        self.send_command(cmd)
2677        self._expect_done()
2678
2679        cmd = 'dataset activetimestamp %d' % activetimestamp
2680        self.send_command(cmd)
2681        self._expect_done()
2682
2683        if panid is not None:
2684            cmd = 'dataset panid %d' % panid
2685            self.send_command(cmd)
2686            self._expect_done()
2687
2688        if channel is not None:
2689            cmd = 'dataset channel %d' % channel
2690            self.send_command(cmd)
2691            self._expect_done()
2692
2693        if delay is not None:
2694            cmd = 'dataset delay %d' % delay
2695            self.send_command(cmd)
2696            self._expect_done()
2697
2698        # Set the meshlocal prefix in config.py
2699        self.send_command('dataset meshlocalprefix %s' % config.MESH_LOCAL_PREFIX.split('/')[0])
2700        self._expect_done()
2701
2702        self.send_command('dataset commit pending')
2703        self._expect_done()
2704
2705    def start_dataset_updater(self, panid=None, channel=None, security_policy=None, delay=None):
2706        self.send_command('dataset clear')
2707        self._expect_done()
2708
2709        if panid is not None:
2710            cmd = 'dataset panid %d' % panid
2711            self.send_command(cmd)
2712            self._expect_done()
2713
2714        if channel is not None:
2715            cmd = 'dataset channel %d' % channel
2716            self.send_command(cmd)
2717            self._expect_done()
2718
2719        if security_policy is not None:
2720            cmd = 'dataset securitypolicy %d %s ' % (security_policy[0], security_policy[1])
2721            if (len(security_policy) >= 3):
2722                cmd += '%d ' % (security_policy[2])
2723            self.send_command(cmd)
2724            self._expect_done()
2725
2726        if delay is not None:
2727            cmd = 'dataset delay %d ' % delay
2728            self.send_command(cmd)
2729            self._expect_done()
2730
2731        self.send_command('dataset updater start')
2732        self._expect_done()
2733
2734    def announce_begin(self, mask, count, period, ipaddr):
2735        cmd = 'commissioner announce %d %d %d %s' % (
2736            mask,
2737            count,
2738            period,
2739            ipaddr,
2740        )
2741        self.send_command(cmd)
2742        self._expect_done()
2743
2744    def send_mgmt_active_set(
2745        self,
2746        active_timestamp=None,
2747        channel=None,
2748        channel_mask=None,
2749        extended_panid=None,
2750        panid=None,
2751        network_key=None,
2752        mesh_local=None,
2753        network_name=None,
2754        security_policy=None,
2755        binary=None,
2756    ):
2757        cmd = 'dataset mgmtsetcommand active '
2758
2759        if active_timestamp is not None:
2760            cmd += 'activetimestamp %d ' % active_timestamp
2761
2762        if channel is not None:
2763            cmd += 'channel %d ' % channel
2764
2765        if channel_mask is not None:
2766            cmd += 'channelmask %d ' % channel_mask
2767
2768        if extended_panid is not None:
2769            cmd += 'extpanid %s ' % extended_panid
2770
2771        if panid is not None:
2772            cmd += 'panid %d ' % panid
2773
2774        if network_key is not None:
2775            cmd += 'networkkey %s ' % network_key
2776
2777        if mesh_local is not None:
2778            cmd += 'localprefix %s ' % mesh_local
2779
2780        if network_name is not None:
2781            cmd += 'networkname %s ' % self._escape_escapable(network_name)
2782
2783        if security_policy is not None:
2784            cmd += 'securitypolicy %d %s ' % (security_policy[0], security_policy[1])
2785            if (len(security_policy) >= 3):
2786                cmd += '%d ' % (security_policy[2])
2787
2788        if binary is not None:
2789            cmd += '-x %s ' % binary
2790
2791        self.send_command(cmd)
2792        self._expect_done()
2793
2794    def send_mgmt_active_get(self, addr='', tlvs=[]):
2795        cmd = 'dataset mgmtgetcommand active'
2796
2797        if addr != '':
2798            cmd += ' address '
2799            cmd += addr
2800
2801        if len(tlvs) != 0:
2802            tlv_str = ''.join('%02x' % tlv for tlv in tlvs)
2803            cmd += ' -x '
2804            cmd += tlv_str
2805
2806        self.send_command(cmd)
2807        self._expect_done()
2808
2809    def send_mgmt_pending_get(self, addr='', tlvs=[]):
2810        cmd = 'dataset mgmtgetcommand pending'
2811
2812        if addr != '':
2813            cmd += ' address '
2814            cmd += addr
2815
2816        if len(tlvs) != 0:
2817            tlv_str = ''.join('%02x' % tlv for tlv in tlvs)
2818            cmd += ' -x '
2819            cmd += tlv_str
2820
2821        self.send_command(cmd)
2822        self._expect_done()
2823
2824    def send_mgmt_pending_set(
2825        self,
2826        pending_timestamp=None,
2827        active_timestamp=None,
2828        delay_timer=None,
2829        channel=None,
2830        panid=None,
2831        network_key=None,
2832        mesh_local=None,
2833        network_name=None,
2834    ):
2835        cmd = 'dataset mgmtsetcommand pending '
2836        if pending_timestamp is not None:
2837            cmd += 'pendingtimestamp %d ' % pending_timestamp
2838
2839        if active_timestamp is not None:
2840            cmd += 'activetimestamp %d ' % active_timestamp
2841
2842        if delay_timer is not None:
2843            cmd += 'delaytimer %d ' % delay_timer
2844
2845        if channel is not None:
2846            cmd += 'channel %d ' % channel
2847
2848        if panid is not None:
2849            cmd += 'panid %d ' % panid
2850
2851        if network_key is not None:
2852            cmd += 'networkkey %s ' % network_key
2853
2854        if mesh_local is not None:
2855            cmd += 'localprefix %s ' % mesh_local
2856
2857        if network_name is not None:
2858            cmd += 'networkname %s ' % self._escape_escapable(network_name)
2859
2860        self.send_command(cmd)
2861        self._expect_done()
2862
2863    def coap_cancel(self):
2864        """
2865        Cancel a CoAP subscription.
2866        """
2867        cmd = 'coap cancel'
2868        self.send_command(cmd)
2869        self._expect_done()
2870
2871    def coap_delete(self, ipaddr, uri, con=False, payload=None):
2872        """
2873        Send a DELETE request via CoAP.
2874        """
2875        return self._coap_rq('delete', ipaddr, uri, con, payload)
2876
2877    def coap_get(self, ipaddr, uri, con=False, payload=None):
2878        """
2879        Send a GET request via CoAP.
2880        """
2881        return self._coap_rq('get', ipaddr, uri, con, payload)
2882
2883    def coap_get_block(self, ipaddr, uri, size=16, count=0):
2884        """
2885        Send a GET request via CoAP.
2886        """
2887        return self._coap_rq_block('get', ipaddr, uri, size, count)
2888
2889    def coap_observe(self, ipaddr, uri, con=False, payload=None):
2890        """
2891        Send a GET request via CoAP with Observe set.
2892        """
2893        return self._coap_rq('observe', ipaddr, uri, con, payload)
2894
2895    def coap_post(self, ipaddr, uri, con=False, payload=None):
2896        """
2897        Send a POST request via CoAP.
2898        """
2899        return self._coap_rq('post', ipaddr, uri, con, payload)
2900
2901    def coap_post_block(self, ipaddr, uri, size=16, count=0):
2902        """
2903        Send a POST request via CoAP.
2904        """
2905        return self._coap_rq_block('post', ipaddr, uri, size, count)
2906
2907    def coap_put(self, ipaddr, uri, con=False, payload=None):
2908        """
2909        Send a PUT request via CoAP.
2910        """
2911        return self._coap_rq('put', ipaddr, uri, con, payload)
2912
2913    def coap_put_block(self, ipaddr, uri, size=16, count=0):
2914        """
2915        Send a PUT request via CoAP.
2916        """
2917        return self._coap_rq_block('put', ipaddr, uri, size, count)
2918
2919    def _coap_rq(self, method, ipaddr, uri, con=False, payload=None):
2920        """
2921        Issue a GET/POST/PUT/DELETE/GET OBSERVE request.
2922        """
2923        cmd = 'coap %s %s %s' % (method, ipaddr, uri)
2924        if con:
2925            cmd += ' con'
2926        else:
2927            cmd += ' non'
2928
2929        if payload is not None:
2930            cmd += ' %s' % payload
2931
2932        self.send_command(cmd)
2933        return self.coap_wait_response()
2934
2935    def _coap_rq_block(self, method, ipaddr, uri, size=16, count=0):
2936        """
2937        Issue a GET/POST/PUT/DELETE/GET OBSERVE BLOCK request.
2938        """
2939        cmd = 'coap %s %s %s' % (method, ipaddr, uri)
2940
2941        cmd += ' block-%d' % size
2942
2943        if count != 0:
2944            cmd += ' %d' % count
2945
2946        self.send_command(cmd)
2947        return self.coap_wait_response()
2948
2949    def coap_wait_response(self):
2950        """
2951        Wait for a CoAP response, and return it.
2952        """
2953        if isinstance(self.simulator, simulator.VirtualTime):
2954            self.simulator.go(5)
2955            timeout = 1
2956        else:
2957            timeout = 5
2958
2959        self._expect(r'coap response from ([\da-f:]+)(?: OBS=(\d+))?'
2960                     r'(?: with payload: ([\da-f]+))?\b',
2961                     timeout=timeout)
2962        (source, observe, payload) = self.pexpect.match.groups()
2963        source = source.decode('UTF-8')
2964
2965        if observe is not None:
2966            observe = int(observe, base=10)
2967
2968        if payload is not None:
2969            try:
2970                payload = binascii.a2b_hex(payload).decode('UTF-8')
2971            except UnicodeDecodeError:
2972                pass
2973
2974        # Return the values received
2975        return dict(source=source, observe=observe, payload=payload)
2976
2977    def coap_wait_request(self):
2978        """
2979        Wait for a CoAP request to be made.
2980        """
2981        if isinstance(self.simulator, simulator.VirtualTime):
2982            self.simulator.go(5)
2983            timeout = 1
2984        else:
2985            timeout = 5
2986
2987        self._expect(r'coap request from ([\da-f:]+)(?: OBS=(\d+))?'
2988                     r'(?: with payload: ([\da-f]+))?\b',
2989                     timeout=timeout)
2990        (source, observe, payload) = self.pexpect.match.groups()
2991        source = source.decode('UTF-8')
2992
2993        if observe is not None:
2994            observe = int(observe, base=10)
2995
2996        if payload is not None:
2997            payload = binascii.a2b_hex(payload).decode('UTF-8')
2998
2999        # Return the values received
3000        return dict(source=source, observe=observe, payload=payload)
3001
3002    def coap_wait_subscribe(self):
3003        """
3004        Wait for a CoAP client to be subscribed.
3005        """
3006        if isinstance(self.simulator, simulator.VirtualTime):
3007            self.simulator.go(5)
3008            timeout = 1
3009        else:
3010            timeout = 5
3011
3012        self._expect(r'Subscribing client\b', timeout=timeout)
3013
3014    def coap_wait_ack(self):
3015        """
3016        Wait for a CoAP notification ACK.
3017        """
3018        if isinstance(self.simulator, simulator.VirtualTime):
3019            self.simulator.go(5)
3020            timeout = 1
3021        else:
3022            timeout = 5
3023
3024        self._expect(r'Received ACK in reply to notification from ([\da-f:]+)\b', timeout=timeout)
3025        (source,) = self.pexpect.match.groups()
3026        source = source.decode('UTF-8')
3027
3028        return source
3029
3030    def coap_set_resource_path(self, path):
3031        """
3032        Set the path for the CoAP resource.
3033        """
3034        cmd = 'coap resource %s' % path
3035        self.send_command(cmd)
3036        self._expect_done()
3037
3038    def coap_set_resource_path_block(self, path, count=0):
3039        """
3040        Set the path for the CoAP resource and how many blocks can be received from this resource.
3041        """
3042        cmd = 'coap resource %s %d' % (path, count)
3043        self.send_command(cmd)
3044        self._expect('Done')
3045
3046    def coap_set_content(self, content):
3047        """
3048        Set the content of the CoAP resource.
3049        """
3050        cmd = 'coap set %s' % content
3051        self.send_command(cmd)
3052        self._expect_done()
3053
3054    def coap_start(self):
3055        """
3056        Start the CoAP service.
3057        """
3058        cmd = 'coap start'
3059        self.send_command(cmd)
3060        self._expect_done()
3061
3062    def coap_stop(self):
3063        """
3064        Stop the CoAP service.
3065        """
3066        cmd = 'coap stop'
3067        self.send_command(cmd)
3068
3069        if isinstance(self.simulator, simulator.VirtualTime):
3070            self.simulator.go(5)
3071            timeout = 1
3072        else:
3073            timeout = 5
3074
3075        self._expect_done(timeout=timeout)
3076
3077    def coaps_start_psk(self, psk, pskIdentity):
3078        cmd = 'coaps psk %s %s' % (psk, pskIdentity)
3079        self.send_command(cmd)
3080        self._expect_done()
3081
3082        cmd = 'coaps start'
3083        self.send_command(cmd)
3084        self._expect_done()
3085
3086    def coaps_start_x509(self):
3087        cmd = 'coaps x509'
3088        self.send_command(cmd)
3089        self._expect_done()
3090
3091        cmd = 'coaps start'
3092        self.send_command(cmd)
3093        self._expect_done()
3094
3095    def coaps_set_resource_path(self, path):
3096        cmd = 'coaps resource %s' % path
3097        self.send_command(cmd)
3098        self._expect_done()
3099
3100    def coaps_stop(self):
3101        cmd = 'coaps stop'
3102        self.send_command(cmd)
3103
3104        if isinstance(self.simulator, simulator.VirtualTime):
3105            self.simulator.go(5)
3106            timeout = 1
3107        else:
3108            timeout = 5
3109
3110        self._expect_done(timeout=timeout)
3111
3112    def coaps_connect(self, ipaddr):
3113        cmd = 'coaps connect %s' % ipaddr
3114        self.send_command(cmd)
3115
3116        if isinstance(self.simulator, simulator.VirtualTime):
3117            self.simulator.go(5)
3118            timeout = 1
3119        else:
3120            timeout = 5
3121
3122        self._expect('coaps connected', timeout=timeout)
3123
3124    def coaps_disconnect(self):
3125        cmd = 'coaps disconnect'
3126        self.send_command(cmd)
3127        self._expect_done()
3128        self.simulator.go(5)
3129
3130    def coaps_get(self):
3131        cmd = 'coaps get test'
3132        self.send_command(cmd)
3133
3134        if isinstance(self.simulator, simulator.VirtualTime):
3135            self.simulator.go(5)
3136            timeout = 1
3137        else:
3138            timeout = 5
3139
3140        self._expect('coaps response', timeout=timeout)
3141
3142    def commissioner_mgmtget(self, tlvs_binary=None):
3143        cmd = 'commissioner mgmtget'
3144        if tlvs_binary is not None:
3145            cmd += ' -x %s' % tlvs_binary
3146        self.send_command(cmd)
3147        self._expect_done()
3148
3149    def commissioner_mgmtset(self, tlvs_binary):
3150        cmd = 'commissioner mgmtset -x %s' % tlvs_binary
3151        self.send_command(cmd)
3152        self._expect_done()
3153
3154    def bytes_to_hex_str(self, src):
3155        return ''.join(format(x, '02x') for x in src)
3156
3157    def commissioner_mgmtset_with_tlvs(self, tlvs):
3158        payload = bytearray()
3159        for tlv in tlvs:
3160            payload += tlv.to_hex()
3161        self.commissioner_mgmtset(self.bytes_to_hex_str(payload))
3162
3163    def udp_start(self, local_ipaddr, local_port, bind_unspecified=False):
3164        cmd = 'udp open'
3165        self.send_command(cmd)
3166        self._expect_done()
3167
3168        cmd = 'udp bind %s %s %s' % ("-u" if bind_unspecified else "", local_ipaddr, local_port)
3169        self.send_command(cmd)
3170        self._expect_done()
3171
3172    def udp_stop(self):
3173        cmd = 'udp close'
3174        self.send_command(cmd)
3175        self._expect_done()
3176
3177    def udp_send(self, bytes, ipaddr, port, success=True):
3178        cmd = 'udp send %s %d -s %d ' % (ipaddr, port, bytes)
3179        self.send_command(cmd)
3180        if success:
3181            self._expect_done()
3182        else:
3183            self._expect('Error')
3184
3185    def udp_check_rx(self, bytes_should_rx):
3186        self._expect('%d bytes' % bytes_should_rx)
3187
3188    def set_routereligible(self, enable: bool):
3189        cmd = f'routereligible {"enable" if enable else "disable"}'
3190        self.send_command(cmd)
3191        self._expect_done()
3192
3193    def router_list(self):
3194        cmd = 'router list'
3195        self.send_command(cmd)
3196        self._expect([r'(\d+)((\s\d+)*)'])
3197
3198        g = self.pexpect.match.groups()
3199        router_list = g[0].decode('utf8') + ' ' + g[1].decode('utf8')
3200        router_list = [int(x) for x in router_list.split()]
3201        self._expect_done()
3202        return router_list
3203
3204    def router_table(self):
3205        cmd = 'router table'
3206        self.send_command(cmd)
3207
3208        self._expect(r'(.*)Done')
3209        g = self.pexpect.match.groups()
3210        output = g[0].decode('utf8')
3211        lines = output.strip().split('\n')
3212        lines = [l.strip() for l in lines]
3213        router_table = {}
3214        for i, line in enumerate(lines):
3215            if not line.startswith('|') or not line.endswith('|'):
3216                if i not in (0, 2):
3217                    # should not happen
3218                    print("unexpected line %d: %s" % (i, line))
3219
3220                continue
3221
3222            line = line[1:][:-1]
3223            line = [x.strip() for x in line.split('|')]
3224            if len(line) < 9:
3225                print("unexpected line %d: %s" % (i, line))
3226                continue
3227
3228            try:
3229                int(line[0])
3230            except ValueError:
3231                if i != 1:
3232                    print("unexpected line %d: %s" % (i, line))
3233                continue
3234
3235            id = int(line[0])
3236            rloc16 = int(line[1], 16)
3237            nexthop = int(line[2])
3238            pathcost = int(line[3])
3239            lqin = int(line[4])
3240            lqout = int(line[5])
3241            age = int(line[6])
3242            emac = str(line[7])
3243            link = int(line[8])
3244
3245            router_table[id] = {
3246                'rloc16': rloc16,
3247                'nexthop': nexthop,
3248                'pathcost': pathcost,
3249                'lqin': lqin,
3250                'lqout': lqout,
3251                'age': age,
3252                'emac': emac,
3253                'link': link,
3254            }
3255
3256        return router_table
3257
3258    def link_metrics_query_single_probe(self, dst_addr: str, linkmetrics_flags: str, block: str = ""):
3259        cmd = 'linkmetrics query %s single %s %s' % (dst_addr, linkmetrics_flags, block)
3260        self.send_command(cmd)
3261        self.simulator.go(5)
3262        return self._parse_linkmetrics_query_result(self._expect_command_output())
3263
3264    def link_metrics_query_forward_tracking_series(self, dst_addr: str, series_id: int, block: str = ""):
3265        cmd = 'linkmetrics query %s forward %d %s' % (dst_addr, series_id, block)
3266        self.send_command(cmd)
3267        self.simulator.go(5)
3268        return self._parse_linkmetrics_query_result(self._expect_command_output())
3269
3270    def _parse_linkmetrics_query_result(self, lines):
3271        """Parse link metrics query result"""
3272
3273        # Example of command output:
3274        # ['Received Link Metrics Report from: fe80:0:0:0:146e:a00:0:1',
3275        #  '- PDU Counter: 1 (Count/Summation)',
3276        #  '- LQI: 0 (Exponential Moving Average)',
3277        #  '- Margin: 80 (dB) (Exponential Moving Average)',
3278        #  '- RSSI: -20 (dBm) (Exponential Moving Average)']
3279        #
3280        # Or 'Link Metrics Report, status: {status}'
3281
3282        result = {}
3283        for line in lines:
3284            if line.startswith('- '):
3285                k, v = line[2:].split(': ')
3286                result[k] = v.split(' ')[0]
3287            elif line.startswith('Link Metrics Report, status: '):
3288                result['Status'] = line[29:]
3289        return result
3290
3291    def link_metrics_mgmt_req_enhanced_ack_based_probing(self,
3292                                                         dst_addr: str,
3293                                                         enable: bool,
3294                                                         metrics_flags: str,
3295                                                         ext_flags=''):
3296        cmd = "linkmetrics mgmt %s enhanced-ack" % (dst_addr)
3297        if enable:
3298            cmd = cmd + (" register %s %s" % (metrics_flags, ext_flags))
3299        else:
3300            cmd = cmd + " clear"
3301        self.send_command(cmd)
3302        self._expect_done()
3303
3304    def link_metrics_mgmt_req_forward_tracking_series(self, dst_addr: str, series_id: int, series_flags: str,
3305                                                      metrics_flags: str):
3306        cmd = "linkmetrics mgmt %s forward %d %s %s" % (dst_addr, series_id, series_flags, metrics_flags)
3307        self.send_command(cmd)
3308        self._expect_done()
3309
3310    def link_metrics_send_link_probe(self, dst_addr: str, series_id: int, length: int):
3311        cmd = "linkmetrics probe %s %d %d" % (dst_addr, series_id, length)
3312        self.send_command(cmd)
3313        self._expect_done()
3314
3315    def link_metrics_mgr_set_enabled(self, enable: bool):
3316        op_str = "enable" if enable else "disable"
3317        cmd = f'linkmetricsmgr {op_str}'
3318        self.send_command(cmd)
3319        self._expect_done()
3320
3321    def send_address_notification(self, dst: str, target: str, mliid: str):
3322        cmd = f'fake /a/an {dst} {target} {mliid}'
3323        self.send_command(cmd)
3324        self._expect_done()
3325
3326    def send_proactive_backbone_notification(self, target: str, mliid: str, ltt: int):
3327        cmd = f'fake /b/ba {target} {mliid} {ltt}'
3328        self.send_command(cmd)
3329        self._expect_done()
3330
3331    def dns_get_config(self):
3332        """
3333        Returns the DNS config as a list of property dictionary (string key and string value).
3334
3335        Example output:
3336        {
3337            'Server': '[fd00:0:0:0:0:0:0:1]:1234'
3338            'ResponseTimeout': '5000 ms'
3339            'MaxTxAttempts': '2'
3340            'RecursionDesired': 'no'
3341        }
3342        """
3343        cmd = f'dns config'
3344        self.send_command(cmd)
3345        output = self._expect_command_output()
3346        config = {}
3347        for line in output:
3348            k, v = line.split(': ')
3349            config[k] = v
3350        return config
3351
3352    def dns_set_config(self, config):
3353        cmd = f'dns config {config}'
3354        self.send_command(cmd)
3355        self._expect_done()
3356
3357    def dns_resolve(self, hostname, server=None, port=53):
3358        cmd = f'dns resolve {hostname}'
3359        if server is not None:
3360            cmd += f' {server} {port}'
3361
3362        self.send_command(cmd)
3363        self.simulator.go(10)
3364        output = self._expect_command_output()
3365        dns_resp = output[0]
3366        # example output: "DNS response for host1.default.service.arpa. - fd00:db8:0:0:fd3d:d471:1e8c:b60 TTL:7190 "
3367        #                 " fd00:db8:0:0:0:ff:fe00:9000 TTL:7190"
3368        addrs = dns_resp.strip().split(' - ')[1].split(' ')
3369        ip = [item.strip() for item in addrs[::2]]
3370        ttl = [int(item.split('TTL:')[1]) for item in addrs[1::2]]
3371
3372        return list(zip(ip, ttl))
3373
3374    def _parse_dns_service_info(self, output):
3375        # Example of `output`
3376        #   Port:22222, Priority:2, Weight:2, TTL:7155
3377        #   Host:host2.default.service.arpa.
3378        #   HostAddress:0:0:0:0:0:0:0:0 TTL:0
3379        #   TXT:[a=00, b=02bb] TTL:7155
3380
3381        m = re.match(
3382            r'.*Port:(\d+), Priority:(\d+), Weight:(\d+), TTL:(\d+)\s+Host:(.*?)\s+HostAddress:(\S+) TTL:(\d+)\s+TXT:\[(.*?)\] TTL:(\d+)',
3383            '\r'.join(output))
3384        if not m:
3385            return {}
3386        port, priority, weight, srv_ttl, hostname, address, aaaa_ttl, txt_data, txt_ttl = m.groups()
3387        return {
3388            'port': int(port),
3389            'priority': int(priority),
3390            'weight': int(weight),
3391            'host': hostname,
3392            'address': address,
3393            'txt_data': txt_data,
3394            'srv_ttl': int(srv_ttl),
3395            'txt_ttl': int(txt_ttl),
3396            'aaaa_ttl': int(aaaa_ttl),
3397        }
3398
3399    def dns_resolve_service(self, instance, service, server=None, port=53):
3400        """
3401        Resolves the service instance and returns the instance information as a dict.
3402
3403        Example return value:
3404            {
3405                'port': 12345,
3406                'priority': 0,
3407                'weight': 0,
3408                'host': 'ins1._ipps._tcp.default.service.arpa.',
3409                'address': '2001::1',
3410                'txt_data': 'a=00, b=02bb',
3411                'srv_ttl': 7100,
3412                'txt_ttl': 7100,
3413                'aaaa_ttl': 7100,
3414            }
3415        """
3416        instance = self._escape_escapable(instance)
3417        cmd = f'dns service {instance} {service}'
3418        if server is not None:
3419            cmd += f' {server} {port}'
3420
3421        self.send_command(cmd)
3422        self.simulator.go(10)
3423        output = self._expect_command_output()
3424        info = self._parse_dns_service_info(output)
3425        if not info:
3426            raise Exception('dns resolve service failed: %s.%s' % (instance, service))
3427        return info
3428
3429    @staticmethod
3430    def __parse_hex_string(hexstr: str) -> bytes:
3431        assert (len(hexstr) % 2 == 0)
3432        return bytes(int(hexstr[i:i + 2], 16) for i in range(0, len(hexstr), 2))
3433
3434    def dns_browse(self, service_name, server=None, port=53):
3435        """
3436        Browse the service and returns the instances.
3437
3438        Example return value:
3439            {
3440                'ins1': {
3441                    'port': 12345,
3442                    'priority': 1,
3443                    'weight': 1,
3444                    'host': 'ins1._ipps._tcp.default.service.arpa.',
3445                    'address': '2001::1',
3446                    'txt_data': 'a=00, b=11cf',
3447                    'srv_ttl': 7100,
3448                    'txt_ttl': 7100,
3449                    'aaaa_ttl': 7100,
3450                },
3451                'ins2': {
3452                    'port': 12345,
3453                    'priority': 2,
3454                    'weight': 2,
3455                    'host': 'ins2._ipps._tcp.default.service.arpa.',
3456                    'address': '2001::2',
3457                    'txt_data': 'a=01, b=23dd',
3458                    'srv_ttl': 7100,
3459                    'txt_ttl': 7100,
3460                    'aaaa_ttl': 7100,
3461                }
3462            }
3463        """
3464        cmd = f'dns browse {service_name}'
3465        if server is not None:
3466            cmd += f' {server} {port}'
3467
3468        self.send_command(cmd)
3469        self.simulator.go(10)
3470        output = self._expect_command_output()
3471
3472        # Example output:
3473        # DNS browse response for _ipps._tcp.default.service.arpa.
3474        # ins2
3475        #     Port:22222, Priority:2, Weight:2, TTL:7175
3476        #     Host:host2.default.service.arpa.
3477        #     HostAddress:fd00:db8:0:0:3205:28dd:5b87:6a63 TTL:7175
3478        #     TXT:[a=00, b=11cf] TTL:7175
3479        # ins1
3480        #     Port:11111, Priority:1, Weight:1, TTL:7170
3481        #     Host:host1.default.service.arpa.
3482        #     HostAddress:fd00:db8:0:0:39f4:d9:eb4f:778 TTL:7170
3483        #     TXT:[a=01, b=23dd] TTL:7170
3484        # Done
3485
3486        result = {}
3487        index = 1  # skip first line
3488        while index < len(output):
3489            ins = output[index].strip()
3490            result[ins] = self._parse_dns_service_info(output[index + 1:index + 6])
3491            index = index + (5 if result[ins] else 1)
3492        return result
3493
3494    def set_mliid(self, mliid: str):
3495        cmd = f'mliid {mliid}'
3496        self.send_command(cmd)
3497        self._expect_command_output()
3498
3499    def history_netinfo(self, num_entries=0):
3500        """
3501        Get the `netinfo` history list, parse each entry and return
3502        a list of dictionary (string key and string value) entries.
3503
3504        Example of return value:
3505        [
3506            {
3507                'age': '00:00:00.000 ago',
3508                'role': 'disabled',
3509                'mode': 'rdn',
3510                'rloc16': '0x7400',
3511                'partition-id': '1318093703'
3512            },
3513            {
3514                'age': '00:00:02.588 ago',
3515                'role': 'leader',
3516                'mode': 'rdn',
3517                'rloc16': '0x7400',
3518                'partition-id': '1318093703'
3519            }
3520        ]
3521        """
3522        cmd = f'history netinfo list {num_entries}'
3523        self.send_command(cmd)
3524        output = self._expect_command_output()
3525        netinfos = []
3526        for entry in output:
3527            netinfo = {}
3528            age, info = entry.split(' -> ')
3529            netinfo['age'] = age
3530            for item in info.split(' '):
3531                k, v = item.split(':')
3532                netinfo[k] = v
3533            netinfos.append(netinfo)
3534        return netinfos
3535
3536    def history_rx(self, num_entries=0):
3537        """
3538        Get the IPv6 RX history list, parse each entry and return
3539        a list of dictionary (string key and string value) entries.
3540
3541        Example of return value:
3542        [
3543            {
3544                'age': '00:00:01.999',
3545                'type': 'ICMP6(EchoReqst)',
3546                'len': '16',
3547                'sec': 'yes',
3548                'prio': 'norm',
3549                'rss': '-20',
3550                'from': '0xac00',
3551                'radio': '15.4',
3552                'src': '[fd00:db8:0:0:2cfa:fd61:58a9:f0aa]:0',
3553                'dst': '[fd00:db8:0:0:ed7e:2d04:e543:eba5]:0',
3554            }
3555        ]
3556        """
3557        cmd = f'history rx list {num_entries}'
3558        self.send_command(cmd)
3559        return self._parse_history_rx_tx_ouput(self._expect_command_output())
3560
3561    def history_tx(self, num_entries=0):
3562        """
3563        Get the IPv6 TX history list, parse each entry and return
3564        a list of dictionary (string key and string value) entries.
3565
3566        Example of return value:
3567        [
3568            {
3569                'age': '00:00:01.999',
3570                'type': 'ICMP6(EchoReply)',
3571                'len': '16',
3572                'sec': 'yes',
3573                'prio': 'norm',
3574                'to': '0xac00',
3575                'tx-success': 'yes',
3576                'radio': '15.4',
3577                'src': '[fd00:db8:0:0:ed7e:2d04:e543:eba5]:0',
3578                'dst': '[fd00:db8:0:0:2cfa:fd61:58a9:f0aa]:0',
3579
3580            }
3581        ]
3582        """
3583        cmd = f'history tx list {num_entries}'
3584        self.send_command(cmd)
3585        return self._parse_history_rx_tx_ouput(self._expect_command_output())
3586
3587    def _parse_history_rx_tx_ouput(self, lines):
3588        rxtx_list = []
3589        for line in lines:
3590            if line.strip().startswith('type:'):
3591                for item in line.strip().split(' '):
3592                    k, v = item.split(':')
3593                    entry[k] = v
3594            elif line.strip().startswith('src:'):
3595                entry['src'] = line[4:]
3596            elif line.strip().startswith('dst:'):
3597                entry['dst'] = line[4:]
3598                rxtx_list.append(entry)
3599            else:
3600                entry = {}
3601                entry['age'] = line
3602
3603        return rxtx_list
3604
3605    def set_router_id_range(self, min_router_id: int, max_router_id: int):
3606        cmd = f'routeridrange {min_router_id} {max_router_id}'
3607        self.send_command(cmd)
3608        self._expect_command_output()
3609
3610    def get_router_id_range(self):
3611        cmd = 'routeridrange'
3612        self.send_command(cmd)
3613        line = self._expect_command_output()[0]
3614        return [int(item) for item in line.split()]
3615
3616    def get_channel_monitor_info(self) -> Dict:
3617        """
3618        Returns:
3619            Dict of channel monitor info, e.g.
3620                {'enabled': '1',
3621                 'interval': '41000',
3622                 'threshold': '-75',
3623                 'window': '960',
3624                 'count': '985',
3625                 'occupancies': {
3626                    '11': '0.00%',
3627                    '12': '3.50%',
3628                    '13': '9.89%',
3629                    '14': '15.36%',
3630                    '15': '20.02%',
3631                    '16': '21.95%',
3632                    '17': '32.71%',
3633                    '18': '35.76%',
3634                    '19': '37.97%',
3635                    '20': '43.68%',
3636                    '21': '48.95%',
3637                    '22': '54.05%',
3638                    '23': '58.65%',
3639                    '24': '68.26%',
3640                    '25': '66.73%',
3641                    '26': '73.12%'
3642                    }
3643                }
3644        """
3645        config = {}
3646        self.send_command('channel monitor')
3647
3648        for line in self._expect_results(r'\S+'):
3649            if re.match(r'.*:\s.*', line):
3650                key, val = line.split(':')
3651                config.update({key: val.strip()})
3652            elif re.match(r'.*:', line):  # occupancy
3653                occ_key, val = line.split(':')
3654                val = {}
3655                config.update({occ_key: val})
3656            elif 'busy' in line:
3657                # channel occupancies
3658                key = line.split()[1]
3659                val = line.split()[3]
3660                config[occ_key].update({key: val})
3661        return config
3662
3663    def set_channel_manager_auto_enable(self, enable: bool):
3664        self.send_command(f'channel manager auto {int(enable)}')
3665        self._expect_done()
3666
3667    def set_channel_manager_autocsl_enable(self, enable: bool):
3668        self.send_command(f'channel manager autocsl {int(enable)}')
3669        self._expect_done()
3670
3671    def set_channel_manager_supported(self, channel_mask: int):
3672        self.send_command(f'channel manager supported {int(channel_mask)}')
3673        self._expect_done()
3674
3675    def set_channel_manager_favored(self, channel_mask: int):
3676        self.send_command(f'channel manager favored {int(channel_mask)}')
3677        self._expect_done()
3678
3679    def set_channel_manager_interval(self, interval: int):
3680        self.send_command(f'channel manager interval {interval}')
3681        self._expect_done()
3682
3683    def set_channel_manager_cca_threshold(self, hex_value: str):
3684        self.send_command(f'channel manager threshold {hex_value}')
3685        self._expect_done()
3686
3687    def get_channel_manager_config(self):
3688        self.send_command('channel manager')
3689        return self._expect_key_value_pairs(r'\S+')
3690
3691
3692class Node(NodeImpl, OtCli):
3693    pass
3694
3695
3696class LinuxHost():
3697    PING_RESPONSE_PATTERN = re.compile(r'\d+ bytes from .*:.*')
3698    ETH_DEV = config.BACKBONE_IFNAME
3699
3700    def enable_ether(self):
3701        """Enable the ethernet interface.
3702        """
3703
3704        self.bash(f'ip link set {self.ETH_DEV} up')
3705
3706    def disable_ether(self):
3707        """Disable the ethernet interface.
3708        """
3709
3710        self.bash(f'ip link set {self.ETH_DEV} down')
3711
3712    def get_ether_addrs(self):
3713        output = self.bash(f'ip -6 addr list dev {self.ETH_DEV}')
3714
3715        addrs = []
3716        for line in output:
3717            # line example: "inet6 fe80::42:c0ff:fea8:903/64 scope link"
3718            line = line.strip().split()
3719
3720            if line and line[0] == 'inet6':
3721                addr = line[1]
3722                if '/' in addr:
3723                    addr = addr.split('/')[0]
3724                addrs.append(addr)
3725
3726        logging.debug('%s: get_ether_addrs: %r', self, addrs)
3727        return addrs
3728
3729    def get_ether_mac(self):
3730        output = self.bash(f'ip addr list dev {self.ETH_DEV}')
3731        for line in output:
3732            # link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
3733            line = line.strip().split()
3734            if line and line[0] == 'link/ether':
3735                return line[1]
3736
3737        assert False, output
3738
3739    def add_ipmaddr_ether(self, ip: str):
3740        cmd = f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/mcast6.py {self.ETH_DEV} {ip} &'
3741        self.bash(cmd)
3742
3743    def ping_ether(self, ipaddr, num_responses=1, size=None, timeout=5, ttl=None, interface='eth0') -> int:
3744
3745        cmd = f'ping -6 {ipaddr} -I {interface} -c {num_responses} -W {timeout}'
3746        if size is not None:
3747            cmd += f' -s {size}'
3748
3749        if ttl is not None:
3750            cmd += f' -t {ttl}'
3751
3752        resp_count = 0
3753
3754        try:
3755            for line in self.bash(cmd):
3756                if self.PING_RESPONSE_PATTERN.match(line):
3757                    resp_count += 1
3758        except subprocess.CalledProcessError:
3759            pass
3760
3761        return resp_count
3762
3763    def get_ip6_address(self, address_type: config.ADDRESS_TYPE):
3764        """Get specific type of IPv6 address configured on thread device.
3765
3766        Args:
3767            address_type: the config.ADDRESS_TYPE type of IPv6 address.
3768
3769        Returns:
3770            IPv6 address string.
3771        """
3772        if address_type == config.ADDRESS_TYPE.BACKBONE_GUA:
3773            return self._getBackboneGua()
3774        elif address_type == config.ADDRESS_TYPE.ONLINK_ULA:
3775            return self._getInfraUla()
3776        elif address_type == config.ADDRESS_TYPE.ONLINK_GUA:
3777            return self._getInfraGua()
3778        else:
3779            raise ValueError(f'unsupported address type: {address_type}')
3780
3781    def _getBackboneGua(self) -> Optional[str]:
3782        for addr in self.get_ether_addrs():
3783            if re.match(config.BACKBONE_PREFIX_REGEX_PATTERN, addr, re.I):
3784                return addr
3785
3786        return None
3787
3788    def _getInfraUla(self) -> Optional[str]:
3789        """ Returns the ULA addresses autoconfigured on the infra link.
3790        """
3791        addrs = []
3792        for addr in self.get_ether_addrs():
3793            if re.match(config.ONLINK_PREFIX_REGEX_PATTERN, addr, re.I):
3794                addrs.append(addr)
3795
3796        return addrs
3797
3798    def _getInfraGua(self) -> Optional[str]:
3799        """ Returns the GUA addresses autoconfigured on the infra link.
3800        """
3801
3802        gua_prefix = config.ONLINK_GUA_PREFIX.split('::/')[0]
3803        return [addr for addr in self.get_ether_addrs() if addr.startswith(gua_prefix)]
3804
3805    def ping(self, *args, **kwargs):
3806        backbone = kwargs.pop('backbone', False)
3807        if backbone:
3808            return self.ping_ether(*args, **kwargs)
3809        else:
3810            return super().ping(*args, **kwargs)
3811
3812    def udp_send_host(self, ipaddr, port, data, hop_limit=None):
3813        if hop_limit is None:
3814            if ipaddress.ip_address(ipaddr).is_multicast:
3815                hop_limit = 10
3816            else:
3817                hop_limit = 64
3818        cmd = f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/udp_send_host.py {ipaddr} {port} "{data}" {hop_limit}'
3819        self.bash(cmd)
3820
3821    def add_ipmaddr(self, *args, **kwargs):
3822        backbone = kwargs.pop('backbone', False)
3823        if backbone:
3824            return self.add_ipmaddr_ether(*args, **kwargs)
3825        else:
3826            return super().add_ipmaddr(*args, **kwargs)
3827
3828    def ip_neighbors_flush(self):
3829        # clear neigh cache on linux
3830        self.bash(f'ip -6 neigh list dev {self.ETH_DEV}')
3831        self.bash(f'ip -6 neigh flush nud all nud failed nud noarp dev {self.ETH_DEV}')
3832        self.bash('ip -6 neigh list nud all dev %s | cut -d " " -f1 | sudo xargs -I{} ip -6 neigh delete {} dev %s' %
3833                  (self.ETH_DEV, self.ETH_DEV))
3834        self.bash(f'ip -6 neigh list dev {self.ETH_DEV}')
3835
3836    def publish_mdns_service(self, instance_name, service_type, port, host_name, txt):
3837        """Publish an mDNS service on the Ethernet.
3838
3839        :param instance_name: the service instance name.
3840        :param service_type: the service type in format of '<service_type>.<protocol>'.
3841        :param port: the port the service is at.
3842        :param host_name: the host name this service points to. The domain
3843                          should not be included.
3844        :param txt: a dictionary containing the key-value pairs of the TXT record.
3845        """
3846        txt_string = ' '.join([f'{key}={value}' for key, value in txt.items()])
3847        self.bash(f'avahi-publish -s {instance_name}  {service_type} {port} -H {host_name}.local {txt_string} &')
3848
3849    def publish_mdns_host(self, hostname, addresses):
3850        """Publish an mDNS host on the Ethernet
3851
3852        :param host_name: the host name this service points to. The domain
3853                          should not be included.
3854        :param addresses: a list of strings representing the addresses to
3855                          be registered with the host.
3856        """
3857        for address in addresses:
3858            self.bash(f'avahi-publish -a {hostname}.local {address} &')
3859
3860    def browse_mdns_services(self, name, timeout=2):
3861        """ Browse mDNS services on the ethernet.
3862
3863        :param name: the service type name in format of '<service-name>.<protocol>'.
3864        :param timeout: timeout value in seconds before returning.
3865        :return: A list of service instance names.
3866        """
3867
3868        self.bash(f'dns-sd -Z {name} local. > /tmp/{name} 2>&1 &')
3869        time.sleep(timeout)
3870        self.bash('pkill dns-sd')
3871
3872        instances = []
3873        for line in self.bash(f'cat /tmp/{name}', encoding='raw_unicode_escape'):
3874            elements = line.split()
3875            if len(elements) >= 3 and elements[0] == name and elements[1] == 'PTR':
3876                instances.append(elements[2][:-len('.' + name)])
3877        return instances
3878
3879    def discover_mdns_service(self, instance, name, host_name, timeout=2):
3880        """ Discover/resolve the mDNS service on ethernet.
3881
3882        :param instance: the service instance name.
3883        :param name: the service name in format of '<service-name>.<protocol>'.
3884        :param host_name: the host name this service points to. The domain
3885                          should not be included.
3886        :param timeout: timeout value in seconds before returning.
3887        :return: a dict of service properties or None.
3888
3889        The return value is a dict with the same key/values of srp_server_get_service
3890        except that we don't have a `deleted` field here.
3891        """
3892        host_name_file = self.bash('mktemp')[0].strip()
3893        service_data_file = self.bash('mktemp')[0].strip()
3894
3895        self.bash(f'dns-sd -Z {name} local. > {service_data_file} 2>&1 &')
3896        time.sleep(timeout)
3897
3898        full_service_name = f'{instance}.{name}'
3899        # When hostname is unspecified, extract hostname from browse result
3900        if host_name is None:
3901            for line in self.bash(f'cat {service_data_file}', encoding='raw_unicode_escape'):
3902                elements = line.split()
3903                if len(elements) >= 6 and elements[0] == full_service_name and elements[1] == 'SRV':
3904                    host_name = elements[5].split('.')[0]
3905                    break
3906
3907        assert (host_name is not None)
3908        self.bash(f'dns-sd -G v6 {host_name}.local. > {host_name_file} 2>&1 &')
3909        time.sleep(timeout)
3910
3911        self.bash('pkill dns-sd')
3912        addresses = []
3913        service = {}
3914
3915        logging.debug(self.bash(f'cat {host_name_file}', encoding='raw_unicode_escape'))
3916        logging.debug(self.bash(f'cat {service_data_file}', encoding='raw_unicode_escape'))
3917
3918        # example output in the host file:
3919        # Timestamp     A/R Flags if Hostname                               Address                                     TTL
3920        # 9:38:09.274  Add     23 48 my-host.local.                         2001:0000:0000:0000:0000:0000:0000:0002%<0>  120
3921        #
3922        for line in self.bash(f'cat {host_name_file}', encoding='raw_unicode_escape'):
3923            elements = line.split()
3924            fullname = f'{host_name}.local.'
3925            if fullname not in elements:
3926                continue
3927            if 'Add' not in elements:
3928                continue
3929            addresses.append(elements[elements.index(fullname) + 1].split('%')[0])
3930
3931        logging.debug(f'addresses of {host_name}: {addresses}')
3932
3933        # example output of in the service file:
3934        # _ipps._tcp                                      PTR     my-service._ipps._tcp
3935        # my-service._ipps._tcp                           SRV     0 0 12345 my-host.local. ; Replace with unicast FQDN of target host
3936        # my-service._ipps._tcp                           TXT     ""
3937        #
3938        is_txt = False
3939        txt = ''
3940        for line in self.bash(f'cat {service_data_file}', encoding='raw_unicode_escape'):
3941            elements = line.split()
3942            if len(elements) >= 2 and elements[0] == full_service_name and elements[1] == 'TXT':
3943                is_txt = True
3944            if is_txt:
3945                txt += line.strip()
3946                if line.strip().endswith('"'):
3947                    is_txt = False
3948                    txt_dict = self.__parse_dns_sd_txt(txt)
3949                    logging.info(f'txt = {txt_dict}')
3950                    service['txt'] = txt_dict
3951
3952            if not elements or elements[0] != full_service_name:
3953                continue
3954            if elements[1] == 'SRV':
3955                service['fullname'] = elements[0]
3956                service['instance'] = instance
3957                service['name'] = name
3958                service['priority'] = int(elements[2])
3959                service['weight'] = int(elements[3])
3960                service['port'] = int(elements[4])
3961                service['host_fullname'] = elements[5]
3962                assert (service['host_fullname'] == f'{host_name}.local.')
3963                service['host'] = host_name
3964                service['addresses'] = addresses
3965        return service or None
3966
3967    def start_radvd_service(self, prefix, slaac):
3968        self.bash("""cat >/etc/radvd.conf <<EOF
3969interface eth0
3970{
3971    AdvSendAdvert on;
3972
3973    AdvReachableTime 200;
3974    AdvRetransTimer 200;
3975    AdvDefaultLifetime 1800;
3976    MinRtrAdvInterval 1200;
3977    MaxRtrAdvInterval 1800;
3978    AdvDefaultPreference low;
3979
3980    prefix %s
3981    {
3982        AdvOnLink on;
3983        AdvAutonomous %s;
3984        AdvRouterAddr off;
3985        AdvPreferredLifetime 1800;
3986        AdvValidLifetime 1800;
3987    };
3988};
3989EOF
3990""" % (prefix, 'on' if slaac else 'off'))
3991        self.bash('service radvd start')
3992        self.bash('service radvd status')  # Make sure radvd service is running
3993
3994    def stop_radvd_service(self):
3995        self.bash('service radvd stop')
3996
3997    def kill_radvd_service(self):
3998        self.bash('pkill radvd')
3999
4000    def __parse_dns_sd_txt(self, line: str):
4001        # Example TXT entry:
4002        # "xp=\\000\\013\\184\\000\\000\\000\\000\\000"
4003        txt = {}
4004        for entry in re.findall(r'"((?:[^\\]|\\.)*?)"', line):
4005            if '=' not in entry:
4006                continue
4007
4008            k, v = entry.split('=', 1)
4009            txt[k] = v
4010
4011        return txt
4012
4013
4014class OtbrNode(LinuxHost, NodeImpl, OtbrDocker):
4015    TUN_DEV = config.THREAD_IFNAME
4016    is_otbr = True
4017    is_bbr = True  # OTBR is also BBR
4018    node_type = 'otbr-docker'
4019
4020    def __repr__(self):
4021        return f'Otbr<{self.nodeid}>'
4022
4023    def start(self):
4024        self._setup_sysctl()
4025        self.set_log_level(5)
4026        super().start()
4027
4028    def add_ipaddr(self, addr):
4029        cmd = f'ip -6 addr add {addr}/64 dev {self.TUN_DEV}'
4030        self.bash(cmd)
4031
4032    def add_ipmaddr_tun(self, ip: str):
4033        cmd = f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/mcast6.py {self.TUN_DEV} {ip} &'
4034        self.bash(cmd)
4035
4036    def get_ip6_address(self, address_type: config.ADDRESS_TYPE):
4037        try:
4038            return super(OtbrNode, self).get_ip6_address(address_type)
4039        except Exception as e:
4040            return super(LinuxHost, self).get_ip6_address(address_type)
4041
4042
4043class HostNode(LinuxHost, OtbrDocker):
4044    is_host = True
4045
4046    def __init__(self, nodeid, name=None, **kwargs):
4047        self.nodeid = nodeid
4048        self.name = name or ('Host%d' % nodeid)
4049        super().__init__(nodeid, **kwargs)
4050        self.bash('service otbr-agent stop')
4051
4052    def start(self, start_radvd=True, prefix=config.DOMAIN_PREFIX, slaac=False):
4053        self._setup_sysctl()
4054        if start_radvd:
4055            self.start_radvd_service(prefix, slaac)
4056        else:
4057            self.stop_radvd_service()
4058
4059    def stop(self):
4060        self.stop_radvd_service()
4061
4062    def get_addrs(self) -> List[str]:
4063        return self.get_ether_addrs()
4064
4065    def __repr__(self):
4066        return f'Host<{self.nodeid}>'
4067
4068    def get_matched_ula_addresses(self, prefix):
4069        """Get the IPv6 addresses that matches given prefix.
4070        """
4071
4072        addrs = []
4073        for addr in self.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA):
4074            if IPv6Address(addr) in IPv6Network(prefix):
4075                addrs.append(addr)
4076
4077        return addrs
4078
4079
4080if __name__ == '__main__':
4081    unittest.main()
4082