mirror of
https://github.com/espressif/openthread.git
synced 2026-06-06 05:24:51 +00:00
[thci] enhance THCI for 1.2 Certification (#6609)
This commit enhances THCI for 1.2 Certification: - OpenThread.py: 1.2 non-BR - OpenThread_BR.py: 1.2 BR and Host (otbr-agent solution) This commit should also make THCI work for both TH1.1 and TH1.2.
This commit is contained in:
+614
-154
File diff suppressed because it is too large
Load Diff
@@ -30,15 +30,15 @@
|
||||
>> Device : OpenThread_BR THCI
|
||||
>> Class : OpenThread_BR
|
||||
"""
|
||||
import re
|
||||
|
||||
import logging
|
||||
import serial
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
import serial
|
||||
from GRLLibs.UtilityModules.ModuleHelper import ModuleHelper
|
||||
from IThci import IThci
|
||||
from THCI.OpenThread import OpenThreadTHCI, watched
|
||||
from THCI.OpenThread import OpenThreadTHCI, watched, API
|
||||
|
||||
RPI_FULL_PROMPT = 'pi@raspberrypi:~$ '
|
||||
RPI_USERNAME_PROMPT = 'raspberrypi login: '
|
||||
@@ -60,35 +60,70 @@ assert OTBR_AGENT_SYSLOG_PATTERN.search(
|
||||
class SSHHandle(object):
|
||||
|
||||
def __init__(self, ip, port, username, password):
|
||||
self.ip = ip
|
||||
self.port = int(port)
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.__handle = None
|
||||
|
||||
self.__connect()
|
||||
|
||||
def __connect(self):
|
||||
import paramiko
|
||||
self.port = '%s:%d' % (ip, port)
|
||||
|
||||
self.close()
|
||||
|
||||
self.__handle = paramiko.SSHClient()
|
||||
self.__handle.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
self.__handle.connect(ip, port=int(port), username=username, password=password)
|
||||
self.__handle.connect(self.ip, port=self.port, username=self.username, password=self.password)
|
||||
|
||||
def close(self):
|
||||
self.__handle.close()
|
||||
if self.__handle is not None:
|
||||
self.__handle.close()
|
||||
self.__handle = None
|
||||
|
||||
def bash(self, cmd, timeout):
|
||||
stdin, stdout, stderr = self.__handle.exec_command(cmd, timeout=timeout)
|
||||
from paramiko import SSHException
|
||||
retry = 3
|
||||
for i in range(retry):
|
||||
try:
|
||||
stdin, stdout, stderr = self.__handle.exec_command(cmd, timeout=timeout)
|
||||
|
||||
sys.stderr.write(stderr.read())
|
||||
output = [r.encode('utf8').rstrip('\r\n') for r in stdout.readlines()]
|
||||
return output
|
||||
sys.stderr.write(stderr.read())
|
||||
output = [r.encode('utf8').rstrip('\r\n') for r in stdout.readlines()]
|
||||
return output
|
||||
|
||||
except Exception:
|
||||
if i < retry - 1:
|
||||
print('SSH connection is lost, try reconnect after 1 second.')
|
||||
time.sleep(1)
|
||||
self.__connect()
|
||||
else:
|
||||
raise
|
||||
|
||||
def log(self, fmt, *args):
|
||||
try:
|
||||
msg = fmt % args
|
||||
print('%s - %s - %s' % (self.port, time.strftime('%b %d %H:%M:%S'), msg))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def SerialHandle(object):
|
||||
class SerialHandle:
|
||||
|
||||
def __init__(self, port, baudrate):
|
||||
self.port = port
|
||||
self.__handle = serial.Serial(port, baudrate, timeout=0)
|
||||
|
||||
self.__lines = ['']
|
||||
assert len(self.__lines) >= 1, self.__lines
|
||||
|
||||
self.log("inputing username ...")
|
||||
self.__bashWriteLine('pi')
|
||||
deadline = time.time() + 20
|
||||
loginOk = False
|
||||
while time.time() < deadline:
|
||||
self.sleep(1)
|
||||
time.sleep(1)
|
||||
|
||||
lastLine = None
|
||||
while True:
|
||||
@@ -122,13 +157,13 @@ def SerialHandle(object):
|
||||
def close(self):
|
||||
self.__handle.close()
|
||||
|
||||
def bash(self, cmd, timeout):
|
||||
def bash(self, cmd, timeout=10):
|
||||
"""
|
||||
Execute the command in bash.
|
||||
"""
|
||||
self.__bashClearLines()
|
||||
self.__bashWriteLine(cmd)
|
||||
self.__bashExpect(cmd, endswith=True)
|
||||
self.__bashExpect(cmd, timeout=timeout, endswith=True)
|
||||
|
||||
response = []
|
||||
|
||||
@@ -136,7 +171,7 @@ def SerialHandle(object):
|
||||
while time.time() < deadline:
|
||||
line = self.__bashReadLine()
|
||||
if line is None:
|
||||
self.sleep(0.01)
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
if line == RPI_FULL_PROMPT:
|
||||
@@ -148,14 +183,14 @@ def SerialHandle(object):
|
||||
self.__bashWrite('\x03')
|
||||
raise Exception('%s: failed to find end of response' % self.port)
|
||||
|
||||
def __bashExpect(self, expected, timeout=DEFAULT_COMMAND_TIMEOUT, endswith=False):
|
||||
def __bashExpect(self, expected, timeout=20, endswith=False):
|
||||
print('[%s] Expecting [%r]' % (self.port, expected))
|
||||
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
line = self.__bashReadLine()
|
||||
if line is None:
|
||||
self.sleep(0.01)
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
print('[%s] Got line [%r]' % (self.port, line))
|
||||
@@ -179,7 +214,7 @@ def SerialHandle(object):
|
||||
data = ''
|
||||
while True:
|
||||
piece = self.__handle.read()
|
||||
data = data + piece
|
||||
data = data + piece.decode('utf8')
|
||||
if piece:
|
||||
continue
|
||||
|
||||
@@ -240,6 +275,8 @@ def SerialHandle(object):
|
||||
class OpenThread_BR(OpenThreadTHCI, IThci):
|
||||
DEFAULT_COMMAND_TIMEOUT = 20
|
||||
|
||||
IsBorderRouter = True
|
||||
|
||||
def _connect(self):
|
||||
self.log("logining Raspberry Pi ...")
|
||||
self.__cli_output_lines = []
|
||||
@@ -250,23 +287,194 @@ class OpenThread_BR(OpenThreadTHCI, IThci):
|
||||
self.__handle = SSHHandle(self.telnetIp, self.telnetPort, self.telnetUsername, self.telnetPassword)
|
||||
else:
|
||||
self.__handle = SerialHandle(self.port, 115200)
|
||||
self.__lines = ['']
|
||||
assert len(self.__lines) >= 1, self.__lines
|
||||
|
||||
self.__truncateSyslog()
|
||||
self.__afterConnect()
|
||||
|
||||
def _disconnect(self):
|
||||
if self.__handle:
|
||||
self.__handle.close()
|
||||
self.__handle = None
|
||||
|
||||
def _onReset(self):
|
||||
def _deviceBeforeReset(self):
|
||||
if self.IsHost:
|
||||
self.__stopRadvdService()
|
||||
self.bash('sudo ip -6 addr del 910b::1 dev eth0 || true')
|
||||
self.bash('sudo ip -6 addr del fd00:7d03:7d03:7d03::1 dev eth0 || true')
|
||||
|
||||
def _deviceAfterReset(self):
|
||||
self.__dumpSyslog()
|
||||
self.__truncateSyslog()
|
||||
|
||||
@API
|
||||
def setupHost(self, setDua=False):
|
||||
self.IsHost = True
|
||||
|
||||
if not setDua:
|
||||
cmd = 'sudo ip -6 addr add 910b::1 dev eth0'
|
||||
else:
|
||||
cmd = 'sudo ip -6 addr add fd00:7d03:7d03:7d03::1 dev eth0'
|
||||
self.bash(cmd)
|
||||
|
||||
self.__startRadvdService()
|
||||
|
||||
def _deviceEscapeEscapable(self, string):
|
||||
"""Escape CLI escapable characters in the given string.
|
||||
|
||||
Args:
|
||||
string (str): UTF-8 input string.
|
||||
|
||||
Returns:
|
||||
[str]: The modified string with escaped characters.
|
||||
"""
|
||||
return '"' + string + '"'
|
||||
|
||||
@watched
|
||||
def bash(self, cmd, timeout=DEFAULT_COMMAND_TIMEOUT):
|
||||
return self.__handle.bash(cmd, timeout=timeout)
|
||||
|
||||
def bash_unwatched(self, cmd, timeout=DEFAULT_COMMAND_TIMEOUT):
|
||||
return self.__handle.bash(cmd, timeout=timeout)
|
||||
|
||||
# Override send_udp
|
||||
@API
|
||||
def send_udp(self, interface, dst, port, payload):
|
||||
if interface == 0: # Thread Interface
|
||||
super(OpenThread_BR, self).send_udp(interface, dst, port, payload)
|
||||
return
|
||||
|
||||
if interface == 1:
|
||||
ifname = 'eth0'
|
||||
else:
|
||||
print('invalid interface')
|
||||
return
|
||||
|
||||
cmd = 'sudo /home/pi/ot-br-posix/script/reference-device/send_udp.py %s %s %s %s' % (ifname, dst, port,
|
||||
payload)
|
||||
print(cmd)
|
||||
self.bash(cmd)
|
||||
|
||||
@API
|
||||
def ip_neighbors_flush(self):
|
||||
print('%s call clear_cache' % self.port)
|
||||
# clear neigh cache on linux
|
||||
cmd1 = 'sudo ip -6 neigh flush nud all nud failed nud noarp dev eth0'
|
||||
cmd2 = 'sudo ip -6 neigh list nud all dev eth0 ' \
|
||||
'| cut -d " " -f1 ' \
|
||||
'| sudo xargs -I{} ip -6 neigh delete {} dev eth0'
|
||||
cmd = '%s ; %s' % (cmd1, cmd2)
|
||||
self.bash(cmd)
|
||||
|
||||
@API
|
||||
def ip_neighbors_add(self, addr, lladdr, nud='noarp'):
|
||||
print('%s ip_neighbors_add' % self.port)
|
||||
cmd1 = 'sudo ip -6 neigh delete %s dev eth0' % addr
|
||||
cmd2 = 'sudo ip -6 neigh add %s dev eth0 lladdr %s nud %s' % (addr, lladdr, nud)
|
||||
cmd = '%s ; %s' % (cmd1, cmd2)
|
||||
self.bash(cmd)
|
||||
|
||||
@API
|
||||
def get_eth_ll(self):
|
||||
print('%s get_eth_ll' % self.port)
|
||||
cmd = "ip -6 addr list dev eth0 | grep 'inet6 fe80' | awk '{print $2}'"
|
||||
ret = self.bash(cmd)[0].split('/')[0]
|
||||
return ret
|
||||
|
||||
@API
|
||||
def ping(self, strDestination, ilength=0, hop_limit=5, timeout=5):
|
||||
""" send ICMPv6 echo request with a given length to a unicast destination
|
||||
address
|
||||
|
||||
Args:
|
||||
strDestination: the unicast destination address of ICMPv6 echo request
|
||||
ilength: the size of ICMPv6 echo request payload
|
||||
hop_limit: the hop limit
|
||||
timeout: time before ping() stops
|
||||
"""
|
||||
if hop_limit is None:
|
||||
hop_limit = 5
|
||||
|
||||
if self.IsHost or self.IsBackboneRouter:
|
||||
ifName = 'eth0'
|
||||
else:
|
||||
ifName = 'wpan0'
|
||||
|
||||
cmd = 'ping -6 -I %s %s -c 1 -s %d -W %d -t %d' % (
|
||||
ifName,
|
||||
strDestination,
|
||||
int(ilength),
|
||||
int(timeout),
|
||||
int(hop_limit),
|
||||
)
|
||||
|
||||
self.bash(cmd)
|
||||
time.sleep(1)
|
||||
|
||||
def multicast_Ping(self, destination, length=20):
|
||||
"""send ICMPv6 echo request with a given length to a multicast destination
|
||||
address
|
||||
|
||||
Args:
|
||||
destination: the multicast destination address of ICMPv6 echo request
|
||||
length: the size of ICMPv6 echo request payload
|
||||
"""
|
||||
print('%s call multicast_Ping' % self.port)
|
||||
print('destination: %s' % destination)
|
||||
hop_limit = 5
|
||||
|
||||
if self.IsHost or self.IsBackboneRouter:
|
||||
ifName = 'eth0'
|
||||
else:
|
||||
ifName = 'wpan0'
|
||||
|
||||
cmd = 'ping -6 -I %s %s -c 1 -s %d -t %d' % (ifName, destination, str(length), hop_limit)
|
||||
|
||||
self.bash(cmd)
|
||||
|
||||
@API
|
||||
def getGUA(self, filterByPrefix=None, eth=False):
|
||||
"""get expected global unicast IPv6 address of Thread device
|
||||
|
||||
note: existing filterByPrefix are string of in lowercase. e.g.
|
||||
'2001' or '2001:0db8:0001:0000".
|
||||
|
||||
Args:
|
||||
filterByPrefix: a given expected global IPv6 prefix to be matched
|
||||
|
||||
Returns:
|
||||
a global IPv6 address
|
||||
"""
|
||||
# get global addrs set if multiple
|
||||
if eth:
|
||||
return self.__getEthGUA(filterByPrefix=filterByPrefix)
|
||||
else:
|
||||
return super(OpenThread_BR, self).getGUA(filterByPrefix=filterByPrefix)
|
||||
|
||||
def __getEthGUA(self, filterByPrefix=None):
|
||||
globalAddrs = []
|
||||
|
||||
cmd = 'ip -6 addr list dev eth0 | grep inet6'
|
||||
output = self.bash(cmd)
|
||||
for line in output:
|
||||
# example: inet6 2401:fa00:41:23:274a:1329:3ab9:d953/64 scope global dynamic noprefixroute
|
||||
line = line.strip().split()
|
||||
|
||||
if len(line) < 4 or line[2] != 'scope':
|
||||
continue
|
||||
|
||||
if line[3] != 'global':
|
||||
continue
|
||||
|
||||
addr = line[1].split('/')[0]
|
||||
addr = ModuleHelper.GetFullIpv6Address(addr).lower()
|
||||
globalAddrs.append(addr)
|
||||
|
||||
if not filterByPrefix:
|
||||
return globalAddrs[0]
|
||||
else:
|
||||
for fullIp in globalAddrs:
|
||||
if fullIp.startswith(filterByPrefix):
|
||||
return fullIp
|
||||
|
||||
def _cliReadLine(self):
|
||||
# read commissioning log if it's commissioning
|
||||
if not self.__cli_output_lines:
|
||||
@@ -277,6 +485,12 @@ class OpenThread_BR(OpenThreadTHCI, IThci):
|
||||
|
||||
return None
|
||||
|
||||
@watched
|
||||
def _deviceGetEtherMac(self):
|
||||
# Harness wants it in string. Because wireshark filter for eth
|
||||
# cannot be applies in hex
|
||||
return self.bash('ip addr list dev eth0 | grep ether')[0].strip().split()[1]
|
||||
|
||||
@watched
|
||||
def _onCommissionStart(self):
|
||||
assert self.__syslog_skip_lines is None
|
||||
@@ -288,6 +502,46 @@ class OpenThread_BR(OpenThreadTHCI, IThci):
|
||||
assert self.__syslog_skip_lines is not None
|
||||
self.__syslog_skip_lines = None
|
||||
|
||||
def _deviceBeforeThreadStart(self):
|
||||
self.bash('sudo sysctl net.ipv6.conf.eth0.accept_ra=2')
|
||||
|
||||
@watched
|
||||
def __startRadvdService(self):
|
||||
assert self.IsHost, "radvd service runs on Host only"
|
||||
|
||||
self.bash("""sudo sh -c "cat >/etc/radvd.conf <<EOF
|
||||
interface eth0
|
||||
{
|
||||
AdvSendAdvert on;
|
||||
|
||||
MinRtrAdvInterval 3;
|
||||
MaxRtrAdvInterval 30;
|
||||
AdvDefaultPreference low;
|
||||
|
||||
prefix 910b::/64
|
||||
{
|
||||
AdvOnLink on;
|
||||
AdvAutonomous on;
|
||||
AdvRouterAddr on;
|
||||
};
|
||||
|
||||
prefix fd00:7d03:7d03:7d03::/64
|
||||
{
|
||||
AdvOnLink on;
|
||||
AdvAutonomous off;
|
||||
AdvRouterAddr off;
|
||||
};
|
||||
};
|
||||
EOF"
|
||||
""")
|
||||
self.bash('sudo service radvd restart')
|
||||
self.bash('sudo service radvd status')
|
||||
|
||||
@watched
|
||||
def __stopRadvdService(self):
|
||||
assert self.IsHost, "radvd service runs on Host only"
|
||||
self.bash('sudo service radvd stop')
|
||||
|
||||
def __readSyslogToCli(self):
|
||||
if self.__syslog_skip_lines is None:
|
||||
return 0
|
||||
@@ -298,7 +552,7 @@ class OpenThread_BR(OpenThreadTHCI, IThci):
|
||||
|
||||
self.__syslog_last_read_ts = time.time()
|
||||
|
||||
lines = self.bash('tail +%d /var/log/syslog' % self.__syslog_skip_lines)
|
||||
lines = self.bash_unwatched('tail +%d /var/log/syslog' % self.__syslog_skip_lines)
|
||||
for line in lines:
|
||||
m = OTBR_AGENT_SYSLOG_PATTERN.search(line)
|
||||
if not m:
|
||||
@@ -310,17 +564,25 @@ class OpenThread_BR(OpenThreadTHCI, IThci):
|
||||
return len(lines)
|
||||
|
||||
def _cliWriteLine(self, line):
|
||||
cmd = 'sudo ot-ctl "%s"' % line
|
||||
cmd = 'sudo ot-ctl -- %s' % line
|
||||
output = self.bash(cmd)
|
||||
# fake the line echo back
|
||||
self.__cli_output_lines.append(line)
|
||||
for line in output:
|
||||
self.__cli_output_lines.append(line)
|
||||
|
||||
def __afterConnect(self):
|
||||
self.__truncateSyslog()
|
||||
self.__checkServiceStatus()
|
||||
|
||||
def __checkServiceStatus(self):
|
||||
self.bash('sudo service radvd stop')
|
||||
self.bash('sudo service otbr-agent restart')
|
||||
|
||||
def __truncateSyslog(self):
|
||||
self.bash('sudo truncate -s 0 /var/log/syslog')
|
||||
|
||||
def __dumpSyslog(self):
|
||||
output = self.bash('sudo grep "otbr-agent" /var/log/syslog')
|
||||
output = self.bash_unwatched('sudo grep "otbr-agent" /var/log/syslog')
|
||||
for line in output:
|
||||
self.log('%s', line)
|
||||
|
||||
Reference in New Issue
Block a user