1#!/usr/bin/env python
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>> Thread Host Controller Interface
30>> Device : OpenThread THCI
31>> Class : OpenThread
32"""
33import base64
34import functools
35import ipaddress
36import logging
37import random
38import traceback
39import re
40import socket
41import time
42import json
43from abc import abstractmethod
44
45import serial
46from Queue import Queue
47from serial.serialutil import SerialException
48
49from GRLLibs.ThreadPacket.PlatformPackets import (
50    PlatformDiagnosticPacket,
51    PlatformPackets,
52)
53from GRLLibs.UtilityModules.ModuleHelper import ModuleHelper, ThreadRunner
54from GRLLibs.UtilityModules.Plugins.AES_CMAC import Thread_PBKDF2
55from GRLLibs.UtilityModules.Test import (
56    Thread_Device_Role,
57    Device_Data_Requirement,
58    MacType,
59)
60from GRLLibs.UtilityModules.enums import (
61    PlatformDiagnosticPacket_Direction,
62    PlatformDiagnosticPacket_Type,
63)
64from GRLLibs.UtilityModules.enums import DevCapb, TestMode
65
66from IThci import IThci
67import commissioner
68from commissioner_impl import OTCommissioner
69
70# Replace by the actual version string for the vendor's reference device
71OT11_VERSION = 'OPENTHREAD'
72OT12_VERSION = 'OPENTHREAD'
73OT13_VERSION = 'OPENTHREAD'
74
75# Supported device capabilites in this THCI implementation
76OT11_CAPBS = DevCapb.V1_1
77OT12_CAPBS = (DevCapb.L_AIO | DevCapb.C_FFD | DevCapb.C_RFD)
78OT12BR_CAPBS = (DevCapb.C_BBR | DevCapb.C_Host | DevCapb.C_Comm)
79OT13_CAPBS = (DevCapb.C_FTD13 | DevCapb.C_MTD13)
80OT13BR_CAPBS = (DevCapb.C_BR13 | DevCapb.C_Host13)
81
82ZEPHYR_PREFIX = 'ot '
83"""CLI prefix used for OpenThread commands in Zephyr systems"""
84
85LINESEPX = re.compile(r'\r\n|\n')
86"""regex: used to split lines"""
87
88LOGX = re.compile(r'((\[(-|C|W|N|I|D)\])'
89                  r'|(-+$)'  # e.x. ------------------------------------------------------------------------
90                  r'|(=+\[.*\]=+$)'  # e.x. ==============================[TX len=108]===============================
91                  r'|(\|.+\|.+\|.+)'  # e.x. | 61 DC D2 CE FA 04 00 00 | 00 00 0A 6E 16 01 00 00 | aRNz......n....
92                  r')')
93"""regex used to filter logs"""
94
95assert LOGX.match('[-]')
96assert LOGX.match('[C]')
97assert LOGX.match('[W]')
98assert LOGX.match('[N]')
99assert LOGX.match('[I]')
100assert LOGX.match('[D]')
101assert LOGX.match('------------------------------------------------------------------------')
102assert LOGX.match('==============================[TX len=108]===============================')
103assert LOGX.match('| 61 DC D2 CE FA 04 00 00 | 00 00 0A 6E 16 01 00 00 | aRNz......n....')
104
105# OT Errors
106OT_ERROR_ALREADY = 24
107
108logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
109
110_callStackDepth = 0
111
112
113def watched(func):
114    func_name = func.func_name
115
116    @functools.wraps(func)
117    def wrapped_api_func(self, *args, **kwargs):
118        global _callStackDepth
119        callstr = '====' * _callStackDepth + "> %s%s%s" % (func_name, str(args) if args else "",
120                                                           str(kwargs) if kwargs else "")
121
122        _callStackDepth += 1
123        try:
124            ret = func(self, *args, **kwargs)
125            self.log("%s returns %r", callstr, ret)
126            return ret
127        except Exception as ex:
128            self.log("FUNC %s failed: %s", func_name, str(ex))
129            raise
130        finally:
131            _callStackDepth -= 1
132            if _callStackDepth == 0:
133                print('\n')
134
135    return wrapped_api_func
136
137
138def retry(n, interval=0):
139    assert n >= 0, n
140
141    def deco(func):
142
143        @functools.wraps(func)
144        def retried_func(*args, **kwargs):
145            for i in range(n + 1):
146                try:
147                    return func(*args, **kwargs)
148                except Exception:
149                    if i == n:
150                        raise
151
152                    time.sleep(interval)
153
154        return retried_func
155
156    return deco
157
158
159def API(api_func):
160    try:
161        return watched(api_func)
162    except Exception:
163        tb = traceback.format_exc()
164        ModuleHelper.WriteIntoDebugLogger("Exception raised while calling %s:\n%s" % (api_func.func_name, tb))
165        raise
166
167
168def commissioning(func):
169
170    @functools.wraps(func)
171    def comm_func(self, *args, **kwargs):
172        self._onCommissionStart()
173        try:
174            return func(self, *args, **kwargs)
175        finally:
176            self._onCommissionStop()
177
178    return comm_func
179
180
181class CommandError(Exception):
182
183    def __init__(self, code, msg):
184        assert isinstance(code, int), code
185        self.code = code
186        self.msg = msg
187
188        super(CommandError, self).__init__("Error %d: %s" % (code, msg))
189
190
191class OpenThreadTHCI(object):
192    LOWEST_POSSIBLE_PARTATION_ID = 0x1
193    LINK_QUALITY_CHANGE_TIME = 100
194    DEFAULT_COMMAND_TIMEOUT = 10
195    firmwarePrefix = 'OPENTHREAD/'
196    DOMAIN_NAME = 'Thread'
197    MLR_TIMEOUT_MIN = 300
198    NETWORK_ATTACHMENT_TIMEOUT = 10
199
200    IsBorderRouter = False
201    IsHost = False
202    IsBeingTestedAsCommercialBBR = False
203    IsReference20200818 = False
204
205    externalCommissioner = None
206    _update_router_status = False
207
208    _cmdPrefix = ''
209    _lineSepX = LINESEPX
210
211    _ROLE_MODE_DICT = {
212        Thread_Device_Role.Leader: 'rdn',
213        Thread_Device_Role.Router: 'rdn',
214        Thread_Device_Role.SED: '-',
215        Thread_Device_Role.EndDevice: 'rn',
216        Thread_Device_Role.REED: 'rdn',
217        Thread_Device_Role.EndDevice_FED: 'rdn',
218        Thread_Device_Role.EndDevice_MED: 'rn',
219        Thread_Device_Role.SSED: '-',
220    }
221
222    def __init__(self, **kwargs):
223        """initialize the serial port and default network parameters
224        Args:
225            **kwargs: Arbitrary keyword arguments
226                      Includes 'EUI' and 'SerialPort'
227        """
228        self.intialize(kwargs)
229
230    @abstractmethod
231    def _connect(self):
232        """
233        Connect to the device.
234        """
235
236    @abstractmethod
237    def _disconnect(self):
238        """
239        Disconnect from the device
240        """
241
242    @abstractmethod
243    def _cliReadLine(self):
244        """Read exactly one line from the device
245
246        Returns:
247            None if no data
248        """
249
250    @abstractmethod
251    def _cliWriteLine(self, line):
252        """Send exactly one line to the device
253
254        Args:
255            line str: data send to device
256        """
257
258    # Override the following empty methods in the dervied classes when needed
259    def _onCommissionStart(self):
260        """Called when commissioning starts"""
261
262    def _onCommissionStop(self):
263        """Called when commissioning stops"""
264
265    def _deviceBeforeReset(self):
266        """Called before the device resets"""
267
268    def _deviceAfterReset(self):
269        """Called after the device resets"""
270
271    def _restartAgentService(self):
272        """Restart the agent service"""
273
274    def _beforeRegisterMulticast(self, sAddr, timeout):
275        """Called before the ipv6 address being subscribed in interface
276
277        Args:
278            sAddr   : str : Multicast address to be subscribed and notified OTA
279            timeout : int : The allowed maximal time to end normally
280        """
281
282    def __sendCommand(self, cmd, expectEcho=True):
283        cmd = self._cmdPrefix + cmd
284        # self.log("command: %s", cmd)
285        self._cliWriteLine(cmd)
286        if expectEcho:
287            self.__expect(cmd, endswith=True)
288
289    _COMMAND_OUTPUT_ERROR_PATTERN = re.compile(r'Error (\d+): (.*)')
290
291    @retry(3)
292    @watched
293    def __executeCommand(self, cmd, timeout=DEFAULT_COMMAND_TIMEOUT):
294        """send specific command to reference unit over serial port
295
296        Args:
297            cmd: OpenThread CLI string
298
299        Returns:
300            Done: successfully send the command to reference unit and parse it
301            Value: successfully retrieve the desired value from reference unit
302            Error: some errors occur, indicates by the followed specific error number
303        """
304        if self.logThreadStatus == self.logStatus['running']:
305            self.logThreadStatus = self.logStatus['pauseReq']
306            while (self.logThreadStatus != self.logStatus['paused'] and
307                   self.logThreadStatus != self.logStatus['stop']):
308                pass
309
310        try:
311            self.__sendCommand(cmd)
312            response = []
313
314            t_end = time.time() + timeout
315            while time.time() < t_end:
316                line = self.__readCliLine()
317                if line is None:
318                    time.sleep(0.01)
319                    continue
320
321                # self.log("readline: %s", line)
322                # skip empty lines
323                if line:
324                    response.append(line)
325
326                if line.endswith('Done'):
327                    break
328                else:
329                    m = OpenThreadTHCI._COMMAND_OUTPUT_ERROR_PATTERN.match(line)
330                    if m is not None:
331                        code, msg = m.groups()
332                        raise CommandError(int(code), msg)
333            else:
334                raise Exception('%s: failed to find end of response: %s' % (self, response))
335
336        except SerialException as e:
337            self.log('__executeCommand() Error: ' + str(e))
338            self._disconnect()
339            self._connect()
340            raise e
341
342        return response
343
344    def __expect(self, expected, timeout=5, endswith=False):
345        """Find the `expected` line within `times` tries.
346
347        Args:
348            expected    str: the expected string
349            times       int: number of tries
350        """
351        #self.log('Expecting [%s]' % (expected))
352
353        deadline = time.time() + timeout
354        while True:
355            line = self.__readCliLine()
356            if line is not None:
357                #self.log("readline: %s", line)
358                pass
359
360            if line is None:
361                if time.time() >= deadline:
362                    break
363
364                self.sleep(0.01)
365                continue
366
367            matched = line.endswith(expected) if endswith else line == expected
368            if matched:
369                #self.log('Expected [%s]' % (expected))
370                return
371
372        raise Exception('failed to find expected string[%s]' % expected)
373
374    def __readCliLine(self, ignoreLogs=True):
375        """Read the next line from OT CLI.d"""
376        line = self._cliReadLine()
377        if ignoreLogs:
378            while line is not None and LOGX.match(line):
379                line = self._cliReadLine()
380
381        return line
382
383    @API
384    def getVersionNumber(self):
385        """get OpenThread stack firmware version number"""
386        return self.__executeCommand('version')[0]
387
388    def log(self, fmt, *args):
389        try:
390            msg = fmt % args
391            # print('%s - %s - %s' % (self.port, time.strftime('%b %d %H:%M:%S'), msg))
392            print('%s %s' % (self.port, msg))
393        except Exception:
394            pass
395
396    def sleep(self, duration):
397        if duration >= 1:
398            self.log("sleeping for %ss ...", duration)
399        time.sleep(duration)
400
401    @API
402    def intialize(self, params):
403        """initialize the serial port with baudrate, timeout parameters"""
404        self.mac = params.get('EUI')
405        self.backboneNetif = params.get('Param8') or 'eth0'
406        self.extraParams = self.__parseExtraParams(params.get('Param9'))
407
408        # Potentially changes `self.extraParams`
409        self._parseConnectionParams(params)
410
411        self.UIStatusMsg = ''
412        self.AutoDUTEnable = False
413        self.isPowerDown = False
414        self._is_net = False  # whether device is through ser2net
415        self.logStatus = {
416            'stop': 'stop',
417            'running': 'running',
418            'pauseReq': 'pauseReq',
419            'paused': 'paused',
420        }
421        self.joinStatus = {
422            'notstart': 'notstart',
423            'ongoing': 'ongoing',
424            'succeed': 'succeed',
425            'failed': 'failed',
426        }
427        self.logThreadStatus = self.logStatus['stop']
428
429        self.deviceConnected = False
430
431        # init serial port
432        self._connect()
433        if not self.IsBorderRouter:
434            self.__detectZephyr()
435        self.__discoverDeviceCapability()
436        self.UIStatusMsg = self.getVersionNumber()
437
438        if self.firmwarePrefix in self.UIStatusMsg:
439            self.deviceConnected = True
440        else:
441            self.UIStatusMsg = ('Firmware Not Matching Expecting ' + self.firmwarePrefix + ', Now is ' +
442                                self.UIStatusMsg)
443            ModuleHelper.WriteIntoDebugLogger('Err: OpenThread device Firmware not matching..')
444
445        # Make this class compatible with Thread referenece 20200818
446        self.__detectReference20200818()
447
448    def __repr__(self):
449        if self.connectType == 'ip':
450            return '[%s:%d]' % (self.telnetIp, self.telnetPort)
451        else:
452            return '[%s]' % self.port
453
454    def _parseConnectionParams(self, params):
455        """Parse parameters related to connection to the device
456
457        Args:
458            params: Arbitrary keyword arguments including 'EUI' and 'SerialPort'
459        """
460        self.port = params.get('SerialPort', '')
461        # params example: {'EUI': 1616240311388864514L, 'SerialBaudRate': None, 'TelnetIP': '192.168.8.181', 'SerialPort': None, 'Param7': None, 'Param6': None, 'Param5': 'ip', 'TelnetPort': '22', 'Param9': None, 'Param8': None}
462        self.log('All parameters: %r', params)
463
464        try:
465            ipaddress.ip_address(self.port)
466            # handle TestHarness Discovery Protocol
467            self.connectType = 'ip'
468            self.telnetIp = self.port
469            self.telnetPort = 22
470            self.telnetUsername = 'pi' if params.get('Param6') is None else params.get('Param6')
471            self.telnetPassword = 'raspberry' if params.get('Param7') is None else params.get('Param7')
472        except ValueError:
473            self.connectType = (params.get('Param5') or 'usb').lower()
474            self.telnetIp = params.get('TelnetIP')
475            self.telnetPort = int(params.get('TelnetPort')) if params.get('TelnetPort') else 22
476            # username for SSH
477            self.telnetUsername = 'pi' if params.get('Param6') is None else params.get('Param6')
478            # password for SSH
479            self.telnetPassword = 'raspberry' if params.get('Param7') is None else params.get('Param7')
480
481    @watched
482    def __parseExtraParams(self, Param9):
483        """
484        Parse `Param9` for extra THCI parameters.
485
486        `Param9` should be a JSON string encoded in URL-safe base64 encoding.
487
488        Defined Extra THCI Parameters:
489        - "cmd-start-otbr-agent"   : The command to start otbr-agent (default: systemctl start otbr-agent)
490        - "cmd-stop-otbr-agent"    : The command to stop otbr-agent (default: systemctl stop otbr-agent)
491        - "cmd-restart-otbr-agent" : The command to restart otbr-agent (default: systemctl restart otbr-agent)
492        - "cmd-restart-radvd"      : The command to restart radvd (default: service radvd restart)
493
494        For example, Param9 can be generated as below:
495        Param9 = base64.urlsafe_b64encode(json.dumps({
496            "cmd-start-otbr-agent": "service otbr-agent start",
497            "cmd-stop-otbr-agent": "service otbr-agent stop",
498            "cmd-restart-otbr-agent": "service otbr-agent restart",
499            "cmd-restart-radvd": "service radvd stop; service radvd start",
500        }))
501
502        :param Param9: A JSON string encoded in URL-safe base64 encoding.
503        :return: A dict containing extra THCI parameters.
504        """
505        if not Param9 or not Param9.strip():
506            return {}
507
508        jsonStr = base64.urlsafe_b64decode(Param9)
509        params = json.loads(jsonStr)
510        return params
511
512    @API
513    def closeConnection(self):
514        """close current serial port connection"""
515        self._disconnect()
516
517    def __disableRouterEligible(self):
518        """disable router role
519        """
520        cmd = 'routereligible disable'
521        self.__executeCommand(cmd)
522
523    def __setDeviceMode(self, mode):
524        """set thread device mode:
525
526        Args:
527           mode: thread device mode
528           r: rx-on-when-idle
529           s: secure IEEE 802.15.4 data request
530           d: full thread device
531           n: full network data
532
533        Returns:
534           True: successful to set the device mode
535           False: fail to set the device mode
536        """
537        cmd = 'mode %s' % mode
538        return self.__executeCommand(cmd)[-1] == 'Done'
539
540    def __setRouterUpgradeThreshold(self, iThreshold):
541        """set router upgrade threshold
542
543        Args:
544            iThreshold: the number of active routers on the Thread network
545                        partition below which a REED may decide to become a Router.
546
547        Returns:
548            True: successful to set the ROUTER_UPGRADE_THRESHOLD
549            False: fail to set ROUTER_UPGRADE_THRESHOLD
550        """
551        cmd = 'routerupgradethreshold %s' % str(iThreshold)
552        return self.__executeCommand(cmd)[-1] == 'Done'
553
554    def __setRouterDowngradeThreshold(self, iThreshold):
555        """set router downgrade threshold
556
557        Args:
558            iThreshold: the number of active routers on the Thread network
559                        partition above which an active router may decide to
560                        become a child.
561
562        Returns:
563            True: successful to set the ROUTER_DOWNGRADE_THRESHOLD
564            False: fail to set ROUTER_DOWNGRADE_THRESHOLD
565        """
566        cmd = 'routerdowngradethreshold %s' % str(iThreshold)
567        return self.__executeCommand(cmd)[-1] == 'Done'
568
569    def __setRouterSelectionJitter(self, iRouterJitter):
570        """set ROUTER_SELECTION_JITTER parameter for REED to upgrade to Router
571
572        Args:
573            iRouterJitter: a random period prior to request Router ID for REED
574
575        Returns:
576            True: successful to set the ROUTER_SELECTION_JITTER
577            False: fail to set ROUTER_SELECTION_JITTER
578        """
579        cmd = 'routerselectionjitter %s' % str(iRouterJitter)
580        return self.__executeCommand(cmd)[-1] == 'Done'
581
582    def __setAddressfilterMode(self, mode):
583        """set address filter mode
584
585        Returns:
586            True: successful to set address filter mode.
587            False: fail to set address filter mode.
588        """
589        cmd = 'macfilter addr ' + mode
590        return self.__executeCommand(cmd)[-1] == 'Done'
591
592    def __startOpenThread(self):
593        """start OpenThread stack
594
595        Returns:
596            True: successful to start OpenThread stack and thread interface up
597            False: fail to start OpenThread stack
598        """
599        if self.hasActiveDatasetToCommit:
600            if self.__executeCommand('dataset commit active')[0] != 'Done':
601                raise Exception('failed to commit active dataset')
602            else:
603                self.hasActiveDatasetToCommit = False
604
605        # Restore allowlist/denylist address filter mode if rejoin after
606        # reset
607        if self.isPowerDown:
608            if self._addressfilterMode == 'allowlist':
609                if self.__setAddressfilterMode(self.__replaceCommands['allowlist']):
610                    for addr in self._addressfilterSet:
611                        self.addAllowMAC(addr)
612            elif self._addressfilterMode == 'denylist':
613                if self.__setAddressfilterMode(self.__replaceCommands['denylist']):
614                    for addr in self._addressfilterSet:
615                        self.addBlockedMAC(addr)
616
617        # Set routerselectionjitter to 1 for certain device roles
618        if self.deviceRole in [
619                Thread_Device_Role.Leader,
620                Thread_Device_Role.Router,
621                Thread_Device_Role.REED,
622        ]:
623            self.__setRouterSelectionJitter(1)
624        elif self.deviceRole in [Thread_Device_Role.BR_1, Thread_Device_Role.BR_2]:
625            if ModuleHelper.CurrentRunningTestMode == TestMode.Commercial:
626                # Allow BBR configurations for 1.2 BR_1/BR_2 roles
627                self.IsBeingTestedAsCommercialBBR = True
628            self.__setRouterSelectionJitter(1)
629
630        if self.IsBeingTestedAsCommercialBBR:
631            # Configure default BBR dataset
632            self.__configBbrDataset(SeqNum=self.bbrSeqNum,
633                                    MlrTimeout=self.bbrMlrTimeout,
634                                    ReRegDelay=self.bbrReRegDelay)
635            # Add default domain prefix is not configured otherwise
636            if self.__useDefaultDomainPrefix:
637                self.__addDefaultDomainPrefix()
638
639        self.__executeCommand('ifconfig up')
640        self.__executeCommand('thread start')
641        self.isPowerDown = False
642        return True
643
644    @watched
645    def __isOpenThreadRunning(self):
646        """check whether or not OpenThread is running
647
648        Returns:
649            True: OpenThread is running
650            False: OpenThread is not running
651        """
652        return self.__executeCommand('state')[0] != 'disabled'
653
654    @watched
655    def __isDeviceAttached(self):
656        """check whether or not OpenThread is running
657
658        Returns:
659            True: OpenThread is running
660            False: OpenThread is not running
661        """
662        detached_states = ["detached", "disabled"]
663        return self.__executeCommand('state')[0] not in detached_states
664
665    # rloc16 might be hex string or integer, need to return actual allocated router id
666    def __convertRlocToRouterId(self, xRloc16):
667        """mapping Rloc16 to router id
668
669        Args:
670            xRloc16: hex rloc16 short address
671
672        Returns:
673            actual router id allocated by leader
674        """
675        routerList = self.__executeCommand('router list')[0].split()
676        rloc16 = None
677        routerid = None
678
679        for index in routerList:
680            cmd = 'router %s' % index
681            router = self.__executeCommand(cmd)
682
683            for line in router:
684                if 'Done' in line:
685                    break
686                elif 'Router ID' in line:
687                    routerid = line.split()[2]
688                elif 'Rloc' in line:
689                    rloc16 = line.split()[1]
690                else:
691                    pass
692
693            # process input rloc16
694            if isinstance(xRloc16, str):
695                rloc16 = '0x' + rloc16
696                if rloc16 == xRloc16:
697                    return routerid
698            elif isinstance(xRloc16, int):
699                if int(rloc16, 16) == xRloc16:
700                    return routerid
701            else:
702                pass
703
704        return None
705
706    # pylint: disable=no-self-use
707    def __convertLongToHex(self, iValue, fillZeros=None):
708        """convert a long hex integer to string
709           remove '0x' and 'L' return string
710
711        Args:
712            iValue: long integer in hex format
713            fillZeros: pad string with zeros on the left to specified width
714
715        Returns:
716            string of this long integer without '0x' and 'L'
717        """
718        fmt = '%x'
719        if fillZeros is not None:
720            fmt = '%%0%dx' % fillZeros
721
722        return fmt % iValue
723
724    @commissioning
725    def __readCommissioningLogs(self, durationInSeconds):
726        """read logs during the commissioning process
727
728        Args:
729            durationInSeconds: time duration for reading commissioning logs
730
731        Returns:
732            Commissioning logs
733        """
734        self.logThreadStatus = self.logStatus['running']
735        logs = Queue()
736        t_end = time.time() + durationInSeconds
737        joinSucceed = False
738
739        while time.time() < t_end:
740
741            if self.logThreadStatus == self.logStatus['pauseReq']:
742                self.logThreadStatus = self.logStatus['paused']
743
744            if self.logThreadStatus != self.logStatus['running']:
745                self.sleep(0.01)
746                continue
747
748            try:
749                line = self.__readCliLine(ignoreLogs=False)
750
751                if line:
752                    self.log("commissioning log: %s", line)
753                    logs.put(line)
754
755                    if 'Join success' in line:
756                        joinSucceed = True
757                        # read commissioning logs for 3 more seconds
758                        t_end = time.time() + 3
759                    elif 'Join failed' in line:
760                        # read commissioning logs for 3 more seconds
761                        t_end = time.time() + 3
762                elif line is None:
763                    self.sleep(0.01)
764
765            except Exception:
766                pass
767
768        self.joinCommissionedStatus = self.joinStatus['succeed'] if joinSucceed else self.joinStatus['failed']
769        self.logThreadStatus = self.logStatus['stop']
770        return logs
771
772    # pylint: disable=no-self-use
773    def __convertChannelMask(self, channelsArray):
774        """convert channelsArray to bitmask format
775
776        Args:
777            channelsArray: channel array (i.e. [21, 22])
778
779        Returns:
780            bitmask format corresponding to a given channel array
781        """
782        maskSet = 0
783
784        for eachChannel in channelsArray:
785            mask = 1 << eachChannel
786            maskSet = maskSet | mask
787
788        return maskSet
789
790    def __setChannelMask(self, channelMask):
791        cmd = 'dataset channelmask %s' % channelMask
792        self.hasActiveDatasetToCommit = True
793        return self.__executeCommand(cmd)[-1] == 'Done'
794
795    def __setSecurityPolicy(self, securityPolicySecs, securityPolicyFlags):
796        cmd = 'dataset securitypolicy %s %s' % (
797            str(securityPolicySecs),
798            securityPolicyFlags,
799        )
800        self.hasActiveDatasetToCommit = True
801        return self.__executeCommand(cmd)[-1] == 'Done'
802
803    def __setKeySwitchGuardTime(self, iKeySwitchGuardTime):
804        """ set the Key switch guard time
805
806        Args:
807            iKeySwitchGuardTime: key switch guard time
808
809        Returns:
810            True: successful to set key switch guard time
811            False: fail to set key switch guard time
812        """
813        cmd = 'keysequence guardtime %s' % str(iKeySwitchGuardTime)
814        if self.__executeCommand(cmd)[-1] == 'Done':
815            self.sleep(1)
816            return True
817        else:
818            return False
819
820    def __getCommissionerSessionId(self):
821        """ get the commissioner session id allocated from Leader """
822        return self.__executeCommand('commissioner sessionid')[0]
823
824    # pylint: disable=no-self-use
825    def _deviceEscapeEscapable(self, string):
826        """Escape CLI escapable characters in the given string.
827
828        Args:
829            string (str): UTF-8 input string.
830
831        Returns:
832            [str]: The modified string with escaped characters.
833        """
834        escapable_chars = '\\ \t\r\n'
835        for char in escapable_chars:
836            string = string.replace(char, '\\%s' % char)
837        return string
838
839    @API
840    def setNetworkName(self, networkName='GRL'):
841        """set Thread Network name
842
843        Args:
844            networkName: the networkname string to be set
845
846        Returns:
847            True: successful to set the Thread Networkname
848            False: fail to set the Thread Networkname
849        """
850        networkName = self._deviceEscapeEscapable(networkName)
851        cmd = 'networkname %s' % networkName
852        datasetCmd = 'dataset networkname %s' % networkName
853        self.hasActiveDatasetToCommit = True
854        return self.__executeCommand(cmd)[-1] == 'Done' and self.__executeCommand(datasetCmd)[-1] == 'Done'
855
856    @API
857    def setChannel(self, channel=11):
858        """set channel of Thread device operates on.
859
860        Args:
861            channel:
862                    (0  - 10: Reserved)
863                    (11 - 26: 2.4GHz channels)
864                    (27 - 65535: Reserved)
865
866        Returns:
867            True: successful to set the channel
868            False: fail to set the channel
869        """
870        cmd = 'channel %s' % channel
871        datasetCmd = 'dataset channel %s' % channel
872        self.hasSetChannel = True
873        self.hasActiveDatasetToCommit = True
874        return self.__executeCommand(cmd)[-1] == 'Done' and self.__executeCommand(datasetCmd)[-1] == 'Done'
875
876    @API
877    def getChannel(self):
878        """get current channel"""
879        return self.__executeCommand('channel')[0]
880
881    @API
882    def setMAC(self, xEUI):
883        """set the extended addresss of Thread device
884
885        Args:
886            xEUI: extended address in hex format
887
888        Returns:
889            True: successful to set the extended address
890            False: fail to set the extended address
891        """
892        if not isinstance(xEUI, str):
893            address64 = self.__convertLongToHex(xEUI, 16)
894        else:
895            address64 = xEUI
896
897        cmd = 'extaddr %s' % address64
898        if self.__executeCommand(cmd)[-1] == 'Done':
899            self.mac = address64
900            return True
901        else:
902            return False
903
904    @API
905    def getMAC(self, bType=MacType.RandomMac):
906        """get one specific type of MAC address
907           currently OpenThread only supports Random MAC address
908
909        Args:
910            bType: indicate which kind of MAC address is required
911
912        Returns:
913            specific type of MAC address
914        """
915        # if power down happens, return extended address assigned previously
916        if self.isPowerDown:
917            macAddr64 = self.mac
918        else:
919            if bType == MacType.FactoryMac:
920                macAddr64 = self.__executeCommand('eui64')[0]
921            elif bType == MacType.HashMac:
922                macAddr64 = self.__executeCommand('joiner id')[0]
923            elif bType == MacType.EthMac and self.IsBorderRouter:
924                return self._deviceGetEtherMac()
925            else:
926                macAddr64 = self.__executeCommand('extaddr')[0]
927
928        return int(macAddr64, 16)
929
930    @API
931    def getLL64(self):
932        """get link local unicast IPv6 address"""
933        return self.__executeCommand('ipaddr linklocal')[0]
934
935    @API
936    def getRloc16(self):
937        """get rloc16 short address"""
938        rloc16 = self.__executeCommand('rloc16')[0]
939        return int(rloc16, 16)
940
941    @API
942    def getRloc(self):
943        """get router locator unicast Ipv6 address"""
944        return self.__executeCommand('ipaddr rloc')[0]
945
946    def __getGlobal(self):
947        """get global unicast IPv6 address set
948           if configuring multiple entries
949        """
950        globalAddrs = []
951        rlocAddr = self.getRloc()
952
953        addrs = self.__executeCommand('ipaddr')
954
955        # take rloc address as a reference for current mesh local prefix,
956        # because for some TCs, mesh local prefix may be updated through
957        # pending dataset management.
958        for ip6Addr in addrs:
959            if ip6Addr == 'Done':
960                break
961
962            fullIp = ModuleHelper.GetFullIpv6Address(ip6Addr).lower()
963
964            if fullIp.startswith('fe80') or fullIp.startswith(rlocAddr[0:19]):
965                continue
966
967            globalAddrs.append(fullIp)
968
969        return globalAddrs
970
971    @API
972    def setNetworkKey(self, key):
973        """set Thread network key
974
975        Args:
976            key: Thread network key used in secure the MLE/802.15.4 packet
977
978        Returns:
979            True: successful to set the Thread network key
980            False: fail to set the Thread network key
981        """
982        cmdName = self.__replaceCommands['networkkey']
983
984        if not isinstance(key, str):
985            networkKey = self.__convertLongToHex(key, 32)
986            cmd = '%s %s' % (cmdName, networkKey)
987            datasetCmd = 'dataset %s %s' % (cmdName, networkKey)
988        else:
989            networkKey = key
990            cmd = '%s %s' % (cmdName, networkKey)
991            datasetCmd = 'dataset %s %s' % (cmdName, networkKey)
992
993        self.networkKey = networkKey
994        self.hasActiveDatasetToCommit = True
995        return self.__executeCommand(cmd)[-1] == 'Done' and self.__executeCommand(datasetCmd)[-1] == 'Done'
996
997    @API
998    def addBlockedMAC(self, xEUI):
999        """add a given extended address to the denylist entry
1000
1001        Args:
1002            xEUI: extended address in hex format
1003
1004        Returns:
1005            True: successful to add a given extended address to the denylist entry
1006            False: fail to add a given extended address to the denylist entry
1007        """
1008        if isinstance(xEUI, str):
1009            macAddr = xEUI
1010        else:
1011            macAddr = self.__convertLongToHex(xEUI)
1012
1013        # if blocked device is itself
1014        if macAddr == self.mac:
1015            print('block device itself')
1016            return True
1017
1018        if self._addressfilterMode != 'denylist':
1019            if self.__setAddressfilterMode(self.__replaceCommands['denylist']):
1020                self._addressfilterMode = 'denylist'
1021
1022        cmd = 'macfilter addr add %s' % macAddr
1023        ret = self.__executeCommand(cmd)[-1] == 'Done'
1024
1025        self._addressfilterSet.add(macAddr)
1026        print('current denylist entries:')
1027        for addr in self._addressfilterSet:
1028            print(addr)
1029
1030        return ret
1031
1032    @API
1033    def addAllowMAC(self, xEUI):
1034        """add a given extended address to the allowlist addressfilter
1035
1036        Args:
1037            xEUI: a given extended address in hex format
1038
1039        Returns:
1040            True: successful to add a given extended address to the allowlist entry
1041            False: fail to add a given extended address to the allowlist entry
1042        """
1043        if isinstance(xEUI, str):
1044            macAddr = xEUI
1045        else:
1046            macAddr = self.__convertLongToHex(xEUI)
1047
1048        if self._addressfilterMode != 'allowlist':
1049            if self.__setAddressfilterMode(self.__replaceCommands['allowlist']):
1050                self._addressfilterMode = 'allowlist'
1051
1052        cmd = 'macfilter addr add %s' % macAddr
1053        ret = self.__executeCommand(cmd)[-1] == 'Done'
1054
1055        self._addressfilterSet.add(macAddr)
1056        print('current allowlist entries:')
1057        for addr in self._addressfilterSet:
1058            print(addr)
1059        return ret
1060
1061    @API
1062    def clearBlockList(self):
1063        """clear all entries in denylist table
1064
1065        Returns:
1066            True: successful to clear the denylist
1067            False: fail to clear the denylist
1068        """
1069        # remove all entries in denylist
1070        print('clearing denylist entries:')
1071        for addr in self._addressfilterSet:
1072            print(addr)
1073
1074        # disable denylist
1075        if self.__setAddressfilterMode('disable'):
1076            self._addressfilterMode = 'disable'
1077            # clear ops
1078            cmd = 'macfilter addr clear'
1079            if self.__executeCommand(cmd)[-1] == 'Done':
1080                self._addressfilterSet.clear()
1081                return True
1082        return False
1083
1084    @API
1085    def clearAllowList(self):
1086        """clear all entries in allowlist table
1087
1088        Returns:
1089            True: successful to clear the allowlist
1090            False: fail to clear the allowlist
1091        """
1092        # remove all entries in allowlist
1093        print('clearing allowlist entries:')
1094        for addr in self._addressfilterSet:
1095            print(addr)
1096
1097        # disable allowlist
1098        if self.__setAddressfilterMode('disable'):
1099            self._addressfilterMode = 'disable'
1100            # clear ops
1101            cmd = 'macfilter addr clear'
1102            if self.__executeCommand(cmd)[-1] == 'Done':
1103                self._addressfilterSet.clear()
1104                return True
1105        return False
1106
1107    @API
1108    def getDeviceRole(self):
1109        """get current device role in Thread Network"""
1110        return self.__executeCommand('state')[0]
1111
1112    @API
1113    def joinNetwork(self, eRoleId):
1114        """make device ready to join the Thread Network with a given role
1115
1116        Args:
1117            eRoleId: a given device role id
1118
1119        Returns:
1120            True: ready to set Thread Network parameter for joining desired Network
1121        """
1122        self.deviceRole = eRoleId
1123        mode = '-'
1124        if ModuleHelper.LeaderDutChannelFound and not self.hasSetChannel:
1125            self.channel = ModuleHelper.Default_Channel
1126
1127        # FIXME: when Harness call setNetworkDataRequirement()?
1128        # only sleep end device requires stable networkdata now
1129        if eRoleId == Thread_Device_Role.Leader:
1130            print('join as leader')
1131            mode = 'rdn'
1132            if self.AutoDUTEnable is False:
1133                # set ROUTER_DOWNGRADE_THRESHOLD
1134                self.__setRouterDowngradeThreshold(33)
1135        elif eRoleId == Thread_Device_Role.Router:
1136            print('join as router')
1137            mode = 'rdn'
1138            if self.AutoDUTEnable is False:
1139                # set ROUTER_DOWNGRADE_THRESHOLD
1140                self.__setRouterDowngradeThreshold(33)
1141        elif eRoleId in (Thread_Device_Role.BR_1, Thread_Device_Role.BR_2):
1142            print('join as BBR')
1143            mode = 'rdn'
1144            if self.AutoDUTEnable is False:
1145                # set ROUTER_DOWNGRADE_THRESHOLD
1146                self.__setRouterDowngradeThreshold(33)
1147        elif eRoleId == Thread_Device_Role.SED:
1148            print('join as sleepy end device')
1149            mode = '-'
1150            self.__setPollPeriod(self.__sedPollPeriod)
1151        elif eRoleId == Thread_Device_Role.SSED:
1152            print('join as SSED')
1153            mode = '-'
1154            self.setCSLperiod(self.cslPeriod)
1155            self.setCSLtout(self.ssedTimeout)
1156            self.setCSLsuspension(False)
1157        elif eRoleId == Thread_Device_Role.EndDevice:
1158            print('join as end device')
1159            mode = 'rn'
1160        elif eRoleId == Thread_Device_Role.REED:
1161            print('join as REED')
1162            mode = 'rdn'
1163            if self.AutoDUTEnable is False:
1164                # set ROUTER_UPGRADE_THRESHOLD
1165                self.__setRouterUpgradeThreshold(0)
1166        elif eRoleId == Thread_Device_Role.EndDevice_FED:
1167            print('join as FED')
1168            mode = 'rdn'
1169            # always remain an ED, never request to be a router
1170            self.__disableRouterEligible()
1171        elif eRoleId == Thread_Device_Role.EndDevice_MED:
1172            print('join as MED')
1173            mode = 'rn'
1174        else:
1175            pass
1176
1177        if self.IsReference20200818:
1178            mode = 's' if mode == '-' else mode + 's'
1179
1180        # set Thread device mode with a given role
1181        self.__setDeviceMode(mode)
1182
1183        # start OpenThread
1184        self.__startOpenThread()
1185        self.wait_for_attach_to_the_network(expected_role=eRoleId,
1186                                            timeout=self.NETWORK_ATTACHMENT_TIMEOUT,
1187                                            raise_assert=True)
1188        return True
1189
1190    def wait_for_attach_to_the_network(self, expected_role, timeout, raise_assert=False):
1191        start_time = time.time()
1192
1193        while time.time() < start_time + timeout:
1194            time.sleep(0.3)
1195            if self.__isDeviceAttached():
1196                break
1197        else:
1198            if raise_assert:
1199                raise AssertionError("OT device {} could not attach to the network after {} s of timeout.".format(
1200                    self, timeout))
1201            else:
1202                return False
1203
1204        if self._update_router_status:
1205            self.__updateRouterStatus()
1206
1207        if expected_role == Thread_Device_Role.Router:
1208            while time.time() < start_time + timeout:
1209                time.sleep(0.3)
1210                if self.getDeviceRole() == "router":
1211                    break
1212            else:
1213                if raise_assert:
1214                    raise AssertionError("OT Router {} could not attach to the network after {} s of timeout.".format(
1215                        self, timeout * 2))
1216                else:
1217                    return False
1218
1219        if self.IsBorderRouter:
1220            self._waitBorderRoutingStabilize()
1221
1222        return True
1223
1224    @API
1225    def getNetworkFragmentID(self):
1226        """get current partition id of Thread Network Partition from LeaderData
1227
1228        Returns:
1229            The Thread network Partition Id
1230        """
1231        if not self.__isOpenThreadRunning():
1232            return None
1233
1234        leaderData = self.__executeCommand('leaderdata')
1235        return int(leaderData[0].split()[2], 16)
1236
1237    @API
1238    def getParentAddress(self):
1239        """get Thread device's parent extended address and rloc16 short address
1240
1241        Returns:
1242            The extended address of parent in hex format
1243        """
1244        eui = None
1245        parentInfo = self.__executeCommand('parent')
1246
1247        for line in parentInfo:
1248            if 'Done' in line:
1249                break
1250            elif 'Ext Addr' in line:
1251                eui = line.split()[2]
1252            else:
1253                pass
1254
1255        return int(eui, 16)
1256
1257    @API
1258    def powerDown(self):
1259        """power down the Thread device"""
1260        self._reset()
1261
1262    @API
1263    def powerUp(self):
1264        """power up the Thread device"""
1265        self.isPowerDown = False
1266
1267        if not self.__isOpenThreadRunning():
1268            if self.deviceRole == Thread_Device_Role.SED:
1269                self.__setPollPeriod(self.__sedPollPeriod)
1270            self.__startOpenThread()
1271
1272    @watched
1273    def _reset(self, timeout=3):
1274        print("Waiting after reset timeout: {} s".format(timeout))
1275        start_time = time.time()
1276        self.__sendCommand('reset', expectEcho=False)
1277        self.isPowerDown = True
1278
1279        while time.time() < start_time + timeout:
1280            time.sleep(0.3)
1281            if not self.IsBorderRouter:
1282                self._disconnect()
1283                self._connect()
1284            try:
1285                self.__executeCommand('state', timeout=0.1)
1286                break
1287            except Exception:
1288                continue
1289        else:
1290            raise AssertionError("Could not connect with OT device {} after reset.".format(self))
1291
1292    def reset_and_wait_for_connection(self, timeout=3):
1293        self._reset(timeout=timeout)
1294        if self.deviceRole == Thread_Device_Role.SED:
1295            self.__setPollPeriod(self.__sedPollPeriod)
1296
1297    @API
1298    def reboot(self):
1299        """reset and rejoin to Thread Network without any timeout
1300
1301        Returns:
1302            True: successful to reset and rejoin the Thread Network
1303            False: fail to reset and rejoin the Thread Network
1304        """
1305        self.reset_and_wait_for_connection()
1306        self.__startOpenThread()
1307        return self.wait_for_attach_to_the_network(expected_role="", timeout=self.NETWORK_ATTACHMENT_TIMEOUT)
1308
1309    @API
1310    def resetAndRejoin(self, timeout):
1311        """reset and join back Thread Network with a given timeout delay
1312
1313        Args:
1314            timeout: a timeout interval before rejoin Thread Network
1315
1316        Returns:
1317            True: successful to reset and rejoin Thread Network
1318            False: fail to reset and rejoin the Thread Network
1319        """
1320        self.powerDown()
1321        time.sleep(timeout)
1322        self.powerUp()
1323        return self.wait_for_attach_to_the_network(expected_role="", timeout=self.NETWORK_ATTACHMENT_TIMEOUT)
1324
1325    @API
1326    def ping(self, strDestination, ilength=0, hop_limit=64, timeout=5):
1327        """ send ICMPv6 echo request with a given length/hoplimit to a unicast
1328            destination address
1329        Args:
1330            srcDestination: the unicast destination address of ICMPv6 echo request
1331            ilength: the size of ICMPv6 echo request payload
1332            hop_limit: hop limit
1333
1334        """
1335        cmd = 'ping %s %s' % (strDestination, str(ilength))
1336        if not self.IsReference20200818:
1337            cmd += ' 1 1 %d %d' % (hop_limit, timeout)
1338
1339        self.__executeCommand(cmd)
1340        if self.IsReference20200818:
1341            # wait echo reply
1342            self.sleep(6)  # increase delay temporarily (+5s) to remedy TH's delay updates
1343
1344    @API
1345    def multicast_Ping(self, destination, length=20):
1346        """send ICMPv6 echo request with a given length to a multicast destination
1347           address
1348
1349        Args:
1350            destination: the multicast destination address of ICMPv6 echo request
1351            length: the size of ICMPv6 echo request payload
1352        """
1353        cmd = 'ping %s %s' % (destination, str(length))
1354        self.__sendCommand(cmd)
1355        # wait echo reply
1356        self.sleep(1)
1357
1358    @API
1359    def setPANID(self, xPAN):
1360        """set Thread Network PAN ID
1361
1362        Args:
1363            xPAN: a given PAN ID in hex format
1364
1365        Returns:
1366            True: successful to set the Thread Network PAN ID
1367            False: fail to set the Thread Network PAN ID
1368        """
1369        panid = ''
1370        if not isinstance(xPAN, str):
1371            panid = str(hex(xPAN))
1372
1373        cmd = 'panid %s' % panid
1374        datasetCmd = 'dataset panid %s' % panid
1375        self.hasActiveDatasetToCommit = True
1376        return self.__executeCommand(cmd)[-1] == 'Done' and self.__executeCommand(datasetCmd)[-1] == 'Done'
1377
1378    @API
1379    def reset(self):
1380        """factory reset"""
1381        self._deviceBeforeReset()
1382
1383        self.__sendCommand('factoryreset', expectEcho=False)
1384        timeout = 10
1385
1386        start_time = time.time()
1387        while time.time() < start_time + timeout:
1388            time.sleep(0.5)
1389            if not self.IsBorderRouter:
1390                self._disconnect()
1391                self._connect()
1392            try:
1393                self.__executeCommand('state', timeout=0.1)
1394                break
1395            except Exception:
1396                self._restartAgentService()
1397                time.sleep(2)
1398                self.__sendCommand('factoryreset', expectEcho=False)
1399                time.sleep(0.5)
1400                continue
1401        else:
1402            raise AssertionError("Could not connect with OT device {} after reset.".format(self))
1403
1404        self.log('factoryreset finished within 10s timeout.')
1405        self._deviceAfterReset()
1406
1407        if self.IsBorderRouter:
1408            self.__executeCommand('log level 5')
1409
1410    @API
1411    def removeRouter(self, xRouterId):
1412        """kickoff router with a given router id from the Thread Network
1413
1414        Args:
1415            xRouterId: a given router id in hex format
1416
1417        Returns:
1418            True: successful to remove the router from the Thread Network
1419            False: fail to remove the router from the Thread Network
1420        """
1421        routerId = self.__convertRlocToRouterId(xRouterId)
1422
1423        if routerId is None:
1424            return False
1425
1426        cmd = 'releaserouterid %s' % routerId
1427        return self.__executeCommand(cmd)[-1] == 'Done'
1428
1429    @API
1430    def setDefaultValues(self):
1431        """set default mandatory Thread Network parameter value"""
1432        # initialize variables
1433        self.networkName = ModuleHelper.Default_NwkName
1434        self.networkKey = ModuleHelper.Default_NwkKey
1435        self.channel = ModuleHelper.Default_Channel
1436        self.channelMask = '0x7fff800'  # (0xffff << 11)
1437        self.panId = ModuleHelper.Default_PanId
1438        self.xpanId = ModuleHelper.Default_XpanId
1439        self.meshLocalPrefix = ModuleHelper.Default_MLPrefix
1440        stretchedPSKc = Thread_PBKDF2.get(ModuleHelper.Default_PSKc, ModuleHelper.Default_XpanId,
1441                                          ModuleHelper.Default_NwkName)
1442        self.pskc = hex(stretchedPSKc).rstrip('L').lstrip('0x')
1443        self.securityPolicySecs = ModuleHelper.Default_SecurityPolicy
1444        self.securityPolicyFlags = 'onrc'
1445        self.activetimestamp = ModuleHelper.Default_ActiveTimestamp
1446        # self.sedPollingRate = ModuleHelper.Default_Harness_SED_Polling_Rate
1447        self.__sedPollPeriod = 3 * 1000  # in milliseconds
1448        self.ssedTimeout = 30  # in seconds
1449        self.cslPeriod = 500  # in milliseconds
1450        self.deviceRole = None
1451        self.provisioningUrl = ''
1452        self.hasActiveDatasetToCommit = False
1453        self.logThread = Queue()
1454        self.logThreadStatus = self.logStatus['stop']
1455        self.joinCommissionedStatus = self.joinStatus['notstart']
1456        # indicate Thread device requests full or stable network data
1457        self.networkDataRequirement = ''
1458        # indicate if Thread device experiences a power down event
1459        self.isPowerDown = False
1460        # indicate AddressFilter mode ['disable', 'allowlist', 'denylist']
1461        self._addressfilterMode = 'disable'
1462        self._addressfilterSet = set()  # cache filter entries
1463        # indicate if Thread device is an active commissioner
1464        self.isActiveCommissioner = False
1465        # indicate that the channel has been set, in case the channel was set
1466        # to default when joining network
1467        self.hasSetChannel = False
1468        self.IsBeingTestedAsCommercialBBR = False
1469        # indicate whether the default domain prefix is used.
1470        self.__useDefaultDomainPrefix = True
1471        self.__isUdpOpened = False
1472        self.IsHost = False
1473
1474        # remove stale multicast addresses
1475        if self.IsBorderRouter:
1476            self.stopListeningToAddrAll()
1477
1478        # BBR dataset
1479        self.bbrSeqNum = random.randint(0, 126)  # 5.21.4.2
1480        self.bbrMlrTimeout = 3600
1481        self.bbrReRegDelay = 5
1482
1483        # initialize device configuration
1484        self.setMAC(self.mac)
1485        self.__setChannelMask(self.channelMask)
1486        self.__setSecurityPolicy(self.securityPolicySecs, self.securityPolicyFlags)
1487        self.setChannel(self.channel)
1488        self.setPANID(self.panId)
1489        self.setXpanId(self.xpanId)
1490        self.setNetworkName(self.networkName)
1491        self.setNetworkKey(self.networkKey)
1492        self.setMLPrefix(self.meshLocalPrefix)
1493        self.setPSKc(self.pskc)
1494        self.setActiveTimestamp(self.activetimestamp)
1495
1496    @API
1497    def getDeviceConncetionStatus(self):
1498        """check if serial port connection is ready or not"""
1499        return self.deviceConnected
1500
1501    @API
1502    def setPollingRate(self, iPollingRate):
1503        """set data polling rate for sleepy end device
1504
1505        Args:
1506            iPollingRate: data poll period of sleepy end device (in seconds)
1507
1508        Returns:
1509            True: successful to set the data polling rate for sleepy end device
1510            False: fail to set the data polling rate for sleepy end device
1511        """
1512        iPollingRate = int(iPollingRate * 1000)
1513
1514        if self.__sedPollPeriod != iPollingRate:
1515            if not iPollingRate:
1516                iPollingRate = 0xFFFF  # T5.2.1, disable polling
1517            elif iPollingRate < 1:
1518                iPollingRate = 1  # T9.2.13
1519            self.__sedPollPeriod = iPollingRate
1520
1521            # apply immediately
1522            if self.__isOpenThreadRunning():
1523                return self.__setPollPeriod(self.__sedPollPeriod)
1524
1525        return True
1526
1527    def __setPollPeriod(self, iPollPeriod):
1528        """set data poll period for sleepy end device
1529
1530        Args:
1531            iPollPeriod: data poll period of sleepy end device (in milliseconds)
1532
1533        Returns:
1534            True: successful to set the data poll period for sleepy end device
1535            False: fail to set the data poll period for sleepy end device
1536        """
1537        cmd = 'pollperiod %d' % iPollPeriod
1538        return self.__executeCommand(cmd)[-1] == 'Done'
1539
1540    @API
1541    def setLinkQuality(self, EUIadr, LinkQuality):
1542        """set custom LinkQualityIn for all receiving messages from the specified EUIadr
1543
1544        Args:
1545            EUIadr: a given extended address
1546            LinkQuality: a given custom link quality
1547                         link quality/link margin mapping table
1548                         3: 21 - 255 (dB)
1549                         2: 11 - 20 (dB)
1550                         1: 3 - 9 (dB)
1551                         0: 0 - 2 (dB)
1552
1553        Returns:
1554            True: successful to set the link quality
1555            False: fail to set the link quality
1556        """
1557        # process EUIadr
1558        euiHex = hex(EUIadr)
1559        euiStr = str(euiHex)
1560        euiStr = euiStr.rstrip('L')
1561        address64 = ''
1562        if '0x' in euiStr:
1563            address64 = self.__lstrip0x(euiStr)
1564            # prepend 0 at the beginning
1565            if len(address64) < 16:
1566                address64 = address64.zfill(16)
1567                print(address64)
1568
1569        cmd = 'macfilter rss add-lqi %s %s' % (address64, str(LinkQuality))
1570        return self.__executeCommand(cmd)[-1] == 'Done'
1571
1572    @API
1573    def setOutBoundLinkQuality(self, LinkQuality):
1574        """set custom LinkQualityIn for all receiving messages from the any address
1575
1576        Args:
1577            LinkQuality: a given custom link quality
1578                         link quality/link margin mapping table
1579                         3: 21 - 255 (dB)
1580                         2: 11 - 20 (dB)
1581                         1: 3 - 9 (dB)
1582                         0: 0 - 2 (dB)
1583
1584        Returns:
1585            True: successful to set the link quality
1586            False: fail to set the link quality
1587        """
1588        cmd = 'macfilter rss add-lqi * %s' % str(LinkQuality)
1589        return self.__executeCommand(cmd)[-1] == 'Done'
1590
1591    @API
1592    def removeRouterPrefix(self, prefixEntry):
1593        """remove the configured prefix on a border router
1594
1595        Args:
1596            prefixEntry: a on-mesh prefix entry in IPv6 dotted-quad format
1597
1598        Returns:
1599            True: successful to remove the prefix entry from border router
1600            False: fail to remove the prefix entry from border router
1601        """
1602        assert (ipaddress.IPv6Network(prefixEntry.decode()))
1603        cmd = 'prefix remove %s/64' % prefixEntry
1604        if self.__executeCommand(cmd)[-1] == 'Done':
1605            # send server data ntf to leader
1606            cmd = self.__replaceCommands['netdata register']
1607            return self.__executeCommand(cmd)[-1] == 'Done'
1608        else:
1609            return False
1610
1611    @API
1612    def configBorderRouter(
1613        self,
1614        P_Prefix="fd00:7d03:7d03:7d03::",
1615        P_stable=1,
1616        P_default=1,
1617        P_slaac_preferred=0,
1618        P_Dhcp=0,
1619        P_preference=0,
1620        P_on_mesh=1,
1621        P_nd_dns=0,
1622        P_dp=0,
1623    ):
1624        """configure the border router with a given prefix entry parameters
1625
1626        Args:
1627            P_Prefix: IPv6 prefix that is available on the Thread Network in IPv6 dotted-quad format
1628            P_stable: true if the default router is expected to be stable network data
1629            P_default: true if border router offers the default route for P_Prefix
1630            P_slaac_preferred: true if allowing auto-configure address using P_Prefix
1631            P_Dhcp: is true if border router is a DHCPv6 Agent
1632            P_preference: is two-bit signed integer indicating router preference
1633            P_on_mesh: is true if P_Prefix is considered to be on-mesh
1634            P_nd_dns: is true if border router is able to supply DNS information obtained via ND
1635
1636        Returns:
1637            True: successful to configure the border router with a given prefix entry
1638            False: fail to configure the border router with a given prefix entry
1639        """
1640        assert (ipaddress.IPv6Network(P_Prefix.decode()))
1641
1642        # turn off default domain prefix if configBorderRouter is called before joining network
1643        if P_dp == 0 and not self.__isOpenThreadRunning():
1644            self.__useDefaultDomainPrefix = False
1645
1646        parameter = ''
1647        prf = ''
1648
1649        if P_dp:
1650            P_slaac_preferred = 1
1651
1652        if P_slaac_preferred == 1:
1653            parameter += 'p'
1654            parameter += 'a'
1655
1656        if P_stable == 1:
1657            parameter += 's'
1658
1659        if P_default == 1:
1660            parameter += 'r'
1661
1662        if P_Dhcp == 1:
1663            parameter += 'd'
1664
1665        if P_on_mesh == 1:
1666            parameter += 'o'
1667
1668        if P_dp == 1:
1669            assert P_slaac_preferred and P_default and P_on_mesh and P_stable
1670            parameter += 'D'
1671
1672        if P_preference == 1:
1673            prf = 'high'
1674        elif P_preference == 0:
1675            prf = 'med'
1676        elif P_preference == -1:
1677            prf = 'low'
1678        else:
1679            pass
1680
1681        cmd = 'prefix add %s/64 %s %s' % (P_Prefix, parameter, prf)
1682        if self.__executeCommand(cmd)[-1] == 'Done':
1683            # if prefix configured before starting OpenThread stack
1684            # do not send out server data ntf pro-actively
1685            if not self.__isOpenThreadRunning():
1686                return True
1687            else:
1688                # send server data ntf to leader
1689                cmd = self.__replaceCommands['netdata register']
1690                return self.__executeCommand(cmd)[-1] == 'Done'
1691        else:
1692            return False
1693
1694    @watched
1695    def getNetworkData(self):
1696        lines = self.__executeCommand('netdata show')
1697        prefixes, routes, services, contexts = [], [], [], []
1698        classify = None
1699
1700        for line in lines:
1701            if line == 'Prefixes:':
1702                classify = prefixes
1703            elif line == 'Routes:':
1704                classify = routes
1705            elif line == 'Services:':
1706                classify = services
1707            elif line == 'Contexts:':
1708                classify = contexts
1709            elif line == 'Done':
1710                classify = None
1711            else:
1712                classify.append(line)
1713
1714        return {
1715            'Prefixes': prefixes,
1716            'Routes': routes,
1717            'Services': services,
1718            'Contexts': contexts,
1719        }
1720
1721    @API
1722    def setNetworkIDTimeout(self, iNwkIDTimeOut):
1723        """set networkid timeout for Thread device
1724
1725        Args:
1726            iNwkIDTimeOut: a given NETWORK_ID_TIMEOUT
1727
1728        Returns:
1729            True: successful to set NETWORK_ID_TIMEOUT
1730            False: fail to set NETWORK_ID_TIMEOUT
1731        """
1732        iNwkIDTimeOut /= 1000
1733        cmd = 'networkidtimeout %s' % str(iNwkIDTimeOut)
1734        return self.__executeCommand(cmd)[-1] == 'Done'
1735
1736    @API
1737    def setKeepAliveTimeOut(self, iTimeOut):
1738        """set child timeout for device
1739
1740        Args:
1741            iTimeOut: child timeout for device
1742
1743        Returns:
1744            True: successful to set the child timeout for device
1745            False: fail to set the child timeout for device
1746        """
1747        cmd = 'childtimeout %d' % iTimeOut
1748        return self.__executeCommand(cmd)[-1] == 'Done'
1749
1750    @API
1751    def setKeySequenceCounter(self, iKeySequenceValue):
1752        """ set the Key sequence counter corresponding to Thread network key
1753
1754        Args:
1755            iKeySequenceValue: key sequence value
1756
1757        Returns:
1758            True: successful to set the key sequence
1759            False: fail to set the key sequence
1760        """
1761        # avoid key switch guard timer protection for reference device
1762        self.__setKeySwitchGuardTime(0)
1763
1764        cmd = 'keysequence counter %s' % str(iKeySequenceValue)
1765        if self.__executeCommand(cmd)[-1] == 'Done':
1766            self.sleep(1)
1767            return True
1768        else:
1769            return False
1770
1771    @API
1772    def getKeySequenceCounter(self):
1773        """get current Thread Network key sequence"""
1774        keySequence = self.__executeCommand('keysequence counter')[0]
1775        return keySequence
1776
1777    @API
1778    def incrementKeySequenceCounter(self, iIncrementValue=1):
1779        """increment the key sequence with a given value
1780
1781        Args:
1782            iIncrementValue: specific increment value to be added
1783
1784        Returns:
1785            True: successful to increment the key sequence with a given value
1786            False: fail to increment the key sequence with a given value
1787        """
1788        # avoid key switch guard timer protection for reference device
1789        self.__setKeySwitchGuardTime(0)
1790        currentKeySeq = self.getKeySequenceCounter()
1791        keySequence = int(currentKeySeq, 10) + iIncrementValue
1792        return self.setKeySequenceCounter(keySequence)
1793
1794    @API
1795    def setNetworkDataRequirement(self, eDataRequirement):
1796        """set whether the Thread device requires the full network data
1797           or only requires the stable network data
1798
1799        Args:
1800            eDataRequirement: is true if requiring the full network data
1801
1802        Returns:
1803            True: successful to set the network requirement
1804        """
1805        if eDataRequirement == Device_Data_Requirement.ALL_DATA:
1806            self.networkDataRequirement = 'n'
1807        return True
1808
1809    @API
1810    def configExternalRouter(self, P_Prefix, P_stable, R_Preference=0):
1811        """configure border router with a given external route prefix entry
1812
1813        Args:
1814            P_Prefix: IPv6 prefix for the route in IPv6 dotted-quad format
1815            P_Stable: is true if the external route prefix is stable network data
1816            R_Preference: a two-bit signed integer indicating Router preference
1817                          1: high
1818                          0: medium
1819                         -1: low
1820
1821        Returns:
1822            True: successful to configure the border router with a given external route prefix
1823            False: fail to configure the border router with a given external route prefix
1824        """
1825        assert (ipaddress.IPv6Network(P_Prefix.decode()))
1826        prf = ''
1827        stable = ''
1828        if R_Preference == 1:
1829            prf = 'high'
1830        elif R_Preference == 0:
1831            prf = 'med'
1832        elif R_Preference == -1:
1833            prf = 'low'
1834        else:
1835            pass
1836
1837        if P_stable:
1838            stable += 's'
1839            cmd = 'route add %s/64 %s %s' % (P_Prefix, stable, prf)
1840        else:
1841            cmd = 'route add %s/64 %s' % (P_Prefix, prf)
1842
1843        if self.__executeCommand(cmd)[-1] == 'Done':
1844            # send server data ntf to leader
1845            cmd = self.__replaceCommands['netdata register']
1846            return self.__executeCommand(cmd)[-1] == 'Done'
1847
1848    @API
1849    def getNeighbouringRouters(self):
1850        """get neighboring routers information
1851
1852        Returns:
1853            neighboring routers' extended address
1854        """
1855        routerInfo = []
1856        routerList = self.__executeCommand('router list')[0].split()
1857
1858        if 'Done' in routerList:
1859            return None
1860
1861        for index in routerList:
1862            router = []
1863            cmd = 'router %s' % index
1864            router = self.__executeCommand(cmd)
1865
1866            for line in router:
1867                if 'Done' in line:
1868                    break
1869                # elif 'Rloc' in line:
1870                #    rloc16 = line.split()[1]
1871                elif 'Ext Addr' in line:
1872                    eui = line.split()[2]
1873                    routerInfo.append(int(eui, 16))
1874                # elif 'LQI In' in line:
1875                #    lqi_in = line.split()[1]
1876                # elif 'LQI Out' in line:
1877                #    lqi_out = line.split()[1]
1878                else:
1879                    pass
1880
1881        return routerInfo
1882
1883    @API
1884    def getChildrenInfo(self):
1885        """get all children information
1886
1887        Returns:
1888            children's extended address
1889        """
1890        eui = None
1891        rloc16 = None
1892        childrenInfoAll = []
1893        childrenInfo = {'EUI': 0, 'Rloc16': 0, 'MLEID': ''}
1894        childrenList = self.__executeCommand('child list')[0].split()
1895
1896        if 'Done' in childrenList:
1897            return None
1898
1899        for index in childrenList:
1900            cmd = 'child %s' % index
1901            child = []
1902            child = self.__executeCommand(cmd)
1903
1904            for line in child:
1905                if 'Done' in line:
1906                    break
1907                elif 'Rloc' in line:
1908                    rloc16 = line.split()[1]
1909                elif 'Ext Addr' in line:
1910                    eui = line.split()[2]
1911                # elif 'Child ID' in line:
1912                #    child_id = line.split()[2]
1913                # elif 'Mode' in line:
1914                #    mode = line.split()[1]
1915                else:
1916                    pass
1917
1918            childrenInfo['EUI'] = int(eui, 16)
1919            childrenInfo['Rloc16'] = int(rloc16, 16)
1920            # children_info['MLEID'] = self.getMLEID()
1921
1922            childrenInfoAll.append(childrenInfo['EUI'])
1923            # childrenInfoAll.append(childrenInfo)
1924
1925        return childrenInfoAll
1926
1927    @API
1928    def setXpanId(self, xPanId):
1929        """set extended PAN ID of Thread Network
1930
1931        Args:
1932            xPanId: extended PAN ID in hex format
1933
1934        Returns:
1935            True: successful to set the extended PAN ID
1936            False: fail to set the extended PAN ID
1937        """
1938        xpanid = ''
1939        if not isinstance(xPanId, str):
1940            xpanid = self.__convertLongToHex(xPanId, 16)
1941            cmd = 'extpanid %s' % xpanid
1942            datasetCmd = 'dataset extpanid %s' % xpanid
1943        else:
1944            xpanid = xPanId
1945            cmd = 'extpanid %s' % xpanid
1946            datasetCmd = 'dataset extpanid %s' % xpanid
1947
1948        self.xpanId = xpanid
1949        self.hasActiveDatasetToCommit = True
1950        return self.__executeCommand(cmd)[-1] == 'Done' and self.__executeCommand(datasetCmd)[-1] == 'Done'
1951
1952    @API
1953    def getNeighbouringDevices(self):
1954        """gets the neighboring devices' extended address to compute the DUT
1955           extended address automatically
1956
1957        Returns:
1958            A list including extended address of neighboring routers, parent
1959            as well as children
1960        """
1961        neighbourList = []
1962
1963        # get parent info
1964        parentAddr = self.getParentAddress()
1965        if parentAddr != 0:
1966            neighbourList.append(parentAddr)
1967
1968        # get ED/SED children info
1969        childNeighbours = self.getChildrenInfo()
1970        if childNeighbours is not None and len(childNeighbours) > 0:
1971            for entry in childNeighbours:
1972                neighbourList.append(entry)
1973
1974        # get neighboring routers info
1975        routerNeighbours = self.getNeighbouringRouters()
1976        if routerNeighbours is not None and len(routerNeighbours) > 0:
1977            for entry in routerNeighbours:
1978                neighbourList.append(entry)
1979
1980        return neighbourList
1981
1982    @API
1983    def setPartationId(self, partationId):
1984        """set Thread Network Partition ID
1985
1986        Args:
1987            partitionId: partition id to be set by leader
1988
1989        Returns:
1990            True: successful to set the Partition ID
1991            False: fail to set the Partition ID
1992        """
1993        cmd = self.__replaceCommands['partitionid preferred'] + ' '
1994        cmd += str(hex(partationId)).rstrip('L')
1995        return self.__executeCommand(cmd)[-1] == 'Done'
1996
1997    @API
1998    def getGUA(self, filterByPrefix=None, eth=False):
1999        """get expected global unicast IPv6 address of Thread device
2000
2001        note: existing filterByPrefix are string of in lowercase. e.g.
2002        '2001' or '2001:0db8:0001:0000".
2003
2004        Args:
2005            filterByPrefix: a given expected global IPv6 prefix to be matched
2006
2007        Returns:
2008            a global IPv6 address
2009        """
2010        assert not eth
2011        # get global addrs set if multiple
2012        globalAddrs = self.__getGlobal()
2013
2014        if filterByPrefix is None:
2015            return globalAddrs[0]
2016        else:
2017            for fullIp in globalAddrs:
2018                if fullIp.startswith(filterByPrefix):
2019                    return fullIp
2020            return str(globalAddrs[0])
2021
2022    @API
2023    def getShortAddress(self):
2024        """get Rloc16 short address of Thread device"""
2025        return self.getRloc16()
2026
2027    @API
2028    def getULA64(self):
2029        """get mesh local EID of Thread device"""
2030        return self.__executeCommand('ipaddr mleid')[0]
2031
2032    @API
2033    def setMLPrefix(self, sMeshLocalPrefix):
2034        """set mesh local prefix"""
2035        cmd = 'dataset meshlocalprefix %s' % sMeshLocalPrefix
2036        self.hasActiveDatasetToCommit = True
2037        return self.__executeCommand(cmd)[-1] == 'Done'
2038
2039    @API
2040    def getML16(self):
2041        """get mesh local 16 unicast address (Rloc)"""
2042        return self.getRloc()
2043
2044    @API
2045    def downgradeToDevice(self):
2046        pass
2047
2048    @API
2049    def upgradeToRouter(self):
2050        pass
2051
2052    @API
2053    def forceSetSlaac(self, slaacAddress):
2054        """force to set a slaac IPv6 address to Thread interface
2055
2056        Args:
2057            slaacAddress: a slaac IPv6 address to be set
2058
2059        Returns:
2060            True: successful to set slaac address to Thread interface
2061            False: fail to set slaac address to Thread interface
2062        """
2063        cmd = 'ipaddr add %s' % str(slaacAddress)
2064        return self.__executeCommand(cmd)[-1] == 'Done'
2065
2066    @API
2067    def setSleepyNodePollTime(self):
2068        pass
2069
2070    @API
2071    def enableAutoDUTObjectFlag(self):
2072        """set AutoDUTenable flag"""
2073        self.AutoDUTEnable = True
2074
2075    @API
2076    def getChildTimeoutValue(self):
2077        """get child timeout"""
2078        childTimeout = self.__executeCommand('childtimeout')[0]
2079        return int(childTimeout)
2080
2081    @API
2082    def diagnosticGet(self, strDestinationAddr, listTLV_ids=()):
2083        if not listTLV_ids:
2084            return
2085
2086        if len(listTLV_ids) == 0:
2087            return
2088
2089        cmd = 'networkdiagnostic get %s %s' % (
2090            strDestinationAddr,
2091            ' '.join([str(tlv) for tlv in listTLV_ids]),
2092        )
2093
2094        return self.__sendCommand(cmd, expectEcho=False)
2095
2096    @API
2097    def diagnosticReset(self, strDestinationAddr, listTLV_ids=()):
2098        if not listTLV_ids:
2099            return
2100
2101        if len(listTLV_ids) == 0:
2102            return
2103
2104        cmd = 'networkdiagnostic reset %s %s' % (
2105            strDestinationAddr,
2106            ' '.join([str(tlv) for tlv in listTLV_ids]),
2107        )
2108
2109        return self.__executeCommand(cmd)
2110
2111    @API
2112    def diagnosticQuery(self, strDestinationAddr, listTLV_ids=()):
2113        self.diagnosticGet(strDestinationAddr, listTLV_ids)
2114
2115    @API
2116    def startNativeCommissioner(self, strPSKc='GRLPASSPHRASE'):
2117        # TODO: Support the whole Native Commissioner functionality
2118        # Currently it only aims to trigger a Discovery Request message to pass
2119        # Certification test 5.8.4
2120        self.__executeCommand('ifconfig up')
2121        cmd = 'joiner start %s' % (strPSKc)
2122        return self.__executeCommand(cmd)[-1] == 'Done'
2123
2124    @API
2125    def startExternalCommissioner(self, baAddr, baPort):
2126        """Start external commissioner
2127        Args:
2128            baAddr: A string represents the border agent address.
2129            baPort: An integer represents the border agent port.
2130        Returns:
2131            A boolean indicates whether this function succeed.
2132        """
2133        if self.externalCommissioner is None:
2134            config = commissioner.Configuration()
2135            config.isCcmMode = False
2136            config.domainName = OpenThreadTHCI.DOMAIN_NAME
2137            config.pskc = bytearray.fromhex(self.pskc)
2138
2139            self.externalCommissioner = OTCommissioner(config, self)
2140
2141        if not self.externalCommissioner.isActive():
2142            self.externalCommissioner.start(baAddr, baPort)
2143
2144        if not self.externalCommissioner.isActive():
2145            raise commissioner.Error("external commissioner is not active")
2146
2147        return True
2148
2149    @API
2150    def stopExternalCommissioner(self):
2151        """Stop external commissioner
2152        Returns:
2153            A boolean indicates whether this function succeed.
2154        """
2155        if self.externalCommissioner is not None:
2156            self.externalCommissioner.stop()
2157            return not self.externalCommissioner.isActive()
2158
2159    @API
2160    def startCollapsedCommissioner(self, role=Thread_Device_Role.Leader):
2161        """start Collapsed Commissioner
2162
2163        Returns:
2164            True: successful to start Commissioner
2165            False: fail to start Commissioner
2166        """
2167        if self.__startOpenThread():
2168            self.wait_for_attach_to_the_network(expected_role=self.deviceRole,
2169                                                timeout=self.NETWORK_ATTACHMENT_TIMEOUT,
2170                                                raise_assert=True)
2171            cmd = 'commissioner start'
2172            if self.__executeCommand(cmd)[-1] == 'Done':
2173                self.isActiveCommissioner = True
2174                self.sleep(20)  # time for petition process
2175                return True
2176        return False
2177
2178    @API
2179    def setJoinKey(self, strPSKc):
2180        pass
2181
2182    @API
2183    def scanJoiner(self, xEUI='*', strPSKd='THREADJPAKETEST'):
2184        """scan Joiner
2185
2186        Args:
2187            xEUI: Joiner's EUI-64
2188            strPSKd: Joiner's PSKd for commissioning
2189
2190        Returns:
2191            True: successful to add Joiner's steering data
2192            False: fail to add Joiner's steering data
2193        """
2194        self.log("scanJoiner on channel %s", self.getChannel())
2195
2196        # long timeout value to avoid automatic joiner removal (in seconds)
2197        timeout = 500
2198
2199        if not isinstance(xEUI, str):
2200            eui64 = self.__convertLongToHex(xEUI, 16)
2201        else:
2202            eui64 = xEUI
2203
2204        strPSKd = self.__normalizePSKd(strPSKd)
2205
2206        cmd = 'commissioner joiner add %s %s %s' % (
2207            self._deviceEscapeEscapable(eui64),
2208            strPSKd,
2209            str(timeout),
2210        )
2211
2212        if self.__executeCommand(cmd)[-1] == 'Done':
2213            if self.logThreadStatus == self.logStatus['stop']:
2214                self.logThread = ThreadRunner.run(target=self.__readCommissioningLogs, args=(120,))
2215            return True
2216        else:
2217            return False
2218
2219    @staticmethod
2220    def __normalizePSKd(strPSKd):
2221        return strPSKd.upper().replace('I', '1').replace('O', '0').replace('Q', '0').replace('Z', '2')
2222
2223    @API
2224    def setProvisioningUrl(self, strURL='grl.com'):
2225        """set provisioning Url
2226
2227        Args:
2228            strURL: Provisioning Url string
2229
2230        Returns:
2231            True: successful to set provisioning Url
2232            False: fail to set provisioning Url
2233        """
2234        self.provisioningUrl = strURL
2235        if self.deviceRole == Thread_Device_Role.Commissioner:
2236            cmd = 'commissioner provisioningurl %s' % (strURL)
2237            return self.__executeCommand(cmd)[-1] == 'Done'
2238        return True
2239
2240    @API
2241    def allowCommission(self):
2242        """start commissioner candidate petition process
2243
2244        Returns:
2245            True: successful to start commissioner candidate petition process
2246            False: fail to start commissioner candidate petition process
2247        """
2248        cmd = 'commissioner start'
2249        if self.__executeCommand(cmd)[-1] == 'Done':
2250            self.isActiveCommissioner = True
2251            # time for petition process and at least one keep alive
2252            self.sleep(3)
2253            return True
2254        else:
2255            return False
2256
2257    @API
2258    def joinCommissioned(self, strPSKd='THREADJPAKETEST', waitTime=20):
2259        """start joiner
2260
2261        Args:
2262            strPSKd: Joiner's PSKd
2263
2264        Returns:
2265            True: successful to start joiner
2266            False: fail to start joiner
2267        """
2268        self.log("joinCommissioned on channel %s", self.getChannel())
2269
2270        if self.deviceRole in [
2271                Thread_Device_Role.Leader,
2272                Thread_Device_Role.Router,
2273                Thread_Device_Role.REED,
2274        ]:
2275            self.__setRouterSelectionJitter(1)
2276        self.__executeCommand('ifconfig up')
2277        strPSKd = self.__normalizePSKd(strPSKd)
2278        cmd = 'joiner start %s %s' % (strPSKd, self.provisioningUrl)
2279        if self.__executeCommand(cmd)[-1] == 'Done':
2280            maxDuration = 150  # seconds
2281            self.joinCommissionedStatus = self.joinStatus['ongoing']
2282
2283            if self.logThreadStatus == self.logStatus['stop']:
2284                self.logThread = ThreadRunner.run(target=self.__readCommissioningLogs, args=(maxDuration,))
2285
2286            t_end = time.time() + maxDuration
2287            while time.time() < t_end:
2288                if self.joinCommissionedStatus == self.joinStatus['succeed']:
2289                    break
2290                elif self.joinCommissionedStatus == self.joinStatus['failed']:
2291                    return False
2292
2293                self.sleep(1)
2294
2295            self.setMAC(self.mac)
2296            self.__executeCommand('thread start')
2297            self.wait_for_attach_to_the_network(expected_role=self.deviceRole,
2298                                                timeout=self.NETWORK_ATTACHMENT_TIMEOUT,
2299                                                raise_assert=True)
2300            return True
2301        else:
2302            return False
2303
2304    @API
2305    def getCommissioningLogs(self):
2306        """get Commissioning logs
2307
2308        Returns:
2309           Commissioning logs
2310        """
2311        rawLogs = self.logThread.get()
2312        ProcessedLogs = []
2313        payload = []
2314
2315        while not rawLogs.empty():
2316            rawLogEach = rawLogs.get()
2317            if '[THCI]' not in rawLogEach:
2318                continue
2319
2320            EncryptedPacket = PlatformDiagnosticPacket()
2321            infoList = rawLogEach.split('[THCI]')[1].split(']')[0].split('|')
2322            for eachInfo in infoList:
2323                info = eachInfo.split('=')
2324                infoType = info[0].strip()
2325                infoValue = info[1].strip()
2326                if 'direction' in infoType:
2327                    EncryptedPacket.Direction = (PlatformDiagnosticPacket_Direction.IN
2328                                                 if 'recv' in infoValue else PlatformDiagnosticPacket_Direction.OUT if
2329                                                 'send' in infoValue else PlatformDiagnosticPacket_Direction.UNKNOWN)
2330                elif 'type' in infoType:
2331                    EncryptedPacket.Type = (PlatformDiagnosticPacket_Type.JOIN_FIN_req if 'JOIN_FIN.req' in infoValue
2332                                            else PlatformDiagnosticPacket_Type.JOIN_FIN_rsp if 'JOIN_FIN.rsp'
2333                                            in infoValue else PlatformDiagnosticPacket_Type.JOIN_ENT_req if
2334                                            'JOIN_ENT.ntf' in infoValue else PlatformDiagnosticPacket_Type.JOIN_ENT_rsp
2335                                            if 'JOIN_ENT.rsp' in infoValue else PlatformDiagnosticPacket_Type.UNKNOWN)
2336                elif 'len' in infoType:
2337                    bytesInEachLine = 16
2338                    EncryptedPacket.TLVsLength = int(infoValue)
2339                    payloadLineCount = (int(infoValue) + bytesInEachLine - 1) / bytesInEachLine
2340                    while payloadLineCount > 0:
2341                        payloadLineCount = payloadLineCount - 1
2342                        payloadLine = rawLogs.get()
2343                        payloadSplit = payloadLine.split('|')
2344                        for block in range(1, 3):
2345                            payloadBlock = payloadSplit[block]
2346                            payloadValues = payloadBlock.split(' ')
2347                            for num in range(1, 9):
2348                                if '..' not in payloadValues[num]:
2349                                    payload.append(int(payloadValues[num], 16))
2350
2351                    EncryptedPacket.TLVs = (PlatformPackets.read(EncryptedPacket.Type, payload)
2352                                            if payload != [] else [])
2353
2354            ProcessedLogs.append(EncryptedPacket)
2355        return ProcessedLogs
2356
2357    @API
2358    def MGMT_ED_SCAN(
2359        self,
2360        sAddr,
2361        xCommissionerSessionId,
2362        listChannelMask,
2363        xCount,
2364        xPeriod,
2365        xScanDuration,
2366    ):
2367        """send MGMT_ED_SCAN message to a given destinaition.
2368
2369        Args:
2370            sAddr: IPv6 destination address for this message
2371            xCommissionerSessionId: commissioner session id
2372            listChannelMask: a channel array to indicate which channels to be scaned
2373            xCount: number of IEEE 802.15.4 ED Scans (milliseconds)
2374            xPeriod: Period between successive IEEE802.15.4 ED Scans (milliseconds)
2375            xScanDuration: ScanDuration when performing an IEEE 802.15.4 ED Scan (milliseconds)
2376
2377        Returns:
2378            True: successful to send MGMT_ED_SCAN message.
2379            False: fail to send MGMT_ED_SCAN message
2380        """
2381        channelMask = '0x' + self.__convertLongToHex(self.__convertChannelMask(listChannelMask))
2382        cmd = 'commissioner energy %s %s %s %s %s' % (
2383            channelMask,
2384            xCount,
2385            xPeriod,
2386            xScanDuration,
2387            sAddr,
2388        )
2389        return self.__executeCommand(cmd)[-1] == 'Done'
2390
2391    @API
2392    def MGMT_PANID_QUERY(self, sAddr, xCommissionerSessionId, listChannelMask, xPanId):
2393        """send MGMT_PANID_QUERY message to a given destination
2394
2395        Args:
2396            xPanId: a given PAN ID to check the conflicts
2397
2398        Returns:
2399            True: successful to send MGMT_PANID_QUERY message.
2400            False: fail to send MGMT_PANID_QUERY message.
2401        """
2402        panid = ''
2403        channelMask = '0x' + self.__convertLongToHex(self.__convertChannelMask(listChannelMask))
2404
2405        if not isinstance(xPanId, str):
2406            panid = str(hex(xPanId))
2407
2408        cmd = 'commissioner panid %s %s %s' % (panid, channelMask, sAddr)
2409        return self.__executeCommand(cmd)[-1] == 'Done'
2410
2411    @API
2412    def MGMT_ANNOUNCE_BEGIN(self, sAddr, xCommissionerSessionId, listChannelMask, xCount, xPeriod):
2413        """send MGMT_ANNOUNCE_BEGIN message to a given destination
2414
2415        Returns:
2416            True: successful to send MGMT_ANNOUNCE_BEGIN message.
2417            False: fail to send MGMT_ANNOUNCE_BEGIN message.
2418        """
2419        channelMask = '0x' + self.__convertLongToHex(self.__convertChannelMask(listChannelMask))
2420        cmd = 'commissioner announce %s %s %s %s' % (
2421            channelMask,
2422            xCount,
2423            xPeriod,
2424            sAddr,
2425        )
2426        return self.__executeCommand(cmd)[-1] == 'Done'
2427
2428    @API
2429    def MGMT_ACTIVE_GET(self, Addr='', TLVs=()):
2430        """send MGMT_ACTIVE_GET command
2431
2432        Returns:
2433            True: successful to send MGMT_ACTIVE_GET
2434            False: fail to send MGMT_ACTIVE_GET
2435        """
2436        cmd = 'dataset mgmtgetcommand active'
2437
2438        if Addr != '':
2439            cmd += ' address '
2440            cmd += Addr
2441
2442        if len(TLVs) != 0:
2443            tlvs = ''.join('%02x' % tlv for tlv in TLVs)
2444            cmd += ' ' + self.__replaceCommands['-x'] + ' '
2445            cmd += tlvs
2446
2447        return self.__executeCommand(cmd)[-1] == 'Done'
2448
2449    @API
2450    def MGMT_ACTIVE_SET(
2451        self,
2452        sAddr='',
2453        xCommissioningSessionId=None,
2454        listActiveTimestamp=None,
2455        listChannelMask=None,
2456        xExtendedPanId=None,
2457        sNetworkName=None,
2458        sPSKc=None,
2459        listSecurityPolicy=None,
2460        xChannel=None,
2461        sMeshLocalPrefix=None,
2462        xMasterKey=None,
2463        xPanId=None,
2464        xTmfPort=None,
2465        xSteeringData=None,
2466        xBorderRouterLocator=None,
2467        BogusTLV=None,
2468        xDelayTimer=None,
2469    ):
2470        """send MGMT_ACTIVE_SET command
2471
2472        Returns:
2473            True: successful to send MGMT_ACTIVE_SET
2474            False: fail to send MGMT_ACTIVE_SET
2475        """
2476        cmd = 'dataset mgmtsetcommand active'
2477
2478        if listActiveTimestamp is not None:
2479            cmd += ' activetimestamp '
2480            cmd += str(listActiveTimestamp[0])
2481
2482        if xExtendedPanId is not None:
2483            cmd += ' extpanid '
2484            xpanid = self.__convertLongToHex(xExtendedPanId, 16)
2485
2486            cmd += xpanid
2487
2488        if sNetworkName is not None:
2489            cmd += ' networkname '
2490            cmd += self._deviceEscapeEscapable(str(sNetworkName))
2491
2492        if xChannel is not None:
2493            cmd += ' channel '
2494            cmd += str(xChannel)
2495
2496        if sMeshLocalPrefix is not None:
2497            cmd += ' localprefix '
2498            cmd += str(sMeshLocalPrefix)
2499
2500        if xMasterKey is not None:
2501            cmd += ' ' + self.__replaceCommands['networkkey'] + ' '
2502            key = self.__convertLongToHex(xMasterKey, 32)
2503
2504            cmd += key
2505
2506        if xPanId is not None:
2507            cmd += ' panid '
2508            cmd += str(xPanId)
2509
2510        if listChannelMask is not None:
2511            cmd += ' channelmask '
2512            cmd += '0x' + self.__convertLongToHex(self.__convertChannelMask(listChannelMask))
2513
2514        if (sPSKc is not None or listSecurityPolicy is not None or xCommissioningSessionId is not None or
2515                xTmfPort is not None or xSteeringData is not None or xBorderRouterLocator is not None or
2516                BogusTLV is not None):
2517            cmd += ' ' + self.__replaceCommands['-x'] + ' '
2518
2519        if sPSKc is not None:
2520            cmd += '0410'
2521            stretchedPskc = Thread_PBKDF2.get(
2522                sPSKc,
2523                ModuleHelper.Default_XpanId,
2524                ModuleHelper.Default_NwkName,
2525            )
2526            pskc = '%x' % stretchedPskc
2527
2528            if len(pskc) < 32:
2529                pskc = pskc.zfill(32)
2530
2531            cmd += pskc
2532
2533        if listSecurityPolicy is not None:
2534            if self.DeviceCapability == DevCapb.V1_1:
2535                cmd += '0c03'
2536            else:
2537                cmd += '0c04'
2538
2539            rotationTime = 0
2540            policyBits = 0
2541
2542            # previous passing way listSecurityPolicy=[True, True, 3600,
2543            # False, False, True]
2544            if len(listSecurityPolicy) == 6:
2545                rotationTime = listSecurityPolicy[2]
2546
2547                # the last three reserved bits must be 1
2548                policyBits = 0b00000111
2549
2550                if listSecurityPolicy[0]:
2551                    policyBits = policyBits | 0b10000000
2552                if listSecurityPolicy[1]:
2553                    policyBits = policyBits | 0b01000000
2554                if listSecurityPolicy[3]:
2555                    policyBits = policyBits | 0b00100000
2556                if listSecurityPolicy[4]:
2557                    policyBits = policyBits | 0b00010000
2558                if listSecurityPolicy[5]:
2559                    policyBits = policyBits | 0b00001000
2560            else:
2561                # new passing way listSecurityPolicy=[3600, 0b11001111]
2562                rotationTime = listSecurityPolicy[0]
2563                # bit order
2564                if len(listSecurityPolicy) > 2:
2565                    policyBits = listSecurityPolicy[2] << 8 | listSecurityPolicy[1]
2566                else:
2567                    policyBits = listSecurityPolicy[1]
2568
2569            policy = str(hex(rotationTime))[2:]
2570
2571            if len(policy) < 4:
2572                policy = policy.zfill(4)
2573
2574            cmd += policy
2575
2576            flags0 = ('%x' % (policyBits & 0x00ff)).ljust(2, '0')
2577            cmd += flags0
2578
2579            if self.DeviceCapability != DevCapb.V1_1:
2580                flags1 = ('%x' % ((policyBits & 0xff00) >> 8)).ljust(2, '0')
2581                cmd += flags1
2582
2583        if xCommissioningSessionId is not None:
2584            cmd += '0b02'
2585            sessionid = str(hex(xCommissioningSessionId))[2:]
2586
2587            if len(sessionid) < 4:
2588                sessionid = sessionid.zfill(4)
2589
2590            cmd += sessionid
2591
2592        if xBorderRouterLocator is not None:
2593            cmd += '0902'
2594            locator = str(hex(xBorderRouterLocator))[2:]
2595
2596            if len(locator) < 4:
2597                locator = locator.zfill(4)
2598
2599            cmd += locator
2600
2601        if xSteeringData is not None:
2602            steeringData = self.__convertLongToHex(xSteeringData)
2603            cmd += '08' + str(len(steeringData) / 2).zfill(2)
2604            cmd += steeringData
2605
2606        if BogusTLV is not None:
2607            cmd += '8202aa55'
2608
2609        return self.__executeCommand(cmd)[-1] == 'Done'
2610
2611    @API
2612    def MGMT_PENDING_GET(self, Addr='', TLVs=()):
2613        """send MGMT_PENDING_GET command
2614
2615        Returns:
2616            True: successful to send MGMT_PENDING_GET
2617            False: fail to send MGMT_PENDING_GET
2618        """
2619        cmd = 'dataset mgmtgetcommand pending'
2620
2621        if Addr != '':
2622            cmd += ' address '
2623            cmd += Addr
2624
2625        if len(TLVs) != 0:
2626            tlvs = ''.join('%02x' % tlv for tlv in TLVs)
2627            cmd += ' ' + self.__replaceCommands['-x'] + ' '
2628            cmd += tlvs
2629
2630        return self.__executeCommand(cmd)[-1] == 'Done'
2631
2632    @API
2633    def MGMT_PENDING_SET(
2634        self,
2635        sAddr='',
2636        xCommissionerSessionId=None,
2637        listPendingTimestamp=None,
2638        listActiveTimestamp=None,
2639        xDelayTimer=None,
2640        xChannel=None,
2641        xPanId=None,
2642        xMasterKey=None,
2643        sMeshLocalPrefix=None,
2644        sNetworkName=None,
2645    ):
2646        """send MGMT_PENDING_SET command
2647
2648        Returns:
2649            True: successful to send MGMT_PENDING_SET
2650            False: fail to send MGMT_PENDING_SET
2651        """
2652        cmd = 'dataset mgmtsetcommand pending'
2653
2654        if listPendingTimestamp is not None:
2655            cmd += ' pendingtimestamp '
2656            cmd += str(listPendingTimestamp[0])
2657
2658        if listActiveTimestamp is not None:
2659            cmd += ' activetimestamp '
2660            cmd += str(listActiveTimestamp[0])
2661
2662        if xDelayTimer is not None:
2663            cmd += ' delaytimer '
2664            cmd += str(xDelayTimer)
2665            # cmd += ' delaytimer 3000000'
2666
2667        if xChannel is not None:
2668            cmd += ' channel '
2669            cmd += str(xChannel)
2670
2671        if xPanId is not None:
2672            cmd += ' panid '
2673            cmd += str(xPanId)
2674
2675        if xMasterKey is not None:
2676            cmd += ' ' + self.__replaceCommands['networkkey'] + ' '
2677            key = self.__convertLongToHex(xMasterKey, 32)
2678
2679            cmd += key
2680
2681        if sMeshLocalPrefix is not None:
2682            cmd += ' localprefix '
2683            cmd += str(sMeshLocalPrefix)
2684
2685        if sNetworkName is not None:
2686            cmd += ' networkname '
2687            cmd += self._deviceEscapeEscapable(str(sNetworkName))
2688
2689        if xCommissionerSessionId is not None:
2690            cmd += ' ' + self.__replaceCommands['-x'] + ' '
2691            cmd += '0b02'
2692            sessionid = str(hex(xCommissionerSessionId))[2:]
2693
2694            if len(sessionid) < 4:
2695                sessionid = sessionid.zfill(4)
2696
2697            cmd += sessionid
2698
2699        return self.__executeCommand(cmd)[-1] == 'Done'
2700
2701    @API
2702    def MGMT_COMM_GET(self, Addr='ff02::1', TLVs=()):
2703        """send MGMT_COMM_GET command
2704
2705        Returns:
2706            True: successful to send MGMT_COMM_GET
2707            False: fail to send MGMT_COMM_GET
2708        """
2709        cmd = 'commissioner mgmtget'
2710
2711        if len(TLVs) != 0:
2712            tlvs = ''.join('%02x' % tlv for tlv in TLVs)
2713            cmd += ' ' + self.__replaceCommands['-x'] + ' '
2714            cmd += tlvs
2715
2716        return self.__executeCommand(cmd)[-1] == 'Done'
2717
2718    @API
2719    def MGMT_COMM_SET(
2720        self,
2721        Addr='ff02::1',
2722        xCommissionerSessionID=None,
2723        xSteeringData=None,
2724        xBorderRouterLocator=None,
2725        xChannelTlv=None,
2726        ExceedMaxPayload=False,
2727    ):
2728        """send MGMT_COMM_SET command
2729
2730        Returns:
2731            True: successful to send MGMT_COMM_SET
2732            False: fail to send MGMT_COMM_SET
2733        """
2734        cmd = 'commissioner mgmtset'
2735
2736        if xCommissionerSessionID is not None:
2737            # use assigned session id
2738            cmd += ' sessionid '
2739            cmd += str(xCommissionerSessionID)
2740        elif xCommissionerSessionID is None:
2741            # use original session id
2742            if self.isActiveCommissioner is True:
2743                cmd += ' sessionid '
2744                cmd += self.__getCommissionerSessionId()
2745            else:
2746                pass
2747
2748        if xSteeringData is not None:
2749            cmd += ' steeringdata '
2750            cmd += str(hex(xSteeringData)[2:])
2751
2752        if xBorderRouterLocator is not None:
2753            cmd += ' locator '
2754            cmd += str(hex(xBorderRouterLocator))
2755
2756        if xChannelTlv is not None:
2757            cmd += ' ' + self.__replaceCommands['-x'] + ' '
2758            cmd += '000300' + '%04x' % xChannelTlv
2759
2760        return self.__executeCommand(cmd)[-1] == 'Done'
2761
2762    @API
2763    def setPSKc(self, strPSKc):
2764        cmd = 'dataset pskc %s' % strPSKc
2765        self.hasActiveDatasetToCommit = True
2766        return self.__executeCommand(cmd)[-1] == 'Done'
2767
2768    @API
2769    def setActiveTimestamp(self, xActiveTimestamp):
2770        self.activetimestamp = xActiveTimestamp
2771        self.hasActiveDatasetToCommit = True
2772        cmd = 'dataset activetimestamp %s' % str(xActiveTimestamp)
2773        return self.__executeCommand(cmd)[-1] == 'Done'
2774
2775    @API
2776    def setUdpJoinerPort(self, portNumber):
2777        """set Joiner UDP Port
2778
2779        Args:
2780            portNumber: Joiner UDP Port number
2781
2782        Returns:
2783            True: successful to set Joiner UDP Port
2784            False: fail to set Joiner UDP Port
2785        """
2786        cmd = 'joinerport %d' % portNumber
2787        return self.__executeCommand(cmd)[-1] == 'Done'
2788
2789    @API
2790    def commissionerUnregister(self):
2791        """stop commissioner
2792
2793        Returns:
2794            True: successful to stop commissioner
2795            False: fail to stop commissioner
2796        """
2797        cmd = 'commissioner stop'
2798        return self.__executeCommand(cmd)[-1] == 'Done'
2799
2800    @API
2801    def sendBeacons(self, sAddr, xCommissionerSessionId, listChannelMask, xPanId):
2802        self.__sendCommand('scan', expectEcho=False)
2803
2804    @API
2805    def updateRouterStatus(self):
2806        """force update to router as if there is child id request"""
2807        self._update_router_status = True
2808
2809    @API
2810    def __updateRouterStatus(self):
2811        cmd = 'state'
2812        while True:
2813            state = self.__executeCommand(cmd)[0]
2814            if state == 'detached':
2815                continue
2816            elif state == 'child':
2817                break
2818            else:
2819                return False
2820
2821        cmd = 'state router'
2822        return self.__executeCommand(cmd)[-1] == 'Done'
2823
2824    @API
2825    def setRouterThresholdValues(self, upgradeThreshold, downgradeThreshold):
2826        self.__setRouterUpgradeThreshold(upgradeThreshold)
2827        self.__setRouterDowngradeThreshold(downgradeThreshold)
2828
2829    @API
2830    def setMinDelayTimer(self, iSeconds):
2831        cmd = 'delaytimermin %s' % iSeconds
2832        return self.__executeCommand(cmd)[-1] == 'Done'
2833
2834    @API
2835    def ValidateDeviceFirmware(self):
2836        assert not self.IsBorderRouter, "Method not expected to be used with border router devices"
2837
2838        if self.DeviceCapability == OT11_CAPBS:
2839            return OT11_VERSION in self.UIStatusMsg
2840        elif self.DeviceCapability == OT12_CAPBS:
2841            return OT12_VERSION in self.UIStatusMsg
2842        elif self.DeviceCapability == OT13_CAPBS:
2843            return OT13_VERSION in self.UIStatusMsg
2844        else:
2845            return False
2846
2847    @API
2848    def setBbrDataset(self, SeqNumInc=False, SeqNum=None, MlrTimeout=None, ReRegDelay=None):
2849        """ set BBR Dataset
2850
2851        Args:
2852            SeqNumInc:  Increase `SeqNum` by 1 if True.
2853            SeqNum:     Set `SeqNum` to a given value if not None.
2854            MlrTimeout: Set `MlrTimeout` to a given value.
2855            ReRegDelay: Set `ReRegDelay` to a given value.
2856
2857            MUST NOT set SeqNumInc to True and SeqNum to non-None value at the same time.
2858
2859        Returns:
2860            True: successful to set BBR Dataset
2861            False: fail to set BBR Dataset
2862        """
2863        assert not (SeqNumInc and SeqNum is not None), "Must not specify both SeqNumInc and SeqNum"
2864
2865        if (MlrTimeout and MlrTimeout != self.bbrMlrTimeout) or (ReRegDelay and ReRegDelay != self.bbrReRegDelay):
2866            if SeqNum is None:
2867                SeqNumInc = True
2868
2869        if SeqNumInc:
2870            if self.bbrSeqNum in (126, 127):
2871                self.bbrSeqNum = 0
2872            elif self.bbrSeqNum in (254, 255):
2873                self.bbrSeqNum = 128
2874            else:
2875                self.bbrSeqNum = (self.bbrSeqNum + 1) % 256
2876        elif SeqNum is not None:
2877            self.bbrSeqNum = SeqNum
2878
2879        return self.__configBbrDataset(SeqNum=self.bbrSeqNum, MlrTimeout=MlrTimeout, ReRegDelay=ReRegDelay)
2880
2881    def __configBbrDataset(self, SeqNum=None, MlrTimeout=None, ReRegDelay=None):
2882        if MlrTimeout is not None and ReRegDelay is None:
2883            ReRegDelay = self.bbrReRegDelay
2884
2885        cmd = 'bbr config'
2886        if SeqNum is not None:
2887            cmd += ' seqno %d' % SeqNum
2888        if ReRegDelay is not None:
2889            cmd += ' delay %d' % ReRegDelay
2890        if MlrTimeout is not None:
2891            cmd += ' timeout %d' % MlrTimeout
2892        ret = self.__executeCommand(cmd)[-1] == 'Done'
2893
2894        if SeqNum is not None:
2895            self.bbrSeqNum = SeqNum
2896
2897        if MlrTimeout is not None:
2898            self.bbrMlrTimeout = MlrTimeout
2899
2900        if ReRegDelay is not None:
2901            self.bbrReRegDelay = ReRegDelay
2902
2903        cmd = self.__replaceCommands['netdata register']
2904        self.__executeCommand(cmd)
2905
2906        return ret
2907
2908    # Low power THCI
2909    @API
2910    def setCSLtout(self, tout=30):
2911        self.ssedTimeout = tout
2912        cmd = 'csl timeout %u' % self.ssedTimeout
2913        return self.__executeCommand(cmd)[-1] == 'Done'
2914
2915    @API
2916    def setCSLchannel(self, ch=11):
2917        cmd = 'csl channel %u' % ch
2918        return self.__executeCommand(cmd)[-1] == 'Done'
2919
2920    @API
2921    def setCSLperiod(self, period=500):
2922        """set Csl Period
2923        Args:
2924            period: csl period in ms
2925
2926        note: OT command 'csl period' accepts parameter in unit of 10 symbols,
2927        period is converted from unit ms to ten symbols (160us per 10 symbols).
2928
2929        """
2930        cmd = 'csl period %u' % (period * 6.25)
2931        return self.__executeCommand(cmd)[-1] == 'Done'
2932
2933    @staticmethod
2934    def getForwardSeriesFlagsFromHexOrStr(flags):
2935        hexFlags = int(flags, 16) if isinstance(flags, str) else flags
2936        strFlags = ''
2937        if hexFlags == 0:
2938            strFlags = 'X'
2939        else:
2940            if hexFlags & 0x1 != 0:
2941                strFlags += 'l'
2942            if hexFlags & 0x2 != 0:
2943                strFlags += 'd'
2944            if hexFlags & 0x4 != 0:
2945                strFlags += 'r'
2946            if hexFlags & 0x8 != 0:
2947                strFlags += 'a'
2948
2949        return strFlags
2950
2951    @staticmethod
2952    def mapMetricsHexToChar(metrics):
2953        metricsFlagMap = {
2954            0x40: 'p',
2955            0x09: 'q',
2956            0x0a: 'm',
2957            0x0b: 'r',
2958        }
2959        metricsReservedFlagMap = {0x11: 'q', 0x12: 'm', 0x13: 'r'}
2960        if metricsFlagMap.get(metrics):
2961            return metricsFlagMap.get(metrics), False
2962        elif metricsReservedFlagMap.get(metrics):
2963            return metricsReservedFlagMap.get(metrics), True
2964        else:
2965            logging.warning("Not found flag mapping for given metrics: {}".format(metrics))
2966            return '', False
2967
2968    @staticmethod
2969    def getMetricsFlagsFromHexStr(metrics):
2970        strMetrics = ''
2971        reserved_flag = ''
2972
2973        if metrics.startswith('0x'):
2974            metrics = metrics[2:]
2975        hexMetricsArray = bytearray.fromhex(metrics)
2976
2977        for metric in hexMetricsArray:
2978            metric_flag, has_reserved_flag = OpenThreadTHCI.mapMetricsHexToChar(metric)
2979            strMetrics += metric_flag
2980            if has_reserved_flag:
2981                reserved_flag = ' r'
2982
2983        return strMetrics + reserved_flag
2984
2985    @API
2986    def LinkMetricsSingleReq(self, dst_addr, metrics):
2987        cmd = 'linkmetrics query %s single %s' % (dst_addr, self.getMetricsFlagsFromHexStr(metrics))
2988        return self.__executeCommand(cmd)[-1] == 'Done'
2989
2990    @API
2991    def LinkMetricsMgmtReq(self, dst_addr, type_, flags, metrics, series_id):
2992        cmd = 'linkmetrics mgmt %s ' % dst_addr
2993        if type_ == 'FWD':
2994            cmd += 'forward %d %s' % (series_id, self.getForwardSeriesFlagsFromHexOrStr(flags))
2995            if flags != 0:
2996                cmd += ' %s' % (self.getMetricsFlagsFromHexStr(metrics))
2997        elif type_ == 'ENH':
2998            cmd += 'enhanced-ack'
2999            if flags != 0:
3000                cmd += ' register %s' % (self.getMetricsFlagsFromHexStr(metrics))
3001            else:
3002                cmd += ' clear'
3003        return self.__executeCommand(cmd)[-1] == 'Done'
3004
3005    @API
3006    def LinkMetricsGetReport(self, dst_addr, series_id):
3007        cmd = 'linkmetrics query %s forward %d' % (dst_addr, series_id)
3008        return self.__executeCommand(cmd)[-1] == 'Done'
3009
3010    # TODO: Series Id is not in this API.
3011    @API
3012    def LinkMetricsSendProbe(self, dst_addr, ack=True, size=0):
3013        cmd = 'linkmetrics probe %s %d' % (dst_addr, size)
3014        return self.__executeCommand(cmd)[-1] == 'Done'
3015
3016    @API
3017    def setTxPower(self, level):
3018        cmd = 'txpower '
3019        if level == 'HIGH':
3020            cmd += '127'
3021        elif level == 'MEDIUM':
3022            cmd += '0'
3023        elif level == 'LOW':
3024            cmd += '-128'
3025        else:
3026            print('wrong Tx Power level')
3027        return self.__executeCommand(cmd)[-1] == 'Done'
3028
3029    @API
3030    def sendUdp(self, destination, port, payload='hello'):
3031        assert payload is not None, 'payload should not be none'
3032        cmd = 'udp send %s %d %s' % (destination, port, payload)
3033        return self.__executeCommand(cmd)[-1] == 'Done'
3034
3035    @API
3036    def send_udp(self, interface, destination, port, payload='12ABcd'):
3037        ''' payload hexstring
3038        '''
3039        assert payload is not None, 'payload should not be none'
3040        assert interface == 0, "non-BR must send UDP to Thread interface"
3041        self.__udpOpen()
3042        time.sleep(0.5)
3043        cmd = 'udp send %s %s -x %s' % (destination, port, payload)
3044        return self.__executeCommand(cmd)[-1] == 'Done'
3045
3046    def __udpOpen(self):
3047        if not self.__isUdpOpened:
3048            cmd = 'udp open'
3049            self.__executeCommand(cmd)
3050
3051            # Bind to RLOC address and first dynamic port
3052            rlocAddr = self.getRloc()
3053
3054            cmd = 'udp bind %s 49152' % rlocAddr
3055            self.__executeCommand(cmd)
3056
3057            self.__isUdpOpened = True
3058
3059    @API
3060    def sendMACcmd(self, enh=False):
3061        cmd = 'mac send datarequest'
3062        return self.__executeCommand(cmd)[-1] == 'Done'
3063
3064    @API
3065    def sendMACdata(self, enh=False):
3066        cmd = 'mac send emptydata'
3067        return self.__executeCommand(cmd)[-1] == 'Done'
3068
3069    @API
3070    def setCSLsuspension(self, suspend):
3071        if suspend:
3072            self.__setPollPeriod(240 * 1000)
3073        else:
3074            self.__setPollPeriod(int(0.9 * self.ssedTimeout * 1000))
3075
3076    @API
3077    def set_max_addrs_per_child(self, num):
3078        cmd = 'childip max %d' % int(num)
3079        self.__executeCommand(cmd)
3080
3081    @API
3082    def config_next_dua_status_rsp(self, mliid, status_code):
3083        if status_code >= 400:
3084            # map status_code to correct COAP response code
3085            a, b = divmod(status_code, 100)
3086            status_code = ((a & 0x7) << 5) + (b & 0x1f)
3087
3088        cmd = 'bbr mgmt dua %d' % status_code
3089
3090        if mliid is not None:
3091            mliid = mliid.replace(':', '')
3092            cmd += ' %s' % mliid
3093
3094        self.__executeCommand(cmd)
3095
3096    @API
3097    def getDUA(self):
3098        dua = self.getGUA('fd00:7d03')
3099        return dua
3100
3101    def __addDefaultDomainPrefix(self):
3102        self.configBorderRouter(P_dp=1, P_stable=1, P_on_mesh=1, P_default=1)
3103
3104    def __setDUA(self, sDua):
3105        """specify the DUA before Thread Starts."""
3106        if isinstance(sDua, str):
3107            sDua = sDua.decode('utf8')
3108        iid = ipaddress.IPv6Address(sDua).packed[-8:]
3109        cmd = 'dua iid %s' % ''.join('%02x' % ord(b) for b in iid)
3110        return self.__executeCommand(cmd)[-1] == 'Done'
3111
3112    def __getMlIid(self):
3113        """get the Mesh Local IID."""
3114        # getULA64() would return the full string representation
3115        mleid = ModuleHelper.GetFullIpv6Address(self.getULA64()).lower()
3116        mliid = mleid[-19:].replace(':', '')
3117        return mliid
3118
3119    def __setMlIid(self, sMlIid):
3120        """Set the Mesh Local IID before Thread Starts."""
3121        assert ':' not in sMlIid
3122        cmd = 'mliid %s' % sMlIid
3123        self.__executeCommand(cmd)
3124
3125    @API
3126    def registerDUA(self, sAddr=''):
3127        self.__setDUA(sAddr)
3128
3129    @API
3130    def config_next_mlr_status_rsp(self, status_code):
3131        cmd = 'bbr mgmt mlr response %d' % status_code
3132        return self.__executeCommand(cmd)[-1] == 'Done'
3133
3134    @API
3135    def setMLRtimeout(self, iMsecs):
3136        """Setup BBR MLR Timeout to `iMsecs` seconds."""
3137        self.setBbrDataset(MlrTimeout=iMsecs)
3138
3139    @API
3140    def stopListeningToAddr(self, sAddr):
3141        cmd = 'ipmaddr del ' + sAddr
3142        try:
3143            self.__executeCommand(cmd)
3144        except CommandError as ex:
3145            if ex.code == OT_ERROR_ALREADY:
3146                pass
3147            else:
3148                raise
3149
3150        return True
3151
3152    @API
3153    def registerMulticast(self, listAddr=('ff04::1234:777a:1',), timeout=MLR_TIMEOUT_MIN):
3154        """subscribe to the given ipv6 address (sAddr) in interface and send MLR.req OTA
3155
3156        Args:
3157            sAddr   : str : Multicast address to be subscribed and notified OTA.
3158        """
3159        for each_sAddr in listAddr:
3160            self._beforeRegisterMulticast(each_sAddr, timeout)
3161
3162        sAddr = ' '.join(listAddr)
3163        cmd = 'ipmaddr add ' + str(sAddr)
3164
3165        try:
3166            self.__executeCommand(cmd)
3167        except CommandError as ex:
3168            if ex.code == OT_ERROR_ALREADY:
3169                pass
3170            else:
3171                raise
3172
3173    @API
3174    def getMlrLogs(self):
3175        return self.externalCommissioner.getMlrLogs()
3176
3177    @API
3178    def migrateNetwork(self, channel=None, net_name=None):
3179        """migrate to another Thread Partition 'net_name' (could be None)
3180            on specified 'channel'. Make sure same Mesh Local IID and DUA
3181            after migration for DUA-TC-06/06b (DEV-1923)
3182        """
3183        if channel is None:
3184            raise Exception('channel None')
3185
3186        if channel not in range(11, 27):
3187            raise Exception('channel %d not in [11, 26] Invalid' % channel)
3188
3189        print('new partition %s on channel %d' % (net_name, channel))
3190
3191        mliid = self.__getMlIid()
3192        dua = self.getDUA()
3193        self.reset()
3194        deviceRole = self.deviceRole
3195        self.setDefaultValues()
3196        self.setChannel(channel)
3197        if net_name is not None:
3198            self.setNetworkName(net_name)
3199        self.__setMlIid(mliid)
3200        self.__setDUA(dua)
3201        return self.joinNetwork(deviceRole)
3202
3203    @API
3204    def setParentPrio(self, prio):
3205        cmd = 'parentpriority %u' % prio
3206        return self.__executeCommand(cmd)[-1] == 'Done'
3207
3208    @API
3209    def role_transition(self, role):
3210        cmd = 'mode %s' % OpenThreadTHCI._ROLE_MODE_DICT[role]
3211        return self.__executeCommand(cmd)[-1] == 'Done'
3212
3213    @API
3214    def setLeaderWeight(self, iWeight=72):
3215        self.__executeCommand('leaderweight %d' % iWeight)
3216
3217    @watched
3218    def isBorderRoutingEnabled(self):
3219        try:
3220            self.__executeCommand('br omrprefix local')
3221            return True
3222        except CommandError:
3223            return False
3224
3225    def __detectZephyr(self):
3226        """Detect if the device is running Zephyr and adapt in that case"""
3227
3228        try:
3229            self._lineSepX = re.compile(r'\r\n|\r|\n')
3230            if self.__executeCommand(ZEPHYR_PREFIX + 'thread version')[0].isdigit():
3231                self._cmdPrefix = ZEPHYR_PREFIX
3232        except CommandError:
3233            self._lineSepX = LINESEPX
3234
3235    def __detectReference20200818(self):
3236        """Detect if the device is a Thread reference 20200818 """
3237
3238        # Running `version api` in Thread reference 20200818 is equivalent to running `version`
3239        # It will not output an API number
3240        self.IsReference20200818 = not self.__executeCommand('version api')[0].isdigit()
3241
3242        if self.IsReference20200818:
3243            self.__replaceCommands = {
3244                '-x': 'binary',
3245                'allowlist': 'whitelist',
3246                'denylist': 'blacklist',
3247                'netdata register': 'netdataregister',
3248                'networkkey': 'masterkey',
3249                'partitionid preferred': 'leaderpartitionid',
3250            }
3251        else:
3252
3253            class IdentityDict:
3254
3255                def __getitem__(self, key):
3256                    return key
3257
3258            self.__replaceCommands = IdentityDict()
3259
3260    def __discoverDeviceCapability(self):
3261        """Discover device capability according to version"""
3262        thver = self.__executeCommand('thread version')[0]
3263        if thver in ['1.3', '4'] and not self.IsBorderRouter:
3264            self.log("Setting capability of {}: (DevCapb.C_FTD13 | DevCapb.C_MTD13)".format(self))
3265            self.DeviceCapability = OT13_CAPBS
3266        elif thver in ['1.3', '4'] and self.IsBorderRouter:
3267            self.log("Setting capability of {}: (DevCapb.C_BR13 | DevCapb.C_Host13)".format(self))
3268            self.DeviceCapability = OT13BR_CAPBS
3269        elif thver in ['1.2', '3'] and not self.IsBorderRouter:
3270            self.log("Setting capability of {}: DevCapb.L_AIO | DevCapb.C_FFD | DevCapb.C_RFD".format(self))
3271            self.DeviceCapability = OT12_CAPBS
3272        elif thver in ['1.2', '3'] and self.IsBorderRouter:
3273            self.log("Setting capability of BR {}: DevCapb.C_BBR | DevCapb.C_Host | DevCapb.C_Comm".format(self))
3274            self.DeviceCapability = OT12BR_CAPBS
3275        elif thver in ['1.1', '2']:
3276            self.log("Setting capability of {}: DevCapb.V1_1".format(self))
3277            self.DeviceCapability = OT11_CAPBS
3278        else:
3279            self.log("Capability not specified for {}".format(self))
3280            self.DeviceCapability = DevCapb.NotSpecified
3281            assert False, thver
3282
3283    @staticmethod
3284    def __lstrip0x(s):
3285        """strip 0x at the beginning of a hex string if it exists
3286
3287        Args:
3288            s: hex string
3289
3290        Returns:
3291            hex string with leading 0x stripped
3292        """
3293        if s.startswith('0x'):
3294            s = s[2:]
3295
3296        return s
3297
3298    @API
3299    def setCcmState(self, state=0):
3300        assert state in (0, 1), state
3301        self.__executeCommand("ccm {}".format("enable" if state == 1 else "disable"))
3302
3303    @API
3304    def setVrCheckSkip(self):
3305        self.__executeCommand("tvcheck disable")
3306
3307    @API
3308    def addBlockedNodeId(self, node_id):
3309        cmd = 'nodeidfilter deny %d' % node_id
3310        self.__executeCommand(cmd)
3311
3312    @API
3313    def clearBlockedNodeIds(self):
3314        cmd = 'nodeidfilter clear'
3315        self.__executeCommand(cmd)
3316
3317
3318class OpenThread(OpenThreadTHCI, IThci):
3319
3320    def _connect(self):
3321        print('My port is %s' % self)
3322        self.__lines = []
3323        timeout = 10
3324        port_error = None
3325
3326        if self.port.startswith('COM'):
3327            for _ in range(int(timeout / 0.5)):
3328                time.sleep(0.5)
3329                try:
3330                    self.__handle = serial.Serial(self.port, 115200, timeout=0, write_timeout=1)
3331                    self.sleep(1)
3332                    self.__handle.write('\r\n')
3333                    self.sleep(0.1)
3334                    self._is_net = False
3335                    break
3336                except SerialException as port_error:
3337                    self.log("{} port not ready, retrying to connect...".format(self.port))
3338            else:
3339                raise SerialException("Could not open {} port: {}".format(self.port, port_error))
3340        elif ':' in self.port:
3341            host, port = self.port.split(':')
3342            self.__handle = socket.create_connection((host, port))
3343            self.__handle.setblocking(False)
3344            self._is_net = True
3345        else:
3346            raise Exception('Unknown port schema')
3347
3348    def _disconnect(self):
3349        if self.__handle:
3350            self.__handle.close()
3351            self.__handle = None
3352
3353    def __socRead(self, size=512):
3354        if self._is_net:
3355            return self.__handle.recv(size)
3356        else:
3357            return self.__handle.read(size)
3358
3359    def __socWrite(self, data):
3360        if self._is_net:
3361            self.__handle.sendall(data)
3362        else:
3363            self.__handle.write(data)
3364
3365    def _cliReadLine(self):
3366        if len(self.__lines) > 1:
3367            return self.__lines.pop(0)
3368
3369        tail = ''
3370        if len(self.__lines) != 0:
3371            tail = self.__lines.pop()
3372
3373        try:
3374            tail += self.__socRead()
3375        except socket.error:
3376            logging.exception('%s: No new data', self)
3377            self.sleep(0.1)
3378
3379        self.__lines += self._lineSepX.split(tail)
3380        if len(self.__lines) > 1:
3381            return self.__lines.pop(0)
3382
3383    def _cliWriteLine(self, line):
3384        if self._cmdPrefix == ZEPHYR_PREFIX:
3385            if not line.startswith(self._cmdPrefix):
3386                line = self._cmdPrefix + line
3387            self.__socWrite(line + '\r')
3388        else:
3389            self.__socWrite(line + '\r\n')
3390