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