diff --git a/tools/harness-thci/OpenThread.py b/tools/harness-thci/OpenThread.py index 32b10d695..9d8747162 100644 --- a/tools/harness-thci/OpenThread.py +++ b/tools/harness-thci/OpenThread.py @@ -42,6 +42,7 @@ from abc import abstractmethod import serial from Queue import Queue +from serial.serialutil import SerialException TESTHARNESS_1_1 = '1.1' TESTHARNESS_1_2 = '1.2' @@ -76,6 +77,8 @@ if TESTHARNESS_VERSION == TESTHARNESS_1_2: from IThci import IThci +ZEPHYR_PREFIX = 'ot ' + LINESEPX = re.compile(r'\r\n|\n') """regex: used to split lines""" @@ -186,6 +189,7 @@ class OpenThreadTHCI(object): firmwarePrefix = 'OPENTHREAD/' DOMAIN_NAME = 'Thread' MLR_TIMEOUT_MIN = 300 + NETWORK_ATTACHMENT_TIMEOUT = 10 IsBorderRouter = False IsBackboneRouter = False @@ -281,29 +285,35 @@ class OpenThreadTHCI(object): self.logThreadStatus != self.logStatus['stop']): pass - self.__sendCommand(cmd) - line = None - response = [] + try: + self.__sendCommand(cmd) + response = [] - t_end = time.time() + timeout - while time.time() < t_end: - line = self.__readCliLine() - if line is None: - time.sleep(0.01) - continue + t_end = time.time() + timeout + while time.time() < t_end: + line = self.__readCliLine() + if line is None: + time.sleep(0.01) + continue - self.log("readline: %s", line) - response.append(line) + self.log("readline: %s", line) + response.append(line) - if line == 'Done': - break + if line.endswith('Done'): + break + else: + m = OpenThreadTHCI._COMMAND_OUTPUT_ERROR_PATTERN.match(line) + if m is not None: + code, msg = m.groups() + raise CommandError(int(code), msg) else: - m = OpenThreadTHCI._COMMAND_OUTPUT_ERROR_PATTERN.match(line) - if m is not None: - code, msg = m.groups() - raise CommandError(int(code), msg) - else: - raise Exception('%s: failed to find end of response: %s' % (self, response)) + raise Exception('%s: failed to find end of response: %s' % (self, response)) + + except SerialException as e: + self.log('__executeCommand() Error: ' + str(e)) + self._disconnect() + self._connect() + raise e return response @@ -550,7 +560,7 @@ class OpenThreadTHCI(object): else: self.hasActiveDatasetToCommit = False - # restore allowlist/denylist address filter mode if rejoin after + # Restore allowlist/denylist address filter mode if rejoin after # reset if self.isPowerDown: if self._addressfilterMode == 'allowlist': @@ -618,9 +628,19 @@ class OpenThreadTHCI(object): True: OpenThread is running False: OpenThread is not running """ - self.log('call isOpenThreadRunning') return self.__executeCommand('state')[0] != 'disabled' + @watched + def __isDeviceAttached(self): + """check whether or not OpenThread is running + + Returns: + True: OpenThread is running + False: OpenThread is not running + """ + detached_states = ["detached", "disabled"] + return self.__executeCommand('state')[0] not in detached_states + # rloc16 might be hex string or integer, need to return actual allocated # router id def __convertRlocToRouterId(self, xRloc16): @@ -632,13 +652,14 @@ class OpenThreadTHCI(object): Returns: actual router id allocated by leader """ - routerList = [] routerList = self.__executeCommand('router list')[0].split() + rloc16 = None + routerid = None + print(routerList) print(xRloc16) for index in routerList: - router = [] cmd = 'router %s' % index router = self.__executeCommand(cmd) @@ -898,7 +919,6 @@ class OpenThreadTHCI(object): """ print('%s call setMAC' % self) print(xEUI) - address64 = '' try: if not isinstance(xEUI, str): address64 = self.__convertLongToHex(xEUI, 16) @@ -1008,9 +1028,9 @@ class OpenThreadTHCI(object): True: successful to set the Thread network key False: fail to set the Thread network key """ - networkKey = '' print('%s call setNetworkKey' % self) print(key) + try: if not isinstance(key, str): networkKey = self.__convertLongToHex(key, 32) @@ -1238,17 +1258,44 @@ class OpenThreadTHCI(object): # start OpenThread self.__startOpenThread() - self.sleep(3) - - if self._update_router_status and eRoleId == Thread_Device_Role.Router: - self.__updateRouterStatus() - - self.sleep(5) # increase delay temporally (+5s) to remedy TH's delay updates - + self.wait_for_attach_to_the_network(expected_role=eRoleId, + timeout=self.NETWORK_ATTACHMENT_TIMEOUT, + raise_assert=True) return True except Exception as e: ModuleHelper.WriteIntoDebugLogger('joinNetwork() Error: ' + str(e)) + def wait_for_attach_to_the_network(self, expected_role, timeout, raise_assert=False): + start_time = time.time() + + while time.time() < start_time + timeout: + time.sleep(0.3) + if self.__isDeviceAttached(): + break + else: + if raise_assert: + raise AssertionError("OT device {} could not attach to the network after {} s of timeout.".format( + self, timeout)) + else: + return False + + if self._update_router_status: + self.__updateRouterStatus() + + if expected_role == Thread_Device_Role.Router: + while time.time() < start_time + timeout: + time.sleep(0.3) + if self.getDeviceRole() == "router": + break + else: + if raise_assert: + raise AssertionError("OT Router {} could not attach to the network after {} s of timeout.".format( + self, timeout * 2)) + else: + return False + + return True + @API def getNetworkFragmentID(self): """get current partition id of Thread Network Partition from LeaderData @@ -1261,7 +1308,6 @@ class OpenThreadTHCI(object): print('OpenThread is not running') return None - leaderData = [] leaderData = self.__executeCommand('leaderdata') return int(leaderData[0].split()[2], 16) @@ -1273,7 +1319,7 @@ class OpenThreadTHCI(object): The extended address of parent in hex format """ print('%s call getParentAddress' % self) - parentInfo = [] + eui = None parentInfo = self.__executeCommand('parent') for line in parentInfo: @@ -1295,6 +1341,11 @@ class OpenThreadTHCI(object): """power down the Thread device""" print('%s call powerDown' % self) self.__sendCommand('reset', expectEcho=False) + + if not self.IsBorderRouter: + self._disconnect() + self._connect() + self.isPowerDown = True @API @@ -1306,6 +1357,28 @@ class OpenThreadTHCI(object): if not self.__isOpenThreadRunning(): self.__startOpenThread() + def reset_and_wait_for_connection(self, timeout=3): + print("Waiting after reset timeout: {} s".format(timeout)) + start_time = time.time() + self.__sendCommand('reset', expectEcho=False) + self.isPowerDown = True + + while time.time() < start_time + timeout: + time.sleep(0.3) + if TESTHARNESS_VERSION == TESTHARNESS_1_2 and not self.IsBorderRouter: + self._disconnect() + self._connect() + try: + self.__executeCommand('state', timeout=0.1) + break + except Exception: + continue + else: + raise AssertionError("Could not connect with OT device {} after reset.".format(self)) + + if self.deviceRole == Thread_Device_Role.SED: + self.__setPollPeriod(self.__sedPollPeriod) + @API def reboot(self): """reset and rejoin to Thread Network without any timeout @@ -1316,28 +1389,35 @@ class OpenThreadTHCI(object): """ print('%s call reboot' % self) try: - self.__sendCommand('reset', expectEcho=False) - self.isPowerDown = True - self.sleep(3) - + self.reset_and_wait_for_connection() self.__startOpenThread() - self.sleep(3) - - if self.__executeCommand('state')[0] == 'disabled': - print('[FAIL] reboot') - return False - else: - return True + return self.wait_for_attach_to_the_network(expected_role="", timeout=self.NETWORK_ATTACHMENT_TIMEOUT) except Exception as e: ModuleHelper.WriteIntoDebugLogger('reboot() Error: ' + str(e)) + @API + def resetAndRejoin(self, timeout): + """reset and join back Thread Network with a given timeout delay + + Args: + timeout: a timeout interval before rejoin Thread Network + + Returns: + True: successful to reset and rejoin Thread Network + False: fail to reset and rejoin the Thread Network + """ + print('%s call resetAndRejoin' % self) + try: + self.reset_and_wait_for_connection(timeout=timeout) + self.__startOpenThread() + return self.wait_for_attach_to_the_network(expected_role="", timeout=self.NETWORK_ATTACHMENT_TIMEOUT) + except Exception as e: + ModuleHelper.WriteIntoDebugLogger('resetAndRejoin() Error: ' + str(e)) + @API def ping(self, strDestination, ilength=0, hop_limit=64, timeout=5): """ send ICMPv6 echo request with a given length/hoplimit to a unicast destination address - - TODO: add hop_limit support - Args: srcDestination: the unicast destination address of ICMPv6 echo request ilength: the size of ICMPv6 echo request payload @@ -1404,17 +1484,23 @@ class OpenThreadTHCI(object): self._deviceBeforeReset() self.__sendCommand('factoryreset', expectEcho=False) + timeout = 10 + start_time = time.time() - while time.time() < start_time + 10: + while time.time() < start_time + timeout: time.sleep(0.3) + if TESTHARNESS_VERSION == TESTHARNESS_1_2 and not self.IsBorderRouter: + self._disconnect() + self._connect() try: self.__executeCommand('state', timeout=0.1) break except Exception: continue + else: + raise AssertionError("Could not connect with OT device {} after reset.".format(self)) - self.log('factoryreset finished in %dms', int(time.time() - start_time)) - + self.log('factoryreset finished within 10s timeout.') self._deviceAfterReset() @API @@ -1430,7 +1516,6 @@ class OpenThreadTHCI(object): """ print('%s call removeRouter' % self) print(xRouterId) - routerId = '' routerId = self.__convertRlocToRouterId(xRouterId) print(routerId) @@ -1648,37 +1733,6 @@ class OpenThreadTHCI(object): 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 - - Args: - timeout: a timeout interval before rejoin Thread Network - - Returns: - True: successful to reset and rejoin Thread Network - False: fail to reset and rejoin the Thread Network - """ - print('%s call resetAndRejoin' % self) - print(timeout) - try: - self.__sendCommand('reset', expectEcho=False) - self.isPowerDown = True - self.sleep(timeout) - - if self.deviceRole == Thread_Device_Role.SED: - self.__setPollPeriod(self.__sedPollPeriod) - - self.__startOpenThread() - self.sleep(3) - - if self.__executeCommand('state')[0] == 'disabled': - print('[FAIL] reset and rejoin') - return False - return True - except Exception as e: - ModuleHelper.WriteIntoDebugLogger('resetAndRejoin() Error: ' + str(e)) - @API def configBorderRouter( self, @@ -1849,7 +1903,6 @@ class OpenThreadTHCI(object): def getKeySequenceCounter(self): """get current Thread Network key sequence""" print('%s call getKeySequenceCounter' % self) - keySequence = '' keySequence = self.__executeCommand('keysequence counter')[0] return keySequence @@ -1866,7 +1919,6 @@ class OpenThreadTHCI(object): """ print('%s call incrementKeySequenceCounter' % self) print(iIncrementValue) - currentKeySeq = '' try: # avoid key switch guard timer protection for reference device self.__setKeySwitchGuardTime(0) @@ -1913,6 +1965,7 @@ class OpenThreadTHCI(object): """ print('%s call configExternalRouter' % self) print(P_Prefix) + prf = '' stable = '' prefix = self.__convertIp6PrefixStringToIp6Address(str(P_Prefix)) try: @@ -1948,7 +2001,6 @@ class OpenThreadTHCI(object): print('%s call getNeighbouringRouters' % self) try: routerInfo = [] - routerList = [] routerList = self.__executeCommand('router list')[0].split() print(routerList) @@ -1988,7 +2040,11 @@ class OpenThreadTHCI(object): Returns: children's extended address """ + eui = None + rloc16 = None + print('%s call getChildrenInfo' % self) + try: childrenInfoAll = [] childrenInfo = {'EUI': 0, 'Rloc16': 0, 'MLEID': ''} @@ -2212,7 +2268,7 @@ class OpenThreadTHCI(object): return int(childTimeout) @API - def diagnosticGet(self, strDestinationAddr, listTLV_ids=[]): + def diagnosticGet(self, strDestinationAddr, listTLV_ids=()): if not listTLV_ids: return @@ -2228,7 +2284,7 @@ class OpenThreadTHCI(object): return self.__sendCommand(cmd, expectEcho=False) @API - def diagnosticReset(self, strDestinationAddr, listTLV_ids=[]): + def diagnosticReset(self, strDestinationAddr, listTLV_ids=()): if not listTLV_ids: return @@ -2244,7 +2300,7 @@ class OpenThreadTHCI(object): return self.__executeCommand(cmd) @API - def diagnosticQuery(self, strDestinationAddr, listTLV_ids=[]): + def diagnosticQuery(self, strDestinationAddr, listTLV_ids=()): self.diagnosticGet(strDestinationAddr, listTLV_ids) @API @@ -2308,7 +2364,9 @@ class OpenThreadTHCI(object): """ print('%s call startCollapsedCommissioner' % self) if self.__startOpenThread(): - self.sleep(20) + self.wait_for_attach_to_the_network(expected_role=self.deviceRole, + timeout=self.NETWORK_ATTACHMENT_TIMEOUT, + raise_assert=True) cmd = 'commissioner start' print(cmd) if self.__executeCommand(cmd)[-1] == 'Done': @@ -2520,7 +2578,6 @@ class OpenThreadTHCI(object): False: fail to send MGMT_ED_SCAN message """ print('%s call MGMT_ED_SCAN' % self) - channelMask = '' channelMask = '0x' + self.__convertLongToHex(self.__convertChannelMask(listChannelMask)) try: cmd = 'commissioner energy %s %s %s %s %s' % ( @@ -2548,7 +2605,6 @@ class OpenThreadTHCI(object): """ print('%s call MGMT_PANID_QUERY' % self) panid = '' - channelMask = '' channelMask = '0x' + self.__convertLongToHex(self.__convertChannelMask(listChannelMask)) if not isinstance(xPanId, str): @@ -2570,7 +2626,6 @@ class OpenThreadTHCI(object): False: fail to send MGMT_ANNOUNCE_BEGIN message. """ print('%s call MGMT_ANNOUNCE_BEGIN' % self) - channelMask = '' channelMask = '0x' + self.__convertLongToHex(self.__convertChannelMask(listChannelMask)) try: cmd = 'commissioner announce %s %s %s %s' % ( @@ -2585,7 +2640,7 @@ class OpenThreadTHCI(object): ModuleHelper.WriteIntoDebugLogger('MGMT_ANNOUNCE_BEGIN() error: ' + str(e)) @API - def MGMT_ACTIVE_GET(self, Addr='', TLVs=[]): + def MGMT_ACTIVE_GET(self, Addr='', TLVs=()): """send MGMT_ACTIVE_GET command Returns: @@ -2770,7 +2825,7 @@ class OpenThreadTHCI(object): ModuleHelper.WriteIntoDebugLogger('MGMT_ACTIVE_SET() Error: ' + str(e)) @API - def MGMT_PENDING_GET(self, Addr='', TLVs=[]): + def MGMT_PENDING_GET(self, Addr='', TLVs=()): """send MGMT_PENDING_GET command Returns: @@ -2874,7 +2929,7 @@ class OpenThreadTHCI(object): ModuleHelper.WriteIntoDebugLogger('MGMT_PENDING_SET() Error: ' + str(e)) @API - def MGMT_COMM_GET(self, Addr='ff02::1', TLVs=[]): + def MGMT_COMM_GET(self, Addr='ff02::1', TLVs=()): """send MGMT_COMM_GET command Returns: @@ -2949,7 +3004,7 @@ class OpenThreadTHCI(object): ModuleHelper.WriteIntoDebugLogger('MGMT_COMM_SET() Error: ' + str(e)) @API - def setActiveDataset(self, listActiveDataset=[]): + def setActiveDataset(self, listActiveDataset=()): print('%s call setActiveDataset' % self) @API @@ -3457,8 +3512,8 @@ class OpenThreadTHCI(object): """Detect if the device is running Zephyr and adapt in that case""" try: - if self.__executeCommand('ot thread version')[0].isdigit(): - self.__cmdPrefix = 'ot ' + if self.__executeCommand(ZEPHYR_PREFIX + 'thread version')[0].isdigit(): + self.__cmdPrefix = ZEPHYR_PREFIX self.__lineSepX = re.compile(r'\r\n|\r|\n') except CommandError: pass @@ -3505,16 +3560,27 @@ class OpenThread(OpenThreadTHCI, IThci): def _connect(self): print('My port is %s' % self) self.__lines = [] + timeout = 10 + port_error = None + 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 + for _ in range(int(timeout / 0.5)): + time.sleep(0.5) + try: + 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 + break + except SerialException as port_error: + self.log("{} port not ready, retrying to connect...".format(self.port)) + else: + raise SerialException("Could not open {} port: {}".format(self.port, port_error)) elif ':' in self.port: host, port = self.port.split(':') self.__handle = socket.create_connection((host, port)) - self.__handle.setblocking(0) + self.__handle.setblocking(False) self._is_net = True else: raise Exception('Unknown port schema') @@ -3561,7 +3627,12 @@ class OpenThread(OpenThreadTHCI, IThci): return self.__lines.pop(0) def _cliWriteLine(self, line): - self.__socWrite(line + '\r\n') + if self.__cmdPrefix == ZEPHYR_PREFIX: + if not line.startswith(self.__cmdPrefix): + line = self.__cmdPrefix + line + self.__socWrite(line + '\r') + else: + self.__socWrite(line + '\r\n') def _onCommissionStart(self): pass