IDFDUT.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. # Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http:#www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """ DUT for IDF applications """
  15. import functools
  16. import io
  17. import os
  18. import os.path
  19. import re
  20. import subprocess
  21. import sys
  22. import tempfile
  23. import time
  24. import pexpect
  25. # python2 and python3 queue package name is different
  26. try:
  27. import Queue as _queue
  28. except ImportError:
  29. import queue as _queue
  30. from serial.tools import list_ports
  31. from tiny_test_fw import DUT, Utility
  32. from tiny_test_fw.Utility import format_case_id
  33. try:
  34. import esptool
  35. except ImportError: # cheat and use IDF's copy of esptool if available
  36. idf_path = os.getenv('IDF_PATH')
  37. if not idf_path or not os.path.exists(idf_path):
  38. raise
  39. sys.path.insert(0, os.path.join(idf_path, 'components', 'esptool_py', 'esptool'))
  40. import esptool
  41. class IDFToolError(OSError):
  42. pass
  43. class IDFDUTException(RuntimeError):
  44. pass
  45. class IDFRecvThread(DUT.RecvThread):
  46. PERFORMANCE_PATTERN = re.compile(r'\[Performance]\[(\w+)]: ([^\r\n]+)\r?\n')
  47. EXCEPTION_PATTERNS = [
  48. re.compile(r"(Guru Meditation Error: Core\s+\d panic'ed \([\w].*?\))"),
  49. re.compile(r'(abort\(\) was called at PC 0x[a-fA-F\d]{8} on core \d)'),
  50. re.compile(r'(rst 0x\d+ \(TG\dWDT_SYS_RESET|TGWDT_CPU_RESET\))')
  51. ]
  52. BACKTRACE_PATTERN = re.compile(r'Backtrace:((\s(0x[0-9a-f]{8}):0x[0-9a-f]{8})+)')
  53. BACKTRACE_ADDRESS_PATTERN = re.compile(r'(0x[0-9a-f]{8}):0x[0-9a-f]{8}')
  54. def __init__(self, read, dut):
  55. super(IDFRecvThread, self).__init__(read, dut)
  56. self.exceptions = _queue.Queue()
  57. self.performance_items = _queue.Queue()
  58. def collect_performance(self, comp_data):
  59. matches = self.PERFORMANCE_PATTERN.findall(comp_data)
  60. for match in matches:
  61. Utility.console_log('[Performance][{}]: {}'.format(format_case_id(match[0], self.dut.app.target, self.dut.app.config_name), match[1]),
  62. color='orange')
  63. self.performance_items.put((match[0], match[1]))
  64. def detect_exception(self, comp_data):
  65. for pattern in self.EXCEPTION_PATTERNS:
  66. start = 0
  67. while True:
  68. match = pattern.search(comp_data, pos=start)
  69. if match:
  70. start = match.end()
  71. self.exceptions.put(match.group(0))
  72. Utility.console_log('[Exception]: {}'.format(match.group(0)), color='red')
  73. else:
  74. break
  75. def detect_backtrace(self, comp_data):
  76. start = 0
  77. while True:
  78. match = self.BACKTRACE_PATTERN.search(comp_data, pos=start)
  79. if match:
  80. start = match.end()
  81. Utility.console_log('[Backtrace]:{}'.format(match.group(1)), color='red')
  82. # translate backtrace
  83. addresses = self.BACKTRACE_ADDRESS_PATTERN.findall(match.group(1))
  84. translated_backtrace = ''
  85. for addr in addresses:
  86. ret = self.dut.lookup_pc_address(addr)
  87. if ret:
  88. translated_backtrace += ret + '\n'
  89. if translated_backtrace:
  90. Utility.console_log('Translated backtrace\n:' + translated_backtrace, color='yellow')
  91. else:
  92. Utility.console_log('Failed to translate backtrace', color='yellow')
  93. else:
  94. break
  95. CHECK_FUNCTIONS = [collect_performance, detect_exception, detect_backtrace]
  96. def _uses_esptool(func):
  97. """ Suspend listener thread, connect with esptool,
  98. call target function with esptool instance,
  99. then resume listening for output
  100. """
  101. @functools.wraps(func)
  102. def handler(self, *args, **kwargs):
  103. self.stop_receive()
  104. settings = self.port_inst.get_settings()
  105. try:
  106. if not self._rom_inst:
  107. self._rom_inst = esptool.ESPLoader.detect_chip(self.port_inst)
  108. self._rom_inst.connect('hard_reset')
  109. esp = self._rom_inst.run_stub()
  110. ret = func(self, esp, *args, **kwargs)
  111. # do hard reset after use esptool
  112. esp.hard_reset()
  113. finally:
  114. # always need to restore port settings
  115. self.port_inst.apply_settings(settings)
  116. self.start_receive()
  117. return ret
  118. return handler
  119. class IDFDUT(DUT.SerialDUT):
  120. """ IDF DUT, extends serial with esptool methods
  121. (Becomes aware of IDFApp instance which holds app-specific data)
  122. """
  123. # /dev/ttyAMA0 port is listed in Raspberry Pi
  124. # /dev/tty.Bluetooth-Incoming-Port port is listed in Mac
  125. INVALID_PORT_PATTERN = re.compile(r'AMA|Bluetooth')
  126. # if need to erase NVS partition in start app
  127. ERASE_NVS = True
  128. RECV_THREAD_CLS = IDFRecvThread
  129. def __init__(self, name, port, log_file, app, allow_dut_exception=False, **kwargs):
  130. super(IDFDUT, self).__init__(name, port, log_file, app, **kwargs)
  131. self.allow_dut_exception = allow_dut_exception
  132. self.exceptions = _queue.Queue()
  133. self.performance_items = _queue.Queue()
  134. self._rom_inst = None
  135. @classmethod
  136. def _get_rom(cls):
  137. raise NotImplementedError('This is an abstraction class, method not defined.')
  138. @classmethod
  139. def get_mac(cls, app, port):
  140. """
  141. get MAC address via esptool
  142. :param app: application instance (to get tool)
  143. :param port: serial port as string
  144. :return: MAC address or None
  145. """
  146. esp = None
  147. try:
  148. esp = cls._get_rom()(port)
  149. esp.connect()
  150. return esp.read_mac()
  151. except RuntimeError:
  152. return None
  153. finally:
  154. if esp:
  155. # do hard reset after use esptool
  156. esp.hard_reset()
  157. esp._port.close()
  158. @classmethod
  159. def confirm_dut(cls, port, **kwargs):
  160. inst = None
  161. try:
  162. expected_rom_class = cls._get_rom()
  163. except NotImplementedError:
  164. expected_rom_class = None
  165. try:
  166. # TODO: check whether 8266 works with this logic
  167. # Otherwise overwrite it in ESP8266DUT
  168. inst = esptool.ESPLoader.detect_chip(port)
  169. if expected_rom_class and type(inst) != expected_rom_class:
  170. raise RuntimeError('Target not expected')
  171. return inst.read_mac() is not None, get_target_by_rom_class(type(inst))
  172. except(esptool.FatalError, RuntimeError):
  173. return False, None
  174. finally:
  175. if inst is not None:
  176. inst._port.close()
  177. @_uses_esptool
  178. def _try_flash(self, esp, erase_nvs, baud_rate):
  179. """
  180. Called by start_app() to try flashing at a particular baud rate.
  181. Structured this way so @_uses_esptool will reconnect each time
  182. """
  183. flash_files = []
  184. encrypt_files = []
  185. try:
  186. # Open the files here to prevents us from having to seek back to 0
  187. # each time. Before opening them, we have to organize the lists the
  188. # way esptool.write_flash needs:
  189. # If encrypt is provided, flash_files contains all the files to
  190. # flash.
  191. # Else, flash_files contains the files to be flashed as plain text
  192. # and encrypt_files contains the ones to flash encrypted.
  193. flash_files = self.app.flash_files
  194. encrypt_files = self.app.encrypt_files
  195. encrypt = self.app.flash_settings.get('encrypt', False)
  196. if encrypt:
  197. flash_files = encrypt_files
  198. encrypt_files = []
  199. else:
  200. flash_files = [entry
  201. for entry in flash_files
  202. if entry not in encrypt_files]
  203. flash_files = [(offs, open(path, 'rb')) for (offs, path) in flash_files]
  204. encrypt_files = [(offs, open(path, 'rb')) for (offs, path) in encrypt_files]
  205. if erase_nvs:
  206. address = self.app.partition_table['nvs']['offset']
  207. size = self.app.partition_table['nvs']['size']
  208. nvs_file = tempfile.TemporaryFile()
  209. nvs_file.write(b'\xff' * size)
  210. nvs_file.seek(0)
  211. if not isinstance(address, int):
  212. address = int(address, 0)
  213. # We have to check whether this file needs to be added to
  214. # flash_files list or encrypt_files.
  215. # Get the CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT macro
  216. # value. If it is set to True, then NVS is always encrypted.
  217. sdkconfig_dict = self.app.get_sdkconfig()
  218. macro_encryption = 'CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT' in sdkconfig_dict
  219. # If the macro is not enabled (plain text flash) or all files
  220. # must be encrypted, add NVS to flash_files.
  221. if not macro_encryption or encrypt:
  222. flash_files.append((address, nvs_file))
  223. else:
  224. encrypt_files.append((address, nvs_file))
  225. # fake flasher args object, this is a hack until
  226. # esptool Python API is improved
  227. class FlashArgs(object):
  228. def __init__(self, attributes):
  229. for key, value in attributes.items():
  230. self.__setattr__(key, value)
  231. # write_flash expects the parameter encrypt_files to be None and not
  232. # an empty list, so perform the check here
  233. flash_args = FlashArgs({
  234. 'flash_size': self.app.flash_settings['flash_size'],
  235. 'flash_mode': self.app.flash_settings['flash_mode'],
  236. 'flash_freq': self.app.flash_settings['flash_freq'],
  237. 'addr_filename': flash_files,
  238. 'encrypt_files': encrypt_files or None,
  239. 'no_stub': False,
  240. 'compress': True,
  241. 'verify': False,
  242. 'encrypt': encrypt,
  243. 'ignore_flash_encryption_efuse_setting': False,
  244. 'erase_all': False,
  245. })
  246. esp.change_baud(baud_rate)
  247. esptool.detect_flash_size(esp, flash_args)
  248. esptool.write_flash(esp, flash_args)
  249. finally:
  250. for (_, f) in flash_files:
  251. f.close()
  252. for (_, f) in encrypt_files:
  253. f.close()
  254. def start_app(self, erase_nvs=ERASE_NVS):
  255. """
  256. download and start app.
  257. :param: erase_nvs: whether erase NVS partition during flash
  258. :return: None
  259. """
  260. last_error = None
  261. for baud_rate in [921600, 115200]:
  262. try:
  263. self._try_flash(erase_nvs, baud_rate)
  264. break
  265. except RuntimeError as e:
  266. last_error = e
  267. else:
  268. raise last_error
  269. def image_info(self, path_to_file):
  270. """
  271. get hash256 of app
  272. :param: path: path to file
  273. :return: sha256 appended to app
  274. """
  275. old_stdout = sys.stdout
  276. new_stdout = io.StringIO()
  277. sys.stdout = new_stdout
  278. class Args(object):
  279. def __init__(self, attributes):
  280. for key, value in attributes.items():
  281. self.__setattr__(key, value)
  282. args = Args({
  283. 'chip': self.TARGET,
  284. 'filename': path_to_file,
  285. })
  286. esptool.image_info(args)
  287. output = new_stdout.getvalue()
  288. sys.stdout = old_stdout
  289. return output
  290. @_uses_esptool
  291. def reset(self, esp):
  292. """
  293. hard reset DUT
  294. :return: None
  295. """
  296. # decorator `_use_esptool` will do reset
  297. # so we don't need to do anything in this method
  298. pass
  299. @_uses_esptool
  300. def erase_partition(self, esp, partition):
  301. """
  302. :param partition: partition name to erase
  303. :return: None
  304. """
  305. raise NotImplementedError() # TODO: implement this
  306. # address = self.app.partition_table[partition]["offset"]
  307. size = self.app.partition_table[partition]['size']
  308. # TODO can use esp.erase_region() instead of this, I think
  309. with open('.erase_partition.tmp', 'wb') as f:
  310. f.write(chr(0xFF) * size)
  311. @_uses_esptool
  312. def erase_flash(self, esp):
  313. """
  314. erase the flash completely
  315. :return: None
  316. """
  317. esp.erase_flash()
  318. @_uses_esptool
  319. def dump_flash(self, esp, output_file, **kwargs):
  320. """
  321. dump flash
  322. :param output_file: output file name, if relative path, will use sdk path as base path.
  323. :keyword partition: partition name, dump the partition.
  324. ``partition`` is preferred than using ``address`` and ``size``.
  325. :keyword address: dump from address (need to be used with size)
  326. :keyword size: dump size (need to be used with address)
  327. :return: None
  328. """
  329. if os.path.isabs(output_file) is False:
  330. output_file = os.path.relpath(output_file, self.app.get_log_folder())
  331. if 'partition' in kwargs:
  332. partition = self.app.partition_table[kwargs['partition']]
  333. _address = partition['offset']
  334. _size = partition['size']
  335. elif 'address' in kwargs and 'size' in kwargs:
  336. _address = kwargs['address']
  337. _size = kwargs['size']
  338. else:
  339. raise IDFToolError("You must specify 'partition' or ('address' and 'size') to dump flash")
  340. content = esp.read_flash(_address, _size)
  341. with open(output_file, 'wb') as f:
  342. f.write(content)
  343. @staticmethod
  344. def _sort_usb_ports(ports):
  345. """
  346. Move the usb ports to the very beginning
  347. :param ports: list of ports
  348. :return: list of ports with usb ports at beginning
  349. """
  350. usb_ports = []
  351. rest_ports = []
  352. for port in ports:
  353. if 'usb' in port.lower():
  354. usb_ports.append(port)
  355. else:
  356. rest_ports.append(port)
  357. return usb_ports + rest_ports
  358. @classmethod
  359. def list_available_ports(cls):
  360. # It will return other kinds of ports as well, such as ttyS* ports.
  361. # Give the usb ports higher priority
  362. ports = cls._sort_usb_ports([x.device for x in list_ports.comports()])
  363. espport = os.getenv('ESPPORT')
  364. if not espport:
  365. # It's a little hard filter out invalid port with `serial.tools.list_ports.grep()`:
  366. # The check condition in `grep` is: `if r.search(port) or r.search(desc) or r.search(hwid)`.
  367. # This means we need to make all 3 conditions fail, to filter out the port.
  368. # So some part of the filters will not be straight forward to users.
  369. # And negative regular expression (`^((?!aa|bb|cc).)*$`) is not easy to understand.
  370. # Filter out invalid port by our own will be much simpler.
  371. return [x for x in ports if not cls.INVALID_PORT_PATTERN.search(x)]
  372. # On MacOs with python3.6: type of espport is already utf8
  373. if isinstance(espport, type(u'')):
  374. port_hint = espport
  375. else:
  376. port_hint = espport.decode('utf8')
  377. # If $ESPPORT is a valid port, make it appear first in the list
  378. if port_hint in ports:
  379. ports.remove(port_hint)
  380. return [port_hint] + ports
  381. # On macOS, user may set ESPPORT to /dev/tty.xxx while
  382. # pySerial lists only the corresponding /dev/cu.xxx port
  383. if sys.platform == 'darwin' and 'tty.' in port_hint:
  384. port_hint = port_hint.replace('tty.', 'cu.')
  385. if port_hint in ports:
  386. ports.remove(port_hint)
  387. return [port_hint] + ports
  388. return ports
  389. def lookup_pc_address(self, pc_addr):
  390. cmd = ['%saddr2line' % self.TOOLCHAIN_PREFIX,
  391. '-pfiaC', '-e', self.app.elf_file, pc_addr]
  392. ret = ''
  393. try:
  394. translation = subprocess.check_output(cmd)
  395. ret = translation.decode()
  396. except OSError:
  397. pass
  398. return ret
  399. @staticmethod
  400. def _queue_read_all(source_queue):
  401. output = []
  402. while True:
  403. try:
  404. output.append(source_queue.get(timeout=0))
  405. except _queue.Empty:
  406. break
  407. return output
  408. def _queue_copy(self, source_queue, dest_queue):
  409. data = self._queue_read_all(source_queue)
  410. for d in data:
  411. dest_queue.put(d)
  412. def _get_from_queue(self, queue_name):
  413. self_queue = getattr(self, queue_name)
  414. if self.receive_thread:
  415. recv_thread_queue = getattr(self.receive_thread, queue_name)
  416. self._queue_copy(recv_thread_queue, self_queue)
  417. return self._queue_read_all(self_queue)
  418. def stop_receive(self):
  419. if self.receive_thread:
  420. for name in ['performance_items', 'exceptions']:
  421. source_queue = getattr(self.receive_thread, name)
  422. dest_queue = getattr(self, name)
  423. self._queue_copy(source_queue, dest_queue)
  424. super(IDFDUT, self).stop_receive()
  425. def get_exceptions(self):
  426. """ Get exceptions detected by DUT receive thread. """
  427. return self._get_from_queue('exceptions')
  428. def get_performance_items(self):
  429. """
  430. DUT receive thread will automatic collect performance results with pattern ``[Performance][name]: value\n``.
  431. This method is used to get all performance results.
  432. :return: a list of performance items.
  433. """
  434. return self._get_from_queue('performance_items')
  435. def close(self):
  436. super(IDFDUT, self).close()
  437. if not self.allow_dut_exception and self.get_exceptions():
  438. Utility.console_log('DUT exception detected on {}'.format(self), color='red')
  439. raise IDFDUTException()
  440. class ESP32DUT(IDFDUT):
  441. TARGET = 'esp32'
  442. TOOLCHAIN_PREFIX = 'xtensa-esp32-elf-'
  443. @classmethod
  444. def _get_rom(cls):
  445. return esptool.ESP32ROM
  446. def erase_partition(self, esp, partition):
  447. raise NotImplementedError()
  448. class ESP32S2DUT(IDFDUT):
  449. TARGET = 'esp32s2'
  450. TOOLCHAIN_PREFIX = 'xtensa-esp32s2-elf-'
  451. @classmethod
  452. def _get_rom(cls):
  453. return esptool.ESP32S2ROM
  454. def erase_partition(self, esp, partition):
  455. raise NotImplementedError()
  456. class ESP32C3DUT(IDFDUT):
  457. TARGET = 'esp32c3'
  458. TOOLCHAIN_PREFIX = 'riscv32-esp-elf-'
  459. @classmethod
  460. def _get_rom(cls):
  461. return esptool.ESP32C3ROM
  462. def erase_partition(self, esp, partition):
  463. raise NotImplementedError()
  464. class ESP8266DUT(IDFDUT):
  465. TARGET = 'esp8266'
  466. TOOLCHAIN_PREFIX = 'xtensa-lx106-elf-'
  467. @classmethod
  468. def _get_rom(cls):
  469. return esptool.ESP8266ROM
  470. def erase_partition(self, esp, partition):
  471. raise NotImplementedError()
  472. def get_target_by_rom_class(cls):
  473. for c in [ESP32DUT, ESP32S2DUT, ESP32C3DUT, ESP8266DUT, IDFQEMUDUT]:
  474. if c._get_rom() == cls:
  475. return c.TARGET
  476. return None
  477. class IDFQEMUDUT(IDFDUT):
  478. TARGET = None
  479. TOOLCHAIN_PREFIX = None
  480. ERASE_NVS = True
  481. DEFAULT_EXPECT_TIMEOUT = 30 # longer timeout, since app startup takes more time in QEMU (due to slow SHA emulation)
  482. QEMU_SERIAL_PORT = 3334
  483. def __init__(self, name, port, log_file, app, allow_dut_exception=False, **kwargs):
  484. self.flash_image = tempfile.NamedTemporaryFile('rb+', suffix='.bin', prefix='qemu_flash_img')
  485. self.app = app
  486. self.flash_size = 4 * 1024 * 1024
  487. self._write_flash_img()
  488. args = [
  489. 'qemu-system-xtensa',
  490. '-nographic',
  491. '-machine', self.TARGET,
  492. '-drive', 'file={},if=mtd,format=raw'.format(self.flash_image.name),
  493. '-nic', 'user,model=open_eth',
  494. '-serial', 'tcp::{},server,nowait'.format(self.QEMU_SERIAL_PORT),
  495. '-S',
  496. '-global driver=timer.esp32.timg,property=wdt_disable,value=true']
  497. # TODO(IDF-1242): generate a temporary efuse binary, pass it to QEMU
  498. if 'QEMU_BIOS_PATH' in os.environ:
  499. args += ['-L', os.environ['QEMU_BIOS_PATH']]
  500. self.qemu = pexpect.spawn(' '.join(args), timeout=self.DEFAULT_EXPECT_TIMEOUT)
  501. self.qemu.expect_exact(b'(qemu)')
  502. super(IDFQEMUDUT, self).__init__(name, port, log_file, app, allow_dut_exception=allow_dut_exception, **kwargs)
  503. def _write_flash_img(self):
  504. self.flash_image.seek(0)
  505. self.flash_image.write(b'\x00' * self.flash_size)
  506. for offs, path in self.app.flash_files:
  507. with open(path, 'rb') as flash_file:
  508. contents = flash_file.read()
  509. self.flash_image.seek(offs)
  510. self.flash_image.write(contents)
  511. self.flash_image.flush()
  512. @classmethod
  513. def _get_rom(cls):
  514. return esptool.ESP32ROM
  515. @classmethod
  516. def get_mac(cls, app, port):
  517. # TODO(IDF-1242): get this from QEMU/efuse binary
  518. return '11:22:33:44:55:66'
  519. @classmethod
  520. def confirm_dut(cls, port, **kwargs):
  521. return True, cls.TARGET
  522. def start_app(self, erase_nvs=ERASE_NVS):
  523. # TODO: implement erase_nvs
  524. # since the flash image is generated every time in the constructor, maybe this isn't needed...
  525. self.qemu.sendline(b'cont\n')
  526. self.qemu.expect_exact(b'(qemu)')
  527. def reset(self):
  528. self.qemu.sendline(b'system_reset\n')
  529. self.qemu.expect_exact(b'(qemu)')
  530. def erase_partition(self, partition):
  531. raise NotImplementedError('method erase_partition not implemented')
  532. def erase_flash(self):
  533. raise NotImplementedError('method erase_flash not implemented')
  534. def dump_flash(self, output_file, **kwargs):
  535. raise NotImplementedError('method dump_flash not implemented')
  536. @classmethod
  537. def list_available_ports(cls):
  538. return ['socket://localhost:{}'.format(cls.QEMU_SERIAL_PORT)]
  539. def close(self):
  540. super(IDFQEMUDUT, self).close()
  541. self.qemu.sendline(b'q\n')
  542. self.qemu.expect_exact(b'(qemu)')
  543. for _ in range(self.DEFAULT_EXPECT_TIMEOUT):
  544. if not self.qemu.isalive():
  545. break
  546. time.sleep(1)
  547. else:
  548. self.qemu.terminate(force=True)
  549. class ESP32QEMUDUT(IDFQEMUDUT):
  550. TARGET = 'esp32'
  551. TOOLCHAIN_PREFIX = 'xtensa-esp32-elf-'