1#!/usr/bin/env python 2# 3# Copyright (c) 2020, 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_BR THCI 31>> Class : OpenThread_BR 32""" 33import logging 34import re 35import sys 36import time 37import ipaddress 38 39import serial 40from IThci import IThci 41from THCI.OpenThread import OpenThreadTHCI, watched, API 42 43RPI_FULL_PROMPT = 'pi@raspberrypi:~$ ' 44RPI_USERNAME_PROMPT = 'raspberrypi login: ' 45RPI_PASSWORD_PROMPT = 'Password: ' 46"""regex: used to split lines""" 47LINESEPX = re.compile(r'\r\n|\n') 48 49LOGX = re.compile(r'.*Under-voltage detected!') 50"""regex: used to filter logging""" 51 52assert LOGX.match('[57522.618196] Under-voltage detected! (0x00050005)') 53 54OTBR_AGENT_SYSLOG_PATTERN = re.compile(r'raspberrypi otbr-agent\[\d+\]: (.*)') 55assert OTBR_AGENT_SYSLOG_PATTERN.search( 56 'Jun 23 05:21:22 raspberrypi otbr-agent[323]: =========[[THCI] direction=send | type=JOIN_FIN.req | len=039]==========]' 57).group(1) == '=========[[THCI] direction=send | type=JOIN_FIN.req | len=039]==========]' 58 59logging.getLogger('paramiko').setLevel(logging.WARNING) 60 61 62class SSHHandle(object): 63 64 def __init__(self, ip, port, username, password): 65 self.ip = ip 66 self.port = int(port) 67 self.username = username 68 self.password = password 69 self.__handle = None 70 71 self.__connect() 72 73 def __connect(self): 74 import paramiko 75 76 self.close() 77 78 self.__handle = paramiko.SSHClient() 79 self.__handle.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 80 try: 81 self.__handle.connect(self.ip, port=self.port, username=self.username, password=self.password) 82 except paramiko.ssh_exception.AuthenticationException: 83 if not self.password: 84 self.__handle.get_transport().auth_none(self.username) 85 else: 86 raise 87 88 def close(self): 89 if self.__handle is not None: 90 self.__handle.close() 91 self.__handle = None 92 93 def bash(self, cmd, timeout): 94 from paramiko import SSHException 95 retry = 3 96 for i in range(retry): 97 try: 98 stdin, stdout, stderr = self.__handle.exec_command(cmd, timeout=timeout) 99 100 sys.stderr.write(stderr.read()) 101 output = [r.encode('utf8').rstrip('\r\n') for r in stdout.readlines()] 102 return output 103 104 except Exception: 105 if i < retry - 1: 106 print('SSH connection is lost, try reconnect after 1 second.') 107 time.sleep(1) 108 self.__connect() 109 else: 110 raise 111 112 def log(self, fmt, *args): 113 try: 114 msg = fmt % args 115 print('%s - %s - %s' % (self.port, time.strftime('%b %d %H:%M:%S'), msg)) 116 except Exception: 117 pass 118 119 120class SerialHandle: 121 122 def __init__(self, port, baudrate): 123 self.port = port 124 self.__handle = serial.Serial(port, baudrate, timeout=0) 125 126 self.__lines = [''] 127 assert len(self.__lines) >= 1, self.__lines 128 129 self.log("inputing username ...") 130 self.__bashWriteLine('pi') 131 deadline = time.time() + 20 132 loginOk = False 133 while time.time() < deadline: 134 time.sleep(1) 135 136 lastLine = None 137 while True: 138 line = self.__bashReadLine(timeout=1) 139 140 if not line: 141 break 142 143 lastLine = line 144 145 if lastLine == RPI_FULL_PROMPT: 146 self.log("prompt found, login success!") 147 loginOk = True 148 break 149 150 if lastLine == RPI_PASSWORD_PROMPT: 151 self.log("inputing password ...") 152 self.__bashWriteLine('raspberry') 153 elif lastLine == RPI_USERNAME_PROMPT: 154 self.log("inputing username ...") 155 self.__bashWriteLine('pi') 156 elif not lastLine: 157 self.log("inputing username ...") 158 self.__bashWriteLine('pi') 159 160 if not loginOk: 161 raise Exception('login fail') 162 163 self.bash('stty cols 256') 164 165 def log(self, fmt, *args): 166 try: 167 msg = fmt % args 168 print('%s - %s - %s' % (self.port, time.strftime('%b %d %H:%M:%S'), msg)) 169 except Exception: 170 pass 171 172 def close(self): 173 self.__handle.close() 174 175 def bash(self, cmd, timeout=10): 176 """ 177 Execute the command in bash. 178 """ 179 self.__bashClearLines() 180 self.__bashWriteLine(cmd) 181 self.__bashExpect(cmd, timeout=timeout, endswith=True) 182 183 response = [] 184 185 deadline = time.time() + timeout 186 while time.time() < deadline: 187 line = self.__bashReadLine() 188 if line is None: 189 time.sleep(0.01) 190 continue 191 192 if line == RPI_FULL_PROMPT: 193 # return response lines without prompt 194 return response 195 196 response.append(line) 197 198 self.__bashWrite('\x03') 199 raise Exception('%s: failed to find end of response' % self.port) 200 201 def __bashExpect(self, expected, timeout=20, endswith=False): 202 self.log('Expecting [%r]' % (expected)) 203 204 deadline = time.time() + timeout 205 while time.time() < deadline: 206 line = self.__bashReadLine() 207 if line is None: 208 time.sleep(0.01) 209 continue 210 211 print('[%s] Got line [%r]' % (self.port, line)) 212 213 if endswith: 214 matched = line.endswith(expected) 215 else: 216 matched = line == expected 217 218 if matched: 219 print('[%s] Expected [%r]' % (self.port, expected)) 220 return 221 222 # failed to find the expected string 223 # send Ctrl+C to terminal 224 self.__bashWrite('\x03') 225 raise Exception('failed to find expected string[%s]' % expected) 226 227 def __bashRead(self, timeout=1): 228 deadline = time.time() + timeout 229 data = '' 230 while True: 231 piece = self.__handle.read() 232 data = data + piece.decode('utf8') 233 if piece: 234 continue 235 236 if data or time.time() >= deadline: 237 break 238 239 if data: 240 self.log('>>> %r', data) 241 242 return data 243 244 def __bashReadLine(self, timeout=1): 245 line = self.__bashGetNextLine() 246 if line is not None: 247 return line 248 249 assert len(self.__lines) == 1, self.__lines 250 tail = self.__lines.pop() 251 252 try: 253 tail += self.__bashRead(timeout=timeout) 254 tail = tail.replace(RPI_FULL_PROMPT, RPI_FULL_PROMPT + '\r\n') 255 tail = tail.replace(RPI_USERNAME_PROMPT, RPI_USERNAME_PROMPT + '\r\n') 256 tail = tail.replace(RPI_PASSWORD_PROMPT, RPI_PASSWORD_PROMPT + '\r\n') 257 finally: 258 self.__lines += [l.rstrip('\r') for l in LINESEPX.split(tail)] 259 assert len(self.__lines) >= 1, self.__lines 260 261 return self.__bashGetNextLine() 262 263 def __bashGetNextLine(self): 264 assert len(self.__lines) >= 1, self.__lines 265 while len(self.__lines) > 1: 266 line = self.__lines.pop(0) 267 assert len(self.__lines) >= 1, self.__lines 268 if LOGX.match(line): 269 logging.info('LOG: %s', line) 270 continue 271 else: 272 return line 273 assert len(self.__lines) >= 1, self.__lines 274 return None 275 276 def __bashWrite(self, data): 277 self.__handle.write(data) 278 self.log("<<< %r", data) 279 280 def __bashClearLines(self): 281 assert len(self.__lines) >= 1, self.__lines 282 while self.__bashReadLine(timeout=0) is not None: 283 pass 284 assert len(self.__lines) >= 1, self.__lines 285 286 def __bashWriteLine(self, line): 287 self.__bashWrite(line + '\n') 288 289 290class OpenThread_BR(OpenThreadTHCI, IThci): 291 DEFAULT_COMMAND_TIMEOUT = 20 292 293 IsBorderRouter = True 294 __is_root = False 295 296 def _connect(self): 297 self.log("logging in to Raspberry Pi ...") 298 self.__cli_output_lines = [] 299 self.__syslog_skip_lines = None 300 self.__syslog_last_read_ts = 0 301 302 if self.connectType == 'ip': 303 self.__handle = SSHHandle(self.telnetIp, self.telnetPort, self.telnetUsername, self.telnetPassword) 304 self.__is_root = self.telnetUsername == 'root' 305 else: 306 self.__handle = SerialHandle(self.port, 115200) 307 308 def _disconnect(self): 309 if self.__handle: 310 self.__handle.close() 311 self.__handle = None 312 313 def _deviceBeforeReset(self): 314 if self.isPowerDown: 315 self.log('Powering up the device') 316 self.powerUp() 317 if self.IsHost: 318 self.__stopRadvdService() 319 self.bash('ip -6 addr del 910b::1 dev %s || true' % self.backboneNetif) 320 self.bash('ip -6 addr del fd00:7d03:7d03:7d03::1 dev %s || true' % self.backboneNetif) 321 322 self.stopListeningToAddrAll() 323 324 def _deviceAfterReset(self): 325 self.__dumpSyslog() 326 self.__truncateSyslog() 327 self.__enableAcceptRa() 328 if not self.IsHost: 329 self.__restartAgentService() 330 time.sleep(2) 331 332 def __enableAcceptRa(self): 333 self.bash('sysctl net.ipv6.conf.%s.accept_ra=2' % self.backboneNetif) 334 335 def _beforeRegisterMulticast(self, sAddr='ff04::1234:777a:1', timeout=300): 336 """subscribe to the given ipv6 address (sAddr) in interface and send MLR.req OTA 337 338 Args: 339 sAddr : str : Multicast address to be subscribed and notified OTA. 340 """ 341 342 if self.externalCommissioner is not None: 343 self.externalCommissioner.MLR([sAddr], timeout) 344 return True 345 346 cmd = 'nohup ~/repo/openthread/tests/scripts/thread-cert/mcast6.py wpan0 %s' % sAddr 347 cmd = cmd + ' > /dev/null 2>&1 &' 348 self.bash(cmd) 349 350 @API 351 def setupHost(self, setDp=False, setDua=False): 352 self.IsHost = True 353 354 self.bash('ip -6 addr add 910b::1 dev %s' % self.backboneNetif) 355 356 if setDua: 357 self.bash('ip -6 addr add fd00:7d03:7d03:7d03::1 dev %s' % self.backboneNetif) 358 359 self.__startRadvdService(setDp) 360 361 def _deviceEscapeEscapable(self, string): 362 """Escape CLI escapable characters in the given string. 363 364 Args: 365 string (str): UTF-8 input string. 366 367 Returns: 368 [str]: The modified string with escaped characters. 369 """ 370 return '"' + string + '"' 371 372 @watched 373 def bash(self, cmd, timeout=DEFAULT_COMMAND_TIMEOUT, sudo=True): 374 return self.bash_unwatched(cmd, timeout=timeout, sudo=sudo) 375 376 def bash_unwatched(self, cmd, timeout=DEFAULT_COMMAND_TIMEOUT, sudo=True): 377 if sudo and not self.__is_root: 378 cmd = 'sudo ' + cmd 379 380 return self.__handle.bash(cmd, timeout=timeout) 381 382 # Override send_udp 383 @API 384 def send_udp(self, interface, dst, port, payload): 385 if interface == 0: # Thread Interface 386 super(OpenThread_BR, self).send_udp(interface, dst, port, payload) 387 return 388 389 if interface == 1: 390 ifname = self.backboneNetif 391 else: 392 raise AssertionError('Invalid interface set to send UDP: {} ' 393 'Available interface options: 0 - Thread; 1 - Ethernet'.format(interface)) 394 cmd = '/home/pi/reference-device/send_udp.py %s %s %s %s' % (ifname, dst, port, payload) 395 self.bash(cmd) 396 397 @API 398 def mldv2_query(self): 399 ifname = self.backboneNetif 400 dst = 'ff02::1' 401 402 cmd = '/home/pi/reference-device/send_mld_query.py %s %s' % (ifname, dst) 403 self.bash(cmd) 404 405 @API 406 def ip_neighbors_flush(self): 407 # clear neigh cache on linux 408 cmd1 = 'sudo ip -6 neigh flush nud all nud failed nud noarp dev %s' % self.backboneNetif 409 cmd2 = ('sudo ip -6 neigh list nud all dev %s ' 410 '| cut -d " " -f1 ' 411 '| sudo xargs -I{} ip -6 neigh delete {} dev %s') % (self.backboneNetif, self.backboneNetif) 412 cmd = '%s ; %s' % (cmd1, cmd2) 413 self.bash(cmd, sudo=False) 414 415 @API 416 def ip_neighbors_add(self, addr, lladdr, nud='noarp'): 417 cmd1 = 'sudo ip -6 neigh delete %s dev %s' % (addr, self.backboneNetif) 418 cmd2 = 'sudo ip -6 neigh add %s dev %s lladdr %s nud %s' % (addr, self.backboneNetif, lladdr, nud) 419 cmd = '%s ; %s' % (cmd1, cmd2) 420 self.bash(cmd, sudo=False) 421 422 @API 423 def get_eth_ll(self): 424 cmd = "ip -6 addr list dev %s | grep 'inet6 fe80' | awk '{print $2}'" % self.backboneNetif 425 ret = self.bash(cmd)[0].split('/')[0] 426 return ret 427 428 @API 429 def ping(self, strDestination, ilength=0, hop_limit=5, timeout=5): 430 """ send ICMPv6 echo request with a given length to a unicast destination 431 address 432 433 Args: 434 strDestination: the unicast destination address of ICMPv6 echo request 435 ilength: the size of ICMPv6 echo request payload 436 hop_limit: the hop limit 437 timeout: time before ping() stops 438 """ 439 if hop_limit is None: 440 hop_limit = 5 441 442 if self.IsHost or self.IsBorderRouter: 443 ifName = self.backboneNetif 444 else: 445 ifName = 'wpan0' 446 447 cmd = 'ping -6 -I %s %s -c 1 -s %d -W %d -t %d' % ( 448 ifName, 449 strDestination, 450 int(ilength), 451 int(timeout), 452 int(hop_limit), 453 ) 454 455 self.bash(cmd, sudo=False) 456 time.sleep(timeout) 457 458 def multicast_Ping(self, destination, length=20): 459 """send ICMPv6 echo request with a given length to a multicast destination 460 address 461 462 Args: 463 destination: the multicast destination address of ICMPv6 echo request 464 length: the size of ICMPv6 echo request payload 465 """ 466 hop_limit = 5 467 468 if self.IsHost or self.IsBorderRouter: 469 ifName = self.backboneNetif 470 else: 471 ifName = 'wpan0' 472 473 cmd = 'ping -6 -I %s %s -c 1 -s %d -t %d' % (ifName, destination, str(length), hop_limit) 474 475 self.bash(cmd, sudo=False) 476 477 @API 478 def getGUA(self, filterByPrefix=None, eth=False): 479 """get expected global unicast IPv6 address of Thread device 480 481 note: existing filterByPrefix are string of in lowercase. e.g. 482 '2001' or '2001:0db8:0001:0000". 483 484 Args: 485 filterByPrefix: a given expected global IPv6 prefix to be matched 486 487 Returns: 488 a global IPv6 address 489 """ 490 # get global addrs set if multiple 491 if eth: 492 return self.__getEthGUA(filterByPrefix=filterByPrefix) 493 else: 494 return super(OpenThread_BR, self).getGUA(filterByPrefix=filterByPrefix) 495 496 def __getEthGUA(self, filterByPrefix=None): 497 globalAddrs = [] 498 499 cmd = 'ip -6 addr list dev %s | grep inet6' % self.backboneNetif 500 output = self.bash(cmd, sudo=False) 501 for line in output: 502 # example: inet6 2401:fa00:41:23:274a:1329:3ab9:d953/64 scope global dynamic noprefixroute 503 line = line.strip().split() 504 505 if len(line) < 4 or line[2] != 'scope': 506 continue 507 508 if line[3] != 'global': 509 continue 510 511 addr = line[1].split('/')[0] 512 addr = str(ipaddress.IPv6Address(addr.decode()).exploded) 513 globalAddrs.append(addr) 514 515 if not filterByPrefix: 516 return globalAddrs[0] 517 else: 518 if filterByPrefix[-2:] != '::': 519 filterByPrefix = '%s::' % filterByPrefix 520 prefix = ipaddress.IPv6Network((filterByPrefix + '/64').decode()) 521 for fullIp in globalAddrs: 522 address = ipaddress.IPv6Address(fullIp.decode()) 523 if address in prefix: 524 return fullIp 525 526 def _cliReadLine(self): 527 # read commissioning log if it's commissioning 528 if not self.__cli_output_lines: 529 self.__readSyslogToCli() 530 531 if self.__cli_output_lines: 532 return self.__cli_output_lines.pop(0) 533 534 return None 535 536 @watched 537 def _deviceGetEtherMac(self): 538 # Harness wants it in string. Because wireshark filter for eth 539 # cannot be applies in hex 540 return self.bash('ip addr list dev %s | grep ether' % self.backboneNetif, sudo=False)[0].strip().split()[1] 541 542 @watched 543 def _onCommissionStart(self): 544 assert self.__syslog_skip_lines is None 545 self.__syslog_skip_lines = int(self.bash('wc -l /var/log/syslog', sudo=False)[0].split()[0]) 546 self.__syslog_last_read_ts = 0 547 548 @watched 549 def _onCommissionStop(self): 550 assert self.__syslog_skip_lines is not None 551 self.__syslog_skip_lines = None 552 553 @watched 554 def __startRadvdService(self, setDp=False): 555 assert self.IsHost, "radvd service runs on Host only" 556 557 conf = "EOF" 558 conf += "\ninterface %s" % self.backboneNetif 559 conf += "\n{" 560 conf += "\n AdvSendAdvert on;" 561 conf += "\n" 562 conf += "\n MinRtrAdvInterval 3;" 563 conf += "\n MaxRtrAdvInterval 30;" 564 conf += "\n AdvDefaultPreference low;" 565 conf += "\n" 566 conf += "\n prefix 910b::/64" 567 conf += "\n {" 568 conf += "\n AdvOnLink on;" 569 conf += "\n AdvAutonomous on;" 570 conf += "\n AdvRouterAddr on;" 571 conf += "\n };" 572 if setDp: 573 conf += "\n" 574 conf += "\n prefix fd00:7d03:7d03:7d03::/64" 575 conf += "\n {" 576 conf += "\n AdvOnLink on;" 577 conf += "\n AdvAutonomous off;" 578 conf += "\n AdvRouterAddr off;" 579 conf += "\n };" 580 conf += "\n};" 581 conf += "\nEOF" 582 cmd = 'sh -c "cat >/etc/radvd.conf <<%s"' % conf 583 584 self.bash(cmd) 585 self.bash('service radvd restart') 586 self.bash('service radvd status') 587 588 @watched 589 def __stopRadvdService(self): 590 assert self.IsHost, "radvd service runs on Host only" 591 self.bash('service radvd stop') 592 593 def __readSyslogToCli(self): 594 if self.__syslog_skip_lines is None: 595 return 0 596 597 # read syslog once per second 598 if time.time() < self.__syslog_last_read_ts + 1: 599 return 0 600 601 self.__syslog_last_read_ts = time.time() 602 603 lines = self.bash_unwatched('tail +%d /var/log/syslog' % self.__syslog_skip_lines, sudo=False) 604 for line in lines: 605 m = OTBR_AGENT_SYSLOG_PATTERN.search(line) 606 if not m: 607 continue 608 609 self.__cli_output_lines.append(m.group(1)) 610 611 self.__syslog_skip_lines += len(lines) 612 return len(lines) 613 614 def _cliWriteLine(self, line): 615 cmd = 'ot-ctl -- %s' % line 616 output = self.bash(cmd) 617 # fake the line echo back 618 self.__cli_output_lines.append(line) 619 for line in output: 620 self.__cli_output_lines.append(line) 621 622 def __restartAgentService(self): 623 restart_cmd = self.extraParams.get('cmd-restart-otbr-agent', 'systemctl restart otbr-agent') 624 self.bash(restart_cmd) 625 626 def __truncateSyslog(self): 627 self.bash('truncate -s 0 /var/log/syslog') 628 629 def __dumpSyslog(self): 630 output = self.bash_unwatched('grep "otbr-agent" /var/log/syslog') 631 for line in output: 632 self.log('%s', line) 633 634 @API 635 def mdns_query(self, dst='ff02::fb', service='_meshcop._udp.local', addrs_blacklist=[]): 636 # For BBR-TC-03 or DH test cases (empty arguments) just send a query 637 if dst == 'ff02::fb' and not addrs_blacklist: 638 self.bash('dig -p 5353 @%s %s ptr' % (dst, service), sudo=False) 639 return 640 641 # For MATN-TC-17 and MATN-TC-18 use Zeroconf to get the BBR address and border agent port 642 cmd = 'python3 ~/repo/openthread/tests/scripts/thread-cert/find_border_agents.py' 643 output = self.bash(cmd) 644 for line in output: 645 print(line) 646 alias, addr, port, thread_status = eval(line) 647 if thread_status == 2 and addr: 648 if (dst and addr in dst) or (addr not in addrs_blacklist): 649 if ipaddress.IPv6Address(addr.decode()).is_link_local: 650 addr = '%s%%%s' % (addr, self.backboneNetif) 651 return addr, port 652 653 raise Exception('No active Border Agents found') 654 655 # Override powerDown 656 @API 657 def powerDown(self): 658 self.log('Powering down BBR') 659 super(OpenThread_BR, self).powerDown() 660 stop_cmd = self.extraParams.get('cmd-stop-otbr-agent', 'systemctl stop otbr-agent') 661 self.bash(stop_cmd) 662 663 # Override powerUp 664 @API 665 def powerUp(self): 666 self.log('Powering up BBR') 667 start_cmd = self.extraParams.get('cmd-start-otbr-agent', 'systemctl start otbr-agent') 668 self.bash(start_cmd) 669 super(OpenThread_BR, self).powerUp() 670 671 # Override forceSetSlaac 672 @API 673 def forceSetSlaac(self, slaacAddress): 674 self.bash('ip -6 addr add %s/64 dev wpan0' % slaacAddress) 675 676 # Override stopListeningToAddr 677 @API 678 def stopListeningToAddr(self, sAddr): 679 """ 680 Unsubscribe to a given IPv6 address which was subscribed earlier wiht `registerMulticast`. 681 682 Args: 683 sAddr : str : Multicast address to be unsubscribed. Use an empty string to unsubscribe 684 all the active multicast addresses. 685 """ 686 cmd = 'pkill -f mcast6.*%s' % sAddr 687 self.bash(cmd) 688 689 def stopListeningToAddrAll(self): 690 return self.stopListeningToAddr('') 691 692 @API 693 def deregisterMulticast(self, sAddr): 694 """ 695 Unsubscribe to a given IPv6 address. 696 Only used by External Commissioner. 697 698 Args: 699 sAddr : str : Multicast address to be unsubscribed. 700 """ 701 self.externalCommissioner.MLR([sAddr], 0) 702 return True 703 704 @watched 705 def _waitBorderRoutingStabilize(self): 706 """ 707 Wait for Network Data to stabilize if BORDER_ROUTING is enabled. 708 """ 709 if not self.isBorderRoutingEnabled(): 710 return 711 712 MAX_TIMEOUT = 30 713 MIN_TIMEOUT = 15 714 CHECK_INTERVAL = 3 715 716 time.sleep(MIN_TIMEOUT) 717 718 lastNetData = self.getNetworkData() 719 for i in range((MAX_TIMEOUT - MIN_TIMEOUT) // CHECK_INTERVAL): 720 time.sleep(CHECK_INTERVAL) 721 curNetData = self.getNetworkData() 722 723 # Wait until the Network Data is not changing, and there is OMR Prefix and External Routes available 724 if curNetData == lastNetData and len(curNetData['Prefixes']) > 0 and len(curNetData['Routes']) > 0: 725 break 726 727 lastNetData = curNetData 728 729 return lastNetData 730