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 capabilities 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 derived 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 reference 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        escapable_chars_present = False
836
837        for char in escapable_chars:
838            if char in string:
839                string = string.replace(char, '\\%s' % char)
840                escapable_chars_present = True
841
842        if self._cmdPrefix == ZEPHYR_PREFIX and escapable_chars_present:
843            string = '"' + string + '"'
844        return string
845
846    @API
847    def setNetworkName(self, networkName='GRL'):
848        """set Thread Network name
849
850        Args:
851            networkName: the networkname string to be set
852
853        Returns:
854            True: successful to set the Thread Networkname
855            False: fail to set the Thread Networkname
856        """
857        networkName = self._deviceEscapeEscapable(networkName)
858        cmd = 'networkname %s' % networkName
859        datasetCmd = 'dataset networkname %s' % networkName
860        self.hasActiveDatasetToCommit = True
861        return self.__executeCommand(cmd)[-1] == 'Done' and self.__executeCommand(datasetCmd)[-1] == 'Done'
862
863    @API
864    def setChannel(self, channel=11):
865        """set channel of Thread device operates on.
866
867        Args:
868            channel:
869                    (0  - 10: Reserved)
870                    (11 - 26: 2.4GHz channels)
871                    (27 - 65535: Reserved)
872
873        Returns:
874            True: successful to set the channel
875            False: fail to set the channel
876        """
877        cmd = 'channel %s' % channel
878        datasetCmd = 'dataset channel %s' % channel
879        self.hasSetChannel = True
880        self.hasActiveDatasetToCommit = True
881        return self.__executeCommand(cmd)[-1] == 'Done' and self.__executeCommand(datasetCmd)[-1] == 'Done'
882
883    @API
884    def getChannel(self):
885        """get current channel"""
886        return self.__executeCommand('channel')[0]
887
888    @API
889    def setMAC(self, xEUI):
890        """set the extended address of Thread device
891
892        Args:
893            xEUI: extended address in hex format
894
895        Returns:
896            True: successful to set the extended address
897            False: fail to set the extended address
898        """
899        if not isinstance(xEUI, str):
900            address64 = self.__convertLongToHex(xEUI, 16)
901        else:
902            address64 = xEUI
903
904        cmd = 'extaddr %s' % address64
905        if self.__executeCommand(cmd)[-1] == 'Done':
906            self.mac = address64
907            return True
908        else:
909            return False
910
911    @API
912    def getMAC(self, bType=MacType.RandomMac):
913        """get one specific type of MAC address
914           currently OpenThread only supports Random MAC address
915
916        Args:
917            bType: indicate which kind of MAC address is required
918
919        Returns:
920            specific type of MAC address
921        """
922        # if power down happens, return extended address assigned previously
923        if self.isPowerDown:
924            macAddr64 = self.mac
925        else:
926            if bType == MacType.FactoryMac:
927                macAddr64 = self.__executeCommand('eui64')[0]
928            elif bType == MacType.HashMac:
929                macAddr64 = self.__executeCommand('joiner id')[0]
930            elif bType == MacType.EthMac and self.IsBorderRouter:
931                return self._deviceGetEtherMac()
932            else:
933                macAddr64 = self.__executeCommand('extaddr')[0]
934
935        return int(macAddr64, 16)
936
937    @API
938    def getLL64(self):
939        """get link local unicast IPv6 address"""
940        return self.__executeCommand('ipaddr linklocal')[0]
941
942    @API
943    def getRloc16(self):
944        """get rloc16 short address"""
945        rloc16 = self.__executeCommand('rloc16')[0]
946        return int(rloc16, 16)
947
948    @API
949    def getRloc(self):
950        """get router locator unicast Ipv6 address"""
951        return self.__executeCommand('ipaddr rloc')[0]
952
953    def __getGlobal(self):
954        """get global unicast IPv6 address set
955           if configuring multiple entries
956        """
957        globalAddrs = []
958        rlocAddr = self.getRloc()
959
960        addrs = self.__executeCommand('ipaddr')
961
962        # take rloc address as a reference for current mesh local prefix,
963        # because for some TCs, mesh local prefix may be updated through
964        # pending dataset management.
965        for ip6Addr in addrs:
966            if ip6Addr == 'Done':
967                break
968
969            fullIp = ModuleHelper.GetFullIpv6Address(ip6Addr).lower()
970
971            if fullIp.startswith('fe80') or fullIp.startswith(rlocAddr[0:19]):
972                continue
973
974            globalAddrs.append(fullIp)
975
976        return globalAddrs
977
978    @API
979    def setNetworkKey(self, key):
980        """set Thread network key
981
982        Args:
983            key: Thread network key used in secure the MLE/802.15.4 packet
984
985        Returns:
986            True: successful to set the Thread network key
987            False: fail to set the Thread network key
988        """
989        cmdName = self.__replaceCommands['networkkey']
990
991        if not isinstance(key, str):
992            networkKey = self.__convertLongToHex(key, 32)
993            cmd = '%s %s' % (cmdName, networkKey)
994            datasetCmd = 'dataset %s %s' % (cmdName, networkKey)
995        else:
996            networkKey = key
997            cmd = '%s %s' % (cmdName, networkKey)
998            datasetCmd = 'dataset %s %s' % (cmdName, networkKey)
999
1000        self.networkKey = networkKey
1001        self.hasActiveDatasetToCommit = True
1002        return self.__executeCommand(cmd)[-1] == 'Done' and self.__executeCommand(datasetCmd)[-1] == 'Done'
1003
1004    @API
1005    def addBlockedMAC(self, xEUI):
1006        """add a given extended address to the denylist entry
1007
1008        Args:
1009            xEUI: extended address in hex format
1010
1011        Returns:
1012            True: successful to add a given extended address to the denylist entry
1013            False: fail to add a given extended address to the denylist entry
1014        """
1015        if isinstance(xEUI, str):
1016            macAddr = xEUI
1017        else:
1018            macAddr = self.__convertLongToHex(xEUI)
1019
1020        # if blocked device is itself
1021        if macAddr == self.mac:
1022            print('block device itself')
1023            return True
1024
1025        if self._addressfilterMode != 'denylist':
1026            if self.__setAddressfilterMode(self.__replaceCommands['denylist']):
1027                self._addressfilterMode = 'denylist'
1028
1029        cmd = 'macfilter addr add %s' % macAddr
1030        ret = self.__executeCommand(cmd)[-1] == 'Done'
1031
1032        self._addressfilterSet.add(macAddr)
1033        print('current denylist entries:')
1034        for addr in self._addressfilterSet:
1035            print(addr)
1036
1037        return ret
1038
1039    @API
1040    def addAllowMAC(self, xEUI):
1041        """add a given extended address to the allowlist addressfilter
1042
1043        Args:
1044            xEUI: a given extended address in hex format
1045
1046        Returns:
1047            True: successful to add a given extended address to the allowlist entry
1048            False: fail to add a given extended address to the allowlist entry
1049        """
1050        if isinstance(xEUI, str):
1051            macAddr = xEUI
1052        else:
1053            macAddr = self.__convertLongToHex(xEUI)
1054
1055        if self._addressfilterMode != 'allowlist':
1056            if self.__setAddressfilterMode(self.__replaceCommands['allowlist']):
1057                self._addressfilterMode = 'allowlist'
1058
1059        cmd = 'macfilter addr add %s' % macAddr
1060        ret = self.__executeCommand(cmd)[-1] == 'Done'
1061
1062        self._addressfilterSet.add(macAddr)
1063        print('current allowlist entries:')
1064        for addr in self._addressfilterSet:
1065            print(addr)
1066        return ret
1067
1068    @API
1069    def clearBlockList(self):
1070        """clear all entries in denylist table
1071
1072        Returns:
1073            True: successful to clear the denylist
1074            False: fail to clear the denylist
1075        """
1076        # remove all entries in denylist
1077        print('clearing denylist entries:')
1078        for addr in self._addressfilterSet:
1079            print(addr)
1080
1081        # disable denylist
1082        if self.__setAddressfilterMode('disable'):
1083            self._addressfilterMode = 'disable'
1084            # clear ops
1085            cmd = 'macfilter addr clear'
1086            if self.__executeCommand(cmd)[-1] == 'Done':
1087                self._addressfilterSet.clear()
1088                return True
1089        return False
1090
1091    @API
1092    def clearAllowList(self):
1093        """clear all entries in allowlist table
1094
1095        Returns:
1096            True: successful to clear the allowlist
1097            False: fail to clear the allowlist
1098        """
1099        # remove all entries in allowlist
1100        print('clearing allowlist entries:')
1101        for addr in self._addressfilterSet:
1102            print(addr)
1103
1104        # disable allowlist
1105        if self.__setAddressfilterMode('disable'):
1106            self._addressfilterMode = 'disable'
1107            # clear ops
1108            cmd = 'macfilter addr clear'
1109            if self.__executeCommand(cmd)[-1] == 'Done':
1110                self._addressfilterSet.clear()
1111                return True
1112        return False
1113
1114    @API
1115    def getDeviceRole(self):
1116        """get current device role in Thread Network"""
1117        return self.__executeCommand('state')[0]
1118
1119    @API
1120    def joinNetwork(self, eRoleId):
1121        """make device ready to join the Thread Network with a given role
1122
1123        Args:
1124            eRoleId: a given device role id
1125
1126        Returns:
1127            True: ready to set Thread Network parameter for joining desired Network
1128        """
1129        self.deviceRole = eRoleId
1130        mode = '-'
1131        if ModuleHelper.LeaderDutChannelFound and not self.hasSetChannel:
1132            self.channel = ModuleHelper.Default_Channel
1133
1134        # FIXME: when Harness call setNetworkDataRequirement()?
1135        # only sleep end device requires stable networkdata now
1136        if eRoleId == Thread_Device_Role.Leader:
1137            print('join as leader')
1138            mode = 'rdn'
1139            if self.AutoDUTEnable is False:
1140                # set ROUTER_DOWNGRADE_THRESHOLD
1141                self.__setRouterDowngradeThreshold(33)
1142        elif eRoleId == Thread_Device_Role.Router:
1143            print('join as router')
1144            mode = 'rdn'
1145            if self.AutoDUTEnable is False:
1146                # set ROUTER_DOWNGRADE_THRESHOLD
1147                self.__setRouterDowngradeThreshold(33)
1148        elif eRoleId in (Thread_Device_Role.BR_1, Thread_Device_Role.BR_2):
1149            print('join as BBR')
1150            mode = 'rdn'
1151            if self.AutoDUTEnable is False:
1152                # set ROUTER_DOWNGRADE_THRESHOLD
1153                self.__setRouterDowngradeThreshold(33)
1154        elif eRoleId == Thread_Device_Role.SED:
1155            print('join as sleepy end device')
1156            mode = '-'
1157            self.__setPollPeriod(self.__sedPollPeriod)
1158        elif eRoleId == Thread_Device_Role.SSED:
1159            print('join as SSED')
1160            mode = '-'
1161            self.setCSLperiod(self.cslPeriod)
1162            self.setCSLtout(self.ssedTimeout)
1163            self.setCSLsuspension(False)
1164        elif eRoleId == Thread_Device_Role.EndDevice:
1165            print('join as end device')
1166            mode = 'rn'
1167        elif eRoleId == Thread_Device_Role.REED:
1168            print('join as REED')
1169            mode = 'rdn'
1170            if self.AutoDUTEnable is False:
1171                # set ROUTER_UPGRADE_THRESHOLD
1172                self.__setRouterUpgradeThreshold(0)
1173        elif eRoleId == Thread_Device_Role.EndDevice_FED:
1174            print('join as FED')
1175            mode = 'rdn'
1176            # always remain an ED, never request to be a router
1177            self.__disableRouterEligible()
1178        elif eRoleId == Thread_Device_Role.EndDevice_MED:
1179            print('join as MED')
1180            mode = 'rn'
1181        else:
1182            pass
1183
1184        if self.IsReference20200818:
1185            mode = 's' if mode == '-' else mode + 's'
1186
1187        # set Thread device mode with a given role
1188        self.__setDeviceMode(mode)
1189
1190        # start OpenThread
1191        self.__startOpenThread()
1192        self.wait_for_attach_to_the_network(expected_role=eRoleId,
1193                                            timeout=self.NETWORK_ATTACHMENT_TIMEOUT,
1194                                            raise_assert=True)
1195        return True
1196
1197    def wait_for_attach_to_the_network(self, expected_role, timeout, raise_assert=False):
1198        start_time = time.time()
1199
1200        while time.time() < start_time + timeout:
1201            time.sleep(0.3)
1202            if self.__isDeviceAttached():
1203                break
1204        else:
1205            if raise_assert:
1206                raise AssertionError("OT device {} could not attach to the network after {} s of timeout.".format(
1207                    self, timeout))
1208            else:
1209                return False
1210
1211        if self._update_router_status:
1212            self.__updateRouterStatus()
1213
1214        if expected_role == Thread_Device_Role.Router:
1215            while time.time() < start_time + timeout:
1216                time.sleep(0.3)
1217                if self.getDeviceRole() == "router":
1218                    break
1219            else:
1220                if raise_assert:
1221                    raise AssertionError("OT Router {} could not attach to the network after {} s of timeout.".format(
1222                        self, timeout * 2))
1223                else:
1224                    return False
1225
1226        if self.IsBorderRouter:
1227            self._waitBorderRoutingStabilize()
1228
1229        return True
1230
1231    @API
1232    def getNetworkFragmentID(self):
1233        """get current partition id of Thread Network Partition from LeaderData
1234
1235        Returns:
1236            The Thread network Partition Id
1237        """
1238        if not self.__isOpenThreadRunning():
1239            return None
1240
1241        leaderData = self.__executeCommand('leaderdata')
1242        return int(leaderData[0].split()[2], 16)
1243
1244    @API
1245    def getParentAddress(self):
1246        """get Thread device's parent extended address and rloc16 short address
1247
1248        Returns:
1249            The extended address of parent in hex format
1250        """
1251        eui = None
1252        parentInfo = self.__executeCommand('parent')
1253
1254        for line in parentInfo:
1255            if 'Done' in line:
1256                break
1257            elif 'Ext Addr' in line:
1258                eui = line.split()[2]
1259            else:
1260                pass
1261
1262        return int(eui, 16)
1263
1264    @API
1265    def powerDown(self):
1266        """power down the Thread device"""
1267        self._reset()
1268
1269    @API
1270    def powerUp(self):
1271        """power up the Thread device"""
1272        self.isPowerDown = False
1273
1274        if not self.__isOpenThreadRunning():
1275            if self.deviceRole == Thread_Device_Role.SED:
1276                self.__setPollPeriod(self.__sedPollPeriod)
1277            self.__startOpenThread()
1278
1279    @watched
1280    def _reset(self, timeout=3):
1281        print("Waiting after reset timeout: {} s".format(timeout))
1282        start_time = time.time()
1283        self.__sendCommand('reset', expectEcho=False)
1284        self.isPowerDown = True
1285
1286        while time.time() < start_time + timeout:
1287            time.sleep(0.3)
1288            if not self.IsBorderRouter:
1289                self._disconnect()
1290                self._connect()
1291            try:
1292                self.__executeCommand('state', timeout=0.1)
1293                break
1294            except Exception:
1295                continue
1296        else:
1297            raise AssertionError("Could not connect with OT device {} after reset.".format(self))
1298
1299    def reset_and_wait_for_connection(self, timeout=3):
1300        self._reset(timeout=timeout)
1301        if self.deviceRole == Thread_Device_Role.SED:
1302            self.__setPollPeriod(self.__sedPollPeriod)
1303
1304    @API
1305    def reboot(self):
1306        """reset and rejoin to Thread Network without any timeout
1307
1308        Returns:
1309            True: successful to reset and rejoin the Thread Network
1310            False: fail to reset and rejoin the Thread Network
1311        """
1312        self.reset_and_wait_for_connection()
1313        self.__startOpenThread()
1314        return self.wait_for_attach_to_the_network(expected_role="", timeout=self.NETWORK_ATTACHMENT_TIMEOUT)
1315
1316    @API
1317    def resetAndRejoin(self, timeout):
1318        """reset and join back Thread Network with a given timeout delay
1319
1320        Args:
1321            timeout: a timeout interval before rejoin Thread Network
1322
1323        Returns:
1324            True: successful to reset and rejoin Thread Network
1325            False: fail to reset and rejoin the Thread Network
1326        """
1327        self.powerDown()
1328        time.sleep(timeout)
1329        self.powerUp()
1330        return self.wait_for_attach_to_the_network(expected_role="", timeout=self.NETWORK_ATTACHMENT_TIMEOUT)
1331
1332    @API
1333    def ping(self, strDestination, ilength=0, hop_limit=64, timeout=5):
1334        """ send ICMPv6 echo request with a given length/hoplimit to a unicast
1335            destination address
1336        Args:
1337            srcDestination: the unicast destination address of ICMPv6 echo request
1338            ilength: the size of ICMPv6 echo request payload
1339            hop_limit: hop limit
1340
1341        """
1342        cmd = 'ping %s %s' % (strDestination, str(ilength))
1343        if not self.IsReference20200818:
1344            cmd += ' 1 1 %d %d' % (hop_limit, timeout)
1345
1346        self.__executeCommand(cmd)
1347        if self.IsReference20200818:
1348            # wait echo reply
1349            self.sleep(6)  # increase delay temporarily (+5s) to remedy TH's delay updates
1350
1351    @API
1352    def multicast_Ping(self, destination, length=20):
1353        """send ICMPv6 echo request with a given length to a multicast destination
1354           address
1355
1356        Args:
1357            destination: the multicast destination address of ICMPv6 echo request
1358            length: the size of ICMPv6 echo request payload
1359        """
1360        cmd = 'ping %s %s' % (destination, str(length))
1361        self.__sendCommand(cmd)
1362        # wait echo reply
1363        self.sleep(1)
1364
1365    @API
1366    def setPANID(self, xPAN):
1367        """set Thread Network PAN ID
1368
1369        Args:
1370            xPAN: a given PAN ID in hex format
1371
1372        Returns:
1373            True: successful to set the Thread Network PAN ID
1374            False: fail to set the Thread Network PAN ID
1375        """
1376        panid = ''
1377        if not isinstance(xPAN, str):
1378            panid = str(hex(xPAN))
1379
1380        cmd = 'panid %s' % panid
1381        datasetCmd = 'dataset panid %s' % panid
1382        self.hasActiveDatasetToCommit = True
1383        return self.__executeCommand(cmd)[-1] == 'Done' and self.__executeCommand(datasetCmd)[-1] == 'Done'
1384
1385    @API
1386    def reset(self):
1387        """factory reset"""
1388        self._deviceBeforeReset()
1389
1390        self.__sendCommand('factoryreset', expectEcho=False)
1391        timeout = 10
1392
1393        start_time = time.time()
1394        while time.time() < start_time + timeout:
1395            time.sleep(0.5)
1396            if not self.IsBorderRouter:
1397                self._disconnect()
1398                self._connect()
1399            try:
1400                self.__executeCommand('state', timeout=0.1)
1401                break
1402            except Exception:
1403                self._restartAgentService()
1404                time.sleep(2)
1405                self.__sendCommand('factoryreset', expectEcho=False)
1406                time.sleep(0.5)
1407                continue
1408        else:
1409            raise AssertionError("Could not connect with OT device {} after reset.".format(self))
1410
1411        self.log('factoryreset finished within 10s timeout.')
1412        self._deviceAfterReset()
1413
1414        if self.IsBorderRouter:
1415            self.__executeCommand('log level 5')
1416
1417    @API
1418    def removeRouter(self, xRouterId):
1419        """kickoff router with a given router id from the Thread Network
1420
1421        Args:
1422            xRouterId: a given router id in hex format
1423
1424        Returns:
1425            True: successful to remove the router from the Thread Network
1426            False: fail to remove the router from the Thread Network
1427        """
1428        routerId = self.__convertRlocToRouterId(xRouterId)
1429
1430        if routerId is None:
1431            return False
1432
1433        cmd = 'releaserouterid %s' % routerId
1434        return self.__executeCommand(cmd)[-1] == 'Done'
1435
1436    @API
1437    def setDefaultValues(self):
1438        """set default mandatory Thread Network parameter value"""
1439        # initialize variables
1440        self.networkName = ModuleHelper.Default_NwkName
1441        self.networkKey = ModuleHelper.Default_NwkKey
1442        self.channel = ModuleHelper.Default_Channel
1443        self.channelMask = '0x7fff800'  # (0xffff << 11)
1444        self.panId = ModuleHelper.Default_PanId
1445        self.xpanId = ModuleHelper.Default_XpanId
1446        self.meshLocalPrefix = ModuleHelper.Default_MLPrefix
1447        stretchedPSKc = Thread_PBKDF2.get(ModuleHelper.Default_PSKc, ModuleHelper.Default_XpanId,
1448                                          ModuleHelper.Default_NwkName)
1449        self.pskc = hex(stretchedPSKc).rstrip('L').lstrip('0x')
1450        self.securityPolicySecs = ModuleHelper.Default_SecurityPolicy
1451        self.securityPolicyFlags = 'onrc'
1452        self.activetimestamp = ModuleHelper.Default_ActiveTimestamp
1453        # self.sedPollingRate = ModuleHelper.Default_Harness_SED_Polling_Rate
1454        self.__sedPollPeriod = 3 * 1000  # in milliseconds
1455        self.ssedTimeout = 30  # in seconds
1456        self.cslPeriod = 500  # in milliseconds
1457        self.deviceRole = None
1458        self.provisioningUrl = ''
1459        self.hasActiveDatasetToCommit = False
1460        self.logThread = Queue()
1461        self.logThreadStatus = self.logStatus['stop']
1462        self.joinCommissionedStatus = self.joinStatus['notstart']
1463        # indicate Thread device requests full or stable network data
1464        self.networkDataRequirement = ''
1465        # indicate if Thread device experiences a power down event
1466        self.isPowerDown = False
1467        # indicate AddressFilter mode ['disable', 'allowlist', 'denylist']
1468        self._addressfilterMode = 'disable'
1469        self._addressfilterSet = set()  # cache filter entries
1470        # indicate if Thread device is an active commissioner
1471        self.isActiveCommissioner = False
1472        # indicate that the channel has been set, in case the channel was set
1473        # to default when joining network
1474        self.hasSetChannel = False
1475        self.IsBeingTestedAsCommercialBBR = False
1476        # indicate whether the default domain prefix is used.
1477        self.__useDefaultDomainPrefix = True
1478        self.__isUdpOpened = False
1479        self.IsHost = False
1480
1481        # remove stale multicast addresses
1482        if self.IsBorderRouter:
1483            self.stopListeningToAddrAll()
1484
1485        # BBR dataset
1486        self.bbrSeqNum = random.randint(0, 126)  # 5.21.4.2
1487        self.bbrMlrTimeout = 3600
1488        self.bbrReRegDelay = 5
1489
1490        # initialize device configuration
1491        self.setMAC(self.mac)
1492        self.__setChannelMask(self.channelMask)
1493        self.__setSecurityPolicy(self.securityPolicySecs, self.securityPolicyFlags)
1494        self.setChannel(self.channel)
1495        self.setPANID(self.panId)
1496        self.setXpanId(self.xpanId)
1497        self.setNetworkName(self.networkName)
1498        self.setNetworkKey(self.networkKey)
1499        self.setMLPrefix(self.meshLocalPrefix)
1500        self.setPSKc(self.pskc)
1501        self.setActiveTimestamp(self.activetimestamp)
1502
1503    @API
1504    def getDeviceConncetionStatus(self):
1505        """check if serial port connection is ready or not"""
1506        return self.deviceConnected
1507
1508    @API
1509    def setPollingRate(self, iPollingRate):
1510        """set data polling rate for sleepy end device
1511
1512        Args:
1513            iPollingRate: data poll period of sleepy end device (in seconds)
1514
1515        Returns:
1516            True: successful to set the data polling rate for sleepy end device
1517            False: fail to set the data polling rate for sleepy end device
1518        """
1519        iPollingRate = int(iPollingRate * 1000)
1520
1521        if self.__sedPollPeriod != iPollingRate:
1522            if not iPollingRate:
1523                iPollingRate = 0xFFFF  # T5.2.1, disable polling
1524            elif iPollingRate < 1:
1525                iPollingRate = 1  # T9.2.13
1526            self.__sedPollPeriod = iPollingRate
1527
1528            # apply immediately
1529            if self.__isOpenThreadRunning():
1530                return self.__setPollPeriod(self.__sedPollPeriod)
1531
1532        return True
1533
1534    def __setPollPeriod(self, iPollPeriod):
1535        """set data poll period for sleepy end device
1536
1537        Args:
1538            iPollPeriod: data poll period of sleepy end device (in milliseconds)
1539
1540        Returns:
1541            True: successful to set the data poll period for sleepy end device
1542            False: fail to set the data poll period for sleepy end device
1543        """
1544        cmd = 'pollperiod %d' % iPollPeriod
1545        return self.__executeCommand(cmd)[-1] == 'Done'
1546
1547    @API
1548    def setLinkQuality(self, EUIadr, LinkQuality):
1549        """set custom LinkQualityIn for all receiving messages from the specified EUIadr
1550
1551        Args:
1552            EUIadr: a given extended address
1553            LinkQuality: a given custom link quality
1554                         link quality/link margin mapping table
1555                         3: 21 - 255 (dB)
1556                         2: 11 - 20 (dB)
1557                         1: 3 - 9 (dB)
1558                         0: 0 - 2 (dB)
1559
1560        Returns:
1561            True: successful to set the link quality
1562            False: fail to set the link quality
1563        """
1564        # process EUIadr
1565        euiHex = hex(EUIadr)
1566        euiStr = str(euiHex)
1567        euiStr = euiStr.rstrip('L')
1568        address64 = ''
1569        if '0x' in euiStr:
1570            address64 = self.__lstrip0x(euiStr)
1571            # prepend 0 at the beginning
1572            if len(address64) < 16:
1573                address64 = address64.zfill(16)
1574                print(address64)
1575
1576        cmd = 'macfilter rss add-lqi %s %s' % (address64, str(LinkQuality))
1577        return self.__executeCommand(cmd)[-1] == 'Done'
1578
1579    @API
1580    def setOutBoundLinkQuality(self, LinkQuality):
1581        """set custom LinkQualityIn for all receiving messages from the any address
1582
1583        Args:
1584            LinkQuality: a given custom link quality
1585                         link quality/link margin mapping table
1586                         3: 21 - 255 (dB)
1587                         2: 11 - 20 (dB)
1588                         1: 3 - 9 (dB)
1589                         0: 0 - 2 (dB)
1590
1591        Returns:
1592            True: successful to set the link quality
1593            False: fail to set the link quality
1594        """
1595        cmd = 'macfilter rss add-lqi * %s' % str(LinkQuality)
1596        return self.__executeCommand(cmd)[-1] == 'Done'
1597
1598    @API
1599    def removeRouterPrefix(self, prefixEntry):
1600        """remove the configured prefix on a border router
1601
1602        Args:
1603            prefixEntry: a on-mesh prefix entry in IPv6 dotted-quad format
1604
1605        Returns:
1606            True: successful to remove the prefix entry from border router
1607            False: fail to remove the prefix entry from border router
1608        """
1609        assert (ipaddress.IPv6Network(prefixEntry.decode()))
1610        cmd = 'prefix remove %s/64' % prefixEntry
1611        if self.__executeCommand(cmd)[-1] == 'Done':
1612            # send server data ntf to leader
1613            cmd = self.__replaceCommands['netdata register']
1614            return self.__executeCommand(cmd)[-1] == 'Done'
1615        else:
1616            return False
1617
1618    @API
1619    def configBorderRouter(
1620        self,
1621        P_Prefix="fd00:7d03:7d03:7d03::",
1622        P_stable=1,
1623        P_default=1,
1624        P_slaac_preferred=0,
1625        P_Dhcp=0,
1626        P_preference=0,
1627        P_on_mesh=1,
1628        P_nd_dns=0,
1629        P_dp=0,
1630    ):
1631        """configure the border router with a given prefix entry parameters
1632
1633        Args:
1634            P_Prefix: IPv6 prefix that is available on the Thread Network in IPv6 dotted-quad format
1635            P_stable: true if the default router is expected to be stable network data
1636            P_default: true if border router offers the default route for P_Prefix
1637            P_slaac_preferred: true if allowing auto-configure address using P_Prefix
1638            P_Dhcp: is true if border router is a DHCPv6 Agent
1639            P_preference: is two-bit signed integer indicating router preference
1640            P_on_mesh: is true if P_Prefix is considered to be on-mesh
1641            P_nd_dns: is true if border router is able to supply DNS information obtained via ND
1642
1643        Returns:
1644            True: successful to configure the border router with a given prefix entry
1645            False: fail to configure the border router with a given prefix entry
1646        """
1647        assert (ipaddress.IPv6Network(P_Prefix.decode()))
1648
1649        # turn off default domain prefix if configBorderRouter is called before joining network
1650        if P_dp == 0 and not self.__isOpenThreadRunning():
1651            self.__useDefaultDomainPrefix = False
1652
1653        parameter = ''
1654        prf = ''
1655
1656        if P_dp:
1657            P_slaac_preferred = 1
1658
1659        if P_slaac_preferred == 1:
1660            parameter += 'p'
1661            parameter += 'a'
1662
1663        if P_stable == 1:
1664            parameter += 's'
1665
1666        if P_default == 1:
1667            parameter += 'r'
1668
1669        if P_Dhcp == 1:
1670            parameter += 'd'
1671
1672        if P_on_mesh == 1:
1673            parameter += 'o'
1674
1675        if P_dp == 1:
1676            assert P_slaac_preferred and P_default and P_on_mesh and P_stable
1677            parameter += 'D'
1678
1679        if P_preference == 1:
1680            prf = 'high'
1681        elif P_preference == 0:
1682            prf = 'med'
1683        elif P_preference == -1:
1684            prf = 'low'
1685        else:
1686            pass
1687
1688        cmd = 'prefix add %s/64 %s %s' % (P_Prefix, parameter, prf)
1689        if self.__executeCommand(cmd)[-1] == 'Done':
1690            # if prefix configured before starting OpenThread stack
1691            # do not send out server data ntf pro-actively
1692            if not self.__isOpenThreadRunning():
1693                return True
1694            else:
1695                # send server data ntf to leader
1696                cmd = self.__replaceCommands['netdata register']
1697                return self.__executeCommand(cmd)[-1] == 'Done'
1698        else:
1699            return False
1700
1701    @watched
1702    def getNetworkData(self):
1703        lines = self.__executeCommand('netdata show')
1704        prefixes, routes, services, contexts, commissioning = [], [], [], []
1705        classify = None
1706
1707        for line in lines:
1708            if line == 'Prefixes:':
1709                classify = prefixes
1710            elif line == 'Routes:':
1711                classify = routes
1712            elif line == 'Services:':
1713                classify = services
1714            elif line == 'Contexts:':
1715                classify = contexts
1716            elif line == 'Commissioning':
1717                classify = commissioning
1718            elif line == 'Done':
1719                classify = None
1720            else:
1721                classify.append(line)
1722
1723        return {
1724            'Prefixes': prefixes,
1725            'Routes': routes,
1726            'Services': services,
1727            'Contexts': contexts,
1728            'Commissioning': commissioning,
1729        }
1730
1731    @API
1732    def setNetworkIDTimeout(self, iNwkIDTimeOut):
1733        """set networkid timeout for Thread device
1734
1735        Args:
1736            iNwkIDTimeOut: a given NETWORK_ID_TIMEOUT
1737
1738        Returns:
1739            True: successful to set NETWORK_ID_TIMEOUT
1740            False: fail to set NETWORK_ID_TIMEOUT
1741        """
1742        iNwkIDTimeOut /= 1000
1743        cmd = 'networkidtimeout %s' % str(iNwkIDTimeOut)
1744        return self.__executeCommand(cmd)[-1] == 'Done'
1745
1746    @API
1747    def setKeepAliveTimeOut(self, iTimeOut):
1748        """set child timeout for device
1749
1750        Args:
1751            iTimeOut: child timeout for device
1752
1753        Returns:
1754            True: successful to set the child timeout for device
1755            False: fail to set the child timeout for device
1756        """
1757        cmd = 'childtimeout %d' % iTimeOut
1758        return self.__executeCommand(cmd)[-1] == 'Done'
1759
1760    @API
1761    def setKeySequenceCounter(self, iKeySequenceValue):
1762        """ set the Key sequence counter corresponding to Thread network key
1763
1764        Args:
1765            iKeySequenceValue: key sequence value
1766
1767        Returns:
1768            True: successful to set the key sequence
1769            False: fail to set the key sequence
1770        """
1771        # avoid key switch guard timer protection for reference device
1772        self.__setKeySwitchGuardTime(0)
1773
1774        cmd = 'keysequence counter %s' % str(iKeySequenceValue)
1775        if self.__executeCommand(cmd)[-1] == 'Done':
1776            self.sleep(1)
1777            return True
1778        else:
1779            return False
1780
1781    @API
1782    def getKeySequenceCounter(self):
1783        """get current Thread Network key sequence"""
1784        keySequence = self.__executeCommand('keysequence counter')[0]
1785        return keySequence
1786
1787    @API
1788    def incrementKeySequenceCounter(self, iIncrementValue=1):
1789        """increment the key sequence with a given value
1790
1791        Args:
1792            iIncrementValue: specific increment value to be added
1793
1794        Returns:
1795            True: successful to increment the key sequence with a given value
1796            False: fail to increment the key sequence with a given value
1797        """
1798        # avoid key switch guard timer protection for reference device
1799        self.__setKeySwitchGuardTime(0)
1800        currentKeySeq = self.getKeySequenceCounter()
1801        keySequence = int(currentKeySeq, 10) + iIncrementValue
1802        return self.setKeySequenceCounter(keySequence)
1803
1804    @API
1805    def setNetworkDataRequirement(self, eDataRequirement):
1806        """set whether the Thread device requires the full network data
1807           or only requires the stable network data
1808
1809        Args:
1810            eDataRequirement: is true if requiring the full network data
1811
1812        Returns:
1813            True: successful to set the network requirement
1814        """
1815        if eDataRequirement == Device_Data_Requirement.ALL_DATA:
1816            self.networkDataRequirement = 'n'
1817        return True
1818
1819    @API
1820    def configExternalRouter(self, P_Prefix, P_stable, R_Preference=0):
1821        """configure border router with a given external route prefix entry
1822
1823        Args:
1824            P_Prefix: IPv6 prefix for the route in IPv6 dotted-quad format
1825            P_Stable: is true if the external route prefix is stable network data
1826            R_Preference: a two-bit signed integer indicating Router preference
1827                          1: high
1828                          0: medium
1829                         -1: low
1830
1831        Returns:
1832            True: successful to configure the border router with a given external route prefix
1833            False: fail to configure the border router with a given external route prefix
1834        """
1835        assert (ipaddress.IPv6Network(P_Prefix.decode()))
1836        prf = ''
1837        stable = ''
1838        if R_Preference == 1:
1839            prf = 'high'
1840        elif R_Preference == 0:
1841            prf = 'med'
1842        elif R_Preference == -1:
1843            prf = 'low'
1844        else:
1845            pass
1846
1847        if P_stable:
1848            stable += 's'
1849            cmd = 'route add %s/64 %s %s' % (P_Prefix, stable, prf)
1850        else:
1851            cmd = 'route add %s/64 %s' % (P_Prefix, prf)
1852
1853        if self.__executeCommand(cmd)[-1] == 'Done':
1854            # send server data ntf to leader
1855            cmd = self.__replaceCommands['netdata register']
1856            return self.__executeCommand(cmd)[-1] == 'Done'
1857
1858    @API
1859    def getNeighbouringRouters(self):
1860        """get neighboring routers information
1861
1862        Returns:
1863            neighboring routers' extended address
1864        """
1865        routerInfo = []
1866        routerList = self.__executeCommand('router list')[0].split()
1867
1868        if 'Done' in routerList:
1869            return None
1870
1871        for index in routerList:
1872            router = []
1873            cmd = 'router %s' % index
1874            router = self.__executeCommand(cmd)
1875
1876            for line in router:
1877                if 'Done' in line:
1878                    break
1879                # elif 'Rloc' in line:
1880                #    rloc16 = line.split()[1]
1881                elif 'Ext Addr' in line:
1882                    eui = line.split()[2]
1883                    routerInfo.append(int(eui, 16))
1884                # elif 'LQI In' in line:
1885                #    lqi_in = line.split()[1]
1886                # elif 'LQI Out' in line:
1887                #    lqi_out = line.split()[1]
1888                else:
1889                    pass
1890
1891        return routerInfo
1892
1893    @API
1894    def getChildrenInfo(self):
1895        """get all children information
1896
1897        Returns:
1898            children's extended address
1899        """
1900        eui = None
1901        rloc16 = None
1902        childrenInfoAll = []
1903        childrenInfo = {'EUI': 0, 'Rloc16': 0, 'MLEID': ''}
1904        childrenList = self.__executeCommand('child list')[0].split()
1905
1906        if 'Done' in childrenList:
1907            return None
1908
1909        for index in childrenList:
1910            cmd = 'child %s' % index
1911            child = []
1912            child = self.__executeCommand(cmd)
1913
1914            for line in child:
1915                if 'Done' in line:
1916                    break
1917                elif 'Rloc' in line:
1918                    rloc16 = line.split()[1]
1919                elif 'Ext Addr' in line:
1920                    eui = line.split()[2]
1921                # elif 'Child ID' in line:
1922                #    child_id = line.split()[2]
1923                # elif 'Mode' in line:
1924                #    mode = line.split()[1]
1925                else:
1926                    pass
1927
1928            childrenInfo['EUI'] = int(eui, 16)
1929            childrenInfo['Rloc16'] = int(rloc16, 16)
1930            # children_info['MLEID'] = self.getMLEID()
1931
1932            childrenInfoAll.append(childrenInfo['EUI'])
1933            # childrenInfoAll.append(childrenInfo)
1934
1935        return childrenInfoAll
1936
1937    @API
1938    def setXpanId(self, xPanId):
1939        """set extended PAN ID of Thread Network
1940
1941        Args:
1942            xPanId: extended PAN ID in hex format
1943
1944        Returns:
1945            True: successful to set the extended PAN ID
1946            False: fail to set the extended PAN ID
1947        """
1948        xpanid = ''
1949        if not isinstance(xPanId, str):
1950            xpanid = self.__convertLongToHex(xPanId, 16)
1951            cmd = 'extpanid %s' % xpanid
1952            datasetCmd = 'dataset extpanid %s' % xpanid
1953        else:
1954            xpanid = xPanId
1955            cmd = 'extpanid %s' % xpanid
1956            datasetCmd = 'dataset extpanid %s' % xpanid
1957
1958        self.xpanId = xpanid
1959        self.hasActiveDatasetToCommit = True
1960        return self.__executeCommand(cmd)[-1] == 'Done' and self.__executeCommand(datasetCmd)[-1] == 'Done'
1961
1962    @API
1963    def getNeighbouringDevices(self):
1964        """gets the neighboring devices' extended address to compute the DUT
1965           extended address automatically
1966
1967        Returns:
1968            A list including extended address of neighboring routers, parent
1969            as well as children
1970        """
1971        neighbourList = []
1972
1973        # get parent info
1974        parentAddr = self.getParentAddress()
1975        if parentAddr != 0:
1976            neighbourList.append(parentAddr)
1977
1978        # get ED/SED children info
1979        childNeighbours = self.getChildrenInfo()
1980        if childNeighbours is not None and len(childNeighbours) > 0:
1981            for entry in childNeighbours:
1982                neighbourList.append(entry)
1983
1984        # get neighboring routers info
1985        routerNeighbours = self.getNeighbouringRouters()
1986        if routerNeighbours is not None and len(routerNeighbours) > 0:
1987            for entry in routerNeighbours:
1988                neighbourList.append(entry)
1989
1990        return neighbourList
1991
1992    @API
1993    def setPartationId(self, partationId):
1994        """set Thread Network Partition ID
1995
1996        Args:
1997            partitionId: partition id to be set by leader
1998
1999        Returns:
2000            True: successful to set the Partition ID
2001            False: fail to set the Partition ID
2002        """
2003        cmd = self.__replaceCommands['partitionid preferred'] + ' '
2004        cmd += str(hex(partationId)).rstrip('L')
2005        return self.__executeCommand(cmd)[-1] == 'Done'
2006
2007    @API
2008    def getGUA(self, filterByPrefix=None, eth=False):
2009        """get expected global unicast IPv6 address of Thread device
2010
2011        note: existing filterByPrefix are string of in lowercase. e.g.
2012        '2001' or '2001:0db8:0001:0000".
2013
2014        Args:
2015            filterByPrefix: a given expected global IPv6 prefix to be matched
2016
2017        Returns:
2018            a global IPv6 address
2019        """
2020        assert not eth
2021        # get global addrs set if multiple
2022        globalAddrs = self.__getGlobal()
2023
2024        if filterByPrefix is None:
2025            return globalAddrs[0]
2026        else:
2027            for fullIp in globalAddrs:
2028                if fullIp.startswith(filterByPrefix):
2029                    return fullIp
2030            return str(globalAddrs[0])
2031
2032    @API
2033    def getShortAddress(self):
2034        """get Rloc16 short address of Thread device"""
2035        return self.getRloc16()
2036
2037    @API
2038    def getULA64(self):
2039        """get mesh local EID of Thread device"""
2040        return self.__executeCommand('ipaddr mleid')[0]
2041
2042    @API
2043    def setMLPrefix(self, sMeshLocalPrefix):
2044        """set mesh local prefix"""
2045        cmd = 'dataset meshlocalprefix %s' % sMeshLocalPrefix
2046        self.hasActiveDatasetToCommit = True
2047        return self.__executeCommand(cmd)[-1] == 'Done'
2048
2049    @API
2050    def getML16(self):
2051        """get mesh local 16 unicast address (Rloc)"""
2052        return self.getRloc()
2053
2054    @API
2055    def downgradeToDevice(self):
2056        pass
2057
2058    @API
2059    def upgradeToRouter(self):
2060        pass
2061
2062    @API
2063    def forceSetSlaac(self, slaacAddress):
2064        """force to set a slaac IPv6 address to Thread interface
2065
2066        Args:
2067            slaacAddress: a slaac IPv6 address to be set
2068
2069        Returns:
2070            True: successful to set slaac address to Thread interface
2071            False: fail to set slaac address to Thread interface
2072        """
2073        cmd = 'ipaddr add %s' % str(slaacAddress)
2074        return self.__executeCommand(cmd)[-1] == 'Done'
2075
2076    @API
2077    def setSleepyNodePollTime(self):
2078        pass
2079
2080    @API
2081    def enableAutoDUTObjectFlag(self):
2082        """set AutoDUTenable flag"""
2083        self.AutoDUTEnable = True
2084
2085    @API
2086    def getChildTimeoutValue(self):
2087        """get child timeout"""
2088        childTimeout = self.__executeCommand('childtimeout')[0]
2089        return int(childTimeout)
2090
2091    @API
2092    def diagnosticGet(self, strDestinationAddr, listTLV_ids=()):
2093        if not listTLV_ids:
2094            return
2095
2096        if len(listTLV_ids) == 0:
2097            return
2098
2099        cmd = 'networkdiagnostic get %s %s' % (
2100            strDestinationAddr,
2101            ' '.join([str(tlv) for tlv in listTLV_ids]),
2102        )
2103
2104        return self.__sendCommand(cmd, expectEcho=False)
2105
2106    @API
2107    def diagnosticReset(self, strDestinationAddr, listTLV_ids=()):
2108        if not listTLV_ids:
2109            return
2110
2111        if len(listTLV_ids) == 0:
2112            return
2113
2114        cmd = 'networkdiagnostic reset %s %s' % (
2115            strDestinationAddr,
2116            ' '.join([str(tlv) for tlv in listTLV_ids]),
2117        )
2118
2119        return self.__executeCommand(cmd)
2120
2121    @API
2122    def diagnosticQuery(self, strDestinationAddr, listTLV_ids=()):
2123        self.diagnosticGet(strDestinationAddr, listTLV_ids)
2124
2125    @API
2126    def startNativeCommissioner(self, strPSKc='GRLPASSPHRASE'):
2127        # TODO: Support the whole Native Commissioner functionality
2128        # Currently it only aims to trigger a Discovery Request message to pass
2129        # Certification test 5.8.4
2130        self.__executeCommand('ifconfig up')
2131        cmd = 'joiner start %s' % (strPSKc)
2132        return self.__executeCommand(cmd)[-1] == 'Done'
2133
2134    @API
2135    def startExternalCommissioner(self, baAddr, baPort):
2136        """Start external commissioner
2137        Args:
2138            baAddr: A string represents the border agent address.
2139            baPort: An integer represents the border agent port.
2140        Returns:
2141            A boolean indicates whether this function succeed.
2142        """
2143        if self.externalCommissioner is None:
2144            config = commissioner.Configuration()
2145            config.isCcmMode = False
2146            config.domainName = OpenThreadTHCI.DOMAIN_NAME
2147            config.pskc = bytearray.fromhex(self.pskc)
2148
2149            self.externalCommissioner = OTCommissioner(config, self)
2150
2151        if not self.externalCommissioner.isActive():
2152            self.externalCommissioner.start(baAddr, baPort)
2153
2154        if not self.externalCommissioner.isActive():
2155            raise commissioner.Error("external commissioner is not active")
2156
2157        return True
2158
2159    @API
2160    def stopExternalCommissioner(self):
2161        """Stop external commissioner
2162        Returns:
2163            A boolean indicates whether this function succeed.
2164        """
2165        if self.externalCommissioner is not None:
2166            self.externalCommissioner.stop()
2167            return not self.externalCommissioner.isActive()
2168
2169    @API
2170    def startCollapsedCommissioner(self, role=Thread_Device_Role.Leader):
2171        """start Collapsed Commissioner
2172
2173        Returns:
2174            True: successful to start Commissioner
2175            False: fail to start Commissioner
2176        """
2177        if self.__startOpenThread():
2178            self.wait_for_attach_to_the_network(expected_role=self.deviceRole,
2179                                                timeout=self.NETWORK_ATTACHMENT_TIMEOUT,
2180                                                raise_assert=True)
2181            cmd = 'commissioner start'
2182            if self.__executeCommand(cmd)[-1] == 'Done':
2183                self.isActiveCommissioner = True
2184                self.sleep(20)  # time for petition process
2185                return True
2186        return False
2187
2188    @API
2189    def setJoinKey(self, strPSKc):
2190        pass
2191
2192    @API
2193    def scanJoiner(self, xEUI='*', strPSKd='THREADJPAKETEST'):
2194        """scan Joiner
2195
2196        Args:
2197            xEUI: Joiner's EUI-64
2198            strPSKd: Joiner's PSKd for commissioning
2199
2200        Returns:
2201            True: successful to add Joiner's steering data
2202            False: fail to add Joiner's steering data
2203        """
2204        self.log("scanJoiner on channel %s", self.getChannel())
2205
2206        # long timeout value to avoid automatic joiner removal (in seconds)
2207        timeout = 500
2208
2209        if not isinstance(xEUI, str):
2210            eui64 = self.__convertLongToHex(xEUI, 16)
2211        else:
2212            eui64 = xEUI
2213
2214        strPSKd = self.__normalizePSKd(strPSKd)
2215
2216        cmd = 'commissioner joiner add %s %s %s' % (
2217            self._deviceEscapeEscapable(eui64),
2218            strPSKd,
2219            str(timeout),
2220        )
2221
2222        if self.__executeCommand(cmd)[-1] == 'Done':
2223            if self.logThreadStatus == self.logStatus['stop']:
2224                self.logThread = ThreadRunner.run(target=self.__readCommissioningLogs, args=(120,))
2225            return True
2226        else:
2227            return False
2228
2229    @staticmethod
2230    def __normalizePSKd(strPSKd):
2231        return strPSKd.upper().replace('I', '1').replace('O', '0').replace('Q', '0').replace('Z', '2')
2232
2233    @API
2234    def setProvisioningUrl(self, strURL='grl.com'):
2235        """set provisioning Url
2236
2237        Args:
2238            strURL: Provisioning Url string
2239
2240        Returns:
2241            True: successful to set provisioning Url
2242            False: fail to set provisioning Url
2243        """
2244        self.provisioningUrl = strURL
2245        if self.deviceRole == Thread_Device_Role.Commissioner:
2246            cmd = 'commissioner provisioningurl %s' % (strURL)
2247            return self.__executeCommand(cmd)[-1] == 'Done'
2248        return True
2249
2250    @API
2251    def allowCommission(self):
2252        """start commissioner candidate petition process
2253
2254        Returns:
2255            True: successful to start commissioner candidate petition process
2256            False: fail to start commissioner candidate petition process
2257        """
2258        cmd = 'commissioner start'
2259        if self.__executeCommand(cmd)[-1] == 'Done':
2260            self.isActiveCommissioner = True
2261            # time for petition process and at least one keep alive
2262            self.sleep(3)
2263            return True
2264        else:
2265            return False
2266
2267    @API
2268    def joinCommissioned(self, strPSKd='THREADJPAKETEST', waitTime=20):
2269        """start joiner
2270
2271        Args:
2272            strPSKd: Joiner's PSKd
2273
2274        Returns:
2275            True: successful to start joiner
2276            False: fail to start joiner
2277        """
2278        self.log("joinCommissioned on channel %s", self.getChannel())
2279
2280        if self.deviceRole in [
2281                Thread_Device_Role.Leader,
2282                Thread_Device_Role.Router,
2283                Thread_Device_Role.REED,
2284        ]:
2285            self.__setRouterSelectionJitter(1)
2286        self.__executeCommand('ifconfig up')
2287        strPSKd = self.__normalizePSKd(strPSKd)
2288        cmd = 'joiner start %s %s' % (strPSKd, self.provisioningUrl)
2289        if self.__executeCommand(cmd)[-1] == 'Done':
2290            maxDuration = 150  # seconds
2291            self.joinCommissionedStatus = self.joinStatus['ongoing']
2292
2293            if self.logThreadStatus == self.logStatus['stop']:
2294                self.logThread = ThreadRunner.run(target=self.__readCommissioningLogs, args=(maxDuration,))
2295
2296            t_end = time.time() + maxDuration
2297            while time.time() < t_end:
2298                if self.joinCommissionedStatus == self.joinStatus['succeed']:
2299                    break
2300                elif self.joinCommissionedStatus == self.joinStatus['failed']:
2301                    return False
2302
2303                self.sleep(1)
2304
2305            self.setMAC(self.mac)
2306            self.__executeCommand('thread start')
2307            self.wait_for_attach_to_the_network(expected_role=self.deviceRole,
2308                                                timeout=self.NETWORK_ATTACHMENT_TIMEOUT,
2309                                                raise_assert=True)
2310            return True
2311        else:
2312            return False
2313
2314    @API
2315    def getCommissioningLogs(self):
2316        """get Commissioning logs
2317
2318        Returns:
2319           Commissioning logs
2320        """
2321        rawLogs = self.logThread.get()
2322        ProcessedLogs = []
2323        payload = []
2324
2325        while not rawLogs.empty():
2326            rawLogEach = rawLogs.get()
2327            if '[THCI]' not in rawLogEach:
2328                continue
2329
2330            EncryptedPacket = PlatformDiagnosticPacket()
2331            infoList = rawLogEach.split('[THCI]')[1].split(']')[0].split('|')
2332            for eachInfo in infoList:
2333                info = eachInfo.split('=')
2334                infoType = info[0].strip()
2335                infoValue = info[1].strip()
2336                if 'direction' in infoType:
2337                    EncryptedPacket.Direction = (PlatformDiagnosticPacket_Direction.IN
2338                                                 if 'recv' in infoValue else PlatformDiagnosticPacket_Direction.OUT if
2339                                                 'send' in infoValue else PlatformDiagnosticPacket_Direction.UNKNOWN)
2340                elif 'type' in infoType:
2341                    EncryptedPacket.Type = (PlatformDiagnosticPacket_Type.JOIN_FIN_req if 'JOIN_FIN.req' in infoValue
2342                                            else PlatformDiagnosticPacket_Type.JOIN_FIN_rsp if 'JOIN_FIN.rsp'
2343                                            in infoValue else PlatformDiagnosticPacket_Type.JOIN_ENT_req if
2344                                            'JOIN_ENT.ntf' in infoValue else PlatformDiagnosticPacket_Type.JOIN_ENT_rsp
2345                                            if 'JOIN_ENT.rsp' in infoValue else PlatformDiagnosticPacket_Type.UNKNOWN)
2346                elif 'len' in infoType:
2347                    bytesInEachLine = 16
2348                    EncryptedPacket.TLVsLength = int(infoValue)
2349                    payloadLineCount = (int(infoValue) + bytesInEachLine - 1) / bytesInEachLine
2350                    while payloadLineCount > 0:
2351                        payloadLine = rawLogs.get()
2352                        if '|' in payloadLine:
2353                            payloadLineCount = payloadLineCount - 1
2354                            payloadSplit = payloadLine.split('|')
2355                            for block in range(1, 3):
2356                                payloadBlock = payloadSplit[block]
2357                                payloadValues = payloadBlock.split(' ')
2358                                for num in range(1, 9):
2359                                    if '..' not in payloadValues[num]:
2360                                        payload.append(int(payloadValues[num], 16))
2361
2362                    EncryptedPacket.TLVs = (PlatformPackets.read(EncryptedPacket.Type, payload)
2363                                            if payload != [] else [])
2364
2365            ProcessedLogs.append(EncryptedPacket)
2366        return ProcessedLogs
2367
2368    @API
2369    def MGMT_ED_SCAN(
2370        self,
2371        sAddr,
2372        xCommissionerSessionId,
2373        listChannelMask,
2374        xCount,
2375        xPeriod,
2376        xScanDuration,
2377    ):
2378        """send MGMT_ED_SCAN message to a given destinaition.
2379
2380        Args:
2381            sAddr: IPv6 destination address for this message
2382            xCommissionerSessionId: commissioner session id
2383            listChannelMask: a channel array to indicate which channels to be scanned
2384            xCount: number of IEEE 802.15.4 ED Scans (milliseconds)
2385            xPeriod: Period between successive IEEE802.15.4 ED Scans (milliseconds)
2386            xScanDuration: ScanDuration when performing an IEEE 802.15.4 ED Scan (milliseconds)
2387
2388        Returns:
2389            True: successful to send MGMT_ED_SCAN message.
2390            False: fail to send MGMT_ED_SCAN message
2391        """
2392        channelMask = '0x' + self.__convertLongToHex(self.__convertChannelMask(listChannelMask))
2393        cmd = 'commissioner energy %s %s %s %s %s' % (
2394            channelMask,
2395            xCount,
2396            xPeriod,
2397            xScanDuration,
2398            sAddr,
2399        )
2400        return self.__executeCommand(cmd)[-1] == 'Done'
2401
2402    @API
2403    def MGMT_PANID_QUERY(self, sAddr, xCommissionerSessionId, listChannelMask, xPanId):
2404        """send MGMT_PANID_QUERY message to a given destination
2405
2406        Args:
2407            xPanId: a given PAN ID to check the conflicts
2408
2409        Returns:
2410            True: successful to send MGMT_PANID_QUERY message.
2411            False: fail to send MGMT_PANID_QUERY message.
2412        """
2413        panid = ''
2414        channelMask = '0x' + self.__convertLongToHex(self.__convertChannelMask(listChannelMask))
2415
2416        if not isinstance(xPanId, str):
2417            panid = str(hex(xPanId))
2418
2419        cmd = 'commissioner panid %s %s %s' % (panid, channelMask, sAddr)
2420        return self.__executeCommand(cmd)[-1] == 'Done'
2421
2422    @API
2423    def MGMT_ANNOUNCE_BEGIN(self, sAddr, xCommissionerSessionId, listChannelMask, xCount, xPeriod):
2424        """send MGMT_ANNOUNCE_BEGIN message to a given destination
2425
2426        Returns:
2427            True: successful to send MGMT_ANNOUNCE_BEGIN message.
2428            False: fail to send MGMT_ANNOUNCE_BEGIN message.
2429        """
2430        channelMask = '0x' + self.__convertLongToHex(self.__convertChannelMask(listChannelMask))
2431        cmd = 'commissioner announce %s %s %s %s' % (
2432            channelMask,
2433            xCount,
2434            xPeriod,
2435            sAddr,
2436        )
2437        return self.__executeCommand(cmd)[-1] == 'Done'
2438
2439    @API
2440    def MGMT_ACTIVE_GET(self, Addr='', TLVs=()):
2441        """send MGMT_ACTIVE_GET command
2442
2443        Returns:
2444            True: successful to send MGMT_ACTIVE_GET
2445            False: fail to send MGMT_ACTIVE_GET
2446        """
2447        cmd = 'dataset mgmtgetcommand active'
2448
2449        if Addr != '':
2450            cmd += ' address '
2451            cmd += Addr
2452
2453        if len(TLVs) != 0:
2454            tlvs = ''.join('%02x' % tlv for tlv in TLVs)
2455            cmd += ' ' + self.__replaceCommands['-x'] + ' '
2456            cmd += tlvs
2457
2458        return self.__executeCommand(cmd)[-1] == 'Done'
2459
2460    @API
2461    def MGMT_ACTIVE_SET(
2462        self,
2463        sAddr='',
2464        xCommissioningSessionId=None,
2465        listActiveTimestamp=None,
2466        listChannelMask=None,
2467        xExtendedPanId=None,
2468        sNetworkName=None,
2469        sPSKc=None,
2470        listSecurityPolicy=None,
2471        xChannel=None,
2472        sMeshLocalPrefix=None,
2473        xMasterKey=None,
2474        xPanId=None,
2475        xTmfPort=None,
2476        xSteeringData=None,
2477        xBorderRouterLocator=None,
2478        BogusTLV=None,
2479        xDelayTimer=None,
2480    ):
2481        """send MGMT_ACTIVE_SET command
2482
2483        Returns:
2484            True: successful to send MGMT_ACTIVE_SET
2485            False: fail to send MGMT_ACTIVE_SET
2486        """
2487        cmd = 'dataset mgmtsetcommand active'
2488
2489        if listActiveTimestamp is not None:
2490            cmd += ' activetimestamp '
2491            cmd += str(listActiveTimestamp[0])
2492
2493        if xExtendedPanId is not None:
2494            cmd += ' extpanid '
2495            xpanid = self.__convertLongToHex(xExtendedPanId, 16)
2496
2497            cmd += xpanid
2498
2499        if sNetworkName is not None:
2500            cmd += ' networkname '
2501            cmd += self._deviceEscapeEscapable(str(sNetworkName))
2502
2503        if xChannel is not None:
2504            cmd += ' channel '
2505            cmd += str(xChannel)
2506
2507        if sMeshLocalPrefix is not None:
2508            cmd += ' localprefix '
2509            cmd += str(sMeshLocalPrefix)
2510
2511        if xMasterKey is not None:
2512            cmd += ' ' + self.__replaceCommands['networkkey'] + ' '
2513            key = self.__convertLongToHex(xMasterKey, 32)
2514
2515            cmd += key
2516
2517        if xPanId is not None:
2518            cmd += ' panid '
2519            cmd += str(xPanId)
2520
2521        if listChannelMask is not None:
2522            cmd += ' channelmask '
2523            cmd += '0x' + self.__convertLongToHex(self.__convertChannelMask(listChannelMask))
2524
2525        if (sPSKc is not None or listSecurityPolicy is not None or xCommissioningSessionId is not None or
2526                xTmfPort is not None or xSteeringData is not None or xBorderRouterLocator is not None or
2527                BogusTLV is not None):
2528            cmd += ' ' + self.__replaceCommands['-x'] + ' '
2529
2530        if sPSKc is not None:
2531            cmd += '0410'
2532            stretchedPskc = Thread_PBKDF2.get(
2533                sPSKc,
2534                ModuleHelper.Default_XpanId,
2535                ModuleHelper.Default_NwkName,
2536            )
2537            pskc = '%x' % stretchedPskc
2538
2539            if len(pskc) < 32:
2540                pskc = pskc.zfill(32)
2541
2542            cmd += pskc
2543
2544        if listSecurityPolicy is not None:
2545            if self.DeviceCapability == DevCapb.V1_1:
2546                cmd += '0c03'
2547            else:
2548                cmd += '0c04'
2549
2550            rotationTime = 0
2551            policyBits = 0
2552
2553            # previous passing way listSecurityPolicy=[True, True, 3600,
2554            # False, False, True]
2555            if len(listSecurityPolicy) == 6:
2556                rotationTime = listSecurityPolicy[2]
2557
2558                # the last three reserved bits must be 1
2559                policyBits = 0b00000111
2560
2561                if listSecurityPolicy[0]:
2562                    policyBits = policyBits | 0b10000000
2563                if listSecurityPolicy[1]:
2564                    policyBits = policyBits | 0b01000000
2565                if listSecurityPolicy[3]:
2566                    policyBits = policyBits | 0b00100000
2567                if listSecurityPolicy[4]:
2568                    policyBits = policyBits | 0b00010000
2569                if listSecurityPolicy[5]:
2570                    policyBits = policyBits | 0b00001000
2571            else:
2572                # new passing way listSecurityPolicy=[3600, 0b11001111]
2573                rotationTime = listSecurityPolicy[0]
2574                # bit order
2575                if len(listSecurityPolicy) > 2:
2576                    policyBits = listSecurityPolicy[2] << 8 | listSecurityPolicy[1]
2577                else:
2578                    policyBits = listSecurityPolicy[1]
2579
2580            policy = str(hex(rotationTime))[2:]
2581
2582            if len(policy) < 4:
2583                policy = policy.zfill(4)
2584
2585            cmd += policy
2586
2587            flags0 = ('%x' % (policyBits & 0x00ff)).ljust(2, '0')
2588            cmd += flags0
2589
2590            if self.DeviceCapability != DevCapb.V1_1:
2591                flags1 = ('%x' % ((policyBits & 0xff00) >> 8)).ljust(2, '0')
2592                cmd += flags1
2593
2594        if xCommissioningSessionId is not None:
2595            cmd += '0b02'
2596            sessionid = str(hex(xCommissioningSessionId))[2:]
2597
2598            if len(sessionid) < 4:
2599                sessionid = sessionid.zfill(4)
2600
2601            cmd += sessionid
2602
2603        if xBorderRouterLocator is not None:
2604            cmd += '0902'
2605            locator = str(hex(xBorderRouterLocator))[2:]
2606
2607            if len(locator) < 4:
2608                locator = locator.zfill(4)
2609
2610            cmd += locator
2611
2612        if xSteeringData is not None:
2613            steeringData = self.__convertLongToHex(xSteeringData)
2614            cmd += '08' + str(len(steeringData) / 2).zfill(2)
2615            cmd += steeringData
2616
2617        if BogusTLV is not None:
2618            cmd += '8202aa55'
2619
2620        return self.__executeCommand(cmd)[-1] == 'Done'
2621
2622    @API
2623    def MGMT_PENDING_GET(self, Addr='', TLVs=()):
2624        """send MGMT_PENDING_GET command
2625
2626        Returns:
2627            True: successful to send MGMT_PENDING_GET
2628            False: fail to send MGMT_PENDING_GET
2629        """
2630        cmd = 'dataset mgmtgetcommand pending'
2631
2632        if Addr != '':
2633            cmd += ' address '
2634            cmd += Addr
2635
2636        if len(TLVs) != 0:
2637            tlvs = ''.join('%02x' % tlv for tlv in TLVs)
2638            cmd += ' ' + self.__replaceCommands['-x'] + ' '
2639            cmd += tlvs
2640
2641        return self.__executeCommand(cmd)[-1] == 'Done'
2642
2643    @API
2644    def MGMT_PENDING_SET(
2645        self,
2646        sAddr='',
2647        xCommissionerSessionId=None,
2648        listPendingTimestamp=None,
2649        listActiveTimestamp=None,
2650        xDelayTimer=None,
2651        xChannel=None,
2652        xPanId=None,
2653        xMasterKey=None,
2654        sMeshLocalPrefix=None,
2655        sNetworkName=None,
2656    ):
2657        """send MGMT_PENDING_SET command
2658
2659        Returns:
2660            True: successful to send MGMT_PENDING_SET
2661            False: fail to send MGMT_PENDING_SET
2662        """
2663        cmd = 'dataset mgmtsetcommand pending'
2664
2665        if listPendingTimestamp is not None:
2666            cmd += ' pendingtimestamp '
2667            cmd += str(listPendingTimestamp[0])
2668
2669        if listActiveTimestamp is not None:
2670            cmd += ' activetimestamp '
2671            cmd += str(listActiveTimestamp[0])
2672
2673        if xDelayTimer is not None:
2674            cmd += ' delaytimer '
2675            cmd += str(xDelayTimer)
2676            # cmd += ' delaytimer 3000000'
2677
2678        if xChannel is not None:
2679            cmd += ' channel '
2680            cmd += str(xChannel)
2681
2682        if xPanId is not None:
2683            cmd += ' panid '
2684            cmd += str(xPanId)
2685
2686        if xMasterKey is not None:
2687            cmd += ' ' + self.__replaceCommands['networkkey'] + ' '
2688            key = self.__convertLongToHex(xMasterKey, 32)
2689
2690            cmd += key
2691
2692        if sMeshLocalPrefix is not None:
2693            cmd += ' localprefix '
2694            cmd += str(sMeshLocalPrefix)
2695
2696        if sNetworkName is not None:
2697            cmd += ' networkname '
2698            cmd += self._deviceEscapeEscapable(str(sNetworkName))
2699
2700        if xCommissionerSessionId is not None:
2701            cmd += ' ' + self.__replaceCommands['-x'] + ' '
2702            cmd += '0b02'
2703            sessionid = str(hex(xCommissionerSessionId))[2:]
2704
2705            if len(sessionid) < 4:
2706                sessionid = sessionid.zfill(4)
2707
2708            cmd += sessionid
2709
2710        return self.__executeCommand(cmd)[-1] == 'Done'
2711
2712    @API
2713    def MGMT_COMM_GET(self, Addr='ff02::1', TLVs=()):
2714        """send MGMT_COMM_GET command
2715
2716        Returns:
2717            True: successful to send MGMT_COMM_GET
2718            False: fail to send MGMT_COMM_GET
2719        """
2720        cmd = 'commissioner mgmtget'
2721
2722        if len(TLVs) != 0:
2723            tlvs = ''.join('%02x' % tlv for tlv in TLVs)
2724            cmd += ' ' + self.__replaceCommands['-x'] + ' '
2725            cmd += tlvs
2726
2727        return self.__executeCommand(cmd)[-1] == 'Done'
2728
2729    @API
2730    def MGMT_COMM_SET(
2731        self,
2732        Addr='ff02::1',
2733        xCommissionerSessionID=None,
2734        xSteeringData=None,
2735        xBorderRouterLocator=None,
2736        xChannelTlv=None,
2737        ExceedMaxPayload=False,
2738    ):
2739        """send MGMT_COMM_SET command
2740
2741        Returns:
2742            True: successful to send MGMT_COMM_SET
2743            False: fail to send MGMT_COMM_SET
2744        """
2745        cmd = 'commissioner mgmtset'
2746
2747        if xCommissionerSessionID is not None:
2748            # use assigned session id
2749            cmd += ' sessionid '
2750            cmd += str(xCommissionerSessionID)
2751        elif xCommissionerSessionID is None:
2752            # use original session id
2753            if self.isActiveCommissioner is True:
2754                cmd += ' sessionid '
2755                cmd += self.__getCommissionerSessionId()
2756            else:
2757                pass
2758
2759        if xSteeringData is not None:
2760            cmd += ' steeringdata '
2761            cmd += str(hex(xSteeringData)[2:])
2762
2763        if xBorderRouterLocator is not None:
2764            cmd += ' locator '
2765            cmd += str(hex(xBorderRouterLocator))
2766
2767        if xChannelTlv is not None:
2768            cmd += ' ' + self.__replaceCommands['-x'] + ' '
2769            cmd += '000300' + '%04x' % xChannelTlv
2770
2771        return self.__executeCommand(cmd)[-1] == 'Done'
2772
2773    @API
2774    def setPSKc(self, strPSKc):
2775        cmd = 'dataset pskc %s' % strPSKc
2776        self.hasActiveDatasetToCommit = True
2777        return self.__executeCommand(cmd)[-1] == 'Done'
2778
2779    @API
2780    def setActiveTimestamp(self, xActiveTimestamp):
2781        self.activetimestamp = xActiveTimestamp
2782        self.hasActiveDatasetToCommit = True
2783        cmd = 'dataset activetimestamp %s' % str(xActiveTimestamp)
2784        return self.__executeCommand(cmd)[-1] == 'Done'
2785
2786    @API
2787    def setUdpJoinerPort(self, portNumber):
2788        """set Joiner UDP Port
2789
2790        Args:
2791            portNumber: Joiner UDP Port number
2792
2793        Returns:
2794            True: successful to set Joiner UDP Port
2795            False: fail to set Joiner UDP Port
2796        """
2797        cmd = 'joinerport %d' % portNumber
2798        return self.__executeCommand(cmd)[-1] == 'Done'
2799
2800    @API
2801    def commissionerUnregister(self):
2802        """stop commissioner
2803
2804        Returns:
2805            True: successful to stop commissioner
2806            False: fail to stop commissioner
2807        """
2808        cmd = 'commissioner stop'
2809        return self.__executeCommand(cmd)[-1] == 'Done'
2810
2811    @API
2812    def sendBeacons(self, sAddr, xCommissionerSessionId, listChannelMask, xPanId):
2813        self.__sendCommand('scan', expectEcho=False)
2814
2815    @API
2816    def updateRouterStatus(self):
2817        """force update to router as if there is child id request"""
2818        self._update_router_status = True
2819
2820    @API
2821    def __updateRouterStatus(self):
2822        cmd = 'state'
2823        while True:
2824            state = self.__executeCommand(cmd)[0]
2825            if state == 'detached':
2826                continue
2827            elif state == 'child':
2828                break
2829            else:
2830                return False
2831
2832        cmd = 'state router'
2833        return self.__executeCommand(cmd)[-1] == 'Done'
2834
2835    @API
2836    def setRouterThresholdValues(self, upgradeThreshold, downgradeThreshold):
2837        self.__setRouterUpgradeThreshold(upgradeThreshold)
2838        self.__setRouterDowngradeThreshold(downgradeThreshold)
2839
2840    @API
2841    def setMinDelayTimer(self, iSeconds):
2842        cmd = 'delaytimermin %s' % iSeconds
2843        return self.__executeCommand(cmd)[-1] == 'Done'
2844
2845    @API
2846    def ValidateDeviceFirmware(self):
2847        assert not self.IsBorderRouter, "Method not expected to be used with border router devices"
2848
2849        if self.DeviceCapability == OT11_CAPBS:
2850            return OT11_VERSION in self.UIStatusMsg
2851        elif self.DeviceCapability == OT12_CAPBS:
2852            return OT12_VERSION in self.UIStatusMsg
2853        elif self.DeviceCapability == OT13_CAPBS:
2854            return OT13_VERSION in self.UIStatusMsg
2855        else:
2856            return False
2857
2858    @API
2859    def setBbrDataset(self, SeqNumInc=False, SeqNum=None, MlrTimeout=None, ReRegDelay=None):
2860        """ set BBR Dataset
2861
2862        Args:
2863            SeqNumInc:  Increase `SeqNum` by 1 if True.
2864            SeqNum:     Set `SeqNum` to a given value if not None.
2865            MlrTimeout: Set `MlrTimeout` to a given value.
2866            ReRegDelay: Set `ReRegDelay` to a given value.
2867
2868            MUST NOT set SeqNumInc to True and SeqNum to non-None value at the same time.
2869
2870        Returns:
2871            True: successful to set BBR Dataset
2872            False: fail to set BBR Dataset
2873        """
2874        assert not (SeqNumInc and SeqNum is not None), "Must not specify both SeqNumInc and SeqNum"
2875
2876        if (MlrTimeout and MlrTimeout != self.bbrMlrTimeout) or (ReRegDelay and ReRegDelay != self.bbrReRegDelay):
2877            if SeqNum is None:
2878                SeqNumInc = True
2879
2880        if SeqNumInc:
2881            if self.bbrSeqNum in (126, 127):
2882                self.bbrSeqNum = 0
2883            elif self.bbrSeqNum in (254, 255):
2884                self.bbrSeqNum = 128
2885            else:
2886                self.bbrSeqNum = (self.bbrSeqNum + 1) % 256
2887        elif SeqNum is not None:
2888            self.bbrSeqNum = SeqNum
2889
2890        return self.__configBbrDataset(SeqNum=self.bbrSeqNum, MlrTimeout=MlrTimeout, ReRegDelay=ReRegDelay)
2891
2892    def __configBbrDataset(self, SeqNum=None, MlrTimeout=None, ReRegDelay=None):
2893        if MlrTimeout is not None and ReRegDelay is None:
2894            ReRegDelay = self.bbrReRegDelay
2895
2896        cmd = 'bbr config'
2897        if SeqNum is not None:
2898            cmd += ' seqno %d' % SeqNum
2899        if ReRegDelay is not None:
2900            cmd += ' delay %d' % ReRegDelay
2901        if MlrTimeout is not None:
2902            cmd += ' timeout %d' % MlrTimeout
2903        ret = self.__executeCommand(cmd)[-1] == 'Done'
2904
2905        if SeqNum is not None:
2906            self.bbrSeqNum = SeqNum
2907
2908        if MlrTimeout is not None:
2909            self.bbrMlrTimeout = MlrTimeout
2910
2911        if ReRegDelay is not None:
2912            self.bbrReRegDelay = ReRegDelay
2913
2914        cmd = self.__replaceCommands['netdata register']
2915        self.__executeCommand(cmd)
2916
2917        return ret
2918
2919    # Low power THCI
2920    @API
2921    def setCSLtout(self, tout=30):
2922        self.ssedTimeout = tout
2923        cmd = 'csl timeout %u' % self.ssedTimeout
2924        return self.__executeCommand(cmd)[-1] == 'Done'
2925
2926    @API
2927    def setCSLchannel(self, ch=11):
2928        cmd = 'csl channel %u' % ch
2929        return self.__executeCommand(cmd)[-1] == 'Done'
2930
2931    @API
2932    def setCSLperiod(self, period=500):
2933        """set Csl Period
2934        Args:
2935            period: csl period in ms
2936
2937        """
2938        cmd = 'csl period %u' % (period * 1000)
2939        return self.__executeCommand(cmd)[-1] == 'Done'
2940
2941    @staticmethod
2942    def getForwardSeriesFlagsFromHexOrStr(flags):
2943        hexFlags = int(flags, 16) if isinstance(flags, str) else flags
2944        strFlags = ''
2945        if hexFlags == 0:
2946            strFlags = 'X'
2947        else:
2948            if hexFlags & 0x1 != 0:
2949                strFlags += 'l'
2950            if hexFlags & 0x2 != 0:
2951                strFlags += 'd'
2952            if hexFlags & 0x4 != 0:
2953                strFlags += 'r'
2954            if hexFlags & 0x8 != 0:
2955                strFlags += 'a'
2956
2957        return strFlags
2958
2959    @staticmethod
2960    def mapMetricsHexToChar(metrics):
2961        metricsFlagMap = {
2962            0x40: 'p',
2963            0x09: 'q',
2964            0x0a: 'm',
2965            0x0b: 'r',
2966        }
2967        metricsReservedFlagMap = {0x11: 'q', 0x12: 'm', 0x13: 'r'}
2968        if metricsFlagMap.get(metrics):
2969            return metricsFlagMap.get(metrics), False
2970        elif metricsReservedFlagMap.get(metrics):
2971            return metricsReservedFlagMap.get(metrics), True
2972        else:
2973            logging.warning("Not found flag mapping for given metrics: {}".format(metrics))
2974            return '', False
2975
2976    @staticmethod
2977    def getMetricsFlagsFromHexStr(metrics):
2978        strMetrics = ''
2979        reserved_flag = ''
2980
2981        if metrics.startswith('0x'):
2982            metrics = metrics[2:]
2983        hexMetricsArray = bytearray.fromhex(metrics)
2984
2985        for metric in hexMetricsArray:
2986            metric_flag, has_reserved_flag = OpenThreadTHCI.mapMetricsHexToChar(metric)
2987            strMetrics += metric_flag
2988            if has_reserved_flag:
2989                reserved_flag = ' r'
2990
2991        return strMetrics + reserved_flag
2992
2993    @API
2994    def LinkMetricsSingleReq(self, dst_addr, metrics):
2995        cmd = 'linkmetrics query %s single %s' % (dst_addr, self.getMetricsFlagsFromHexStr(metrics))
2996        return self.__executeCommand(cmd)[-1] == 'Done'
2997
2998    @API
2999    def LinkMetricsMgmtReq(self, dst_addr, type_, flags, metrics, series_id):
3000        cmd = 'linkmetrics mgmt %s ' % dst_addr
3001        if type_ == 'FWD':
3002            cmd += 'forward %d %s' % (series_id, self.getForwardSeriesFlagsFromHexOrStr(flags))
3003            if flags != 0:
3004                cmd += ' %s' % (self.getMetricsFlagsFromHexStr(metrics))
3005        elif type_ == 'ENH':
3006            cmd += 'enhanced-ack'
3007            if flags != 0:
3008                cmd += ' register %s' % (self.getMetricsFlagsFromHexStr(metrics))
3009            else:
3010                cmd += ' clear'
3011        return self.__executeCommand(cmd)[-1] == 'Done'
3012
3013    @API
3014    def LinkMetricsGetReport(self, dst_addr, series_id):
3015        cmd = 'linkmetrics query %s forward %d' % (dst_addr, series_id)
3016        return self.__executeCommand(cmd)[-1] == 'Done'
3017
3018    # TODO: Series Id is not in this API.
3019    @API
3020    def LinkMetricsSendProbe(self, dst_addr, ack=True, size=0):
3021        cmd = 'linkmetrics probe %s %d' % (dst_addr, size)
3022        return self.__executeCommand(cmd)[-1] == 'Done'
3023
3024    @API
3025    def setTxPower(self, level):
3026        cmd = 'txpower '
3027        if level == 'HIGH':
3028            cmd += '127'
3029        elif level == 'MEDIUM':
3030            cmd += '0'
3031        elif level == 'LOW':
3032            cmd += '-128'
3033        else:
3034            print('wrong Tx Power level')
3035        return self.__executeCommand(cmd)[-1] == 'Done'
3036
3037    @API
3038    def sendUdp(self, destination, port, payload='hello'):
3039        assert payload is not None, 'payload should not be none'
3040        cmd = 'udp send %s %d %s' % (destination, port, payload)
3041        return self.__executeCommand(cmd)[-1] == 'Done'
3042
3043    @API
3044    def send_udp(self, interface, destination, port, payload='12ABcd'):
3045        ''' payload hexstring
3046        '''
3047        assert payload is not None, 'payload should not be none'
3048        assert interface == 0, "non-BR must send UDP to Thread interface"
3049        self.__udpOpen()
3050        time.sleep(0.5)
3051        cmd = 'udp send %s %s -x %s' % (destination, port, payload)
3052        return self.__executeCommand(cmd)[-1] == 'Done'
3053
3054    def __udpOpen(self):
3055        if not self.__isUdpOpened:
3056            cmd = 'udp open'
3057            self.__executeCommand(cmd)
3058
3059            # Bind to RLOC address and first dynamic port
3060            rlocAddr = self.getRloc()
3061
3062            cmd = 'udp bind %s 49152' % rlocAddr
3063            self.__executeCommand(cmd)
3064
3065            self.__isUdpOpened = True
3066
3067    @API
3068    def sendMACcmd(self, enh=False):
3069        cmd = 'mac send datarequest'
3070        return self.__executeCommand(cmd)[-1] == 'Done'
3071
3072    @API
3073    def sendMACdata(self, enh=False):
3074        cmd = 'mac send emptydata'
3075        return self.__executeCommand(cmd)[-1] == 'Done'
3076
3077    @API
3078    def setCSLsuspension(self, suspend):
3079        if suspend:
3080            self.__setPollPeriod(240 * 1000)
3081        else:
3082            self.__setPollPeriod(int(0.9 * self.ssedTimeout * 1000))
3083
3084    @API
3085    def set_max_addrs_per_child(self, num):
3086        cmd = 'childip max %d' % int(num)
3087        self.__executeCommand(cmd)
3088
3089    @API
3090    def config_next_dua_status_rsp(self, mliid, status_code):
3091        if status_code >= 400:
3092            # map status_code to correct COAP response code
3093            a, b = divmod(status_code, 100)
3094            status_code = ((a & 0x7) << 5) + (b & 0x1f)
3095
3096        cmd = 'bbr mgmt dua %d' % status_code
3097
3098        if mliid is not None:
3099            mliid = mliid.replace(':', '')
3100            cmd += ' %s' % mliid
3101
3102        self.__executeCommand(cmd)
3103
3104    @API
3105    def getDUA(self):
3106        dua = self.getGUA('fd00:7d03')
3107        return dua
3108
3109    def __addDefaultDomainPrefix(self):
3110        self.configBorderRouter(P_dp=1, P_stable=1, P_on_mesh=1, P_default=1)
3111
3112    def __setDUA(self, sDua):
3113        """specify the DUA before Thread Starts."""
3114        if isinstance(sDua, str):
3115            sDua = sDua.decode('utf8')
3116        iid = ipaddress.IPv6Address(sDua).packed[-8:]
3117        cmd = 'dua iid %s' % ''.join('%02x' % ord(b) for b in iid)
3118        return self.__executeCommand(cmd)[-1] == 'Done'
3119
3120    def __getMlIid(self):
3121        """get the Mesh Local IID."""
3122        # getULA64() would return the full string representation
3123        mleid = ModuleHelper.GetFullIpv6Address(self.getULA64()).lower()
3124        mliid = mleid[-19:].replace(':', '')
3125        return mliid
3126
3127    def __setMlIid(self, sMlIid):
3128        """Set the Mesh Local IID before Thread Starts."""
3129        assert ':' not in sMlIid
3130        cmd = 'mliid %s' % sMlIid
3131        self.__executeCommand(cmd)
3132
3133    @API
3134    def registerDUA(self, sAddr=''):
3135        self.__setDUA(sAddr)
3136
3137    @API
3138    def config_next_mlr_status_rsp(self, status_code):
3139        cmd = 'bbr mgmt mlr response %d' % status_code
3140        return self.__executeCommand(cmd)[-1] == 'Done'
3141
3142    @API
3143    def setMLRtimeout(self, iMsecs):
3144        """Setup BBR MLR Timeout to `iMsecs` seconds."""
3145        self.setBbrDataset(MlrTimeout=iMsecs)
3146
3147    @API
3148    def stopListeningToAddr(self, sAddr):
3149        cmd = 'ipmaddr del ' + sAddr
3150        try:
3151            self.__executeCommand(cmd)
3152        except CommandError as ex:
3153            if ex.code == OT_ERROR_ALREADY:
3154                pass
3155            else:
3156                raise
3157
3158        return True
3159
3160    @API
3161    def registerMulticast(self, listAddr=('ff04::1234:777a:1',), timeout=MLR_TIMEOUT_MIN):
3162        """subscribe to the given ipv6 address (sAddr) in interface and send MLR.req OTA
3163
3164        Args:
3165            sAddr   : str : Multicast address to be subscribed and notified OTA.
3166        """
3167        for each_sAddr in listAddr:
3168            self._beforeRegisterMulticast(each_sAddr, timeout)
3169
3170        sAddr = ' '.join(listAddr)
3171        cmd = 'ipmaddr add ' + str(sAddr)
3172
3173        try:
3174            self.__executeCommand(cmd)
3175        except CommandError as ex:
3176            if ex.code == OT_ERROR_ALREADY:
3177                pass
3178            else:
3179                raise
3180
3181    @API
3182    def getMlrLogs(self):
3183        return self.externalCommissioner.getMlrLogs()
3184
3185    @API
3186    def migrateNetwork(self, channel=None, net_name=None):
3187        """migrate to another Thread Partition 'net_name' (could be None)
3188            on specified 'channel'. Make sure same Mesh Local IID and DUA
3189            after migration for DUA-TC-06/06b (DEV-1923)
3190        """
3191        if channel is None:
3192            raise Exception('channel None')
3193
3194        if channel not in range(11, 27):
3195            raise Exception('channel %d not in [11, 26] Invalid' % channel)
3196
3197        print('new partition %s on channel %d' % (net_name, channel))
3198
3199        mliid = self.__getMlIid()
3200        dua = self.getDUA()
3201        self.reset()
3202        deviceRole = self.deviceRole
3203        self.setDefaultValues()
3204        self.setChannel(channel)
3205        if net_name is not None:
3206            self.setNetworkName(net_name)
3207        self.__setMlIid(mliid)
3208        self.__setDUA(dua)
3209        return self.joinNetwork(deviceRole)
3210
3211    @API
3212    def setParentPrio(self, prio):
3213        cmd = 'parentpriority %u' % prio
3214        return self.__executeCommand(cmd)[-1] == 'Done'
3215
3216    @API
3217    def role_transition(self, role):
3218        cmd = 'mode %s' % OpenThreadTHCI._ROLE_MODE_DICT[role]
3219        return self.__executeCommand(cmd)[-1] == 'Done'
3220
3221    @API
3222    def setLeaderWeight(self, iWeight=72):
3223        self.__executeCommand('leaderweight %d' % iWeight)
3224
3225    @watched
3226    def isBorderRoutingEnabled(self):
3227        try:
3228            self.__executeCommand('br omrprefix local')
3229            return True
3230        except CommandError:
3231            return False
3232
3233    def __detectZephyr(self):
3234        """Detect if the device is running Zephyr and adapt in that case"""
3235
3236        try:
3237            self._lineSepX = re.compile(r'\r\n|\r|\n')
3238            if self.__executeCommand(ZEPHYR_PREFIX + 'thread version')[0].isdigit():
3239                self._cmdPrefix = ZEPHYR_PREFIX
3240        except CommandError:
3241            self._lineSepX = LINESEPX
3242
3243    def __detectReference20200818(self):
3244        """Detect if the device is a Thread reference 20200818 """
3245
3246        # Running `version api` in Thread reference 20200818 is equivalent to running `version`
3247        # It will not output an API number
3248        self.IsReference20200818 = not self.__executeCommand('version api')[0].isdigit()
3249
3250        if self.IsReference20200818:
3251            self.__replaceCommands = {
3252                '-x': 'binary',
3253                'allowlist': 'whitelist',
3254                'denylist': 'blacklist',
3255                'netdata register': 'netdataregister',
3256                'networkkey': 'masterkey',
3257                'partitionid preferred': 'leaderpartitionid',
3258            }
3259        else:
3260
3261            class IdentityDict:
3262
3263                def __getitem__(self, key):
3264                    return key
3265
3266            self.__replaceCommands = IdentityDict()
3267
3268    def __discoverDeviceCapability(self):
3269        """Discover device capability according to version"""
3270        thver = self.__executeCommand('thread version')[0]
3271        if thver in ['1.3', '4'] and not self.IsBorderRouter:
3272            self.log("Setting capability of {}: (DevCapb.C_FTD13 | DevCapb.C_MTD13)".format(self))
3273            self.DeviceCapability = OT13_CAPBS
3274        elif thver in ['1.3', '4'] and self.IsBorderRouter:
3275            self.log("Setting capability of {}: (DevCapb.C_BR13 | DevCapb.C_Host13)".format(self))
3276            self.DeviceCapability = OT13BR_CAPBS
3277        elif thver in ['1.2', '3'] and not self.IsBorderRouter:
3278            self.log("Setting capability of {}: DevCapb.L_AIO | DevCapb.C_FFD | DevCapb.C_RFD".format(self))
3279            self.DeviceCapability = OT12_CAPBS
3280        elif thver in ['1.2', '3'] and self.IsBorderRouter:
3281            self.log("Setting capability of BR {}: DevCapb.C_BBR | DevCapb.C_Host | DevCapb.C_Comm".format(self))
3282            self.DeviceCapability = OT12BR_CAPBS
3283        elif thver in ['1.1', '2']:
3284            self.log("Setting capability of {}: DevCapb.V1_1".format(self))
3285            self.DeviceCapability = OT11_CAPBS
3286        else:
3287            self.log("Capability not specified for {}".format(self))
3288            self.DeviceCapability = DevCapb.NotSpecified
3289            assert False, thver
3290
3291    @staticmethod
3292    def __lstrip0x(s):
3293        """strip 0x at the beginning of a hex string if it exists
3294
3295        Args:
3296            s: hex string
3297
3298        Returns:
3299            hex string with leading 0x stripped
3300        """
3301        if s.startswith('0x'):
3302            s = s[2:]
3303
3304        return s
3305
3306    @API
3307    def setCcmState(self, state=0):
3308        assert state in (0, 1), state
3309        self.__executeCommand("ccm {}".format("enable" if state == 1 else "disable"))
3310
3311    @API
3312    def setVrCheckSkip(self):
3313        self.__executeCommand("tvcheck disable")
3314
3315    @API
3316    def addBlockedNodeId(self, node_id):
3317        cmd = 'nodeidfilter deny %d' % node_id
3318        self.__executeCommand(cmd)
3319
3320    @API
3321    def clearBlockedNodeIds(self):
3322        cmd = 'nodeidfilter clear'
3323        self.__executeCommand(cmd)
3324
3325
3326class OpenThread(OpenThreadTHCI, IThci):
3327
3328    def _connect(self):
3329        print('My port is %s' % self)
3330        self.__lines = []
3331        timeout = 10
3332        port_error = None
3333
3334        if self.port.startswith('COM'):
3335            for _ in range(int(timeout / 0.5)):
3336                time.sleep(0.5)
3337                try:
3338                    self.__handle = serial.Serial(self.port, 115200, timeout=0, write_timeout=1)
3339                    self.sleep(1)
3340                    self.__handle.write('\r\n')
3341                    self.sleep(0.1)
3342                    self._is_net = False
3343                    break
3344                except SerialException as port_error:
3345                    self.log("{} port not ready, retrying to connect...".format(self.port))
3346            else:
3347                raise SerialException("Could not open {} port: {}".format(self.port, port_error))
3348        elif ':' in self.port:
3349            host, port = self.port.split(':')
3350            self.__handle = socket.create_connection((host, port))
3351            self.__handle.setblocking(False)
3352            self._is_net = True
3353        else:
3354            raise Exception('Unknown port schema')
3355
3356    def _disconnect(self):
3357        if self.__handle:
3358            self.__handle.close()
3359            self.__handle = None
3360
3361    def __socRead(self, size=512):
3362        if self._is_net:
3363            return self.__handle.recv(size)
3364        else:
3365            return self.__handle.read(size)
3366
3367    def __socWrite(self, data):
3368        if self._is_net:
3369            self.__handle.sendall(data)
3370        else:
3371            self.__handle.write(data)
3372
3373    def _cliReadLine(self):
3374        if len(self.__lines) > 1:
3375            return self.__lines.pop(0)
3376
3377        tail = ''
3378        if len(self.__lines) != 0:
3379            tail = self.__lines.pop()
3380
3381        try:
3382            tail += self.__socRead()
3383        except socket.error:
3384            logging.exception('%s: No new data', self)
3385            self.sleep(0.1)
3386
3387        self.__lines += self._lineSepX.split(tail)
3388        if len(self.__lines) > 1:
3389            return self.__lines.pop(0)
3390
3391    def _cliWriteLine(self, line):
3392        if self._cmdPrefix == ZEPHYR_PREFIX:
3393            if not line.startswith(self._cmdPrefix):
3394                line = self._cmdPrefix + line
3395            self.__socWrite(line + '\r')
3396        else:
3397            self.__socWrite(line + '\r\n')
3398