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