diff --git a/tools/harness-thci/OpenThread.py b/tools/harness-thci/OpenThread.py index d38838415..75e27dad8 100644 --- a/tools/harness-thci/OpenThread.py +++ b/tools/harness-thci/OpenThread.py @@ -31,14 +31,23 @@ >> Class : OpenThread """ +import functools import re -import time -import socket -import logging +import traceback from Queue import Queue +from abc import abstractmethod +import logging import serial -from IThci import IThci +import socket +import time + +from GRLLibs.ThreadPacket.PlatformPackets import ( + PlatformDiagnosticPacket, + PlatformPackets, +) +from GRLLibs.UtilityModules.ModuleHelper import ModuleHelper, ThreadRunner +from GRLLibs.UtilityModules.Plugins.AES_CMAC import Thread_PBKDF2 from GRLLibs.UtilityModules.Test import ( Thread_Device_Role, Device_Data_Requirement, @@ -48,152 +57,145 @@ from GRLLibs.UtilityModules.enums import ( PlatformDiagnosticPacket_Direction, PlatformDiagnosticPacket_Type, ) -from GRLLibs.UtilityModules.ModuleHelper import ModuleHelper, ThreadRunner -from GRLLibs.ThreadPacket.PlatformPackets import ( - PlatformDiagnosticPacket, - PlatformPackets, -) -from GRLLibs.UtilityModules.Plugins.AES_CMAC import Thread_PBKDF2 +from IThci import IThci LINESEPX = re.compile(r'\r\n|\n') """regex: used to split lines""" +logging.basicConfig(level=logging.DEBUG, + format="%(asctime)s - %(levelname)s - %(message)s") -class OpenThread(IThci): +_callStackDepth = 0 + + +def watched(func): + func_name = func.func_name + + @functools.wraps(func) + def wrapped_api_func(self, *args, **kwargs): + global _callStackDepth + callstr = '====' * _callStackDepth + "> %s%s%s" % ( + func_name, str(args) if args else "", str(kwargs) if kwargs else "") + + _callStackDepth += 1 + try: + self.log("%s starts ...", callstr) + ret = func(self, *args, **kwargs) + self.log("%s returns %r", callstr, ret) + return ret + except Exception as ex: + self.log("FUNC %s failed: %s\n%s", func_name, str(ex), + traceback.format_exc()) + raise + finally: + _callStackDepth -= 1 + + return wrapped_api_func + + +def retry(n, interval=0): + assert n >= 0, n + + def deco(func): + + @functools.wraps(func) + def retried_func(*args, **kwargs): + for i in range(n + 1): + try: + return func(*args, **kwargs) + except Exception: + if i == n: + raise + + time.sleep(interval) + + return retried_func + + return deco + + +def API(api_func): + return watched(api_func) + + +def commissioning(func): + + @functools.wraps(func) + def comm_func(self, *args, **kwargs): + self._onCommissionStart() + try: + return func(self, *args, **kwargs) + finally: + self._onCommissionStop() + + return comm_func + + +class OpenThreadTHCI(object): LOWEST_POSSIBLE_PARTATION_ID = 0x1 LINK_QUALITY_CHANGE_TIME = 100 - - # Used for reference firmware version control for Test Harness. - # This variable will be updated to match the OpenThread reference firmware - # officially released. firmwarePrefix = 'OPENTHREAD/' - _update_router_status = False - # def __init__(self, SerialPort=COMPortName, EUI=MAC_Address): def __init__(self, **kwargs): """initialize the serial port and default network parameters Args: **kwargs: Arbitrary keyword arguments Includes 'EUI' and 'SerialPort' """ - try: - self.UIStatusMsg = '' - self.mac = kwargs.get('EUI') - self.port = kwargs.get('SerialPort') - self.handle = None - self.AutoDUTEnable = False - self._is_net = False # whether device is through ser2net - self.logStatus = { - 'stop': 'stop', - 'running': 'running', - 'pauseReq': 'pauseReq', - 'paused': 'paused', - } - self.joinStatus = { - 'notstart': 'notstart', - 'ongoing': 'ongoing', - 'succeed': 'succeed', - 'failed': 'failed', - } - self.logThreadStatus = self.logStatus['stop'] - self.intialize() - except Exception as e: - ModuleHelper.WriteIntoDebugLogger('initialize() Error: ' + str(e)) + self.intialize(kwargs) - def __del__(self): - """close the serial port connection""" - try: - self.closeConnection() - self.deviceConnected = False - except Exception as e: - ModuleHelper.WriteIntoDebugLogger('delete() Error: ' + str(e)) - - def _expect(self, expected, times=50): - """Find the `expected` line within `times` trials. - - Args: - expected str: the expected string - times int: number of trials + @abstractmethod + def _connect(self): """ - print('[%s] Expecting [%s]' % (self.port, expected)) + Connect to the device. + """ + pass - retry_times = 10 - while times > 0 and retry_times > 0: - line = self._readline() - print('[%s] Got line [%s]' % (self.port, line)) + @abstractmethod + def _disconnect(self): + """ + Disconnect from the device + """ + pass - if line == expected: - print('[%s] Expected [%s]' % (self.port, expected)) - return - - if not line: - retry_times -= 1 - time.sleep(0.1) - - times -= 1 - - raise Exception('failed to find expected string[%s]' % expected) - - def _read(self, size=512): - logging.info('%s: reading', self.port) - if self._is_net: - return self.handle.recv(size) - else: - return self.handle.read(size) - - def _write(self, data): - logging.info('%s: writing', self.port) - if self._is_net: - self.handle.sendall(data) - else: - self.handle.write(data) - - def _readline(self): + @abstractmethod + def _cliReadLine(self): """Read exactly one line from the device Returns: - None on no data + None if no data """ - logging.info('%s: reading line', self.port) - if len(self._lines) > 1: - return self._lines.pop(0) + pass - tail = '' - if len(self._lines): - tail = self._lines.pop() - - try: - tail += self._read() - except socket.error: - logging.exception('%s: No new data', self.port) - time.sleep(0.1) - - self._lines += LINESEPX.split(tail) - if len(self._lines) > 1: - return self._lines.pop(0) - - def _sendline(self, line): + @abstractmethod + def _cliWriteLine(self, line): """Send exactly one line to the device Args: line str: data send to device """ - logging.info('%s: sending line', self.port) - # clear buffer - self._lines = [] - try: - self._read() - except socket.error: - logging.debug('%s: Nothing cleared', self.port) + pass - print('sending [%s]' % line) - self._write(line + '\r\n') + @abstractmethod + def _onCommissionStart(self): + """Called when commissioning starts.""" + pass - # wait for write to complete - time.sleep(0.1) + @abstractmethod + def _onCommissionStop(self): + """Called when commissioning stops.""" + pass - def __sendCommand(self, cmd): + def __sendCommand(self, cmd, expectEcho=True): + self.log("command: %s", cmd) + self._cliWriteLine(cmd) + if expectEcho: + self.__expect(cmd, endswith=True) + + @retry(3) + @watched + def __executeCommand(self, cmd, timeout=10): """send specific command to reference unit over serial port Args: @@ -204,54 +206,126 @@ class OpenThread(IThci): Value: successfully retrieve the desired value from reference unit Error: some errors occur, indicates by the followed specific error number """ - logging.info('%s: sendCommand[%s]', self.port, cmd) if self.logThreadStatus == self.logStatus['running']: self.logThreadStatus = self.logStatus['pauseReq'] while (self.logThreadStatus != self.logStatus['paused'] and self.logThreadStatus != self.logStatus['stop']): pass - try: - # command retransmit times - retry_times = 3 - while retry_times > 0: - retry_times -= 1 - try: - self._sendline(cmd) - self._expect(cmd) - except Exception as e: - logging.exception( - '%s: failed to send command[%s]: %s', - self.port, - cmd, - str(e), - ) - if retry_times == 0: - raise - else: + self.__sendCommand(cmd) + line = None + response = [] + + t_end = time.time() + timeout + while time.time() < t_end: + line = self._cliReadLine() + if line is None: + time.sleep(0.01) + continue + + self.log("readline: %s", line) + response.append(line) + + if line == 'Done': + break + + if line != 'Done': + raise Exception('%s: failed to find end of response: %s' % + (self.port, response)) + + return response + + def __expect(self, expected, timeout=5, endswith=False): + """Find the `expected` line within `times` tries. + + Args: + expected str: the expected string + times int: number of tries + """ + print('[%s] Expecting [%s]' % (self.port, expected)) + + deadline = time.time() + timeout + while True: + line = self._cliReadLine() + if line is not None: + self.log("readline: %s", line) + + if line is None: + if time.time() >= deadline: break - line = None - response = [] - retry_times = 10 - while retry_times > 0: - line = self._readline() - logging.info('%s: the read line is[%s]', self.port, line) - if line: - response.append(line) - if line == 'Done': - break - else: - retry_times -= 1 - time.sleep(0.2) - if line != 'Done': - raise Exception('%s: failed to find end of response' % - self.port) - logging.info('%s: send command[%s] done!', self.port, cmd) - return response - except Exception as e: - ModuleHelper.WriteIntoDebugLogger('sendCommand() Error: ' + str(e)) - raise + self.sleep(0.01) + continue + + matched = line.endswith(expected) if endswith else line == expected + if matched: + print('[%s] Expected [%s]' % (self.port, expected)) + return + + raise Exception('failed to find expected string[%s]' % expected) + + @API + def getVersionNumber(self): + """get OpenThread stack firmware version number""" + return self.__executeCommand('version')[0] + + def log(self, fmt, *args): + try: + msg = fmt % args + logging.info('%s - %s', self.port, msg) + except Exception: + pass + + def sleep(self, duration): + if duration >= 1: + self.log("sleeping for %ss ...", duration) + time.sleep(duration) + + @API + def intialize(self, params): + """initialize the serial port with baudrate, timeout parameters""" + self.log('%s intialize: %s', self.__class__.__name__, params) + + self.mac = params.get('EUI') + self.port = params.get('SerialPort') + + self.UIStatusMsg = '' + self.AutoDUTEnable = False + self._is_net = False # whether device is through ser2net + self.logStatus = { + 'stop': 'stop', + 'running': 'running', + 'pauseReq': 'pauseReq', + 'paused': 'paused', + } + self.joinStatus = { + 'notstart': 'notstart', + 'ongoing': 'ongoing', + 'succeed': 'succeed', + 'failed': 'failed', + } + self.logThreadStatus = self.logStatus['stop'] + + self.deviceConnected = False + + # init serial port + self._connect() + + self.UIStatusMsg = self.getVersionNumber() + + if self.firmwarePrefix in self.UIStatusMsg: + self.deviceConnected = True + else: + self.UIStatusMsg = ('Firmware Not Matching Expecting ' + + self.firmwarePrefix + ', Now is ' + + self.UIStatusMsg) + ModuleHelper.WriteIntoDebugLogger( + 'Err: OpenThread device Firmware not matching..') + + @API + def closeConnection(self): + """close current serial port connection""" + self._disconnect() def __disableRouterEligible(self): """disable router role @@ -259,7 +333,7 @@ class OpenThread(IThci): print('call __disableRouterEligible') try: cmd = 'routereligible disable' - self.__sendCommand(cmd) + self.__executeCommand(cmd) except Exception as e: ModuleHelper.WriteIntoDebugLogger( '__disableRouterEligible() Error: ' + str(e)) @@ -282,7 +356,7 @@ class OpenThread(IThci): print(mode) try: cmd = 'mode %s' % mode - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('setDeviceMode() Error: ' + str(e)) @@ -302,7 +376,7 @@ class OpenThread(IThci): try: cmd = 'routerupgradethreshold %s' % str(iThreshold) print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger( 'setRouterUpgradeThreshold() Error: ' + str(e)) @@ -323,7 +397,7 @@ class OpenThread(IThci): try: cmd = 'routerdowngradethreshold %s' % str(iThreshold) print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger( 'setRouterDowngradeThreshold() Error: ' + str(e)) @@ -342,7 +416,7 @@ class OpenThread(IThci): try: cmd = 'routerselectionjitter %s' % str(iRouterJitter) print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger( 'setRouterSelectionJitter() Error: ' + str(e)) @@ -357,7 +431,7 @@ class OpenThread(IThci): print('call setAddressFilterMode() ' + mode) try: cmd = 'macfilter addr ' + mode - if self.__sendCommand(cmd)[-1] == 'Done': + if self.__executeCommand(cmd)[-1] == 'Done': return True return False except Exception as e: @@ -374,7 +448,7 @@ class OpenThread(IThci): print('call startOpenThread') try: if self.hasActiveDatasetToCommit: - if self.__sendCommand('dataset commit active')[0] != 'Done': + if self.__executeCommand('dataset commit active')[0] != 'Done': raise Exception('failed to commit active dataset') else: self.hasActiveDatasetToCommit = False @@ -398,8 +472,8 @@ class OpenThread(IThci): ]: self.__setRouterSelectionJitter(1) - if self.__sendCommand('ifconfig up')[-1] == 'Done': - if self.__sendCommand('thread start')[-1] == 'Done': + if self.__executeCommand('ifconfig up')[-1] == 'Done': + if self.__executeCommand('thread start')[-1] == 'Done': self.isPowerDown = False return True else: @@ -417,8 +491,8 @@ class OpenThread(IThci): """ print('call stopOpenThread') try: - if self.__sendCommand('thread stop')[-1] == 'Done': - return self.__sendCommand('ifconfig down')[-1] == 'Done' + if self.__executeCommand('thread stop')[-1] == 'Done': + return self.__executeCommand('ifconfig down')[-1] == 'Done' else: return False except Exception as e: @@ -433,7 +507,7 @@ class OpenThread(IThci): False: OpenThread is not running """ print('call isOpenThreadRunning') - return self.__sendCommand('state')[0] != 'disabled' + return self.__executeCommand('state')[0] != 'disabled' # rloc16 might be hex string or integer, need to return actual allocated # router id @@ -447,14 +521,14 @@ class OpenThread(IThci): actual router id allocated by leader """ routerList = [] - routerList = self.__sendCommand('router list')[0].split() + routerList = self.__executeCommand('router list')[0].split() print(routerList) print(xRloc16) for index in routerList: router = [] cmd = 'router %s' % index - router = self.__sendCommand(cmd) + router = self.__executeCommand(cmd) for line in router: if 'Done' in line: @@ -518,6 +592,7 @@ class OpenThread(IThci): return fmt % iValue + @commissioning def __readCommissioningLogs(self, durationInSeconds): """read logs during the commissioning process @@ -530,31 +605,39 @@ class OpenThread(IThci): self.logThreadStatus = self.logStatus['running'] logs = Queue() t_end = time.time() + durationInSeconds + joinSucceed = False + while time.time() < t_end: - time.sleep(0.3) if self.logThreadStatus == self.logStatus['pauseReq']: self.logThreadStatus = self.logStatus['paused'] if self.logThreadStatus != self.logStatus['running']: + self.sleep(0.01) continue try: - line = self._readline() + line = self._cliReadLine() + if line: - print(line) + self.log("commissioning log: %s", line) logs.put(line) if 'Join success' in line: - self.joinCommissionedStatus = self.joinStatus['succeed'] - break + joinSucceed = True + # read commissioning logs for 3 more seconds + t_end = time.time() + 3 elif 'Join failed' in line: - self.joinCommissionedStatus = self.joinStatus['failed'] - break + # read commissioning logs for 3 more seconds + t_end = time.time() + 3 + elif line is None: + self.sleep(0.01) except Exception: pass + self.joinCommissionedStatus = self.joinStatus[ + 'succeed'] if joinSucceed else self.joinStatus['failed'] self.logThreadStatus = self.logStatus['stop'] return logs @@ -580,7 +663,7 @@ class OpenThread(IThci): try: cmd = 'dataset channelmask %s' % channelMask self.hasActiveDatasetToCommit = True - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('setChannelMask() Error: ' + str(e)) @@ -593,7 +676,7 @@ class OpenThread(IThci): securityPolicyFlags, ) self.hasActiveDatasetToCommit = True - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('setSecurityPolicy() Error: ' + str(e)) @@ -612,8 +695,8 @@ class OpenThread(IThci): print(iKeySwitchGuardTime) try: cmd = 'keysequence guardtime %s' % str(iKeySwitchGuardTime) - if self.__sendCommand(cmd)[-1] == 'Done': - time.sleep(1) + if self.__executeCommand(cmd)[-1] == 'Done': + self.sleep(1) return True else: return False @@ -624,7 +707,7 @@ class OpenThread(IThci): def __getCommissionerSessionId(self): """ get the commissioner session id allocated from Leader """ print('%s call getCommissionerSessionId' % self.port) - return self.__sendCommand('commissioner sessionid')[0] + return self.__executeCommand('commissioner sessionid')[0] def __escapeEscapable(self, string): """Escape CLI escapable characters in the given string. @@ -640,56 +723,7 @@ class OpenThread(IThci): string = string.replace(char, '\\%s' % char) return string - def _connect(self): - print('My port is %s' % self.port) - if self.port.startswith('COM'): - self.handle = serial.Serial(self.port, 115200, timeout=0) - time.sleep(1) - self.handle.write('\r\n') - time.sleep(0.1) - self._is_net = False - elif ':' in self.port: - host, port = self.port.split(':') - self.handle = socket.create_connection((host, port)) - self.handle.setblocking(0) - self._is_net = True - else: - raise Exception('Unknown port schema') - self.UIStatusMsg = self.getVersionNumber() - - def closeConnection(self): - """close current serial port connection""" - print('%s call closeConnection' % self.port) - try: - if self.handle: - self.handle.close() - self.handle = None - except Exception as e: - ModuleHelper.WriteIntoDebugLogger('closeConnection() Error: ' + - str(e)) - - def intialize(self): - """initialize the serial port with baudrate, timeout parameters""" - print('%s call intialize' % self.port) - try: - self.deviceConnected = False - - # init serial port - self._connect() - - if self.firmwarePrefix in self.UIStatusMsg: - self.deviceConnected = True - else: - self.UIStatusMsg = ('Firmware Not Matching Expecting ' + - self.firmwarePrefix + ' Now is ' + - self.UIStatusMsg) - ModuleHelper.WriteIntoDebugLogger( - 'Err: OpenThread device Firmware not matching..') - - except Exception as e: - ModuleHelper.WriteIntoDebugLogger('intialize() Error: ' + str(e)) - self.deviceConnected = False - + @API def setNetworkName(self, networkName='GRL'): """set Thread Network name @@ -707,12 +741,13 @@ class OpenThread(IThci): cmd = 'networkname %s' % networkName datasetCmd = 'dataset networkname %s' % networkName self.hasActiveDatasetToCommit = True - return (self.__sendCommand(cmd)[-1] == 'Done' and - self.__sendCommand(datasetCmd)[-1] == 'Done') + return (self.__executeCommand(cmd)[-1] == 'Done' and + self.__executeCommand(datasetCmd)[-1] == 'Done') except Exception as e: ModuleHelper.WriteIntoDebugLogger('setNetworkName() Error: ' + str(e)) + @API def setChannel(self, channel=11): """set channel of Thread device operates on. @@ -732,16 +767,18 @@ class OpenThread(IThci): cmd = 'channel %s' % channel datasetCmd = 'dataset channel %s' % channel self.hasActiveDatasetToCommit = True - return (self.__sendCommand(cmd)[-1] == 'Done' and - self.__sendCommand(datasetCmd)[-1] == 'Done') + return (self.__executeCommand(cmd)[-1] == 'Done' and + self.__executeCommand(datasetCmd)[-1] == 'Done') except Exception as e: ModuleHelper.WriteIntoDebugLogger('setChannel() Error: ' + str(e)) + @API def getChannel(self): """get current channel""" print('%s call getChannel' % self.port) - return self.__sendCommand('channel')[0] + return self.__executeCommand('channel')[0] + @API def setMAC(self, xEUI): """set the extended addresss of Thread device @@ -763,7 +800,7 @@ class OpenThread(IThci): cmd = 'extaddr %s' % address64 print(cmd) - if self.__sendCommand(cmd)[-1] == 'Done': + if self.__executeCommand(cmd)[-1] == 'Done': self.mac = address64 return True else: @@ -771,6 +808,7 @@ class OpenThread(IThci): except Exception as e: ModuleHelper.WriteIntoDebugLogger('setMAC() Error: ' + str(e)) + @API def getMAC(self, bType=MacType.RandomMac): """get one specific type of MAC address currently OpenThread only supports Random MAC address @@ -788,30 +826,33 @@ class OpenThread(IThci): macAddr64 = self.mac else: if bType == MacType.FactoryMac: - macAddr64 = self.__sendCommand('eui64')[0] + macAddr64 = self.__executeCommand('eui64')[0] elif bType == MacType.HashMac: - macAddr64 = self.__sendCommand('joiner id')[0] + macAddr64 = self.__executeCommand('joiner id')[0] else: - macAddr64 = self.__sendCommand('extaddr')[0] + macAddr64 = self.__executeCommand('extaddr')[0] print(macAddr64) return int(macAddr64, 16) + @API def getLL64(self): """get link local unicast IPv6 address""" print('%s call getLL64' % self.port) - return self.__sendCommand('ipaddr linklocal')[0] + return self.__executeCommand('ipaddr linklocal')[0] + @API def getRloc16(self): """get rloc16 short address""" print('%s call getRloc16' % self.port) - rloc16 = self.__sendCommand('rloc16')[0] + rloc16 = self.__executeCommand('rloc16')[0] return int(rloc16, 16) + @API def getRloc(self): """get router locator unicast Ipv6 address""" print('%s call getRloc' % self.port) - return self.__sendCommand('ipaddr rloc')[0] + return self.__executeCommand('ipaddr rloc')[0] def __getGlobal(self): """get global unicast IPv6 address set @@ -821,7 +862,7 @@ class OpenThread(IThci): globalAddrs = [] rlocAddr = self.getRloc() - addrs = self.__sendCommand('ipaddr') + addrs = self.__executeCommand('ipaddr') # take rloc address as a reference for current mesh local prefix, # because for some TCs, mesh local prefix may be updated through @@ -847,6 +888,7 @@ class OpenThread(IThci): return globalAddrs + @API def setNetworkKey(self, key): """set Thread Network master key @@ -872,12 +914,13 @@ class OpenThread(IThci): self.networkKey = masterKey self.hasActiveDatasetToCommit = True - return (self.__sendCommand(cmd)[-1] == 'Done' and - self.__sendCommand(datasetCmd)[-1] == 'Done') + return (self.__executeCommand(cmd)[-1] == 'Done' and + self.__executeCommand(datasetCmd)[-1] == 'Done') except Exception as e: ModuleHelper.WriteIntoDebugLogger('setNetworkkey() Error: ' + str(e)) + @API def addBlockedMAC(self, xEUI): """add a given extended address to the blacklist entry @@ -907,7 +950,7 @@ class OpenThread(IThci): cmd = 'macfilter addr add %s' % macAddr print(cmd) - ret = self.__sendCommand(cmd)[-1] == 'Done' + ret = self.__executeCommand(cmd)[-1] == 'Done' self._addressfilterSet.add(macAddr) print('current blacklist entries:') @@ -919,6 +962,7 @@ class OpenThread(IThci): ModuleHelper.WriteIntoDebugLogger('addBlockedMAC() Error: ' + str(e)) + @API def addAllowMAC(self, xEUI): """add a given extended address to the whitelist addressfilter @@ -943,7 +987,7 @@ class OpenThread(IThci): cmd = 'macfilter addr add %s' % macAddr print(cmd) - ret = self.__sendCommand(cmd)[-1] == 'Done' + ret = self.__executeCommand(cmd)[-1] == 'Done' self._addressfilterSet.add(macAddr) print('current whitelist entries:') @@ -954,6 +998,7 @@ class OpenThread(IThci): except Exception as e: ModuleHelper.WriteIntoDebugLogger('addAllowMAC() Error: ' + str(e)) + @API def clearBlockList(self): """clear all entries in blacklist table @@ -974,7 +1019,7 @@ class OpenThread(IThci): self._addressfilterMode = 'disable' # clear ops cmd = 'macfilter addr clear' - if self.__sendCommand(cmd)[-1] == 'Done': + if self.__executeCommand(cmd)[-1] == 'Done': self._addressfilterSet.clear() return True return False @@ -982,6 +1027,7 @@ class OpenThread(IThci): ModuleHelper.WriteIntoDebugLogger('clearBlockList() Error: ' + str(e)) + @API def clearAllowList(self): """clear all entries in whitelist table @@ -1002,7 +1048,7 @@ class OpenThread(IThci): self._addressfilterMode = 'disable' # clear ops cmd = 'macfilter addr clear' - if self.__sendCommand(cmd)[-1] == 'Done': + if self.__executeCommand(cmd)[-1] == 'Done': self._addressfilterSet.clear() return True return False @@ -1010,11 +1056,13 @@ class OpenThread(IThci): ModuleHelper.WriteIntoDebugLogger('clearAllowList() Error: ' + str(e)) + @API def getDeviceRole(self): """get current device role in Thread Network""" print('%s call getDeviceRole' % self.port) - return self.__sendCommand('state')[0] + return self.__executeCommand('state')[0] + @API def joinNetwork(self, eRoleId): """make device ready to join the Thread Network with a given role @@ -1075,12 +1123,12 @@ class OpenThread(IThci): # start OpenThread self.__startOpenThread() - time.sleep(3) + self.sleep(3) if self._update_router_status and eRoleId == Thread_Device_Role.Router: self.__updateRouterStatus() - time.sleep( + self.sleep( 5 ) # increase delay temporally (+5s) to remedy TH's delay updates @@ -1088,6 +1136,7 @@ class OpenThread(IThci): except Exception as e: ModuleHelper.WriteIntoDebugLogger('joinNetwork() Error: ' + str(e)) + @API def getNetworkFragmentID(self): """get current partition id of Thread Network Partition from LeaderData @@ -1100,9 +1149,10 @@ class OpenThread(IThci): return None leaderData = [] - leaderData = self.__sendCommand('leaderdata') + leaderData = self.__executeCommand('leaderdata') return int(leaderData[0].split()[2], 16) + @API def getParentAddress(self): """get Thread device's parent extended address and rloc16 short address @@ -1111,7 +1161,7 @@ class OpenThread(IThci): """ print('%s call getParentAddress' % self.port) parentInfo = [] - parentInfo = self.__sendCommand('parent') + parentInfo = self.__executeCommand('parent') for line in parentInfo: if 'Done' in line: @@ -1127,23 +1177,23 @@ class OpenThread(IThci): return int(eui, 16) + @API def powerDown(self): """power down the Thread device""" print('%s call powerDown' % self.port) - self._sendline('reset') + self.__sendCommand('reset', expectEcho=False) self.isPowerDown = True + @API def powerUp(self): """power up the Thread device""" print('%s call powerUp' % self.port) - if not self.handle: - self._connect() - self.isPowerDown = False if not self.__isOpenThreadRunning(): self.__startOpenThread() + @API def reboot(self): """reset and rejoin to Thread Network without any timeout @@ -1153,14 +1203,14 @@ class OpenThread(IThci): """ print('%s call reboot' % self.port) try: - self._sendline('reset') + self.__sendCommand('reset', expectEcho=False) self.isPowerDown = True - time.sleep(3) + self.sleep(3) self.__startOpenThread() - time.sleep(3) + self.sleep(3) - if self.__sendCommand('state')[0] == 'disabled': + if self.__executeCommand('state')[0] == 'disabled': print('[FAIL] reboot') return False else: @@ -1168,6 +1218,7 @@ class OpenThread(IThci): except Exception as e: ModuleHelper.WriteIntoDebugLogger('reboot() Error: ' + str(e)) + @API def ping(self, destination, length=20): """ send ICMPv6 echo request with a given length to a unicast destination address @@ -1181,15 +1232,15 @@ class OpenThread(IThci): try: cmd = 'ping %s %s' % (destination, str(length)) print(cmd) - self._sendline(cmd) - self._expect(cmd) + self.__sendCommand(cmd) # wait echo reply - time.sleep( + self.sleep( 6 ) # increase delay temporally (+5s) to remedy TH's delay updates except Exception as e: ModuleHelper.WriteIntoDebugLogger('ping() Error: ' + str(e)) + @API def multicast_Ping(self, destination, length=20): """send ICMPv6 echo request with a given length to a multicast destination address @@ -1203,19 +1254,14 @@ class OpenThread(IThci): try: cmd = 'ping %s %s' % (destination, str(length)) print(cmd) - self._sendline(cmd) - self._expect(cmd) + self.__sendCommand(cmd) # wait echo reply - time.sleep(1) + self.sleep(1) except Exception as e: ModuleHelper.WriteIntoDebugLogger('multicast_ping() Error: ' + str(e)) - def getVersionNumber(self): - """get OpenThread stack firmware version number""" - print('%s call getVersionNumber' % self.port) - return self.__sendCommand('version')[0] - + @API def setPANID(self, xPAN): """set Thread Network PAN ID @@ -1237,22 +1283,19 @@ class OpenThread(IThci): cmd = 'panid %s' % panid datasetCmd = 'dataset panid %s' % panid self.hasActiveDatasetToCommit = True - return (self.__sendCommand(cmd)[-1] == 'Done' and - self.__sendCommand(datasetCmd)[-1] == 'Done') + return (self.__executeCommand(cmd)[-1] == 'Done' and + self.__executeCommand(datasetCmd)[-1] == 'Done') except Exception as e: ModuleHelper.WriteIntoDebugLogger('setPANID() Error: ' + str(e)) + @API def reset(self): """factory reset""" print('%s call reset' % self.port) - try: - self._sendline('factoryreset') - self._read() - time.sleep(0.5) - - except Exception as e: - ModuleHelper.WriteIntoDebugLogger('reset() Error: ' + str(e)) + self.__sendCommand('factoryreset', expectEcho=False) + self.sleep(0.5) + @API def removeRouter(self, xRouterId): """kickoff router with a given router id from the Thread Network @@ -1275,10 +1318,11 @@ class OpenThread(IThci): try: cmd = 'releaserouterid %s' % routerId - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('removeRouter() Error: ' + str(e)) + @API def setDefaultValues(self): """set default mandatory Thread Network parameter value""" print('%s call setDefaultValues' % self.port) @@ -1313,7 +1357,6 @@ class OpenThread(IThci): self._addressfilterSet = set() # cache filter entries # indicate if Thread device is an active commissioner self.isActiveCommissioner = False - self._lines = None # buffered lines read from device # initialize device configuration try: @@ -1333,11 +1376,13 @@ class OpenThread(IThci): ModuleHelper.WriteIntoDebugLogger('setDefaultValue() Error: ' + str(e)) + @API def getDeviceConncetionStatus(self): """check if serial port connection is ready or not""" print('%s call getDeviceConnectionStatus' % self.port) return self.deviceConnected + @API def setPollingRate(self, iPollingRate): """set data polling rate for sleepy end device @@ -1375,11 +1420,12 @@ class OpenThread(IThci): try: cmd = 'pollperiod %d' % iPollPeriod print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('__setPollPeriod() Error: ' + str(e)) + @API def setLinkQuality(self, EUIadr, LinkQuality): """set custom LinkQualityIn for all receiving messages from the specified EUIadr @@ -1414,11 +1460,12 @@ class OpenThread(IThci): cmd = 'macfilter rss add-lqi %s %s' % (address64, str(LinkQuality)) print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('setLinkQuality() Error: ' + str(e)) + @API def setOutBoundLinkQuality(self, LinkQuality): """set custom LinkQualityIn for all receiving messages from the any address @@ -1439,11 +1486,12 @@ class OpenThread(IThci): try: cmd = 'macfilter rss add-lqi * %s' % str(LinkQuality) print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger( 'setOutBoundLinkQuality() Error: ' + str(e)) + @API def removeRouterPrefix(self, prefixEntry): """remove the configured prefix on a border router @@ -1461,15 +1509,16 @@ class OpenThread(IThci): prefixLen = 64 cmd = 'prefix remove %s/%d' % (prefix, prefixLen) print(cmd) - if self.__sendCommand(cmd)[-1] == 'Done': + if self.__executeCommand(cmd)[-1] == 'Done': # send server data ntf to leader - return self.__sendCommand('netdataregister')[-1] == 'Done' + return self.__executeCommand('netdataregister')[-1] == 'Done' else: return False except Exception as e: ModuleHelper.WriteIntoDebugLogger('removeRouterPrefix() Error: ' + str(e)) + @API def resetAndRejoin(self, timeout): """reset and join back Thread Network with a given timeout delay @@ -1483,17 +1532,17 @@ class OpenThread(IThci): print('%s call resetAndRejoin' % self.port) print(timeout) try: - self._sendline('reset') + self.__sendCommand('reset', expectEcho=False) self.isPowerDown = True - time.sleep(timeout) + self.sleep(timeout) if self.deviceRole == Thread_Device_Role.SED: self.__setPollPeriod(self.__sedPollPeriod) self.__startOpenThread() - time.sleep(3) + self.sleep(3) - if self.__sendCommand('state')[0] == 'disabled': + if self.__executeCommand('state')[0] == 'disabled': print('[FAIL] reset and rejoin') return False return True @@ -1501,6 +1550,7 @@ class OpenThread(IThci): ModuleHelper.WriteIntoDebugLogger('resetAndRejoin() Error: ' + str(e)) + @API def configBorderRouter( self, P_Prefix, @@ -1562,20 +1612,22 @@ class OpenThread(IThci): cmd = 'prefix add %s/64 %s %s' % (prefix, parameter, prf) print(cmd) - if self.__sendCommand(cmd)[-1] == 'Done': + if self.__executeCommand(cmd)[-1] == 'Done': # if prefix configured before starting OpenThread stack # do not send out server data ntf pro-actively if not self.__isOpenThreadRunning(): return True else: # send server data ntf to leader - return self.__sendCommand('netdataregister')[-1] == 'Done' + return self.__executeCommand( + 'netdataregister')[-1] == 'Done' else: return False except Exception as e: ModuleHelper.WriteIntoDebugLogger('configBorderRouter() Error: ' + str(e)) + @API def setNetworkIDTimeout(self, iNwkIDTimeOut): """set networkid timeout for Thread device @@ -1592,11 +1644,12 @@ class OpenThread(IThci): try: cmd = 'networkidtimeout %s' % str(iNwkIDTimeOut) print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('setNetworkIDTimeout() Error: ' + str(e)) + @API def setKeepAliveTimeOut(self, iTimeOut): """set keep alive timeout for device has been deprecated and also set SED polling rate @@ -1614,11 +1667,12 @@ class OpenThread(IThci): try: cmd = 'pollperiod %d' % int(iTimeOut) print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('setKeepAliveTimeOut() Error: ' + str(e)) + @API def setKeySequenceCounter(self, iKeySequenceValue): """ set the Key sequence counter corresponding to Thread Network master key @@ -1636,8 +1690,8 @@ class OpenThread(IThci): self.__setKeySwitchGuardTime(0) cmd = 'keysequence counter %s' % str(iKeySequenceValue) - if self.__sendCommand(cmd)[-1] == 'Done': - time.sleep(1) + if self.__executeCommand(cmd)[-1] == 'Done': + self.sleep(1) return True else: return False @@ -1645,13 +1699,15 @@ class OpenThread(IThci): ModuleHelper.WriteIntoDebugLogger( 'setKeySequenceCounter() Error; ' + str(e)) + @API def getKeySequenceCounter(self): """get current Thread Network key sequence""" print('%s call getKeySequenceCounter' % self.port) keySequence = '' - keySequence = self.__sendCommand('keysequence counter')[0] + keySequence = self.__executeCommand('keysequence counter')[0] return keySequence + @API def incrementKeySequenceCounter(self, iIncrementValue=1): """increment the key sequence with a given value @@ -1676,6 +1732,7 @@ class OpenThread(IThci): ModuleHelper.WriteIntoDebugLogger( 'incrementKeySequenceCounter() Error: ' + str(e)) + @API def setNetworkDataRequirement(self, eDataRequirement): """set whether the Thread device requires the full network data or only requires the stable network data @@ -1693,6 +1750,7 @@ class OpenThread(IThci): self.networkDataRequirement = 'n' return True + @API def configExternalRouter(self, P_Prefix, P_stable, R_Preference=0): """configure border router with a given external route prefix entry @@ -1729,13 +1787,14 @@ class OpenThread(IThci): cmd = 'route add %s/64 %s' % (prefix, prf) print(cmd) - if self.__sendCommand(cmd)[-1] == 'Done': + if self.__executeCommand(cmd)[-1] == 'Done': # send server data ntf to leader - return self.__sendCommand('netdataregister')[-1] == 'Done' + return self.__executeCommand('netdataregister')[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('configExternalRouter() Error: ' + str(e)) + @API def getNeighbouringRouters(self): """get neighboring routers information @@ -1746,7 +1805,7 @@ class OpenThread(IThci): try: routerInfo = [] routerList = [] - routerList = self.__sendCommand('router list')[0].split() + routerList = self.__executeCommand('router list')[0].split() print(routerList) if 'Done' in routerList: @@ -1756,7 +1815,7 @@ class OpenThread(IThci): for index in routerList: router = [] cmd = 'router %s' % index - router = self.__sendCommand(cmd) + router = self.__executeCommand(cmd) for line in router: if 'Done' in line: @@ -1779,6 +1838,7 @@ class OpenThread(IThci): ModuleHelper.WriteIntoDebugLogger( 'getNeighbouringDevice() Error: ' + str(e)) + @API def getChildrenInfo(self): """get all children information @@ -1789,7 +1849,7 @@ class OpenThread(IThci): try: childrenInfoAll = [] childrenInfo = {'EUI': 0, 'Rloc16': 0, 'MLEID': ''} - childrenList = self.__sendCommand('child list')[0].split() + childrenList = self.__executeCommand('child list')[0].split() print(childrenList) if 'Done' in childrenList: @@ -1799,7 +1859,7 @@ class OpenThread(IThci): for index in childrenList: cmd = 'child %s' % index child = [] - child = self.__sendCommand(cmd) + child = self.__executeCommand(cmd) for line in child: if 'Done' in line: @@ -1828,6 +1888,7 @@ class OpenThread(IThci): ModuleHelper.WriteIntoDebugLogger('getChildrenInfo() Error: ' + str(e)) + @API def setXpanId(self, xPanId): """set extended PAN ID of Thread Network @@ -1853,11 +1914,12 @@ class OpenThread(IThci): self.xpanId = xpanid self.hasActiveDatasetToCommit = True - return (self.__sendCommand(cmd)[-1] == 'Done' and - self.__sendCommand(datasetCmd)[-1] == 'Done') + return (self.__executeCommand(cmd)[-1] == 'Done' and + self.__executeCommand(datasetCmd)[-1] == 'Done') except Exception as e: ModuleHelper.WriteIntoDebugLogger('setXpanId() Error: ' + str(e)) + @API def getNeighbouringDevices(self): """gets the neighboring devices' extended address to compute the DUT extended address automatically @@ -1889,6 +1951,7 @@ class OpenThread(IThci): print(neighbourList) return neighbourList + @API def setPartationId(self, partationId): """set Thread Network Partition ID @@ -1904,8 +1967,9 @@ class OpenThread(IThci): cmd = 'leaderpartitionid %s' % (str(hex(partationId)).rstrip('L')) print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' + @API def getGUA(self, filterByPrefix=None): """get expected global unicast IPv6 address of Thread device @@ -1937,37 +2001,44 @@ class OpenThread(IThci): except Exception as e: ModuleHelper.WriteIntoDebugLogger('getGUA() Error: ' + str(e)) + @API def getShortAddress(self): """get Rloc16 short address of Thread device""" print('%s call getShortAddress' % self.port) return self.getRloc16() + @API def getULA64(self): """get mesh local EID of Thread device""" print('%s call getULA64' % self.port) - return self.__sendCommand('ipaddr mleid')[0] + return self.__executeCommand('ipaddr mleid')[0] + @API def setMLPrefix(self, sMeshLocalPrefix): """set mesh local prefix""" print('%s call setMLPrefix' % self.port) try: cmd = 'dataset meshlocalprefix %s' % sMeshLocalPrefix self.hasActiveDatasetToCommit = True - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('setMLPrefix() Error: ' + str(e)) + @API def getML16(self): """get mesh local 16 unicast address (Rloc)""" print('%s call getML16' % self.port) return self.getRloc() + @API def downgradeToDevice(self): pass + @API def upgradeToRouter(self): pass + @API def forceSetSlaac(self, slaacAddress): """force to set a slaac IPv6 address to Thread interface @@ -1983,25 +2054,29 @@ class OpenThread(IThci): try: cmd = 'ipaddr add %s' % str(slaacAddress) print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('forceSetSlaac() Error: ' + str(e)) + @API def setSleepyNodePollTime(self): pass + @API def enableAutoDUTObjectFlag(self): """set AutoDUTenable flag""" print('%s call enableAutoDUTObjectFlag' % self.port) self.AutoDUTEnable = True + @API def getChildTimeoutValue(self): """get child timeout""" print('%s call getChildTimeoutValue' % self.port) - childTimeout = self.__sendCommand('childtimeout')[0] + childTimeout = self.__executeCommand('childtimeout')[0] return int(childTimeout) + @API def diagnosticGet(self, strDestinationAddr, listTLV_ids=[]): if not listTLV_ids: return @@ -2015,8 +2090,9 @@ class OpenThread(IThci): ) print(cmd) - return self._sendline(cmd) + return self.__sendCommand(cmd, expectEcho=False) + @API def diagnosticReset(self, strDestinationAddr, listTLV_ids=[]): if not listTLV_ids: return @@ -2030,24 +2106,27 @@ class OpenThread(IThci): ) print(cmd) - return self.__sendCommand(cmd) + return self.__executeCommand(cmd) + @API def diagnosticQuery(self, strDestinationAddr, listTLV_ids=[]): self.diagnosticGet(strDestinationAddr, listTLV_ids) + @API def startNativeCommissioner(self, strPSKc='GRLPASSPHRASE'): # TODO: Support the whole Native Commissioner functionality # Currently it only aims to trigger a Discovery Request message to pass # Certification test 5.8.4 print('%s call startNativeCommissioner' % self.port) - self.__sendCommand('ifconfig up') + self.__executeCommand('ifconfig up') cmd = 'joiner start %s' % (strPSKc) print(cmd) - if self.__sendCommand(cmd)[-1] == 'Done': + if self.__executeCommand(cmd)[-1] == 'Done': return True else: return False + @API def startCollapsedCommissioner(self): """start Collapsed Commissioner @@ -2057,18 +2136,20 @@ class OpenThread(IThci): """ print('%s call startCollapsedCommissioner' % self.port) if self.__startOpenThread(): - time.sleep(20) + self.sleep(20) cmd = 'commissioner start' print(cmd) - if self.__sendCommand(cmd)[-1] == 'Done': + if self.__executeCommand(cmd)[-1] == 'Done': self.isActiveCommissioner = True - time.sleep(20) # time for petition process + self.sleep(20) # time for petition process return True return False + @API def setJoinKey(self, strPSKc): pass + @API def scanJoiner(self, xEUI='*', strPSKd='THREADJPAKETEST'): """scan Joiner @@ -2080,7 +2161,7 @@ class OpenThread(IThci): True: successful to add Joiner's steering data False: fail to add Joiner's steering data """ - print('%s call scanJoiner' % self.port) + self.log("scanJoiner on channel %s", self.getChannel()) # long timeout value to avoid automatic joiner removal (in seconds) timeout = 500 @@ -2095,8 +2176,9 @@ class OpenThread(IThci): strPSKd, str(timeout), ) + print(cmd) - if self.__sendCommand(cmd)[-1] == 'Done': + if self.__executeCommand(cmd)[-1] == 'Done': if self.logThreadStatus == self.logStatus['stop']: self.logThread = ThreadRunner.run( target=self.__readCommissioningLogs, args=(120,)) @@ -2104,6 +2186,7 @@ class OpenThread(IThci): else: return False + @API def setProvisioningUrl(self, strURL='grl.com'): """set provisioning Url @@ -2118,9 +2201,10 @@ class OpenThread(IThci): self.provisioningUrl = strURL if self.deviceRole == Thread_Device_Role.Commissioner: cmd = 'commissioner provisioningurl %s' % (strURL) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' return True + @API def allowCommission(self): """start commissioner candidate petition process @@ -2132,17 +2216,18 @@ class OpenThread(IThci): try: cmd = 'commissioner start' print(cmd) - if self.__sendCommand(cmd)[-1] == 'Done': + if self.__executeCommand(cmd)[-1] == 'Done': self.isActiveCommissioner = True # time for petition process and at least one keep alive - time.sleep(40) + self.sleep(3) return True else: return False except Exception as e: - ModuleHelper.writeintodebuglogger('allowcommission() error: ' + + ModuleHelper.WriteIntoDebugLogger('allowcommission() error: ' + str(e)) + @API def joinCommissioned(self, strPSKd='THREADJPAKETEST', waitTime=20): """start joiner @@ -2153,11 +2238,11 @@ class OpenThread(IThci): True: successful to start joiner False: fail to start joiner """ - print('%s call joinCommissioned' % self.port) - self.__sendCommand('ifconfig up') + self.log("joinCommissioned on channel %s", self.getChannel()) + self.__executeCommand('ifconfig up') cmd = 'joiner start %s %s' % (strPSKd, self.provisioningUrl) print(cmd) - if self.__sendCommand(cmd)[-1] == 'Done': + if self.__executeCommand(cmd)[-1] == 'Done': maxDuration = 150 # seconds self.joinCommissionedStatus = self.joinStatus['ongoing'] @@ -2172,14 +2257,15 @@ class OpenThread(IThci): elif self.joinCommissionedStatus == self.joinStatus['failed']: return False - time.sleep(1) + self.sleep(1) - self.__sendCommand('thread start') - time.sleep(30) + self.__executeCommand('thread start') + self.sleep(30) return True else: return False + @API def getCommissioningLogs(self): """get Commissioning logs @@ -2242,6 +2328,7 @@ class OpenThread(IThci): ProcessedLogs.append(EncryptedPacket) return ProcessedLogs + @API def MGMT_ED_SCAN( self, sAddr, @@ -2278,10 +2365,11 @@ class OpenThread(IThci): sAddr, ) print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: - ModuleHelper.writeintodebuglogger('MGMT_ED_SCAN() error: ' + str(e)) + ModuleHelper.WriteIntoDebugLogger('MGMT_ED_SCAN() error: ' + str(e)) + @API def MGMT_PANID_QUERY(self, sAddr, xCommissionerSessionId, listChannelMask, xPanId): """send MGMT_PANID_QUERY message to a given destination @@ -2305,11 +2393,12 @@ class OpenThread(IThci): try: cmd = 'commissioner panid %s %s %s' % (panid, channelMask, sAddr) print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: - ModuleHelper.writeintodebuglogger('MGMT_PANID_QUERY() error: ' + + ModuleHelper.WriteIntoDebugLogger('MGMT_PANID_QUERY() error: ' + str(e)) + @API def MGMT_ANNOUNCE_BEGIN(self, sAddr, xCommissionerSessionId, listChannelMask, xCount, xPeriod): """send MGMT_ANNOUNCE_BEGIN message to a given destination @@ -2330,11 +2419,12 @@ class OpenThread(IThci): sAddr, ) print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: - ModuleHelper.writeintodebuglogger('MGMT_ANNOUNCE_BEGIN() error: ' + + ModuleHelper.WriteIntoDebugLogger('MGMT_ANNOUNCE_BEGIN() error: ' + str(e)) + @API def MGMT_ACTIVE_GET(self, Addr='', TLVs=[]): """send MGMT_ACTIVE_GET command @@ -2357,12 +2447,13 @@ class OpenThread(IThci): print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('MGMT_ACTIVE_GET() Error: ' + str(e)) + @API def MGMT_ACTIVE_SET( self, sAddr='', @@ -2516,12 +2607,13 @@ class OpenThread(IThci): print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('MGMT_ACTIVE_SET() Error: ' + str(e)) + @API def MGMT_PENDING_GET(self, Addr='', TLVs=[]): """send MGMT_PENDING_GET command @@ -2544,12 +2636,13 @@ class OpenThread(IThci): print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('MGMT_PENDING_GET() Error: ' + str(e)) + @API def MGMT_PENDING_SET( self, sAddr='', @@ -2620,12 +2713,13 @@ class OpenThread(IThci): print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('MGMT_PENDING_SET() Error: ' + str(e)) + @API def MGMT_COMM_GET(self, Addr='ff02::1', TLVs=[]): """send MGMT_COMM_GET command @@ -2644,12 +2738,13 @@ class OpenThread(IThci): print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('MGMT_COMM_GET() Error: ' + str(e)) + @API def MGMT_COMM_SET( self, Addr='ff02::1', @@ -2695,38 +2790,43 @@ class OpenThread(IThci): print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('MGMT_COMM_SET() Error: ' + str(e)) + @API def setActiveDataset(self, listActiveDataset=[]): print('%s call setActiveDataset' % self.port) + @API def setCommisionerMode(self): print('%s call setCommissionerMode' % self.port) + @API def setPSKc(self, strPSKc): print('%s call setPSKc' % self.port) try: cmd = 'dataset pskc %s' % strPSKc self.hasActiveDatasetToCommit = True - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('setPSKc() Error: ' + str(e)) + @API def setActiveTimestamp(self, xActiveTimestamp): print('%s call setActiveTimestamp' % self.port) try: self.activetimestamp = xActiveTimestamp cmd = 'dataset activetimestamp %s' % str(xActiveTimestamp) self.hasActiveDatasetToCommit = True - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' except Exception as e: ModuleHelper.WriteIntoDebugLogger('setActiveTimestamp() Error: ' + str(e)) + @API def setUdpJoinerPort(self, portNumber): """set Joiner UDP Port @@ -2740,8 +2840,9 @@ class OpenThread(IThci): print('%s call setUdpJoinerPort' % self.port) cmd = 'joinerport %d' % portNumber print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' + @API def commissionerUnregister(self): """stop commissioner @@ -2752,23 +2853,26 @@ class OpenThread(IThci): print('%s call commissionerUnregister' % self.port) cmd = 'commissioner stop' print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' + @API def sendBeacons(self, sAddr, xCommissionerSessionId, listChannelMask, xPanId): print('%s call sendBeacons' % self.port) - self._sendline('scan') + self.__sendCommand('scan', expectEcho=False) return True + @API def updateRouterStatus(self): """force update to router as if there is child id request""" self._update_router_status = True + @API def __updateRouterStatus(self): print('%s call updateRouterStatus' % self.port) cmd = 'state' while True: - state = self.__sendCommand(cmd)[0] + state = self.__executeCommand(cmd)[0] if state == 'detached': continue elif state == 'child': @@ -2777,19 +2881,22 @@ class OpenThread(IThci): return False cmd = 'state router' - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' + @API def setRouterThresholdValues(self, upgradeThreshold, downgradeThreshold): print('%s call setRouterThresholdValues' % self.port) self.__setRouterUpgradeThreshold(upgradeThreshold) self.__setRouterDowngradeThreshold(downgradeThreshold) + @API def setMinDelayTimer(self, iSeconds): print('%s call setMinDelayTimer' % self.port) cmd = 'delaytimermin %s' % iSeconds print(cmd) - return self.__sendCommand(cmd)[-1] == 'Done' + return self.__executeCommand(cmd)[-1] == 'Done' + @API def ValidateDeviceFirmware(self): print('%s call ValidateDeviceFirmware' % self.port) if 'OPENTHREAD' in self.UIStatusMsg: @@ -2811,3 +2918,71 @@ class OpenThread(IThci): s = s[2:] return s + + +class OpenThread(OpenThreadTHCI, IThci): + + # Used for reference firmware version control for Test Harness. + # This variable will be updated to match the OpenThread reference firmware + # officially released. + + def _connect(self): + print('My port is %s' % self.port) + self.__lines = [] + if self.port.startswith('COM'): + self.__handle = serial.Serial(self.port, 115200, timeout=0) + self.sleep(1) + self.__handle.write('\r\n') + self.sleep(0.1) + self._is_net = False + elif ':' in self.port: + host, port = self.port.split(':') + self.__handle = socket.create_connection((host, port)) + self.__handle.setblocking(0) + self._is_net = True + else: + raise Exception('Unknown port schema') + + def _disconnect(self): + if self.__handle: + self.__handle.close() + self.__handle = None + + def __socRead(self, size=512): + if self._is_net: + return self.__handle.recv(size) + else: + return self.__handle.read(size) + + def __socWrite(self, data): + if self._is_net: + self.__handle.sendall(data) + else: + self.__handle.write(data) + + def _cliReadLine(self): + if len(self.__lines) > 1: + return self.__lines.pop(0) + + tail = '' + if len(self.__lines): + tail = self.__lines.pop() + + try: + tail += self.__socRead() + except socket.error: + logging.exception('%s: No new data', self.port) + self.sleep(0.1) + + self.__lines += LINESEPX.split(tail) + if len(self.__lines) > 1: + return self.__lines.pop(0) + + def _cliWriteLine(self, line): + self.__socWrite(line + '\r\n') + + def _onCommissionStart(self): + pass + + def _onCommissionStop(self): + pass diff --git a/tools/harness-thci/OpenThread_BR.png b/tools/harness-thci/OpenThread_BR.png new file mode 100644 index 000000000..546f54ded Binary files /dev/null and b/tools/harness-thci/OpenThread_BR.png differ diff --git a/tools/harness-thci/OpenThread_BR.py b/tools/harness-thci/OpenThread_BR.py new file mode 100644 index 000000000..064e66fbc --- /dev/null +++ b/tools/harness-thci/OpenThread_BR.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python +# +# Copyright (c) 2020, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +""" +>> Thread Host Controller Interface +>> Device : OpenThread_BR THCI +>> Class : OpenThread_BR +""" +import re + +import logging +import serial +import time + +from IThci import IThci +from THCI.OpenThread import OpenThreadTHCI, watched + +RPI_FULL_PROMPT = 'pi@raspberrypi:~$ ' +RPI_USERNAME_PROMPT = 'raspberrypi login: ' +RPI_PASSWORD_PROMPT = 'Password: ' +"""regex: used to split lines""" +LINESEPX = re.compile(r'\r\n|\n') + +LOGX = re.compile(r'.*Under-voltage detected!') +"""regex: used to filter logging""" + +assert LOGX.match('[57522.618196] Under-voltage detected! (0x00050005)') + +OTBR_AGENT_SYSLOG_PATTERN = re.compile(r'raspberrypi otbr-agent\[\d+\]: (.*)') +assert OTBR_AGENT_SYSLOG_PATTERN.search( + 'Jun 23 05:21:22 raspberrypi otbr-agent[323]: =========[[THCI] direction=send | type=JOIN_FIN.req | len=039]==========]' +).group( + 1 +) == '=========[[THCI] direction=send | type=JOIN_FIN.req | len=039]==========]' + + +class OpenThread_BR(OpenThreadTHCI, IThci): + DEFAULT_COMMAND_TIMEOUT = 20 + + def _connect(self): + self.log("logining Raspberry Pi ...") + self.__cli_output_lines = [] + self.__syslog_skip_lines = None + self.__syslog_last_read_ts = 0 + + self.__handle = serial.Serial(self.port, 115200, 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) + + lastLine = None + while True: + line = self.__bashReadLine(timeout=1) + + if not line: + break + + lastLine = line + + if lastLine == RPI_FULL_PROMPT: + self.log("prompt found, login success!") + loginOk = True + break + + if lastLine == RPI_PASSWORD_PROMPT: + self.log("inputing password ...") + self.__bashWriteLine('raspberry') + elif lastLine == RPI_USERNAME_PROMPT: + self.log("inputing username ...") + self.__bashWriteLine('pi') + elif not lastLine: + self.log("inputing username ...") + self.__bashWriteLine('pi') + + if not loginOk: + raise Exception('login fail') + + self.bash('stty cols 256') + self.__truncateSyslog() + + def _disconnect(self): + if self.__handle: + self.__handle.close() + self.__handle = None + + def _cliReadLine(self): + # read commissioning log if it's commissioning + if not self.__cli_output_lines: + self.__readSyslogToCli() + + if self.__cli_output_lines: + return self.__cli_output_lines.pop(0) + + return None + + @watched + def _onCommissionStart(self): + assert self.__syslog_skip_lines is None + self.__syslog_skip_lines = int( + self.bash('wc -l /var/log/syslog')[0].split()[0]) + self.__syslog_last_read_ts = 0 + + @watched + def _onCommissionStop(self): + assert self.__syslog_skip_lines is not None + self.__syslog_skip_lines = None + + def __readSyslogToCli(self): + if self.__syslog_skip_lines is None: + return 0 + + # read syslog once per second + if time.time() < self.__syslog_last_read_ts + 1: + return 0 + + self.__syslog_last_read_ts = time.time() + + lines = self.bash('tail +%d /var/log/syslog' % self.__syslog_skip_lines) + for line in lines: + m = OTBR_AGENT_SYSLOG_PATTERN.search(line) + if not m: + continue + + self.__cli_output_lines.append(m.group(1)) + + self.__syslog_skip_lines += len(lines) + return len(lines) + + def _cliWriteLine(self, 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) + + @watched + def bash(self, cmd, timeout=DEFAULT_COMMAND_TIMEOUT): + """ + Execute the command in bash. + """ + self.__bashClearLines() + self.__bashWriteLine(cmd) + self.__bashExpect(cmd, endswith=True) + + response = [] + + deadline = time.time() + timeout + while time.time() < deadline: + line = self.__bashReadLine() + if line is None: + self.sleep(0.01) + continue + + if line == RPI_FULL_PROMPT: + # return response lines without prompt + return response + + response.append(line) + + self.__bashWrite('\x03') + raise Exception('%s: failed to find end of response' % self.port) + + def __bashExpect(self, + expected, + timeout=DEFAULT_COMMAND_TIMEOUT, + 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) + continue + + print('[%s] Got line [%r]' % (self.port, line)) + + if endswith: + matched = line.endswith(expected) + else: + matched = line == expected + + if matched: + print('[%s] Expected [%r]' % (self.port, expected)) + return + + # failed to find the expected string + # send Ctrl+C to terminal + self.__bashWrite('\x03') + raise Exception('failed to find expected string[%s]' % expected) + + def __bashRead(self, timeout=1): + deadline = time.time() + timeout + data = '' + while True: + piece = self.__handle.read(self.__handle.inWaiting()) + data = data + piece + if piece: + continue + + if data or time.time() >= deadline: + break + + if data: + self.log('>>> %r', data) + + return data + + def __bashReadLine(self, timeout=1): + line = self.__bashGetNextLine() + if line is not None: + return line + + assert len(self.__lines) == 1, self.__lines + tail = self.__lines.pop() + + try: + tail += self.__bashRead(timeout=timeout) + tail = tail.replace(RPI_FULL_PROMPT, RPI_FULL_PROMPT + '\r\n') + tail = tail.replace(RPI_USERNAME_PROMPT, + RPI_USERNAME_PROMPT + '\r\n') + tail = tail.replace(RPI_PASSWORD_PROMPT, + RPI_PASSWORD_PROMPT + '\r\n') + finally: + self.__lines += [l.rstrip('\r') for l in LINESEPX.split(tail)] + assert len(self.__lines) >= 1, self.__lines + + return self.__bashGetNextLine() + + def __bashGetNextLine(self): + assert len(self.__lines) >= 1, self.__lines + while len(self.__lines) > 1: + line = self.__lines.pop(0) + assert len(self.__lines) >= 1, self.__lines + if LOGX.match(line): + logging.info('LOG: %s', line) + continue + else: + return line + assert len(self.__lines) >= 1, self.__lines + return None + + def __bashWrite(self, data): + self.__handle.write(data) + self.log("<<< %r", data) + + def __bashClearLines(self): + assert len(self.__lines) >= 1, self.__lines + while self.__bashReadLine(timeout=0) is not None: + pass + assert len(self.__lines) >= 1, self.__lines + + def __bashWriteLine(self, line): + self.__bashWrite(line + '\n') + + def __truncateSyslog(self): + self.bash('sudo truncate -s 0 /var/log/syslog')