esp32_firmware_utils.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. #!/usr/bin/env python3
  2. # Copyright (c) 2020 Project CHIP Authors
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """Flash an ESP32 device.
  16. This is layered so that a caller can perform individual operations
  17. through an `Flasher` instance, or operations according to a command line.
  18. For `Flasher`, see the class documentation. For the parse_command()
  19. interface or standalone execution:
  20. usage: esp32_firmware_utils.py [-h] [--verbose] [--erase] [--application FILE]
  21. [--verify_application] [--reset] [--skip_reset]
  22. [--esptool FILE] [--parttool FILE]
  23. [--sdkconfig FILE] [--chip CHIP] [--port PORT]
  24. [--baud BAUD] [--before ACTION]
  25. [--after ACTION] [--flash_mode MODE]
  26. [--flash_freq FREQ] [--flash_size SIZE]
  27. [--compress] [--bootloader FILE]
  28. [--bootloader_offset OFFSET] [--partition FILE]
  29. [--partition_offset OFFSET]
  30. [--application_offset OFFSET]
  31. Flash ESP32 device
  32. optional arguments:
  33. -h, --help show this help message and exit
  34. configuration:
  35. --verbose, -v Report more verbosely
  36. --esptool FILE File name of the esptool executable
  37. --parttool FILE File name of the parttool executable
  38. --sdkconfig FILE File containing option defaults
  39. --chip CHIP Target chip type
  40. --port PORT Serial port device
  41. --baud BAUD Serial port baud rate
  42. --before ACTION What to do before connecting
  43. --after ACTION What to do when finished
  44. --flash_mode MODE, --flash-mode MODE
  45. Flash mode
  46. --flash_freq FREQ, --flash-freq FREQ
  47. Flash frequency
  48. --flash_size SIZE, --flash-size SIZE
  49. Flash size
  50. --compress, -z Compress data in transfer
  51. --bootloader FILE Bootloader image
  52. --bootloader_offset OFFSET, --bootloader-offset OFFSET
  53. Bootloader offset
  54. --partition FILE Partition table image
  55. --partition_offset OFFSET, --partition-offset OFFSET
  56. Partition table offset
  57. --application_offset OFFSET, --application-offset OFFSET
  58. Application offset
  59. operations:
  60. --erase Erase device
  61. --application FILE Flash an image
  62. --verify_application, --verify-application
  63. Verify the image after flashing
  64. --reset Reset device after flashing
  65. --skip_reset, --skip-reset
  66. Do not reset device after flashing
  67. """
  68. import os
  69. import pathlib
  70. import sys
  71. import firmware_utils
  72. # Additional options that can be use to configure an `Flasher`
  73. # object (as dictionary keys) and/or passed as command line options.
  74. ESP32_OPTIONS = {
  75. # Configuration options define properties used in flashing operations.
  76. 'configuration': {
  77. # Tool configuration options.
  78. 'esptool': {
  79. 'help': 'File name of the esptool executable',
  80. 'default': None,
  81. 'argparse': {
  82. 'metavar': 'FILE',
  83. },
  84. 'command': [
  85. {'option': 'esptool'},
  86. {'optional': 'port'},
  87. {'optional': 'baud'},
  88. {'optional': 'before'},
  89. {'optional': 'after'},
  90. ()
  91. ],
  92. 'verify': ['{esptool}', 'version'],
  93. 'error':
  94. """\
  95. Unable to execute {esptool}.
  96. Please ensure that this tool is installed and
  97. that $IDF_PATH is set. See the ESP32 example
  98. README for installation instructions.
  99. """,
  100. },
  101. 'parttool': {
  102. 'help': 'File name of the parttool executable',
  103. 'default': None,
  104. 'argparse': {
  105. 'metavar': 'FILE'
  106. },
  107. 'command': [
  108. {'option': 'parttool'},
  109. {'optional': 'port'},
  110. {'optional': 'baud'},
  111. {
  112. 'option': 'partition',
  113. 'result': ['--partition-table-file', '{partition}'],
  114. 'expand': True
  115. },
  116. ()
  117. ],
  118. 'verify': ['{parttool}', '--quiet'],
  119. 'error':
  120. """\
  121. Unable to execute {parttool}.
  122. Please ensure that this tool is installed and
  123. that $IDF_PATH is set. See the ESP32 example
  124. README for installation instructions.
  125. """,
  126. },
  127. 'sdkconfig': {
  128. 'help': 'File containing option defaults',
  129. 'default': None,
  130. 'argparse': {
  131. 'metavar': 'FILE'
  132. },
  133. },
  134. # Device configuration options.
  135. 'chip': {
  136. 'help': 'Target chip type',
  137. 'default': 'esp32',
  138. 'argparse': {
  139. 'metavar': 'CHIP'
  140. },
  141. 'sdkconfig': 'CONFIG_IDF_TARGET',
  142. },
  143. 'port': {
  144. 'help': 'Serial port device',
  145. 'default': None,
  146. 'argparse': {
  147. 'metavar': 'PORT',
  148. },
  149. 'sdkconfig': 'CONFIG_ESPTOOLPY_PORT',
  150. },
  151. 'baud': {
  152. 'help': 'Serial port baud rate',
  153. 'default': None,
  154. 'argparse': {
  155. 'metavar': 'BAUD',
  156. },
  157. 'sdkconfig': 'CONFIG_ESPTOOLPY_BAUD',
  158. },
  159. 'before': {
  160. 'help': 'What to do before connecting',
  161. 'default': None,
  162. 'argparse': {
  163. 'metavar': 'ACTION',
  164. },
  165. 'sdkconfig': 'CONFIG_ESPTOOLPY_BEFORE',
  166. },
  167. 'after': {
  168. 'help': 'What to do when finished',
  169. 'default': None,
  170. 'argparse': {
  171. 'metavar': 'ACTION',
  172. },
  173. 'sdkconfig': 'CONFIG_ESPTOOLPY_AFTER',
  174. },
  175. 'flash_mode': {
  176. 'help': 'Flash mode',
  177. 'default': None,
  178. 'argparse': {
  179. 'metavar': 'MODE',
  180. },
  181. 'sdkconfig': 'CONFIG_ESPTOOLPY_FLASHMODE',
  182. },
  183. 'flash_freq': {
  184. 'help': 'Flash frequency',
  185. 'default': None,
  186. 'argparse': {
  187. 'metavar': 'FREQ',
  188. },
  189. 'sdkconfig': 'CONFIG_ESPTOOLPY_FLASHFREQ',
  190. },
  191. 'flash_size': {
  192. 'help': 'Flash size',
  193. 'default': None,
  194. 'argparse': {
  195. 'metavar': 'SIZE',
  196. },
  197. 'sdkconfig': 'CONFIG_ESPTOOLPY_FLASHSIZE',
  198. },
  199. 'compress': {
  200. 'help': 'Compress data in transfer',
  201. 'default': None,
  202. 'alias': ['-z'],
  203. 'argparse': {
  204. 'action': 'store_true'
  205. },
  206. 'sdkconfig': 'CONFIG_ESPTOOLPY_COMPRESSED',
  207. },
  208. # Flashing things.
  209. 'bootloader': {
  210. 'help': 'Bootloader image',
  211. 'default': None,
  212. 'argparse': {
  213. 'metavar': 'FILE',
  214. 'type': pathlib.Path,
  215. },
  216. },
  217. 'bootloader_offset': {
  218. 'help': 'Bootloader offset',
  219. 'default': '0x1000',
  220. 'argparse': {
  221. 'metavar': 'OFFSET'
  222. },
  223. 'sdkconfig': 'CONFIG_BOOTLOADER_OFFSET_IN_FLASH',
  224. },
  225. 'partition': {
  226. 'help': 'Partition table image',
  227. 'default': None,
  228. 'argparse': {
  229. 'metavar': 'FILE',
  230. 'type': pathlib.Path,
  231. },
  232. },
  233. 'partition_offset': {
  234. 'help': 'Partition table offset',
  235. 'default': None,
  236. 'argparse': {
  237. 'metavar': 'OFFSET'
  238. },
  239. 'sdkconfig': 'CONFIG_PARTITION_TABLE_OFFSET',
  240. },
  241. 'application_offset': {
  242. 'help': 'Application offset',
  243. 'default': None,
  244. 'argparse': {
  245. 'metavar': 'OFFSET'
  246. },
  247. },
  248. },
  249. }
  250. def namespace_defaults(dst, src):
  251. for key, value in src.items():
  252. if key not in dst or getattr(dst, key) is None:
  253. setattr(dst, key, value)
  254. class Flasher(firmware_utils.Flasher):
  255. """Manage esp32 flashing."""
  256. def __init__(self, **options):
  257. super().__init__(platform='ESP32', module=__name__, **options)
  258. self.define_options(ESP32_OPTIONS)
  259. def _postprocess_argv(self):
  260. if self.option.sdkconfig:
  261. namespace_defaults(self.option,
  262. self.read_sdkconfig(self.option.sdkconfig))
  263. # idf = os.environ.get('IDF_PATH')
  264. # if idf:
  265. # if self.option.esptool is None:
  266. # self.option.esptool = os.path.join(
  267. # idf,
  268. # 'components/esptool_py/esptool/esptool.py')
  269. # if self.option.parttool is None:
  270. # self.option.parttool = os.path.join(
  271. # idf,
  272. # 'components/partition_table/parttool.py')
  273. def read_sdkconfig(self, filename):
  274. """Given an ESP32 sdkconfig file, read it for values of options
  275. not otherwise set.
  276. """
  277. config_map = {}
  278. for key, info in vars(self.info).items():
  279. config_key = info.get('sdkconfig')
  280. if config_key:
  281. config_map[config_key] = key
  282. result = {}
  283. with open(filename) as f:
  284. for line in f:
  285. k, eq, v = line.strip().partition('=')
  286. if eq == '=' and k in config_map:
  287. result[config_map[k]] = v.strip('"')
  288. return result
  289. IDF_PATH_TOOLS = {
  290. 'esptool': 'components/esptool_py/esptool/esptool.py',
  291. 'parttool': 'components/partition_table/parttool.py',
  292. }
  293. def locate_tool(self, tool):
  294. if tool in self.IDF_PATH_TOOLS:
  295. idf_path = os.environ.get('IDF_PATH')
  296. if idf_path:
  297. return os.path.join(idf_path, self.IDF_PATH_TOOLS[tool])
  298. return super().locate_tool(tool)
  299. # Common command line arguments for esptool flashing subcommands.
  300. FLASH_ARGUMENTS = [
  301. {'optional': 'flash_mode'},
  302. {'optional': 'flash_freq'},
  303. {'optional': 'flash_size'},
  304. {
  305. 'match': '{compress}',
  306. 'test': [(True, '-z'), ('y', '-z')],
  307. 'default': '-u'
  308. },
  309. ]
  310. def erase(self):
  311. """Perform `commander device masserase`."""
  312. return self.run_tool('esptool', ['erase_flash'], {}, 'Erase device')
  313. def verify(self, image, address=0):
  314. """Verify image(s)."""
  315. if not isinstance(image, list):
  316. image = [address, image]
  317. return self.run_tool(
  318. 'esptool',
  319. ['verify_flash', self.FLASH_ARGUMENTS, image],
  320. name='Verify',
  321. pass_message='Verified',
  322. fail_message='Not verified',
  323. fail_level=2)
  324. def flash(self, image, address=0):
  325. """Flash image(s)."""
  326. if not isinstance(image, list):
  327. image = [address, image]
  328. return self.run_tool(
  329. 'esptool',
  330. ['write_flash', self.FLASH_ARGUMENTS, image],
  331. name='Flash')
  332. PARTITION_INFO = {
  333. 'phy': ['--partition-type', 'data', '--partition-subtype', 'phy'],
  334. 'application': ['--partition-boot-default'],
  335. 'ota': ['--partition-type', 'data', '--partition-subtype', 'ota'],
  336. 'factory': [
  337. '--partition-type', 'app', '--partition-subtype', 'factory'
  338. ],
  339. }
  340. def get_partition_info(self, item, info, options=None):
  341. """Run parttool to get partition information."""
  342. return self.run_tool(
  343. 'parttool',
  344. ['get_partition_info', self.PARTITION_INFO[item], '--info', info],
  345. options or {},
  346. capture_output=True).stdout.strip()
  347. def make_wrapper(self, argv):
  348. self.parser.add_argument(
  349. '--use-parttool',
  350. metavar='FILENAME',
  351. help='partition tool to configure flashing script')
  352. self.parser.add_argument(
  353. '--use-partition-file',
  354. metavar='FILENAME',
  355. help='partition file to configure flashing script')
  356. self.parser.add_argument(
  357. '--use-sdkconfig',
  358. metavar='FILENAME',
  359. help='sdkconfig to configure flashing script')
  360. super().make_wrapper(argv)
  361. def _platform_wrapper_args(self, args):
  362. if args.use_sdkconfig:
  363. # Include values from sdkconfig so that it isn't needed at
  364. # flashing time.
  365. namespace_defaults(args, self.read_sdkconfig(args.use_sdkconfig))
  366. parttool = args.use_parttool or args.parttool
  367. partfile = args.use_partition_file or args.partition
  368. if parttool and partfile:
  369. # Get unspecified offsets from the partition file now,
  370. # so that parttool isn't needed at flashing time.
  371. if args.application and args.application_offset is None:
  372. args.application_offset = self.get_partition_info(
  373. 'application', 'offset',
  374. {'parttool': parttool, 'partition': partfile})
  375. def actions(self):
  376. """Perform actions on the device according to self.option."""
  377. self.log(3, 'Options:', self.option)
  378. if self.option.erase:
  379. if self.erase().err:
  380. return self
  381. if self.option.application:
  382. application = self.option.application
  383. bootloader = self.option.bootloader
  384. partition = self.option.partition
  385. # Collect the flashable items.
  386. flash = []
  387. if bootloader:
  388. flash += [self.option.bootloader_offset, bootloader]
  389. if application:
  390. offset = self.option.application_offset
  391. if offset is None:
  392. offset = self.get_partition_info('application', 'offset')
  393. flash += [offset, application]
  394. if partition:
  395. flash += [self.option.partition_offset, partition]
  396. # esptool.py doesn't have an independent reset command, so we add
  397. # an `--after` option to the final operation, which may be either
  398. # flash or verify.
  399. if self.option.after is None:
  400. if self.option.reset or self.option.reset is None:
  401. self.option.after = 'hard_reset'
  402. else:
  403. self.option.after = 'no_reset'
  404. if self.option.verify_application:
  405. verify_after = self.option.after
  406. self.option.after = 'no_reset'
  407. if self.flash(flash).err:
  408. return self
  409. if self.option.verify_application:
  410. self.option.after = verify_after
  411. if self.verify(flash).err:
  412. return self
  413. return self
  414. # Mobly integration
  415. class ESP32Platform:
  416. def __init__(self, flasher_args):
  417. self.flasher = Flasher(**flasher_args)
  418. def flash(self):
  419. self.flasher.flash_command([os.getcwd()])
  420. def verify_platform_args(platform_args):
  421. required_args = [
  422. 'application',
  423. 'parttool',
  424. 'port',
  425. 'baud',
  426. 'before',
  427. 'after',
  428. 'flash_mode',
  429. 'flash_freq',
  430. 'flash_size',
  431. 'compress',
  432. 'bootloader',
  433. 'partition',
  434. 'partition_offset',
  435. 'application_offset',
  436. ]
  437. difference = set(required_args) - set(platform_args)
  438. if difference:
  439. raise ValueError("Required arguments missing: %s" % difference)
  440. def create_platform(platform_args):
  441. verify_platform_args(platform_args[0])
  442. return ESP32Platform(platform_args[0])
  443. # End of Mobly integration
  444. if __name__ == '__main__':
  445. sys.exit(Flasher().flash_command(sys.argv))