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