diff --git a/.gitignore b/.gitignore index b1fa3e5..b5a7601 100644 --- a/.gitignore +++ b/.gitignore @@ -110,3 +110,6 @@ package-lock.json notarize.sh staple.sh +# Exclude the pyinstaller directory (if it's here) +pyinstaller/ + diff --git a/About.py b/About.py index 9830018..ed340e7 100644 --- a/About.py +++ b/About.py @@ -21,7 +21,7 @@ class AboutDlg(wx.Dialog):

Version {1}

-

BrewFlasher - © 2019 John Beeler. Licensed under MIT.

+

BrewFlasher - © 2019-2021 John Beeler. Licensed under MIT.

Based on NodeMCU PyFlasher - © 2019 Marcel Stör. Licensed under MIT.

diff --git a/Distribution.xml b/Distribution.xml new file mode 100644 index 0000000..afb917f --- /dev/null +++ b/Distribution.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + brewflasher.pkg + \ No newline at end of file diff --git a/Entitlements.plist b/Entitlements.plist index 3118b6a..c618c82 100644 --- a/Entitlements.plist +++ b/Entitlements.plist @@ -3,13 +3,8 @@ - com.apple.security.cs.allow-jit - com.apple.security.cs.allow-unsigned-executable-memory - com.apple.security.automation.apple-events - - com.apple.security.cs.disable-library-validation - + diff --git a/LICENSE b/LICENSE index ca16109..ad0da4e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License NodeMCU Flasher Code - Copyright (c) 2016 Marcel Stör -BrewFlasher Changes/ Additions - Copyright (c) 2020 John Beeler +BrewFlasher Changes/ Additions - Copyright (c) 2020-2022 John Beeler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Main.py b/Main.py index 07a02e3..d2976b8 100644 --- a/Main.py +++ b/Main.py @@ -19,27 +19,27 @@ from argparse import Namespace # Load the import & initialize the firmware_list -import fermentrack_integration -firmware_list = fermentrack_integration.FirmwareList() - -__version__ = "1.0" -__flash_help__ = ''' -

This setting depends on your device - but in most cases you will want to use DIO.

-

-

-

-

- For more information, details are found at at http://bit.ly/2v5Rd32 - and in the esptool - documentation - -

-''' +import brewflasher_com_integration +firmware_list = brewflasher_com_integration.FirmwareList() + +__version__ = "1.2" +# __flash_help__ = ''' +#

This setting depends on your device - but in most cases you will want to use DIO.

+#

+#

+#

+#

+# For more information, details are found at at http://bit.ly/2v5Rd32 +# and in the esptool +# documentation +# +#

+# ''' __auto_select__ = "Auto-select" __auto_select_explanation__ = "(first port with Espressif device)" __supported_baud_rates__ = [9600, 57600, 74880, 115200, 230400, 460800, 921600] @@ -68,6 +68,9 @@ def flush(self): # noinspection PyStatementEffect None + def isatty(self): + return False + # --------------------------------------------------------------------------- @@ -94,13 +97,20 @@ def run(self): print("Must select the firmware to flash before flashing.") return + print("Verifying firmware list is up-to-date before downloading...") + if not self._config.firmware_obj.pre_flash_web_verify(brewflasher_version=__version__): + print("Firmware list is not up to date. Relaunch BrewFlasher and try again.") + return + print("Downloading firmware...") if self._config.firmware_obj.download_to_file(): print("Downloaded successfully!\n") else: print("Error - unable to download firmware.\n") + return if self._config.device_family_string == "ESP32": + # This command matches the ESP32 flash options JSON from BrewFlasher.com command_extension = ["--chip", "esp32", "--baud", str(self._config.baud), "--before", "default_reset", "--after", "hard_reset", @@ -123,7 +133,8 @@ def run(self): else: command_extension = ["--chip", "esp8266", "write_flash", - "--flash_mode", self._config.mode, "0x00000", + # "--flash_mode", self._config.mode, + "0x00000", self._config.firmware_obj.full_filepath("firmware")] # For both ESP32 and ESP8266 we can directly flash an image to SPIFFS. @@ -158,9 +169,19 @@ def run(self): if self._config.erase_before_flash: command.append("--erase-all") + # There is a breaking change in esptool 3.0 that changes the flash size from detect to keep. We want to + # support "detect" by default. + command.append("-fs") + command.append("detect") + print("Command: esptool.py %s\n" % " ".join(command)) - esptool.main(command) + try: + esptool.main(command) + except: + print("Firmware flashing FAILED. esptool.py raised an error.") + print("Try flashing again, or try flashing with a slower speed.") + return # The last line printed by esptool is "Staying in bootloader." -> some indication that the process is # done is needed @@ -180,7 +201,7 @@ class FlashConfig: def __init__(self): self.baud = 115200 self.erase_before_flash = False - self.mode = "dio" + # self.mode = "dio" self.firmware_path = None # self.port = None self.port = __auto_select__ @@ -201,7 +222,7 @@ def load(cls, file_path): data = json.load(f) conf.port = data['port'] conf.baud = data['baud'] - conf.mode = data['mode'] + # conf.mode = data['mode'] conf.erase_before_flash = data['erase'] return conf @@ -209,7 +230,7 @@ def safe(self, file_path): data = { 'port': self.port, 'baud': self.baud, - 'mode': self.mode, + # 'mode': self.mode, 'erase': self.erase_before_flash, } with open(file_path, 'w') as f: @@ -252,11 +273,11 @@ def on_baud_changed(event): if radio_button.GetValue(): self._config.baud = radio_button.rate - def on_mode_changed(event): - radio_button = event.GetEventObject() - - if radio_button.GetValue(): - self._config.mode = radio_button.mode + # def on_mode_changed(event): + # radio_button = event.GetEventObject() + # + # if radio_button.GetValue(): + # self._config.mode = radio_button.mode def on_erase_changed(event): radio_button = event.GetEventObject() @@ -345,7 +366,7 @@ def on_select_firmware(event): serial_boxsizer = wx.BoxSizer(wx.HORIZONTAL) serial_boxsizer.Add(self.choice, 1, wx.EXPAND) serial_boxsizer.AddStretchSpacer(0) - serial_boxsizer.Add(reload_button, 0, wx.ALIGN_RIGHT, 20) + serial_boxsizer.Add(reload_button, 0, 0, 20) project_boxsizer = wx.BoxSizer(wx.HORIZONTAL) project_boxsizer.Add(self.project_choice, 1, wx.EXPAND) @@ -371,20 +392,21 @@ def add_baud_radio_button(sizer, index, baud_rate): for idx, rate in enumerate(__supported_baud_rates__): add_baud_radio_button(baud_boxsizer, idx, rate) - flashmode_boxsizer = wx.BoxSizer(wx.HORIZONTAL) - - def add_flash_mode_radio_button(sizer, index, mode, label): - style = wx.RB_GROUP if index == 0 else 0 - radio_button = wx.RadioButton(panel, name="mode-%s" % mode, label="%s" % label, style=style) - radio_button.Bind(wx.EVT_RADIOBUTTON, on_mode_changed) - radio_button.mode = mode - radio_button.SetValue(mode == self._config.mode) - sizer.Add(radio_button) - sizer.AddSpacer(10) - - add_flash_mode_radio_button(flashmode_boxsizer, 0, "qio", "Quad I/O (QIO)") - add_flash_mode_radio_button(flashmode_boxsizer, 1, "dio", "Dual I/O (DIO)") - add_flash_mode_radio_button(flashmode_boxsizer, 2, "dout", "Dual Output (DOUT)") + # As of esptool 2.0 flash_mode is autodetected from the firmware. We've also always ignored it for ESP32. + # flashmode_boxsizer = wx.BoxSizer(wx.HORIZONTAL) + # + # def add_flash_mode_radio_button(sizer, index, mode, label): + # style = wx.RB_GROUP if index == 0 else 0 + # radio_button = wx.RadioButton(panel, name="mode-%s" % mode, label="%s" % label, style=style) + # radio_button.Bind(wx.EVT_RADIOBUTTON, on_mode_changed) + # radio_button.mode = mode + # radio_button.SetValue(mode == self._config.mode) + # sizer.Add(radio_button) + # sizer.AddSpacer(10) + # + # add_flash_mode_radio_button(flashmode_boxsizer, 0, "qio", "Quad I/O (QIO)") + # add_flash_mode_radio_button(flashmode_boxsizer, 1, "dio", "Dual I/O (DIO)") + # add_flash_mode_radio_button(flashmode_boxsizer, 2, "dout", "Dual Output (DOUT)") erase_boxsizer = wx.BoxSizer(wx.HORIZONTAL) @@ -416,26 +438,26 @@ def add_erase_radio_button(sizer, index, erase_before_flash, label, value): device_family_label = wx.StaticText(panel, label="Device Family ") firmware_label = wx.StaticText(panel, label="Firmware ") baud_label = wx.StaticText(panel, label="Baud rate ") - flashmode_label = wx.StaticText(panel, label="Flash mode ") - - def on_info_hover(event): - from HtmlPopupTransientWindow import HtmlPopupTransientWindow - win = HtmlPopupTransientWindow(self, wx.SIMPLE_BORDER, __flash_help__, "#FFB6C1", (410, 140)) - - image = event.GetEventObject() - image_position = image.ClientToScreen((0, 0)) - image_size = image.GetSize() - win.Position(image_position, (0, image_size[1])) - - win.Popup() - - icon = wx.StaticBitmap(panel, wx.ID_ANY, images.Info.GetBitmap()) - icon.Bind(wx.EVT_MOTION, on_info_hover) - - flashmode_label_boxsizer = wx.BoxSizer(wx.HORIZONTAL) - flashmode_label_boxsizer.Add(flashmode_label, 1, wx.EXPAND) - flashmode_label_boxsizer.AddStretchSpacer(0) - flashmode_label_boxsizer.Add(icon, 0, wx.ALIGN_RIGHT, 20) + # flashmode_label = wx.StaticText(panel, label="Flash mode ") + + # def on_info_hover(event): + # from HtmlPopupTransientWindow import HtmlPopupTransientWindow + # win = HtmlPopupTransientWindow(self, wx.SIMPLE_BORDER, __flash_help__, "#FFB6C1", (410, 140)) + # + # image = event.GetEventObject() + # image_position = image.ClientToScreen((0, 0)) + # image_size = image.GetSize() + # win.Position(image_position, (0, image_size[1])) + # + # win.Popup() + # + # icon = wx.StaticBitmap(panel, wx.ID_ANY, images.Info.GetBitmap()) + # icon.Bind(wx.EVT_MOTION, on_info_hover) + + # flashmode_label_boxsizer = wx.BoxSizer(wx.HORIZONTAL) + # flashmode_label_boxsizer.Add(flashmode_label, 1, wx.EXPAND) + # flashmode_label_boxsizer.AddStretchSpacer(0) + # flashmode_label_boxsizer.Add(icon, 0, 0, 20) erase_label = wx.StaticText(panel, label="Erase flash") console_label = wx.StaticText(panel, label="Console") @@ -446,11 +468,11 @@ def on_info_hover(event): device_family_label, (device_family_boxsizer, 1, wx.EXPAND), firmware_label, (firmware_boxsizer, 1, wx.EXPAND), baud_label, baud_boxsizer, - flashmode_label_boxsizer, flashmode_boxsizer, + # flashmode_label_boxsizer, flashmode_boxsizer, erase_label, erase_boxsizer, (wx.StaticText(panel, label="")), (button, 1, wx.EXPAND), (console_label, 1, wx.EXPAND), (self.console_ctrl, 1, wx.EXPAND)]) - fgs.AddGrowableRow(8, 1) + fgs.AddGrowableRow(7, 1) fgs.AddGrowableCol(1, 1) hbox.Add(fgs, proportion=2, flag=wx.ALL | wx.EXPAND, border=15) panel.SetSizer(hbox) diff --git a/PRIVACY.md b/PRIVACY.md index 468c428..30ffee5 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,14 +1,19 @@ ## Privacy Policy -BrewFlasher does not explicitly collect or transmit any user information or usage statistics. +BrewFlasher does not explicitly collect or transmit any user information. -BrewFlasher accesses [fermentrack.com](http://www.fermentrack.com) to download a list of +BrewFlasher accesses [brewflasher.com](http://www.brewflasher.com) to download a list of available firmware, as well as other websites to download the firmware itself. These websites - like all websites - may keep logs of access history which could include -(amongst other things) the IP address from which the request was made. +(amongst other things) the IP address from which the request was made. BrewFlasher.com +does anonymously log when a specific firmware item is selected to be flashed for the +purposes of tracking the popularity of various projects. -Although the author of BrewFlasher does not use this access information for any marketing -purposes, it is possible that the websites serving individual firmware files might. Using -BrewFlasher requires that you understand and accept this risk. +Although BrewFlasher.com provides BrewFlasher with links to the firmware to be flashed, +neither BrewFlasher or BrewFlasher.com actually provide the firmware itself. Firmware is +downloaded directly from the sites on which it is hosted. The privacy policies of these +websites may - and likely do - differ from this privacy policy, and may log additional +information beyond what this policy specifies. This is outside BrewFlasher's control. +Using BrewFlasher requires that you understand and accept this risk. diff --git a/PackageOptions.plist b/PackageOptions.plist new file mode 100644 index 0000000..009c572 --- /dev/null +++ b/PackageOptions.plist @@ -0,0 +1,18 @@ + + + + + + BundleHasStrictIdentifier + + BundleIsRelocatable + + BundleIsVersionChecked + + BundleOverwriteAction + upgrade + RootRelativeBundlePath + BrewFlasher.app + + + \ No newline at end of file diff --git a/README.md b/README.md index 9305330..1bc00ad 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ A standalone desktop application for flashing brewing-related firmware to your E ![Image of NodeMCU PyFlasher GUI](images/gui.png) ## About -BrewFlasher is a standalone desktop application that allows the user to flash specific firmware (upload software) to an ESP32 or ESP8266. It will automatically locate the firmare on the internet, download it, and flash it to your chip with minimal input required. Simple, fast, and magic. +BrewFlasher is a standalone desktop application that allows the user to flash specific firmware (upload software) to an +ESP32 or ESP8266. It will automatically locate the firmware on the internet, download it, and flash it to your chip with +minimal input required. Simple, fast, and magic. This project was built as a natural complement to a handful of other projects I either support or collaborate on, where a user is expected to flash firmware to an ESP8266 or ESP32 controller. For many users, this step of the installation @@ -25,6 +27,9 @@ ESP8266. Those projects include: - BrewPi-ESP8266 - TiltBridge - BrewBubbles +- Keg Cop +- Flite +- iSpindHub If you have a beer- or brewing-related project you would like supported, raise an [issue](https://github.com/thorrak/brewflasher/issues) and we can discuss it. @@ -32,7 +37,7 @@ If you are looking to flash generic firmware, I recommend the [NodeMCU PyFlasher ## Installation -BrewFlasher doesn't have to be installed, just double-click it and it'll start. BrewFlasher is available for both MacOS +BrewFlasher doesn't have to be installed; just double-click it and it'll start. BrewFlasher is available for both MacOS and Windows. @@ -40,13 +45,16 @@ and Windows. ## Background & Development BrewFlasher is based *heavily* on the [NodeMCU PyFlasher](https://github.com/marcelstoer/nodemcu-pyflasher) project. In comparison to that project, BrewFlasher adds explicit ESP32 support as well as a workflow to automate selecting and -downloading brewing-related firmware to flash. The firmware selection is taken from +downloading brewing-related firmware to flash. The firmware selection is taken from a curated list maintained +behind-the-scenes on Fermentrack.com. ## License -This package was based on the NodeMCU PyFlasher project which is licensed under [MIT](http://opensource.org/licenses/MIT) license, the code for which is © Marcel Stör +This package was based on the NodeMCU PyFlasher project which is licensed under [MIT](http://opensource.org/licenses/MIT) +license, the code for which is © Marcel Stör -All subsequent additions are additionally licensed under the [MIT](http://opensource.org/licenses/MIT) license and are © John Beeler +All subsequent additions are additionally licensed under the [MIT](http://opensource.org/licenses/MIT) license and are +© John Beeler As noted in the license, BrewFlasher is provided without warranty of any kind. As when flashing any microcontroller, a possibility of damage exists. Be careful, as all use of BrewFlasher is at your own risk. diff --git a/fermentrack_integration.py b/brewflasher_com_integration.py similarity index 84% rename from fermentrack_integration.py rename to brewflasher_com_integration.py index 119b7ef..6835152 100644 --- a/fermentrack_integration.py +++ b/brewflasher_com_integration.py @@ -1,10 +1,13 @@ +from pathlib import Path +import os.path import requests import copy -import os.path, sys +import sys import fhash -FERMENTRACK_COM_URL = "http://www.fermentrack.com" +BREWFLASHER_COM_URL = "https://www.brewflasher.com/firmware" +MODEL_VERSION = 3 class Project: @@ -26,15 +29,14 @@ def __str__(self): class Firmware: - def __init__(self, name="", version="", revision="", family_id=0, variant="", is_fermentrack_supported="", + def __init__(self, name="", version="", family_id=0, variant="", is_fermentrack_supported="", in_error="", description="", variant_description="", download_url="", id=0, project_id=0, - project_url="", documentation_url="", weight="", download_url_partitions="", + post_install_instructions="", weight="", download_url_partitions="", download_url_spiffs="", checksum="", checksum_partitions="", checksum_spiffs="", spiffs_address="", download_url_bootloader="", checksum_bootloader="", download_url_otadata="", otadata_address="", checksum_otadata=""): self.name = name self.version = version - self.revision = revision self.family_id = family_id self.variant = variant self.is_fermentrack_supported = is_fermentrack_supported @@ -42,8 +44,7 @@ def __init__(self, name="", version="", revision="", family_id=0, variant="", is self.description = description self.variant_description = variant_description self.download_url = download_url - self.project_url = project_url - self.documentation_url = documentation_url + self.post_install_instructions = post_install_instructions self.weight = weight self.download_url_partitions = download_url_partitions self.download_url_spiffs = download_url_spiffs @@ -64,8 +65,6 @@ def __str__(self): if len(self.version) > 0: str_rep += " - v{}".format(self.version) - if len(self.revision) > 0: - str_rep += " - rev {}".format(self.revision) if len(self.variant) > 0: str_rep += " - {}".format(self.variant) return str_rep @@ -114,28 +113,51 @@ def full_filepath(self, bintype: str): def download_to_file(self, check_checksum=True, force_download=False): # If this is a multi-part firmware (ESP32, with partitions or SPIFFS) then download the additional parts. if len(self.download_url_partitions) > 12: + print("Downloading partitions file...") if not self.download_file(self.full_filepath("partitions"), self.download_url_partitions, self.checksum_partitions, check_checksum, force_download): + print("Error downloading partitions file!") return False if len(self.download_url_spiffs) > 12 and len(self.spiffs_address) > 2: + print("Downloading SPIFFS/LittleFS file...") if not self.download_file(self.full_filepath("spiffs"), self.download_url_spiffs, self.checksum_spiffs, check_checksum, force_download): + print("Error downloading SPIFFS/LittleFS file!") return False if len(self.download_url_bootloader) > 12: + print("Downloading bootloader file...") if not self.download_file(self.full_filepath("bootloader"), self.download_url_bootloader, self.checksum_bootloader, check_checksum, force_download): + print("Error downloading bootloader file!") return False if len(self.download_url_otadata) > 12 and len(self.otadata_address) > 2: + print("Downloading otadata file...") if not self.download_file(self.full_filepath("otadata"), self.download_url_otadata, self.checksum_otadata, check_checksum, force_download): + print("Error downloading otadata file!") return False # Always download the main firmware + print("Downloading main firmware file...") return self.download_file(self.full_filepath("firmware"), self.download_url, self.checksum, check_checksum, force_download) + def pre_flash_web_verify(self, brewflasher_version): + """Recheck that the checksum we have cached is still the one that brewflasher.com reports""" + request_dict = { + 'firmware_id': self.id, + 'flasher': "BrewFlasher", + 'flasher_version': brewflasher_version + } + url = BREWFLASHER_COM_URL + "/api/flash_verify/" + r = requests.post(url, json=request_dict) + response = r.json() + if response['status'] == "success": + if response['message'] == self.checksum: + return True + return False class DeviceFamily: @@ -162,32 +184,33 @@ def __str__(self): return "Device Families" def load_projects_from_website(self) -> bool: - try: - url = FERMENTRACK_COM_URL + "/api/project_list/all/" - response = requests.get(url) - data = response.json() - except: - return False +# try: + url = BREWFLASHER_COM_URL + "/api/project_list/all/" + response = requests.get(url) + data = response.json() + # except: + # return False if len(data) > 0: for row in data: try: - # This gets wrapped in a try/except as I don't want this failing if the local copy of Fermentrack - # is slightly behind what is available at Fermentrack.com (eg - if there are new device families) + # This gets wrapped in a try/except as I don't want this failing if the local copy of BrewFlasher + # is slightly behind what is available at Brewflasher.com (eg - if there are new device families) newProject = Project(name=row['name'], weight=row['weight'], id=row['id'], description=row['description'], support_url=row['support_url'], project_url=row['project_url'], documentation_url=row['documentation_url'], show=row['show_in_standalone_flasher']) self.Projects[row['id']] = copy.deepcopy(newProject) except: + # TODO - Display an error message pass return True - return False # We didn't get data back from Fermentrack.com, or there was an error + return False # We didn't get data back from Brewflasher.com, or there was an error def load_families_from_website(self) -> bool: try: - url = FERMENTRACK_COM_URL + "/api/firmware_family_list/" + url = BREWFLASHER_COM_URL + "/api/firmware_family_list/" response = requests.get(url) data = response.json() except: @@ -196,8 +219,8 @@ def load_families_from_website(self) -> bool: if len(data) > 0: for row in data: try: - # This gets wrapped in a try/except as I don't want this failing if the local copy of Fermentrack - # is slightly behind what is available at Fermentrack.com (eg - if there are new device families) + # This gets wrapped in a try/except as I don't want this failing if the local copy of BrewFlasher + # is slightly behind what is available at Brewflasher.com (eg - if there are new device families) newFamily = DeviceFamily(name=row['name'], flash_method=row['flash_method'], id=row['id'], detection_family=row['detection_family']) if newFamily.flash_method == "esptool": # Only save families that use esptool @@ -206,15 +229,16 @@ def load_families_from_website(self) -> bool: for this_project in self.Projects: self.Projects[this_project].device_families[newFamily.id] = copy.deepcopy(newFamily) except: + # TODO - Display an error message pass return True - return False # We didn't get data back from Fermentrack.com, or there was an error + return False # We didn't get data back from Brewflasher.com, or there was an error def load_firmware_from_website(self) -> bool: # This is intended to be run after load_families_from_website try: - url = FERMENTRACK_COM_URL + "/api/firmware_list/all/" + url = BREWFLASHER_COM_URL + "/api/firmware_list/all/" response = requests.get(url) data = response.json() except: @@ -224,18 +248,18 @@ def load_firmware_from_website(self) -> bool: # Then loop through the data we received and recreate it again for row in data: try: - # This gets wrapped in a try/except as I don't want this failing if the local copy of Fermentrack - # is slightly behind what is available at Fermentrack.com (eg - if there are new device families) + # This gets wrapped in a try/except as I don't want this failing if the local copy of BrewFlasher + # is slightly behind what is available at Brewflasher.com (eg - if there are new device families) newFirmware = Firmware( - name=row['name'], version=row['version'], revision=row['revision'], family_id=row['family_id'], + name=row['name'], version=row['version'], family_id=row['family_id'], variant=row['variant'], is_fermentrack_supported=row['is_fermentrack_supported'], in_error=row['in_error'], description=row['description'], variant_description=row['variant_description'], download_url=row['download_url'], - project_url=row['project_url'], documentation_url=row['documentation_url'], weight=row['weight'], + post_install_instructions=row['post_install_instructions'], weight=row['weight'], download_url_partitions=row['download_url_partitions'], download_url_spiffs=row['download_url_spiffs'], checksum=row['checksum'], checksum_partitions=row['checksum_partitions'], checksum_spiffs=row['checksum_spiffs'], - spiffs_address=row['spiffs_address'], project_id=row['project_id'], + spiffs_address=row['spiffs_address'], project_id=row['project_id'], id=row['id'], download_url_bootloader=row['download_url_bootloader'], checksum_bootloader=row['checksum_bootloader'], download_url_otadata=row['download_url_otadata'], @@ -247,10 +271,11 @@ def load_firmware_from_website(self) -> bool: self.Projects[newFirmware.project_id].device_families[newFirmware.family_id].firmware.append( newFirmware) except: + # TODO - Display an error message pass return True # Firmware table is updated - return False # We didn't get data back from Fermentrack.com, or there was an error + return False # We didn't get data back from Brewflasher.com, or there was an error def cleanse_projects(self): for this_project_id in list(self.Projects): diff --git a/build-on-mac-m1.spec b/build-on-mac-m1.spec new file mode 100644 index 0000000..a52fbbc --- /dev/null +++ b/build-on-mac-m1.spec @@ -0,0 +1,44 @@ +# -*- mode: python -*- + +block_cipher = None + +a = Analysis(['Main.py'], + binaries=None, + datas=[("images", "images")], + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name='BrewFlasher', + debug=False, + strip=False, + upx=True, + target_arch='arm64', + entitlements_file='Entitlements.plist', + codesign_identity='Developer ID Application: John Beeler (RAS94LVJ7S)', + console=False , icon='images/icon-512.icns') +app = BUNDLE(exe, + name='BrewFlasher-1.2.app', + icon='./images/icon-512.icns', + bundle_identifier='com.brewflasher.macos', + info_plist={ + 'NSPrincipalClass': 'NSApplication', + 'NSAppleScriptEnabled': False, + 'LSRequiresIPhoneOS': False, + 'LSApplicationCategoryType': 'public.app-category.utilities', + 'CFBundleVersion': '1.2.0', + 'CFBundleShortVersionString': '1.2', + 'CFBundleSignature': 'BFLS', + 'LSMinimumSystemVersion': '10.4.0' + }, + ) diff --git a/build-on-mac.spec b/build-on-mac.spec index 1794b4f..820a43f 100644 --- a/build-on-mac.spec +++ b/build-on-mac.spec @@ -2,7 +2,7 @@ block_cipher = None -a = Analysis(['nodemcu-pyflasher.py'], +a = Analysis(['Main.py'], binaries=None, datas=[("images", "images")], hiddenimports=[], @@ -23,19 +23,22 @@ exe = EXE(pyz, debug=False, strip=False, upx=True, - console=False , icon='images/icon-256.icns') + target_arch='x86_64', + entitlements_file='Entitlements.plist', + codesign_identity='Developer ID Application: John Beeler (RAS94LVJ7S)', + console=False , icon='images/icon-512.icns') app = BUNDLE(exe, - name='BrewFlasher-1.0.app', - icon='./images/icon-256.icns', + name='BrewFlasher-1.2.app', + icon='./images/icon-512.icns', bundle_identifier='com.brewflasher.macos', info_plist={ 'NSPrincipalClass': 'NSApplication', 'NSAppleScriptEnabled': False, 'LSRequiresIPhoneOS': False, 'LSApplicationCategoryType': 'public.app-category.utilities', - 'CFBundleVersion': '1.0.1', - 'CFBundleShortVersionString': '1.0', + 'CFBundleVersion': '1.2.0', + 'CFBundleShortVersionString': '1.2', 'CFBundleSignature': 'BFLS', - 'LSMinimumSystemVersion': '10.0.0' + 'LSMinimumSystemVersion': '10.4.0' }, ) diff --git a/build-on-win.spec b/build-on-win.spec index ebe3462..e2a774d 100644 --- a/build-on-win.spec +++ b/build-on-win.spec @@ -1,26 +1,40 @@ -# -*- mode: python -*- +# -*- mode: python ; coding: utf-8 -*- + block_cipher = None -a = Analysis(['nodemcu-pyflasher.py'], + +a = Analysis(['Main.py'], + pathex=[], binaries=[], datas=[("images", "images")], hiddenimports=[], hookspath=[], + hooksconfig={}, runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, - cipher=block_cipher) + cipher=block_cipher, + noarchive=False) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, - name='BrewFlasher-1.0', + [], + name='BrewFlasher-1.2', debug=False, + bootloader_ignore_signals=False, strip=False, upx=True, + upx_exclude=[], + runtime_tmpdir=None, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, console=False , icon='images\\icon-256.ico') diff --git a/build.bat b/build.bat index 60d9a51..aafdb40 100755 --- a/build.bat +++ b/build.bat @@ -5,33 +5,32 @@ REM pyinstaller --log-level=DEBUG ^ REM --noconfirm ^ REM build-on-win.spec REM -REM This command uses the default python installation to build the app. We want to build two versions: -REM a 32 bit version and a 64 bit version. To accomplish this, I'm going to call two separate installations -REM of Python. I'm assuming Python 3.7 (since that's what I have) installed for the local user (as -REM opposed to systemwide). +REM This command uses the default python installation to build the app. While we used to build an x32 +REM and x64 version, beginning with BrewFlasher 1.2 I'm only going to support x64 going forward. +REM I'm assuming Python 3.9 (since that's what I have) installed for the local user (as opposed to systemwide). REM First, we'll create the 64 bit version REM Delete any previous versions -del "dist\BrewFlasher-1.0.exe" -del "dist\BrewFlasher-1.0 x64.exe" -REM Make sure packages for the 32 bit version are up-to-date -C:\Users\%USERNAME%\AppData\Local\Programs\Python\Python37\Scripts\pip.exe" install --upgrade -r requirements.txt +del "dist\BrewFlasher-1.2.exe" +del "dist\BrewFlasher-1.2 x64.exe" +REM Make sure packages for the 64 bit version are up-to-date +C:\Users\%USERNAME%\AppData\Local\Programs\Python\Python39\Scripts\pip.exe" install --upgrade -r requirements.txt REM And then, run pyinstaller -"C:\Users\%USERNAME%\AppData\Local\Programs\Python\Python37\Scripts\pyinstaller.exe" --log-level=DEBUG --noconfirm build-on-win.spec +"C:\Users\%USERNAME%\AppData\Local\Programs\Python\Python39\Scripts\pyinstaller.exe" --log-level=DEBUG --noconfirm build-on-win.spec REM Rename the file to x64 move "dist\BrewFlasher-1.0.exe" "dist\BrewFlasher-1.0 x64.exe" REM Next, we'll create the 32 bit version. REM Delete any previous versions -del "dist\BrewFlasher-1.0.exe" -del "dist\BrewFlasher-1.0 x32.exe" +REM del "dist\BrewFlasher-1.2.exe" +REM del "dist\BrewFlasher-1.2 x32.exe" REM Make sure packages for the 32 bit version are up-to-date -"C:\Users\%USERNAME%\AppData\Local\Programs\Python\Python37-32\Scripts\pip.exe" install --upgrade -r requirements.txt +REM "C:\Users\%USERNAME%\AppData\Local\Programs\Python\Python37-32\Scripts\pip.exe" install --upgrade -r requirements.txt REM And then, we'll actually run pyinstaller -"C:\Users\%USERNAME%\AppData\Local\Programs\Python\Python37-32\Scripts\pyinstaller.exe" --log-level=DEBUG --noconfirm build-on-win.spec +REM "C:\Users\%USERNAME%\AppData\Local\Programs\Python\Python37-32\Scripts\pyinstaller.exe" --log-level=DEBUG --noconfirm build-on-win.spec REM Rename the file to x32 -move "dist\BrewFlasher-1.0.exe" "dist\BrewFlasher-1.0 x32.exe" +REM move "dist\BrewFlasher-1.0.exe" "dist\BrewFlasher-1.0 x32.exe" diff --git a/build.sh b/build.sh index 16f55dd..34c366b 100755 --- a/build.sh +++ b/build.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash #rm -fr build dist -VERSION=1.0 -NAME=BrewFlasher +#VERSION=1.2 +#NAME=BrewFlasher -pyinstaller --log-level=DEBUG \ +pyinstaller --log-level=INFO \ --noconfirm \ - build-on-mac.spec + build-on-mac-m1.spec #https://github.com/sindresorhus/create-dmg #create-dmg dist/$NAME-$VERSION.app diff --git a/esptool.py b/esptool.py index 39de031..d2b1f3f 100755 --- a/esptool.py +++ b/esptool.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -# ESP8266 & ESP32 ROM Bootloader Utility -# Copyright (C) 2014-2016 Fredrik Ahlberg, Angus Gratton, Espressif Systems (Shanghai) PTE LTD, other contributors as noted. +# ESP8266 & ESP32 family & ESP8684 ROM Bootloader Utility +# Copyright (C) 2014-2021 Fredrik Ahlberg, Angus Gratton, Espressif Systems (Shanghai) CO LTD, other contributors as noted. # https://github.com/espressif/esptool # # This program is free software; you can redistribute it and/or modify it under @@ -25,13 +25,15 @@ import hashlib import inspect import io +import itertools import os +import re import shlex +import string import struct import sys import time import zlib -import string try: import serial @@ -59,8 +61,15 @@ print("The installed version (%s) of pyserial appears to be too old for esptool.py (Python interpreter %s). " "Check the README for installation instructions." % (sys.VERSION, sys.executable)) raise +except Exception: + if sys.platform == "darwin": + # swallow the exception, this is a known issue in pyserial+macOS Big Sur preview ref https://github.com/espressif/esptool/issues/540 + list_ports = None + else: + raise + -__version__ = "2.8" +__version__ = "3.2" MAX_UINT32 = 0xffffffff MAX_UINT24 = 0xffffff @@ -72,8 +81,12 @@ SYNC_TIMEOUT = 0.1 # timeout for syncing with bootloader MD5_TIMEOUT_PER_MB = 8 # timeout (per megabyte) for calculating md5sum ERASE_REGION_TIMEOUT_PER_MB = 30 # timeout (per megabyte) for erasing a region +ERASE_WRITE_TIMEOUT_PER_MB = 40 # timeout (per megabyte) for erasing and writing data MEM_END_ROM_TIMEOUT = 0.05 # special short timeout for ESP_MEM_END, as it may never respond DEFAULT_SERIAL_WRITE_TIMEOUT = 10 # timeout for serial port write +DEFAULT_CONNECT_ATTEMPTS = 7 # default number of times to try connection + +SUPPORTED_CHIPS = ['esp8266', 'esp32', 'esp32s2', 'esp32s3beta2', 'esp32s3', 'esp32c3', 'esp32c6beta', 'esp32h2', 'esp8684'] def timeout_per_mb(seconds_per_mb, size_bytes): @@ -84,8 +97,45 @@ def timeout_per_mb(seconds_per_mb, size_bytes): return result +def _chip_to_rom_loader(chip): + return { + 'esp8266': ESP8266ROM, + 'esp32': ESP32ROM, + 'esp32s2': ESP32S2ROM, + 'esp32s3beta2': ESP32S3BETA2ROM, + 'esp32s3': ESP32S3ROM, + 'esp32c3': ESP32C3ROM, + 'esp32c6beta': ESP32C6BETAROM, + 'esp32h2': ESP32H2ROM, + 'esp8684': ESP8684ROM, + }[chip] + + +def get_default_connected_device(serial_list, port, connect_attempts, initial_baud, chip='auto', trace=False, + before='default_reset'): + _esp = None + for each_port in reversed(serial_list): + print("Serial port %s" % each_port) + try: + if chip == 'auto': + _esp = ESPLoader.detect_chip(each_port, initial_baud, before, trace, + connect_attempts) + else: + chip_class = _chip_to_rom_loader(chip) + _esp = chip_class(each_port, initial_baud, trace) + _esp.connect(before, connect_attempts) + break + except (FatalError, OSError) as err: + if port is not None: + raise + print("%s failed to connect: %s" % (each_port, err)) + _esp = None + return _esp + + DETECTED_FLASH_SIZES = {0x12: '256KB', 0x13: '512KB', 0x14: '1MB', - 0x15: '2MB', 0x16: '4MB', 0x17: '8MB', 0x18: '16MB'} + 0x15: '2MB', 0x16: '4MB', 0x17: '8MB', + 0x18: '16MB', 0x19: '32MB', 0x1a: '64MB'} def check_supported_function(func, check_func): @@ -94,7 +144,7 @@ def check_supported_function(func, check_func): bootloader function to check if it's supported. This is used to capture the multidimensional differences in - functionality between the ESP8266 & ESP32 ROM loaders, and the + functionality between the ESP8266 & ESP32/32S2/32S3/32C3 & ESP8684 ROM loaders, and the software stub that runs on both. Not possible to do this cleanly via inheritance alone. """ @@ -113,8 +163,13 @@ def stub_function_only(func): def stub_and_esp32_function_only(func): - """ Attribute for a function only supported by software stubs or ESP32 ROM """ - return check_supported_function(func, lambda o: o.IS_STUB or o.CHIP_NAME == "ESP32") + """ Attribute for a function only supported by software stubs or ESP32/32S2/32S3/32C3 ROM """ + return check_supported_function(func, lambda o: o.IS_STUB or isinstance(o, ESP32ROM)) + + +def esp32s3_or_newer_function_only(func): + """ Attribute for a function only supported by ESP32S3/32C3 & ESP8684 ROM """ + return check_supported_function(func, lambda o: isinstance(o, ESP32S3ROM) or isinstance(o, ESP32C3ROM)) PYTHON2 = sys.version_info[0] < 3 # True if on pre-Python 3 @@ -135,6 +190,21 @@ def byte(bitstr, index): basestring = str +def print_overwrite(message, last_line=False): + """ Print a message, overwriting the currently printed line. + + If last_line is False, don't append a newline at the end (expecting another subsequent call will overwrite this one.) + + After a sequence of calls with last_line=False, call once with last_line=True. + + If output is not a TTY (for example redirected a pipe), no overwriting happens and this function is the same as print(). + """ + if sys.stdout.isatty(): + print("\r%s" % message, end='\n' if last_line else '') + else: + print(message) + + def _mask_to_shift(mask): """ Return the index of the least significant bit in the mask """ shift = 0 @@ -151,7 +221,7 @@ def esp8266_function_only(func): class ESPLoader(object): """ Base class providing access to ESP ROM & software stub bootloaders. - Subclasses provide ESP8266 & ESP32 specific functionality. + Subclasses provide ESP8266 & ESP32 Family & ESP8684 specific functionality. Don't instantiate this base class directly, either instantiate a subclass or call ESPLoader.detect_chip() which will interrogate the chip and return the @@ -161,6 +231,8 @@ class ESPLoader(object): CHIP_NAME = "Espressif device" IS_STUB = False + FPGA_SLOW_BOOT = False + DEFAULT_PORT = "/dev/ttyUSB0" # Commands supported by ESP8266 ROM bootloader @@ -174,24 +246,31 @@ class ESPLoader(object): ESP_WRITE_REG = 0x09 ESP_READ_REG = 0x0a - # Some comands supported by ESP32 ROM bootloader (or -8266 w/ stub) + # Some comands supported by ESP32 & ESP8684 ROM bootloader (or -8266 w/ stub) ESP_SPI_SET_PARAMS = 0x0B ESP_SPI_ATTACH = 0x0D + ESP_READ_FLASH_SLOW = 0x0e # ROM only, much slower than the stub flash read ESP_CHANGE_BAUDRATE = 0x0F ESP_FLASH_DEFL_BEGIN = 0x10 ESP_FLASH_DEFL_DATA = 0x11 ESP_FLASH_DEFL_END = 0x12 ESP_SPI_FLASH_MD5 = 0x13 + # Commands supported by ESP32-S2/S3/C3/C6 & ESP8684 ROM bootloader only + ESP_GET_SECURITY_INFO = 0x14 + # Some commands supported by stub only ESP_ERASE_FLASH = 0xD0 ESP_ERASE_REGION = 0xD1 ESP_READ_FLASH = 0xD2 ESP_RUN_USER_CODE = 0xD3 - # Flash encryption debug more command + # Flash encryption encrypted data command ESP_FLASH_ENCRYPT_DATA = 0xD4 + # Response code(s) sent by ROM + ROM_INVALID_RECV_MSG = 0x05 # response if an invalid message is received + # Maximum block sized for RAM and Flash writes, respectively. ESP_RAM_BLOCK = 0x1800 @@ -209,8 +288,9 @@ class ESPLoader(object): # Flash sector size, minimum unit of erase. FLASH_SECTOR_SIZE = 0x1000 - # This register happens to exist on both ESP8266 & ESP32 - UART_DATA_REG_ADDR = 0x60000078 + UART_DATE_REG_ADDR = 0x60000078 + + CHIP_DETECT_MAGIC_REG_ADDR = 0x40001000 # This ROM address has a different value on each chip model UART_CLKDIV_MASK = 0xFFFFF @@ -221,6 +301,15 @@ class ESPLoader(object): # The number of bytes in the UART response that signify command status STATUS_BYTES_LENGTH = 2 + # Response to ESP_SYNC might indicate that flasher stub is running instead of the ROM bootloader + sync_stub_detected = False + + # Device PIDs + USB_JTAG_SERIAL_PID = 0x1001 + + # Chip IDs that are no longer supported by esptool + UNSUPPORTED_CHIPS = {6: "ESP32-S3(beta 3)"} + def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False): """Base constructor for ESPLoader bootloader interaction @@ -233,6 +322,9 @@ def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False): with ones which throw NotImplementedInROMError(). """ + self.secure_download_mode = False # flag is set to True if esptool detects the ROM is in Secure Download Mode + self.stub_is_disabled = False # flag is set to True if esptool detects conditions which require the stub to be disabled + if isinstance(port, basestring): self._port = serial.serial_for_url(port) else: @@ -252,6 +344,10 @@ def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False): # need to set the property back to None or it will continue to fail self._port.write_timeout = None + @property + def serial_port(self): + return self._port.port + def _set_port_baudrate(self, baud): try: self._port.baudrate = baud @@ -259,33 +355,58 @@ def _set_port_baudrate(self, baud): raise FatalError("Failed to set baud rate %d. The driver may not support this rate." % baud) @staticmethod - def detect_chip(port=DEFAULT_PORT, baud=ESP_ROM_BAUD, connect_mode='default_reset', trace_enabled=False): + def detect_chip(port=DEFAULT_PORT, baud=ESP_ROM_BAUD, connect_mode='default_reset', trace_enabled=False, + connect_attempts=DEFAULT_CONNECT_ATTEMPTS): """ Use serial access to detect the chip type. - We use the UART's datecode register for this, it's mapped at - the same address on ESP8266 & ESP32 so we can use one - memory read and compare to the datecode register for each chip - type. + First, get_security_info command is sent to detect the ID of the chip + (supported only by ESP32-C3 and later, works even in the Secure Download Mode). + If this fails, we reconnect and fall-back to reading the magic number. + It's mapped at a specific ROM address and has a different value on each chip model. + This way we can use one memory read and compare it to the magic number for each chip type. This routine automatically performs ESPLoader.connect() (passing connect_mode parameter) as part of querying the chip. """ + inst = None detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled) - detect_port.connect(connect_mode) + detect_port.connect(connect_mode, connect_attempts, detecting=True) try: print('Detecting chip type...', end='') - sys.stdout.flush() - date_reg = detect_port.read_reg(ESPLoader.UART_DATA_REG_ADDR) + res = detect_port.check_command('get security info', ESPLoader.ESP_GET_SECURITY_INFO, b'') + res = struct.unpack(" 0 else itertools.count(), itertools.cycle((False, True))): + last_error = self._connect_attempt(mode=mode, usb_jtag_serial=usb_jtag_serial, extra_delay=extra_delay) if last_error is None: - return - last_error = self._connect_attempt(mode=mode, esp32r0_delay=True) - if last_error is None: - return + break finally: print('') # end 'Connecting...' line - raise FatalError('Failed to connect to %s: %s' % (self.CHIP_NAME, last_error)) - def read_reg(self, addr): + if last_error is not None: + raise FatalError('Failed to connect to %s: %s' + '\nFor troubleshooting steps visit: https://github.com/espressif/esptool#troubleshooting' % (self.CHIP_NAME, last_error)) + + if not detecting: + try: + # check the date code registers match what we expect to see + chip_magic_value = self.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR) + if chip_magic_value not in self.CHIP_DETECT_MAGIC_VALUE: + actually = None + for cls in [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3ROM, ESP32C3ROM, ESP32H2ROM, ESP8684ROM]: + if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE: + actually = cls + break + if warnings and actually is None: + print(("WARNING: This chip doesn't appear to be a %s (chip magic value 0x%08x). " + "Probably it is unsupported by this version of esptool.") % (self.CHIP_NAME, chip_magic_value)) + else: + raise FatalError("This chip is %s not %s. Wrong --chip argument?" % (actually.CHIP_NAME, self.CHIP_NAME)) + except UnsupportedCommandError: + self.secure_download_mode = True + self._post_connect() + self.check_chip_id() + + def _post_connect(self): + """ + Additional initialization hook, may be overridden by the chip-specific class. + Gets called after connect, and after auto-detection. + """ + pass + + def read_reg(self, addr, timeout=DEFAULT_TIMEOUT): """ Read memory address in target """ # we don't call check_command here because read_reg() function is called # when detecting chip type, and the way we check for success (STATUS_BYTES_LENGTH) is different # for different chip types (!) - val, data = self.command(self.ESP_READ_REG, struct.pack(' 0: + # add a dummy write to a date register as an excuse to have a delay + command += struct.pack(' start: - raise FatalError(("Software loader is resident at 0x%08x-0x%08x. " + - "Can't load binary at overlapping address range 0x%08x-0x%08x. " + - "Either change binary loading address, or use the --no-stub " + + raise FatalError(("Software loader is resident at 0x%08x-0x%08x. " + "Can't load binary at overlapping address range 0x%08x-0x%08x. " + "Either change binary loading address, or use the --no-stub " "option to disable the software loader.") % (start, end, load_start, load_end)) return self.check_command("enter RAM download mode", self.ESP_MEM_BEGIN, @@ -557,7 +773,7 @@ def mem_finish(self, entrypoint=0): Returns number of blocks (of size self.FLASH_WRITE_SIZE) to write. """ - def flash_begin(self, size, offset): + def flash_begin(self, size, offset, begin_rom_encrypted=False): num_blocks = (size + self.FLASH_WRITE_SIZE - 1) // self.FLASH_WRITE_SIZE erase_size = self.get_erase_size(offset, size) @@ -566,9 +782,12 @@ def flash_begin(self, size, offset): timeout = DEFAULT_TIMEOUT else: timeout = timeout_per_mb(ERASE_REGION_TIMEOUT_PER_MB, size) # ROM performs the erase up front + + params = struct.pack(' 0: self.write_reg(SPI_MOSI_DLEN_REG, mosi_bits - 1) if miso_bits > 0: @@ -842,7 +1094,7 @@ def set_data_lengths(mosi_bits, miso_bits): SPI_CMD_USR = (1 << 18) # shift values - SPI_USR2_DLEN_SHIFT = 28 + SPI_USR2_COMMAND_LEN_SHIFT = 28 if read_bits > 32: raise FatalError("Reading more than 32 bits back from a SPI flash operation is unsupported") @@ -860,7 +1112,7 @@ def set_data_lengths(mosi_bits, miso_bits): set_data_lengths(data_bits, read_bits) self.write_reg(SPI_USR_REG, flags) self.write_reg(SPI_USR2_REG, - (7 << SPI_USR2_DLEN_SHIFT) | spiflash_command) + (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflash_command) if data_bits == 0: self.write_reg(SPI_W0_REG, 0) # clear data register before we read it else: @@ -957,6 +1209,7 @@ def get_crystal_freq(self): return norm_xtal def hard_reset(self): + print('Hard resetting via RTS pin...') self._setRTS(True) # EN->LOW time.sleep(0.1) self._setRTS(False) @@ -982,6 +1235,17 @@ def soft_reset(self, stay_in_bootloader): # in the stub loader self.command(self.ESP_RUN_USER_CODE, wait_response=False) + def check_chip_id(self): + try: + chip_id = self.get_chip_id() + if chip_id != self.IMAGE_CHIP_ID: + print("WARNING: Chip ID {} ({}) doesn't match expected Chip ID {}. esptool may not work correctly." + .format(chip_id, self.UNSUPPORTED_CHIPS.get(chip_id, 'Unknown'), self.IMAGE_CHIP_ID)) + # Try to flash anyways by disabling stub + self.stub_is_disabled = True + except NotImplementedInROMError: + pass + class ESP8266ROM(ESPLoader): """ Access class for ESP8266 ROM bootloader @@ -989,7 +1253,7 @@ class ESP8266ROM(ESPLoader): CHIP_NAME = "ESP8266" IS_STUB = False - DATE_REG_VALUE = 0x00062000 + CHIP_DETECT_MAGIC_VALUE = [0xfff0c101] # OTP ROM addresses ESP_OTP_MAC0 = 0x3ff00050 @@ -997,23 +1261,27 @@ class ESP8266ROM(ESPLoader): ESP_OTP_MAC3 = 0x3ff0005c SPI_REG_BASE = 0x60000200 + SPI_USR_OFFS = 0x1c + SPI_USR1_OFFS = 0x20 + SPI_USR2_OFFS = 0x24 + SPI_MOSI_DLEN_OFFS = None + SPI_MISO_DLEN_OFFS = None SPI_W0_OFFS = 0x40 - SPI_HAS_MOSI_DLEN_REG = False UART_CLKDIV_REG = 0x60000014 XTAL_CLK_DIVIDER = 2 FLASH_SIZES = { - '512KB':0x00, - '256KB':0x10, - '1MB':0x20, - '2MB':0x30, - '4MB':0x40, + '512KB': 0x00, + '256KB': 0x10, + '1MB': 0x20, + '2MB': 0x30, + '4MB': 0x40, '2MB-c1': 0x50, - '4MB-c1':0x60, - '8MB':0x80, - '16MB':0x90, + '4MB-c1': 0x60, + '8MB': 0x80, + '16MB': 0x90, } BOOTLOADER_FLASH_OFFSET = 0 @@ -1025,19 +1293,47 @@ class ESP8266ROM(ESPLoader): def get_efuses(self): # Return the 128 bits of ESP8266 efuse as a single Python integer - return (self.read_reg(0x3ff0005c) << 96 | - self.read_reg(0x3ff00058) << 64 | - self.read_reg(0x3ff00054) << 32 | - self.read_reg(0x3ff00050)) + result = self.read_reg(0x3ff0005c) << 96 + result |= self.read_reg(0x3ff00058) << 64 + result |= self.read_reg(0x3ff00054) << 32 + result |= self.read_reg(0x3ff00050) + return result + + def _get_flash_size(self, efuses): + # rX_Y = EFUSE_DATA_OUTX[Y] + r0_4 = (efuses & (1 << 4)) != 0 + r3_25 = (efuses & (1 << 121)) != 0 + r3_26 = (efuses & (1 << 122)) != 0 + r3_27 = (efuses & (1 << 123)) != 0 + + if r0_4 and not r3_25: + if not r3_27 and not r3_26: + return 1 + elif not r3_27 and r3_26: + return 2 + if not r0_4 and r3_25: + if not r3_27 and not r3_26: + return 2 + elif not r3_27 and r3_26: + return 4 + return -1 def get_chip_description(self): efuses = self.get_efuses() is_8285 = (efuses & ((1 << 4) | 1 << 80)) != 0 # One or the other efuse bit is set for ESP8285 - return "ESP8285" if is_8285 else "ESP8266EX" + if is_8285: + flash_size = self._get_flash_size(efuses) + max_temp = (efuses & (1 << 5)) != 0 # This efuse bit identifies the max flash temperature + chip_name = { + 1: "ESP8285H08" if max_temp else "ESP8285N08", + 2: "ESP8285H16" if max_temp else "ESP8285N16" + }.get(flash_size, "ESP8285") + return chip_name + return "ESP8266EX" def get_chip_features(self): features = ["WiFi"] - if self.get_chip_description() == "ESP8285": + if "ESP8285" in self.get_chip_description(): features += ["Embedded Flash"] return features @@ -1105,6 +1401,7 @@ class ESP8266StubLoader(ESP8266ROM): IS_STUB = True def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode self._port = rom_loader._port self._trace_enabled = rom_loader._trace_enabled self.flush_input() # resets _slip_reader @@ -1124,46 +1421,56 @@ class ESP32ROM(ESPLoader): IMAGE_CHIP_ID = 0 IS_STUB = False - DATE_REG_VALUE = 0x15122500 + FPGA_SLOW_BOOT = True + + CHIP_DETECT_MAGIC_VALUE = [0x00f01d83] IROM_MAP_START = 0x400d0000 IROM_MAP_END = 0x40400000 + DROM_MAP_START = 0x3F400000 DROM_MAP_END = 0x3F800000 # ESP32 uses a 4 byte status reply STATUS_BYTES_LENGTH = 4 - SPI_REG_BASE = 0x60002000 - EFUSE_REG_BASE = 0x6001a000 + SPI_REG_BASE = 0x3ff42000 + SPI_USR_OFFS = 0x1c + SPI_USR1_OFFS = 0x20 + SPI_USR2_OFFS = 0x24 + SPI_MOSI_DLEN_OFFS = 0x28 + SPI_MISO_DLEN_OFFS = 0x2c + EFUSE_RD_REG_BASE = 0x3ff5a000 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + 0x18 + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = (1 << 7) # EFUSE_RD_DISABLE_DL_ENCRYPT DR_REG_SYSCON_BASE = 0x3ff66000 SPI_W0_OFFS = 0x80 - SPI_HAS_MOSI_DLEN_REG = True UART_CLKDIV_REG = 0x3ff40014 XTAL_CLK_DIVIDER = 1 FLASH_SIZES = { - '1MB':0x00, - '2MB':0x10, - '4MB':0x20, - '8MB':0x30, - '16MB':0x40 + '1MB': 0x00, + '2MB': 0x10, + '4MB': 0x20, + '8MB': 0x30, + '16MB': 0x40 } BOOTLOADER_FLASH_OFFSET = 0x1000 OVERRIDE_VDDSDIO_CHOICES = ["1.8V", "1.9V", "OFF"] - MEMORY_MAP = [[0x3F400000, 0x3F800000, "DROM"], + MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], + [0x3F400000, 0x3F800000, "DROM"], [0x3F800000, 0x3FC00000, "EXTRAM_DATA"], [0x3FF80000, 0x3FF82000, "RTC_DRAM"], [0x3FF90000, 0x40000000, "BYTE_ACCESSIBLE"], [0x3FFAE000, 0x40000000, "DRAM"], - [0x3FFAE000, 0x40000000, "DMA"], [0x3FFE0000, 0x3FFFFFFC, "DIRAM_DRAM"], [0x40000000, 0x40070000, "IROM"], [0x40070000, 0x40078000, "CACHE_PRO"], @@ -1174,6 +1481,8 @@ class ESP32ROM(ESPLoader): [0x400D0000, 0x40400000, "IROM"], [0x50000000, 0x50002000, "RTC_DATA"]] + FLASH_ENCRYPTED_WRITE_ALIGN = 32 + """ Try to read the BLOCK1 (encryption key) and check if it is valid """ def is_flash_encryption_key_valid(self): @@ -1187,10 +1496,10 @@ def is_flash_encryption_key_valid(self): if rd_disable: return True else: - """ reading of BLOCK1 is ALLOWED so we will read and verify for non-zero. - When ESP32 has not generated AES/encryption key in BLOCK1, the contents will be readable and 0. - If the flash encryption is enabled it is expected to have a valid non-zero key. We break out on - first occurance of non-zero value """ + # reading of BLOCK1 is ALLOWED so we will read and verify for non-zero. + # When ESP32 has not generated AES/encryption key in BLOCK1, the contents will be readable and 0. + # If the flash encryption is enabled it is expected to have a valid non-zero key. We break out on + # first occurance of non-zero value key_word = [0] * 7 for i in range(len(key_word)): key_word[i] = self.read_efuse(14 + i) @@ -1199,15 +1508,14 @@ def is_flash_encryption_key_valid(self): return True return False - """ For flash encryption related commands we need to make sure - user has programmed all the relevant efuse correctly so at - the end of write_flash_encrypt esptool will verify the values - of flash_crypt_config to be non zero if they are not read - protected. If the values are zero a warning will be printed - """ - def get_flash_crypt_config(self): - """ bit 3 in efuse_rd_disable[3:0] is mapped to flash_crypt_config + """ For flash encryption related commands we need to make sure + user has programmed all the relevant efuse correctly so before + writing encrypted write_flash_encrypt esptool will verify the values + of flash_crypt_config to be non zero if they are not read + protected. If the values are zero a warning will be printed + + bit 3 in efuse_rd_disable[3:0] is mapped to flash_crypt_config this bit is at position 19 in EFUSE_BLK0_RDATA0_REG """ word0 = self.read_efuse(0) rd_disable = (word0 >> 19) & 0x1 @@ -1222,31 +1530,55 @@ def get_flash_crypt_config(self): # if read of the efuse is disabled we assume it is set correctly return 0xF - def get_chip_description(self): + def get_encrypted_download_disabled(self): + if self.read_reg(self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG) & self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT: + return True + else: + return False + + def get_pkg_version(self): + word3 = self.read_efuse(3) + pkg_version = (word3 >> 9) & 0x07 + pkg_version += ((word3 >> 2) & 0x1) << 3 + return pkg_version + + def get_chip_revision(self): word3 = self.read_efuse(3) word5 = self.read_efuse(5) apb_ctl_date = self.read_reg(self.DR_REG_SYSCON_BASE + 0x7C) + rev_bit0 = (word3 >> 15) & 0x1 rev_bit1 = (word5 >> 20) & 0x1 rev_bit2 = (apb_ctl_date >> 31) & 0x1 - pkg_version = (word3 >> 9) & 0x07 - - chip_name = { - 0: "ESP32D0WDQ6", - 1: "ESP32D0WDQ5", - 2: "ESP32D2WDQ5", - 5: "ESP32-PICO-D4", - }.get(pkg_version, "unknown ESP32") - - chip_revision = 0 if rev_bit0: if rev_bit1: if rev_bit2: - chip_revision = 3 + return 3 else: - chip_revision = 2 + return 2 else: - chip_revision = 1 + return 1 + return 0 + + def get_chip_description(self): + pkg_version = self.get_pkg_version() + chip_revision = self.get_chip_revision() + rev3 = (chip_revision == 3) + single_core = self.read_efuse(3) & (1 << 0) # CHIP_VER DIS_APP_CPU + + chip_name = { + 0: "ESP32-S0WDQ6" if single_core else "ESP32-D0WDQ6", + 1: "ESP32-S0WD" if single_core else "ESP32-D0WD", + 2: "ESP32-D2WD", + 4: "ESP32-U4WDH", + 5: "ESP32-PICO-V3" if rev3 else "ESP32-PICO-D4", + 6: "ESP32-PICO-V3-02", + }.get(pkg_version, "unknown ESP32") + + # ESP32-D0WD-V3, ESP32-D0WDQ6-V3 + if chip_name.startswith("ESP32-D0WD") and rev3: + chip_name += "-V3" + return "%s (revision %d)" % (chip_name, chip_revision) def get_chip_features(self): @@ -1275,90 +1607,925 @@ def get_chip_features(self): else: features += ["240MHz"] - pkg_version = (word3 >> 9) & 0x07 - if pkg_version in [2, 4, 5]: + pkg_version = self.get_pkg_version() + if pkg_version in [2, 4, 5, 6]: features += ["Embedded Flash"] + if pkg_version == 6: + features += ["Embedded PSRAM"] + word4 = self.read_efuse(4) adc_vref = (word4 >> 8) & 0x1F if adc_vref: features += ["VRef calibration in efuse"] - blk3_part_res = word3 >> 14 & 0x1 - if blk3_part_res: - features += ["BLK3 partially reserved"] + blk3_part_res = word3 >> 14 & 0x1 + if blk3_part_res: + features += ["BLK3 partially reserved"] + + word6 = self.read_efuse(6) + coding_scheme = word6 & 0x3 + features += ["Coding Scheme %s" % { + 0: "None", + 1: "3/4", + 2: "Repeat (UNSUPPORTED)", + 3: "Invalid"}[coding_scheme]] + + return features + + def read_efuse(self, n): + """ Read the nth word of the ESP3x EFUSE region. """ + return self.read_reg(self.EFUSE_RD_REG_BASE + (4 * n)) + + def chip_id(self): + raise NotSupportedError(self, "chip_id") + + def read_mac(self): + """ Read MAC from EFUSE region """ + words = [self.read_efuse(2), self.read_efuse(1)] + bitstring = struct.pack(">II", *words) + bitstring = bitstring[2:8] # trim the 2 byte CRC + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_erase_size(self, offset, size): + return size + + def override_vddsdio(self, new_voltage): + new_voltage = new_voltage.upper() + if new_voltage not in self.OVERRIDE_VDDSDIO_CHOICES: + raise FatalError("The only accepted VDDSDIO overrides are '1.8V', '1.9V' and 'OFF'") + RTC_CNTL_SDIO_CONF_REG = 0x3ff48074 + RTC_CNTL_XPD_SDIO_REG = (1 << 31) + RTC_CNTL_DREFH_SDIO_M = (3 << 29) + RTC_CNTL_DREFM_SDIO_M = (3 << 27) + RTC_CNTL_DREFL_SDIO_M = (3 << 25) + # RTC_CNTL_SDIO_TIEH = (1 << 23) # not used here, setting TIEH=1 would set 3.3V output, not safe for esptool.py to do + RTC_CNTL_SDIO_FORCE = (1 << 22) + RTC_CNTL_SDIO_PD_EN = (1 << 21) + + reg_val = RTC_CNTL_SDIO_FORCE # override efuse setting + reg_val |= RTC_CNTL_SDIO_PD_EN + if new_voltage != "OFF": + reg_val |= RTC_CNTL_XPD_SDIO_REG # enable internal LDO + if new_voltage == "1.9V": + reg_val |= (RTC_CNTL_DREFH_SDIO_M | RTC_CNTL_DREFM_SDIO_M | RTC_CNTL_DREFL_SDIO_M) # boost voltage + self.write_reg(RTC_CNTL_SDIO_CONF_REG, reg_val) + print("VDDSDIO regulator set to %s" % new_voltage) + + def read_flash_slow(self, offset, length, progress_fn): + BLOCK_LEN = 64 # ROM read limit per command (this limit is why it's so slow) + + data = b'' + while len(data) < length: + block_len = min(BLOCK_LEN, length - len(data)) + r = self.check_command("read flash block", self.ESP_READ_FLASH_SLOW, + struct.pack('> 0) & 0x0F + return pkg_version + + def get_flash_version(self): + num_word = 3 + block1_addr = self.EFUSE_BASE + 0x044 + word3 = self.read_reg(block1_addr + (4 * num_word)) + pkg_version = (word3 >> 21) & 0x0F + return pkg_version + + def get_psram_version(self): + num_word = 3 + block1_addr = self.EFUSE_BASE + 0x044 + word3 = self.read_reg(block1_addr + (4 * num_word)) + pkg_version = (word3 >> 28) & 0x0F + return pkg_version + + def get_block2_version(self): + num_word = 4 + block2_addr = self.EFUSE_BASE + 0x05C + word4 = self.read_reg(block2_addr + (4 * num_word)) + block2_version = (word4 >> 4) & 0x07 + return block2_version + + def get_chip_description(self): + chip_name = { + 0: "ESP32-S2", + 1: "ESP32-S2FH2", + 2: "ESP32-S2FH4", + 102: "ESP32-S2FNR2", + 100: "ESP32-S2R2", + }.get(self.get_flash_version() + self.get_psram_version() * 100, "unknown ESP32-S2") + + return "%s" % (chip_name) + + def get_chip_features(self): + features = ["WiFi"] + + if self.secure_download_mode: + features += ["Secure Download Mode Enabled"] + + flash_version = { + 0: "No Embedded Flash", + 1: "Embedded Flash 2MB", + 2: "Embedded Flash 4MB", + }.get(self.get_flash_version(), "Unknown Embedded Flash") + features += [flash_version] + + psram_version = { + 0: "No Embedded PSRAM", + 1: "Embedded PSRAM 2MB", + 2: "Embedded PSRAM 4MB", + }.get(self.get_psram_version(), "Unknown Embedded PSRAM") + features += [psram_version] + + block2_version = { + 0: "No calibration in BLK2 of efuse", + 1: "ADC and temperature sensor calibration in BLK2 of efuse V1", + 2: "ADC and temperature sensor calibration in BLK2 of efuse V2", + }.get(self.get_block2_version(), "Unknown Calibration in BLK2") + features += [block2_version] + + return features + + def get_crystal_freq(self): + # ESP32-S2 XTAL is fixed to 40MHz + return 40 + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-S2") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-S2 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see either an AES-128 key or two AES-256 keys + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes): + return True + + return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) \ + and any(p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes) + + def uses_usb(self, _cache=[]): + if self.secure_download_mode: + return False # can't detect native USB in secure download mode + if not _cache: + buf_no = self.read_reg(self.UARTDEV_BUF_NO) & 0xff + _cache.append(buf_no == self.UARTDEV_BUF_NO_USB) + return _cache[0] + + def _post_connect(self): + if self.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + + def _check_if_can_reset(self): + """ + Check the strapping register to see if we can reset out of download mode. + """ + if os.getenv("ESPTOOL_TESTING") is not None: + print("ESPTOOL_TESTING is set, ignoring strapping mode check") + # Esptool tests over USB CDC run with GPIO0 strapped low, don't complain in this case. + return + strap_reg = self.read_reg(self.GPIO_STRAP_REG) + force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG) + if strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0 and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0: + print("WARNING: {} chip was placed into download mode using GPIO0.\n" + "esptool.py can not exit the download mode over USB. " + "To run the app, reset the chip manually.\n" + "To suppress this note, set --after option to 'no_reset'.".format(self.get_chip_description())) + raise SystemExit(1) + + def hard_reset(self): + if self.uses_usb(): + self._check_if_can_reset() + + print('Hard resetting via RTS pin...') + self._setRTS(True) # EN->LOW + if self.uses_usb(): + # Give the chip some time to come out of reset, to be able to handle further DTR/RTS transitions + time.sleep(0.2) + self._setRTS(False) + time.sleep(0.2) + else: + self._setRTS(False) + + +class ESP32S3ROM(ESP32ROM): + CHIP_NAME = "ESP32-S3" + + IMAGE_CHIP_ID = 9 + + CHIP_DETECT_MAGIC_VALUE = [0x9] + + FPGA_SLOW_BOOT = False + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x44000000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3e000000 + + UART_DATE_REG_ADDR = 0x60000080 + + SPI_REG_BASE = 0x60002000 + SPI_USR_OFFS = 0x18 + SPI_USR1_OFFS = 0x1c + SPI_USR2_OFFS = 0x20 + SPI_MOSI_DLEN_OFFS = 0x24 + SPI_MISO_DLEN_OFFS = 0x28 + SPI_W0_OFFS = 0x58 + + FLASH_ENCRYPTED_WRITE_ALIGN = 16 + + # todo: use espefuse APIs to get this info + EFUSE_BASE = 0x60007000 # BLOCK0 read base address + MAC_EFUSE_REG = EFUSE_BASE + 0x044 + + EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address + + EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY0_SHIFT = 24 + EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY1_SHIFT = 28 + EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY2_SHIFT = 0 + EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY3_SHIFT = 4 + EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY4_SHIFT = 8 + EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY5_SHIFT = 12 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20 + + PURPOSE_VAL_XTS_AES256_KEY_1 = 2 + PURPOSE_VAL_XTS_AES256_KEY_2 = 3 + PURPOSE_VAL_XTS_AES128_KEY = 4 + + UARTDEV_BUF_NO = 0x3fcef14c # Variable in ROM .bss which indicates the port in use + UARTDEV_BUF_NO_USB = 3 # Value of the above variable indicating that USB is in use + + USB_RAM_BLOCK = 0x800 # Max block size USB CDC is used + + GPIO_STRAP_REG = 0x60004038 + GPIO_STRAP_SPI_BOOT_MASK = 0x8 # Not download mode + RTC_CNTL_OPTION1_REG = 0x6000812C + RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = 0x1 # Is download mode forced over USB? + + UART_CLKDIV_REG = 0x60000014 + + MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], + [0x3C000000, 0x3D000000, "DROM"], + [0x3D000000, 0x3E000000, "EXTRAM_DATA"], + [0x600FE000, 0x60100000, "RTC_DRAM"], + [0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"], + [0x3FC88000, 0x403E2000, "MEM_INTERNAL"], + [0x3FC88000, 0x3FD00000, "DRAM"], + [0x40000000, 0x4001A100, "IROM_MASK"], + [0x40370000, 0x403E0000, "IRAM"], + [0x600FE000, 0x60100000, "RTC_IRAM"], + [0x42000000, 0x42800000, "IROM"], + [0x50000000, 0x50002000, "RTC_DATA"]] + + def get_chip_description(self): + return "ESP32-S3" + + def get_chip_features(self): + return ["WiFi", "BLE"] + + def get_crystal_freq(self): + # ESP32S3 XTAL is fixed to 40MHz + return 40 + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-S3 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see either an AES-128 key or two AES-256 keys + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes): + return True + + return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) \ + and any(p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes) + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-S3") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def uses_usb(self, _cache=[]): + if self.secure_download_mode: + return False # can't detect native USB in secure download mode + if not _cache: + buf_no = self.read_reg(self.UARTDEV_BUF_NO) & 0xff + _cache.append(buf_no == self.UARTDEV_BUF_NO_USB) + return _cache[0] + + def _post_connect(self): + if self.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + + def _check_if_can_reset(self): + """ + Check the strapping register to see if we can reset out of download mode. + """ + if os.getenv("ESPTOOL_TESTING") is not None: + print("ESPTOOL_TESTING is set, ignoring strapping mode check") + # Esptool tests over USB CDC run with GPIO0 strapped low, don't complain in this case. + return + strap_reg = self.read_reg(self.GPIO_STRAP_REG) + force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG) + if strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0 and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0: + print("WARNING: {} chip was placed into download mode using GPIO0.\n" + "esptool.py can not exit the download mode over USB. " + "To run the app, reset the chip manually.\n" + "To suppress this note, set --after option to 'no_reset'.".format(self.get_chip_description())) + raise SystemExit(1) + + def hard_reset(self): + if self.uses_usb(): + self._check_if_can_reset() + + print('Hard resetting via RTS pin...') + self._setRTS(True) # EN->LOW + if self.uses_usb(): + # Give the chip some time to come out of reset, to be able to handle further DTR/RTS transitions + time.sleep(0.2) + self._setRTS(False) + time.sleep(0.2) + else: + self._setRTS(False) + + +class ESP32S3BETA2ROM(ESP32S3ROM): + CHIP_NAME = "ESP32-S3(beta2)" + IMAGE_CHIP_ID = 4 + + CHIP_DETECT_MAGIC_VALUE = [0xeb004136] + + EFUSE_BASE = 0x6001A000 # BLOCK0 read base address + + def get_chip_description(self): + return "ESP32-S3(beta2)" + + +class ESP32C3ROM(ESP32ROM): + CHIP_NAME = "ESP32-C3" + IMAGE_CHIP_ID = 5 + + FPGA_SLOW_BOOT = False + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x42800000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3c800000 + + SPI_REG_BASE = 0x60002000 + SPI_USR_OFFS = 0x18 + SPI_USR1_OFFS = 0x1C + SPI_USR2_OFFS = 0x20 + SPI_MOSI_DLEN_OFFS = 0x24 + SPI_MISO_DLEN_OFFS = 0x28 + SPI_W0_OFFS = 0x58 + + BOOTLOADER_FLASH_OFFSET = 0x0 + + # Magic value for ESP32C3 eco 1+2 and ESP32C3 eco3 respectivly + CHIP_DETECT_MAGIC_VALUE = [0x6921506f, 0x1b31506f] + + UART_DATE_REG_ADDR = 0x60000000 + 0x7c + + EFUSE_BASE = 0x60008800 + MAC_EFUSE_REG = EFUSE_BASE + 0x044 + + EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address + + EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY0_SHIFT = 24 + EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY1_SHIFT = 28 + EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY2_SHIFT = 0 + EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY3_SHIFT = 4 + EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY4_SHIFT = 8 + EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY5_SHIFT = 12 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20 + + PURPOSE_VAL_XTS_AES128_KEY = 4 + + GPIO_STRAP_REG = 0x3f404038 + + FLASH_ENCRYPTED_WRITE_ALIGN = 16 + + MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], + [0x3C000000, 0x3C800000, "DROM"], + [0x3FC80000, 0x3FCE0000, "DRAM"], + [0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"], + [0x3FF00000, 0x3FF20000, "DROM_MASK"], + [0x40000000, 0x40060000, "IROM_MASK"], + [0x42000000, 0x42800000, "IROM"], + [0x4037C000, 0x403E0000, "IRAM"], + [0x50000000, 0x50002000, "RTC_IRAM"], + [0x50000000, 0x50002000, "RTC_DRAM"], + [0x600FE000, 0x60100000, "MEM_INTERNAL2"]] + + def get_pkg_version(self): + num_word = 3 + block1_addr = self.EFUSE_BASE + 0x044 + word3 = self.read_reg(block1_addr + (4 * num_word)) + pkg_version = (word3 >> 21) & 0x07 + return pkg_version + + def get_chip_revision(self): + # reads WAFER_VERSION field from EFUSE_RD_MAC_SPI_SYS_3_REG + block1_addr = self.EFUSE_BASE + 0x044 + num_word = 3 + pos = 18 + return (self.read_reg(block1_addr + (4 * num_word)) & (0x7 << pos)) >> pos + + def get_chip_description(self): + chip_name = { + 0: "ESP32-C3", + }.get(self.get_pkg_version(), "unknown ESP32-C3") + chip_revision = self.get_chip_revision() + + return "%s (revision %d)" % (chip_name, chip_revision) + + def get_chip_features(self): + return ["Wi-Fi"] + + def get_crystal_freq(self): + # ESP32C3 XTAL is fixed to 40MHz + return 40 + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-C3") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-C3 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see an AES-128 key + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes) + + +class ESP32H2ROM(ESP32ROM): + CHIP_NAME = "ESP32-H2" + IMAGE_CHIP_ID = 10 + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x42800000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3c800000 + + SPI_REG_BASE = 0x60002000 + SPI_USR_OFFS = 0x18 + SPI_USR1_OFFS = 0x1C + SPI_USR2_OFFS = 0x20 + SPI_MOSI_DLEN_OFFS = 0x24 + SPI_MISO_DLEN_OFFS = 0x28 + SPI_W0_OFFS = 0x58 + + BOOTLOADER_FLASH_OFFSET = 0x0 + + CHIP_DETECT_MAGIC_VALUE = [0xca26cc22] + + UART_DATE_REG_ADDR = 0x60000000 + 0x7c + + EFUSE_BASE = 0x6001A000 + MAC_EFUSE_REG = EFUSE_BASE + 0x044 + + EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address + + EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY0_SHIFT = 24 + EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY1_SHIFT = 28 + EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY2_SHIFT = 0 + EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY3_SHIFT = 4 + EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY4_SHIFT = 8 + EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY5_SHIFT = 12 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20 + + PURPOSE_VAL_XTS_AES128_KEY = 4 + + GPIO_STRAP_REG = 0x3f404038 + + FLASH_ENCRYPTED_WRITE_ALIGN = 16 + + MEMORY_MAP = [] + + def get_pkg_version(self): + num_word = 3 + block1_addr = self.EFUSE_BASE + 0x044 + word3 = self.read_reg(block1_addr + (4 * num_word)) + pkg_version = (word3 >> 21) & 0x0F + return pkg_version + + def get_chip_revision(self): + # reads WAFER_VERSION field from EFUSE_RD_MAC_SPI_SYS_3_REG + block1_addr = self.EFUSE_BASE + 0x044 + num_word = 3 + pos = 18 + return (self.read_reg(block1_addr + (4 * num_word)) & (0x7 << pos)) >> pos + + def get_chip_description(self): + chip_name = { + 0: "ESP32-H2", + }.get(self.get_pkg_version(), "unknown ESP32-H2") + chip_revision = self.get_chip_revision() + + return "%s (revision %d)" % (chip_name, chip_revision) + + def get_chip_features(self): + return ["BLE/802.15.4"] + + def get_crystal_freq(self): + return 40 + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-H2") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-H2 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see an AES-128 key + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes) + + +class ESP8684ROM(ESP32C3ROM): + CHIP_NAME = "ESP8684" + IMAGE_CHIP_ID = 12 + + CHIP_DETECT_MAGIC_VALUE = [0x6451806f] + + def get_pkg_version(self): + num_word = 3 + block1_addr = self.EFUSE_BASE + 0x044 + word3 = self.read_reg(block1_addr + (4 * num_word)) + pkg_version = (word3 >> 21) & 0x0F + return pkg_version + + def get_chip_description(self): + chip_name = { + 0: "ESP8684", + }.get(self.get_pkg_version(), "unknown ESP8684") + chip_revision = self.get_chip_revision() + + return "%s (revision %d)" % (chip_name, chip_revision) + + +class ESP32C6BETAROM(ESP32C3ROM): + CHIP_NAME = "ESP32-C6(beta)" + IMAGE_CHIP_ID = 7 + + CHIP_DETECT_MAGIC_VALUE = [0x0da1806f] + + UART_DATE_REG_ADDR = 0x00000500 + + def get_chip_description(self): + chip_name = { + 0: "ESP32-C6", + }.get(self.get_pkg_version(), "unknown ESP32-C6") + chip_revision = self.get_chip_revision() + + return "%s (revision %d)" % (chip_name, chip_revision) + + +class ESP32StubLoader(ESP32ROM): + """ Access class for ESP32 stub loader, runs on top of ROM. + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32ROM.STUB_CLASS = ESP32StubLoader + + +class ESP32S2StubLoader(ESP32S2ROM): + """ Access class for ESP32-S2 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + if rom_loader.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK + + +ESP32S2ROM.STUB_CLASS = ESP32S2StubLoader + + +class ESP32S3BETA2StubLoader(ESP32S3BETA2ROM): + """ Access class for ESP32S3 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32S3BETA2ROM.STUB_CLASS = ESP32S3BETA2StubLoader + + +class ESP32S3StubLoader(ESP32S3ROM): + """ Access class for ESP32S3 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + if rom_loader.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK + + +ESP32S3ROM.STUB_CLASS = ESP32S3StubLoader + + +class ESP32C3StubLoader(ESP32C3ROM): + """ Access class for ESP32C3 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + - word6 = self.read_efuse(6) - coding_scheme = word6 & 0x3 - features += ["Coding Scheme %s" % { - 0: "None", - 1: "3/4", - 2: "Repeat (UNSUPPORTED)", - 3: "Invalid"}[coding_scheme]] +ESP32C3ROM.STUB_CLASS = ESP32C3StubLoader - return features - def read_efuse(self, n): - """ Read the nth word of the ESP3x EFUSE region. """ - return self.read_reg(self.EFUSE_REG_BASE + (4 * n)) +class ESP32H2StubLoader(ESP32H2ROM): + """ Access class for ESP32H2 stub loader, runs on top of ROM. - def chip_id(self): - raise NotSupportedError(self, "chip_id") + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True - def read_mac(self): - """ Read MAC from EFUSE region """ - words = [self.read_efuse(2), self.read_efuse(1)] - bitstring = struct.pack(">II", *words) - bitstring = bitstring[2:8] # trim the 2 byte CRC - try: - return tuple(ord(b) for b in bitstring) - except TypeError: # Python 3, bitstring elements are already bytes - return tuple(bitstring) + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader - def get_erase_size(self, offset, size): - return size - def override_vddsdio(self, new_voltage): - new_voltage = new_voltage.upper() - if new_voltage not in self.OVERRIDE_VDDSDIO_CHOICES: - raise FatalError("The only accepted VDDSDIO overrides are '1.8V', '1.9V' and 'OFF'") - RTC_CNTL_SDIO_CONF_REG = 0x3ff48074 - RTC_CNTL_XPD_SDIO_REG = (1 << 31) - RTC_CNTL_DREFH_SDIO_M = (3 << 29) - RTC_CNTL_DREFM_SDIO_M = (3 << 27) - RTC_CNTL_DREFL_SDIO_M = (3 << 25) - # RTC_CNTL_SDIO_TIEH = (1 << 23) # not used here, setting TIEH=1 would set 3.3V output, not safe for esptool.py to do - RTC_CNTL_SDIO_FORCE = (1 << 22) - RTC_CNTL_SDIO_PD_EN = (1 << 21) +ESP32H2ROM.STUB_CLASS = ESP32H2StubLoader - reg_val = RTC_CNTL_SDIO_FORCE # override efuse setting - reg_val |= RTC_CNTL_SDIO_PD_EN - if new_voltage != "OFF": - reg_val |= RTC_CNTL_XPD_SDIO_REG # enable internal LDO - if new_voltage == "1.9V": - reg_val |= (RTC_CNTL_DREFH_SDIO_M | RTC_CNTL_DREFM_SDIO_M | RTC_CNTL_DREFL_SDIO_M) # boost voltage - self.write_reg(RTC_CNTL_SDIO_CONF_REG, reg_val) - print("VDDSDIO regulator set to %s" % new_voltage) +class ESP8684StubLoader(ESP8684ROM): + """ Access class for ESP8684 stub loader, runs on top of ROM. -class ESP32StubLoader(ESP32ROM): - """ Access class for ESP32 stub loader, runs on top of ROM. + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) """ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM IS_STUB = True def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode self._port = rom_loader._port self._trace_enabled = rom_loader._trace_enabled self.flush_input() # resets _slip_reader -ESP32ROM.STUB_CLASS = ESP32StubLoader +ESP8684ROM.STUB_CLASS = ESP8684StubLoader class ESPBOOTLOADER(object): - """ These are constants related to software ESP bootloader, working with 'v2' image files """ + """ These are constants related to software ESP8266 bootloader, working with 'v2' image files """ # First byte of the "v2" application image IMAGE_V2_MAGIC = 0xea @@ -1368,14 +2535,31 @@ class ESPBOOTLOADER(object): def LoadFirmwareImage(chip, filename): - """ Load a firmware image. Can be for ESP8266 or ESP32. ESP8266 images will be examined to determine if they are - original ROM firmware images (ESP8266ROMFirmwareImage) or "v2" OTA bootloader images. + """ Load a firmware image. Can be for any supported SoC. + + ESP8266 images will be examined to determine if they are original ROM firmware images (ESP8266ROMFirmwareImage) + or "v2" OTA bootloader images. Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1) or ESP8266V2FirmwareImage (v2). """ + chip = chip.lower().replace("-", "") with open(filename, 'rb') as f: - if chip.lower() == 'esp32': + if chip == 'esp32': return ESP32FirmwareImage(f) + elif chip == "esp32s2": + return ESP32S2FirmwareImage(f) + elif chip == "esp32s3beta2": + return ESP32S3BETA2FirmwareImage(f) + elif chip == "esp32s3": + return ESP32S3FirmwareImage(f) + elif chip == 'esp32c3': + return ESP32C3FirmwareImage(f) + elif chip == 'esp32c6beta': + return ESP32C6BETAFirmwareImage(f) + elif chip == 'esp32h2': + return ESP32H2FirmwareImage(f) + elif chip == 'esp8684': + return ESP8684FirmwareImage(f) else: # Otherwise, ESP8266 so look at magic to determine the image type magic = ord(f.read(1)) f.seek(0) @@ -1421,6 +2605,13 @@ def __repr__(self): r += " file_offs 0x%08x" % (self.file_offs) return r + def get_memory_type(self, image): + """ + Return a list describing the memory type(s) that is covered by this + segment's start address. + """ + return [map_range[2] for map_range in image.ROM_LOADER.MEMORY_MAP if map_range[0] <= self.addr < map_range[1]] + def pad_to_alignment(self, alignment): self.data = pad_to(self.data, alignment, b'\x00') @@ -1485,15 +2676,15 @@ def maybe_patch_segment_data(self, f, segment_data): patch_offset = self.elf_sha256_offset - file_pos # Sanity checks if patch_offset < self.SEG_HEADER_LEN or patch_offset + self.SHA256_DIGEST_LEN > segment_len: - raise FatalError('Cannot place SHA256 digest on segment boundary' + + raise FatalError('Cannot place SHA256 digest on segment boundary' '(elf_sha256_offset=%d, file_pos=%d, segment_size=%d)' % (self.elf_sha256_offset, file_pos, segment_len)) + # offset relative to the data part + patch_offset -= self.SEG_HEADER_LEN if segment_data[patch_offset:patch_offset + self.SHA256_DIGEST_LEN] != b'\x00' * self.SHA256_DIGEST_LEN: raise FatalError('Contents of segment at SHA256 digest offset 0x%x are not all zero. Refusing to overwrite.' % self.elf_sha256_offset) assert(len(self.elf_sha256) == self.SHA256_DIGEST_LEN) - # offset relative to the data part - patch_offset -= self.SEG_HEADER_LEN segment_data = segment_data[0:patch_offset] + self.elf_sha256 + \ segment_data[patch_offset + self.SHA256_DIGEST_LEN:] return segment_data @@ -1550,6 +2741,41 @@ def get_non_irom_segments(self): irom_segment = self.get_irom_segment() return [s for s in self.segments if s != irom_segment] + def merge_adjacent_segments(self): + if not self.segments: + return # nothing to merge + + segments = [] + # The easiest way to merge the sections is the browse them backward. + for i in range(len(self.segments) - 1, 0, -1): + # elem is the previous section, the one `next_elem` may need to be + # merged in + elem = self.segments[i - 1] + next_elem = self.segments[i] + if all((elem.get_memory_type(self) == next_elem.get_memory_type(self), + elem.include_in_checksum == next_elem.include_in_checksum, + next_elem.addr == elem.addr + len(elem.data))): + # Merge any segment that ends where the next one starts, without spanning memory types + # + # (don't 'pad' any gaps here as they may be excluded from the image due to 'noinit' + # or other reasons.) + elem.data += next_elem.data + else: + # The section next_elem cannot be merged into the previous one, + # which means it needs to be part of the final segments. + # As we are browsing the list backward, the elements need to be + # inserted at the beginning of the final list. + segments.insert(0, next_elem) + + # The first segment will always be here as it cannot be merged into any + # "previous" section. + segments.insert(0, self.segments[0]) + + # note: we could sort segments here as well, but the ordering of segments is sometimes + # important for other reasons (like embedded ELF SHA-256), so we assume that the linker + # script will have produced any adjacent sections in linear order in the ELF, anyhow. + self.segments = segments + class ESP8266ROMFirmwareImage(BaseFirmwareImage): """ 'Version 1' firmware image, segments loaded directly by the ROM bootloader. """ @@ -1683,11 +2909,6 @@ def save(self, filename): f.write(struct.pack(b' 0: last_addr = flash_segments[0].addr for segment in flash_segments[1:]: if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN: - raise FatalError(("Segment loaded at 0x%08x lands in same 64KB flash mapping as segment loaded at 0x%08x. " + + raise FatalError(("Segment loaded at 0x%08x lands in same 64KB flash mapping as segment loaded at 0x%08x. " "Can't generate binary. Suggest changing linker script or ELF to merge sections.") % (segment.addr, last_addr)) last_addr = segment.addr @@ -1846,8 +3066,12 @@ def get_alignment_data_needed(segment): align_past = (f.tell() + self.SEG_HEADER_LEN) % self.IROM_ALIGN # 16 byte aligned checksum (force the alignment to simplify calculations) checksum_space = 16 - # after checksum: SHA-256 digest + (to be added by signing process) version, signature + 12 trailing bytes due to alignment - space_after_checksum = 32 + 4 + 64 + 12 + if self.secure_pad == '1': + # after checksum: SHA-256 digest + (to be added by signing process) version, signature + 12 trailing bytes due to alignment + space_after_checksum = 32 + 4 + 64 + 12 + elif self.secure_pad == '2': # Secure Boot V2 + # after checksum: SHA-256 digest + signature sector, but we place signature sector after the 64KB boundary + space_after_checksum = 32 pad_len = (self.IROM_ALIGN - align_past - checksum_space - space_after_checksum) % self.IROM_ALIGN pad_segment = ImageSegment(0, b'\x00' * pad_len, f.tell()) @@ -1904,8 +3128,8 @@ def split_byte(n): chip_id = fields[4] if chip_id != self.ROM_LOADER.IMAGE_CHIP_ID: - print(("Unexpected chip id in image. Expected %d but value was %d. " + - "Is this image for a different chip model?") % (self.ROM_LOADER.IMAGE_CHIP_ID, chip_id)) + print(("Unexpected chip id in image. Expected %d but value was %d. " + "Is this image for a different chip model?") % (self.ROM_LOADER.IMAGE_CHIP_ID, chip_id)) # reserved fields in the middle should all be zero if any(f for f in fields[6:-1] if f != 0): @@ -1918,7 +3142,7 @@ def split_byte(n): raise RuntimeError("Invalid value for append_digest field (0x%02x). Should be 0 or 1.", append_digest) def save_extended_header(self, save_file): - def join_byte(ln,hn): + def join_byte(ln, hn): return (ln & 0x0F) + ((hn & 0x0F) << 4) append_digest = 1 if self.append_digest else 0 @@ -1936,15 +3160,168 @@ def join_byte(ln,hn): save_file.write(packed) +class ESP8266V3FirmwareImage(ESP32FirmwareImage): + """ ESP8266 V3 firmware image is very similar to ESP32 image + """ + + EXTENDED_HEADER_STRUCT_FMT = "B" * 16 + + def is_flash_addr(self, addr): + return (addr > ESP8266ROM.IROM_MAP_START) + + def save(self, filename): + total_segments = 0 + with io.BytesIO() as f: # write file to memory first + self.write_common_header(f, self.segments) + + checksum = ESPLoader.ESP_CHECKSUM_MAGIC + + # split segments into flash-mapped vs ram-loaded, and take copies so we can mutate them + flash_segments = [copy.deepcopy(s) for s in sorted(self.segments, key=lambda s:s.addr) if self.is_flash_addr(s.addr) and len(s.data)] + ram_segments = [copy.deepcopy(s) for s in sorted(self.segments, key=lambda s:s.addr) if not self.is_flash_addr(s.addr) and len(s.data)] + + # check for multiple ELF sections that are mapped in the same flash mapping region. + # this is usually a sign of a broken linker script, but if you have a legitimate + # use case then let us know + if len(flash_segments) > 0: + last_addr = flash_segments[0].addr + for segment in flash_segments[1:]: + if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN: + raise FatalError(("Segment loaded at 0x%08x lands in same 64KB flash mapping as segment loaded at 0x%08x. " + "Can't generate binary. Suggest changing linker script or ELF to merge sections.") % + (segment.addr, last_addr)) + last_addr = segment.addr + + # try to fit each flash segment on a 64kB aligned boundary + # by padding with parts of the non-flash segments... + while len(flash_segments) > 0: + segment = flash_segments[0] + # remove 8 bytes empty data for insert segment header + if segment.name == '.flash.rodata': + segment.data = segment.data[8:] + # write the flash segment + checksum = self.save_segment(f, segment, checksum) + flash_segments.pop(0) + total_segments += 1 + + # flash segments all written, so write any remaining RAM segments + for segment in ram_segments: + checksum = self.save_segment(f, segment, checksum) + total_segments += 1 + + # done writing segments + self.append_checksum(f, checksum) + image_length = f.tell() + + # kinda hacky: go back to the initial header and write the new segment count + # that includes padding segments. This header is not checksummed + f.seek(1) + try: + f.write(chr(total_segments)) + except TypeError: # Python 3 + f.write(bytes([total_segments])) + + if self.append_digest: + # calculate the SHA256 of the whole file and append it + f.seek(0) + digest = hashlib.sha256() + digest.update(f.read(image_length)) + f.write(digest.digest()) + + with open(filename, 'wb') as real_file: + real_file.write(f.getvalue()) + + def load_extended_header(self, load_file): + def split_byte(n): + return (n & 0x0F, (n >> 4) & 0x0F) + + fields = list(struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))) + + self.wp_pin = fields[0] + + # SPI pin drive stengths are two per byte + self.clk_drv, self.q_drv = split_byte(fields[1]) + self.d_drv, self.cs_drv = split_byte(fields[2]) + self.hd_drv, self.wp_drv = split_byte(fields[3]) + + if fields[15] in [0, 1]: + self.append_digest = (fields[15] == 1) + else: + raise RuntimeError("Invalid value for append_digest field (0x%02x). Should be 0 or 1.", fields[15]) + + # remaining fields in the middle should all be zero + if any(f for f in fields[4:15] if f != 0): + print("Warning: some reserved header fields have non-zero values. This image may be from a newer esptool.py?") + + ESP32ROM.BOOTLOADER_IMAGE = ESP32FirmwareImage +class ESP32S2FirmwareImage(ESP32FirmwareImage): + """ ESP32S2 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32S2ROM + + +ESP32S2ROM.BOOTLOADER_IMAGE = ESP32S2FirmwareImage + + +class ESP32S3BETA2FirmwareImage(ESP32FirmwareImage): + """ ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32S3BETA2ROM + + +ESP32S3BETA2ROM.BOOTLOADER_IMAGE = ESP32S3BETA2FirmwareImage + + +class ESP32S3FirmwareImage(ESP32FirmwareImage): + """ ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32S3ROM + + +ESP32S3ROM.BOOTLOADER_IMAGE = ESP32S3FirmwareImage + + +class ESP32C3FirmwareImage(ESP32FirmwareImage): + """ ESP32C3 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32C3ROM + + +ESP32C3ROM.BOOTLOADER_IMAGE = ESP32C3FirmwareImage + + +class ESP32C6BETAFirmwareImage(ESP32FirmwareImage): + """ ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32C6BETAROM + + +ESP32C6BETAROM.BOOTLOADER_IMAGE = ESP32C6BETAFirmwareImage + + +class ESP32H2FirmwareImage(ESP32FirmwareImage): + """ ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32H2ROM + + +ESP32H2ROM.BOOTLOADER_IMAGE = ESP32H2FirmwareImage + + +class ESP8684FirmwareImage(ESP32FirmwareImage): + """ ESP8684 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP8684ROM + + +ESP8684ROM.BOOTLOADER_IMAGE = ESP8684FirmwareImage + + class ELFFile(object): SEC_TYPE_PROGBITS = 0x01 SEC_TYPE_STRTAB = 0x03 LEN_SEC_HEADER = 0x28 + SEG_TYPE_LOAD = 0x01 + LEN_SEG_HEADER = 0x20 + def __init__(self, name): # Load sections from the ELF file self.name = name @@ -1961,22 +3338,23 @@ def _read_elf_file(self, f): # read the ELF file header LEN_FILE_HEADER = 0x34 try: - (ident,_type,machine,_version, - self.entrypoint,_phoff,shoff,_flags, - _ehsize, _phentsize,_phnum, shentsize, - shnum,shstrndx) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER)) + (ident, _type, machine, _version, + self.entrypoint, _phoff, shoff, _flags, + _ehsize, _phentsize, _phnum, shentsize, + shnum, shstrndx) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER)) except struct.error as e: raise FatalError("Failed to read a valid ELF header from %s: %s" % (self.name, e)) if byte(ident, 0) != 0x7f or ident[1:4] != b'ELF': raise FatalError("%s has invalid ELF magic header" % self.name) - if machine != 0x5e: - raise FatalError("%s does not appear to be an Xtensa ELF file. e_machine=%04x" % (self.name, machine)) + if machine not in [0x5e, 0xf3]: + raise FatalError("%s does not appear to be an Xtensa or an RISCV ELF file. e_machine=%04x" % (self.name, machine)) if shentsize != self.LEN_SEC_HEADER: - raise FatalError("%s has unexpected section header entry size 0x%x (not 0x28)" % (self.name, shentsize, self.LEN_SEC_HEADER)) + raise FatalError("%s has unexpected section header entry size 0x%x (not 0x%x)" % (self.name, shentsize, self.LEN_SEC_HEADER)) if shnum == 0: raise FatalError("%s has 0 section headers" % (self.name)) self._read_sections(f, shoff, shnum, shstrndx) + self._read_segments(f, _phoff, _phnum, shstrndx) def _read_sections(self, f, section_header_offs, section_header_count, shstrndx): f.seek(section_header_offs) @@ -1991,7 +3369,7 @@ def _read_sections(self, f, section_header_offs, section_header_count, shstrndx) section_header_offsets = range(0, len(section_header), self.LEN_SEC_HEADER) def read_section_header(offs): - name_offs,sec_type,_flags,lma,sec_offs,size = struct.unpack_from(" 0] self.sections = prog_sections + def _read_segments(self, f, segment_header_offs, segment_header_count, shstrndx): + f.seek(segment_header_offs) + len_bytes = segment_header_count * self.LEN_SEG_HEADER + segment_header = f.read(len_bytes) + if len(segment_header) == 0: + raise FatalError("No segment header found at offset %04x in ELF file." % segment_header_offs) + if len(segment_header) != (len_bytes): + raise FatalError("Only read 0x%x bytes from segment header (expected 0x%x.) Truncated ELF file?" % (len(segment_header), len_bytes)) + + # walk through the segment header and extract all segments + segment_header_offsets = range(0, len(segment_header), self.LEN_SEG_HEADER) + + def read_segment_header(offs): + seg_type, seg_offs, _vaddr, lma, size, _memsize, _flags, _align = struct.unpack_from(" 0] + self.segments = prog_segments + def sha256(self): # return SHA256 hash of the input ELF file sha256 = hashlib.sha256() @@ -2036,13 +3440,17 @@ def slip_reader(port, trace_function): """ partial_packet = None in_escape = False + successful_slip = False while True: waiting = port.inWaiting() read_bytes = port.read(1 if waiting == 0 else waiting) if read_bytes == b'': - waiting_for = "header" if partial_packet is None else "content" - trace_function("Timed out waiting for packet %s", waiting_for) - raise FatalError("Timed out waiting for packet %s" % waiting_for) + if partial_packet is None: # fail due to no data + msg = "Serial data stream stopped: Possible serial noise or corruption." if successful_slip else "No serial data received." + else: # fail during packet transfer + msg = "Packet content transfer stopped (received {} bytes)".format(len(partial_packet)) + trace_function(msg) + raise FatalError(msg) trace_function("Read %d bytes: %s", len(read_bytes), HexFormatter(read_bytes)) for b in read_bytes: if type(b) is int: @@ -2054,7 +3462,7 @@ def slip_reader(port, trace_function): else: trace_function("Read invalid data: %s", HexFormatter(read_bytes)) trace_function("Remaining data in serial buffer: %s", HexFormatter(port.read(port.inWaiting()))) - raise FatalError('Invalid head of packet (0x%s)' % hexify(b)) + raise FatalError('Invalid head of packet (0x%s): Possible serial noise or corruption.' % hexify(b)) elif in_escape: # part-way through escape sequence in_escape = False if b == b'\xdc': @@ -2071,6 +3479,7 @@ def slip_reader(port, trace_function): trace_function("Received full packet: %s", HexFormatter(partial_packet)) yield partial_packet partial_packet = None + successful_slip = True else: # normal byte in packet partial_packet += b @@ -2168,9 +3577,25 @@ def __init__(self, message): def WithResult(message, result): """ Return a fatal error object that appends the hex values of - 'result' as a string formatted argument. + 'result' and its meaning as a string formatted argument. """ - message += " (result was %s)" % hexify(result) + + err_defs = { + 0x101: 'Out of memory', + 0x102: 'Invalid argument', + 0x103: 'Invalid state', + 0x104: 'Invalid size', + 0x105: 'Requested resource not found', + 0x106: 'Operation or feature not supported', + 0x107: 'Operation timed out', + 0x108: 'Received response was invalid', + 0x109: 'CRC or checksum was invalid', + 0x10A: 'Version was invalid', + 0x10B: 'MAC address was invalid', + } + + err_code = struct.unpack(">H", result[:2]) + message += " (result was {}: {})".format(hexify(result), err_defs.get(err_code[0], 'Unknown result')) return FatalError(message) @@ -2193,6 +3618,20 @@ def __init__(self, esp, function_name): # argument. +class UnsupportedCommandError(RuntimeError): + """ + Wrapper class for when ROM loader returns an invalid command response. + + Usually this indicates the loader is running in Secure Download Mode. + """ + def __init__(self, esp, op): + if esp.secure_download_mode: + msg = "This command (0x%x) is not supported in Secure Download Mode" % op + else: + msg = "Invalid (unsupported) command 0x%x" % op + RuntimeError.__init__(self, msg) + + def load_ram(esp, args): image = LoadFirmwareImage(esp.CHIP_NAME, args.filename) @@ -2229,15 +3668,17 @@ def dump_mem(esp, args): d = esp.read_reg(args.address + (i * 4)) f.write(struct.pack(b'> 16 args.flash_size = DETECTED_FLASH_SIZES.get(size_id) @@ -2273,15 +3714,15 @@ def _update_image_flash_params(esp, address, args, image): test_image = esp.BOOTLOADER_IMAGE(io.BytesIO(image)) test_image.verify() except Exception: - print("Warning: Image file at 0x%x is not a valid %s image, so not changing any flash settings." % (address,esp.CHIP_NAME)) + print("Warning: Image file at 0x%x is not a valid %s image, so not changing any flash settings." % (address, esp.CHIP_NAME)) return image if args.flash_mode != 'keep': - flash_mode = {'qio':0, 'qout':1, 'dio':2, 'dout': 3}[args.flash_mode] + flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode] flash_freq = flash_size_freq & 0x0F if args.flash_freq != 'keep': - flash_freq = {'40m':0, '26m':1, '20m':2, '80m': 0xf}[args.flash_freq] + flash_freq = {'40m': 0, '26m': 1, '20m': 2, '80m': 0xf}[args.flash_freq] flash_size = flash_size_freq & 0xF0 if args.flash_size != 'keep': @@ -2301,99 +3742,168 @@ def write_flash(esp, args): if args.compress is None and not args.no_compress: args.compress = not args.no_stub - # For encrypt option we do few sanity checks before actual flash write - if args.encrypt: + # In case we have encrypted files to write, we first do few sanity checks before actual flash + if args.encrypt or args.encrypt_files is not None: do_write = True - crypt_cfg_efuse = esp.get_flash_crypt_config() - if crypt_cfg_efuse != 0xF: - print('\nWARNING: Unexpected FLASH_CRYPT_CONFIG value', hex(crypt_cfg_efuse)) - print('\nMake sure flash encryption is enabled correctly, refer to Flash Encryption documentation') - do_write = False + if not esp.secure_download_mode: + if esp.get_encrypted_download_disabled(): + raise FatalError("This chip has encrypt functionality in UART download mode disabled. " + "This is the Flash Encryption configuration for Production mode instead of Development mode.") + + crypt_cfg_efuse = esp.get_flash_crypt_config() + + if crypt_cfg_efuse is not None and crypt_cfg_efuse != 0xF: + print('Unexpected FLASH_CRYPT_CONFIG value: 0x%x' % (crypt_cfg_efuse)) + do_write = False - enc_key_valid = esp.is_flash_encryption_key_valid() + enc_key_valid = esp.is_flash_encryption_key_valid() - if not enc_key_valid: - print('\nFlash encryption key is not programmed') - print('\nMake sure flash encryption is enabled correctly, refer to Flash Encryption documentation') - do_write = False + if not enc_key_valid: + print('Flash encryption key is not programmed') + do_write = False - if (esp.FLASH_WRITE_SIZE % 32) != 0: - print('\nWARNING - Flash write address is not aligned to the recommeded 32 bytes') - do_write = False + # Determine which files list contain the ones to encrypt + files_to_encrypt = args.addr_filename if args.encrypt else args.encrypt_files + + for address, argfile in files_to_encrypt: + if address % esp.FLASH_ENCRYPTED_WRITE_ALIGN: + print("File %s address 0x%x is not %d byte aligned, can't flash encrypted" % + (argfile.name, address, esp.FLASH_ENCRYPTED_WRITE_ALIGN)) + do_write = False if not do_write and not args.ignore_flash_encryption_efuse_setting: - raise FatalError("Incorrect efuse setting: aborting flash write") + raise FatalError("Can't perform encrypted flash write, consult Flash Encryption documentation for more information") # verify file sizes fit in flash if args.flash_size != 'keep': # TODO: check this even with 'keep' flash_end = flash_size_bytes(args.flash_size) for address, argfile in args.addr_filename: - argfile.seek(0,2) # seek to end + argfile.seek(0, os.SEEK_END) if address + argfile.tell() > flash_end: - raise FatalError(("File %s (length %d) at offset %d will not fit in %d bytes of flash. " + + raise FatalError(("File %s (length %d) at offset %d will not fit in %d bytes of flash. " "Use --flash-size argument, or change flashing address.") % (argfile.name, argfile.tell(), address, flash_end)) argfile.seek(0) if args.erase_all: erase_flash(esp, args) + else: + for address, argfile in args.addr_filename: + argfile.seek(0, os.SEEK_END) + write_end = address + argfile.tell() + argfile.seek(0) + bytes_over = address % esp.FLASH_SECTOR_SIZE + if bytes_over != 0: + print("WARNING: Flash address {:#010x} is not aligned to a {:#x} byte flash sector. " + "{:#x} bytes before this address will be erased." + .format(address, esp.FLASH_SECTOR_SIZE, bytes_over)) + # Print the address range of to-be-erased flash memory region + print("Flash will be erased from {:#010x} to {:#010x}..." + .format(address - bytes_over, div_roundup(write_end, esp.FLASH_SECTOR_SIZE) * esp.FLASH_SECTOR_SIZE - 1)) + + """ Create a list describing all the files we have to flash. Each entry holds an "encrypt" flag + marking whether the file needs encryption or not. This list needs to be sorted. + + First, append to each entry of our addr_filename list the flag args.encrypt + For example, if addr_filename is [(0x1000, "partition.bin"), (0x8000, "bootloader")], + all_files will be [(0x1000, "partition.bin", args.encrypt), (0x8000, "bootloader", args.encrypt)], + where, of course, args.encrypt is either True or False + """ + all_files = [(offs, filename, args.encrypt) for (offs, filename) in args.addr_filename] + + """Now do the same with encrypt_files list, if defined. + In this case, the flag is True + """ + if args.encrypt_files is not None: + encrypted_files_flag = [(offs, filename, True) for (offs, filename) in args.encrypt_files] - if args.encrypt and args.compress: - print('\nWARNING: - compress and encrypt options are mutually exclusive ') - print('Will flash uncompressed') - args.compress = False + # Concatenate both lists and sort them. + # As both list are already sorted, we could simply do a merge instead, + # but for the sake of simplicity and because the lists are very small, + # let's use sorted. + all_files = sorted(all_files + encrypted_files_flag, key=lambda x: x[0]) + + for address, argfile, encrypted in all_files: + compress = args.compress + + # Check whether we can compress the current file before flashing + if compress and encrypted: + print('\nWARNING: - compress and encrypt options are mutually exclusive ') + print('Will flash %s uncompressed' % argfile.name) + compress = False - for address, argfile in args.addr_filename: if args.no_stub: print('Erasing flash...') - image = pad_to(argfile.read(), 32 if args.encrypt else 4) + image = pad_to(argfile.read(), esp.FLASH_ENCRYPTED_WRITE_ALIGN if encrypted else 4) if len(image) == 0: print('WARNING: File %s is empty' % argfile.name) continue image = _update_image_flash_params(esp, address, args, image) calcmd5 = hashlib.md5(image).hexdigest() uncsize = len(image) - if args.compress: + if compress: uncimage = image image = zlib.compress(uncimage, 9) - ratio = uncsize / len(image) + # Decompress the compressed binary a block at a time, to dynamically calculate the + # timeout based on the real write size + decompress = zlib.decompressobj() blocks = esp.flash_defl_begin(uncsize, len(image), address) else: - ratio = 1.0 - blocks = esp.flash_begin(uncsize, address) + blocks = esp.flash_begin(uncsize, address, begin_rom_encrypted=encrypted) argfile.seek(0) # in case we need it again seq = 0 - written = 0 + bytes_sent = 0 # bytes sent on wire + bytes_written = 0 # bytes written to flash t = time.time() + + timeout = DEFAULT_TIMEOUT + while len(image) > 0: - print('\rWriting at 0x%08x... (%d %%)' % (address + seq * esp.FLASH_WRITE_SIZE, 100 * (seq + 1) // blocks), end='') + print_overwrite('Writing at 0x%08x... (%d %%)' % (address + bytes_written, 100 * (seq + 1) // blocks)) sys.stdout.flush() block = image[0:esp.FLASH_WRITE_SIZE] - if args.compress: - esp.flash_defl_block(block, seq, timeout=DEFAULT_TIMEOUT * ratio * 2) + if compress: + # feeding each compressed block into the decompressor lets us see block-by-block how much will be written + block_uncompressed = len(decompress.decompress(block)) + bytes_written += block_uncompressed + block_timeout = max(DEFAULT_TIMEOUT, timeout_per_mb(ERASE_WRITE_TIMEOUT_PER_MB, block_uncompressed)) + if not esp.IS_STUB: + timeout = block_timeout # ROM code writes block to flash before ACKing + esp.flash_defl_block(block, seq, timeout=timeout) + if esp.IS_STUB: + timeout = block_timeout # Stub ACKs when block is received, then writes to flash while receiving the block after it else: # Pad the last block block = block + b'\xff' * (esp.FLASH_WRITE_SIZE - len(block)) - if args.encrypt: + if encrypted: esp.flash_encrypt_block(block, seq) else: esp.flash_block(block, seq) + bytes_written += len(block) + bytes_sent += len(block) image = image[esp.FLASH_WRITE_SIZE:] seq += 1 - written += len(block) + + if esp.IS_STUB: + # Stub only writes each block to flash after 'ack'ing the receive, so do a final dummy operation which will + # not be 'ack'ed until the last block has actually been written out to flash + esp.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR, timeout=timeout) + t = time.time() - t speed_msg = "" - if args.compress: + if compress: if t > 0.0: speed_msg = " (effective %.1f kbit/s)" % (uncsize / t * 8 / 1000) - print('\rWrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s...' % (uncsize, written, address, t, speed_msg)) + print_overwrite('Wrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s...' % (uncsize, + bytes_sent, + address, t, speed_msg), last_line=True) else: if t > 0.0: - speed_msg = " (%.1f kbit/s)" % (written / t * 8 / 1000) - print('\rWrote %d bytes at 0x%08x in %.1f seconds%s...' % (written, address, t, speed_msg)) + speed_msg = " (%.1f kbit/s)" % (bytes_written / t * 8 / 1000) + print_overwrite('Wrote %d bytes at 0x%08x in %.1f seconds%s...' % (bytes_written, address, t, speed_msg), last_line=True) - if not args.encrypt: + if not encrypted and not esp.secure_download_mode: try: res = esp.flash_md5sum(address, uncsize) if res != calcmd5: @@ -2412,7 +3922,14 @@ def write_flash(esp, args): # skip sending flash_finish to ROM loader here, # as it causes the loader to exit and run user code esp.flash_begin(0, 0) - if args.compress: + + # Get the "encrypted" flag for the last file flashed + # Note: all_files list contains triplets like: + # (address: Integer, filename: String, encrypted: Boolean) + last_file_encrypted = all_files[-1][2] + + # Check whether the last file flashed was compressed or not + if args.compress and not last_file_encrypted: esp.flash_defl_finish(False) else: esp.flash_finish(False) @@ -2420,7 +3937,12 @@ def write_flash(esp, args): if args.verify: print('Verifying just-written flash...') print('(This option is deprecated, flash contents are now always read back after flashing.)') - verify_flash(esp, args) + # If some encrypted files have been flashed print a warning saying that we won't check them + if args.encrypt or args.encrypt_files is not None: + print('WARNING: - cannot verify encrypted files, they will be ignored') + # Call verify_flash function only if there at least one non-encrypted file flashed + if not args.encrypt: + verify_flash(esp, args) def image_info(args): @@ -2432,7 +3954,8 @@ def image_info(args): idx = 0 for seg in image.segments: idx += 1 - seg_name = ", ".join([seg_range[2] for seg_range in image.ROM_LOADER.MEMORY_MAP if seg_range[0] <= seg.addr < seg_range[1]]) + segs = seg.get_memory_type(image) + seg_name = ",".join(segs) print('Segment %d: %r [%s]' % (idx, seg, seg_name)) calc_checksum = image.calculate_checksum() print('Checksum: %02x (%s)' % (image.checksum, @@ -2470,22 +3993,66 @@ def elf2image(args): if args.chip == 'esp32': image = ESP32FirmwareImage() - image.secure_pad = args.secure_pad - image.min_rev = int(args.min_rev) + if args.secure_pad: + image.secure_pad = '1' + elif args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32s2': + image = ESP32S2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32s3beta2': + image = ESP32S3BETA2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32s3': + image = ESP32S3FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32c3': + image = ESP32C3FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32c6beta': + image = ESP32C6BETAFirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32h2': + image = ESP32H2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp8684': + image = ESP8684FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' elif args.version == '1': # ESP8266 image = ESP8266ROMFirmwareImage() - else: + elif args.version == '2': image = ESP8266V2FirmwareImage() + else: + image = ESP8266V3FirmwareImage() image.entrypoint = e.entrypoint - image.segments = e.sections # ELFSection is a subclass of ImageSegment - image.flash_mode = {'qio':0, 'qout':1, 'dio':2, 'dout': 3}[args.flash_mode] + image.flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode] + + if args.chip != 'esp8266': + image.min_rev = int(args.min_rev) + + # ELFSection is a subclass of ImageSegment, so can use interchangeably + image.segments = e.segments if args.use_segments else e.sections + image.flash_size_freq = image.ROM_LOADER.FLASH_SIZES[args.flash_size] - image.flash_size_freq += {'40m':0, '26m':1, '20m':2, '80m': 0xf}[args.flash_freq] + image.flash_size_freq += {'40m': 0, '26m': 1, '20m': 2, '80m': 0xf}[args.flash_freq] if args.elf_sha256_offset: image.elf_sha256 = e.sha256() image.elf_sha256_offset = args.elf_sha256_offset + before = len(image.segments) + image.merge_adjacent_segments() + if len(image.segments) != before: + delta = before - len(image.segments) + print("Merged %d ELF section%s" % (delta, "s" if delta > 1 else "")) + image.verify() if args.output is None: @@ -2550,8 +4117,8 @@ def flash_progress(progress, length): t = time.time() data = esp.read_flash(args.address, args.size, flash_progress) t = time.time() - t - print('\rRead %d bytes at 0x%x in %.1f seconds (%.1f kbit/s)...' - % (len(data), args.address, t, len(data) / t * 8 / 1000)) + print_overwrite('Read %d bytes at 0x%x in %.1f seconds (%.1f kbit/s)...' + % (len(data), args.address, t, len(data) / t * 8 / 1000), last_line=True) with open(args.filename, 'wb') as f: f.write(data) @@ -2607,6 +4174,50 @@ def write_flash_status(esp, args): print(('After flash status: ' + fmt) % esp.read_status(args.bytes)) +def get_security_info(esp, args): + si = esp.get_security_info() + # TODO: better display and tests + print('Flags: {:#010x} ({})'.format(si["flags"], bin(si["flags"]))) + print('Flash_Crypt_Cnt: {:#x}'.format(si["flash_crypt_cnt"])) + print('Key_Purposes: {}'.format(si["key_purposes"])) + if si["chip_id"] is not None and si["api_version"] is not None: + print('Chip_ID: {}'.format(si["chip_id"])) + print('Api_Version: {}'.format(si["api_version"])) + + +def merge_bin(args): + try: + chip_class = _chip_to_rom_loader(args.chip) + except KeyError: + msg = "Please specify the chip argument" if args.chip == "auto" else "Invalid chip choice: '{}'".format(args.chip) + msg = msg + " (choose from {})".format(', '.join(SUPPORTED_CHIPS)) + raise FatalError(msg) + + # sort the files by offset. The AddrFilenamePairAction has already checked for overlap + input_files = sorted(args.addr_filename, key=lambda x: x[0]) + if not input_files: + raise FatalError("No input files specified") + first_addr = input_files[0][0] + if first_addr < args.target_offset: + raise FatalError("Output file target offset is 0x%x. Input file offset 0x%x is before this." % (args.target_offset, first_addr)) + + if args.format != 'raw': + raise FatalError("This version of esptool only supports the 'raw' output format") + + with open(args.output, 'wb') as of: + def pad_to(flash_offs): + # account for output file offset if there is any + of.write(b'\xFF' * (flash_offs - args.target_offset - of.tell())) + for addr, argfile in input_files: + pad_to(addr) + image = argfile.read() + image = _update_image_flash_params(chip_class, addr, args, image) + of.write(image) + if args.fill_flash_size: + pad_to(flash_size_bytes(args.fill_flash_size)) + print("Wrote 0x%x bytes to file %s, ready to flash to offset 0x%x" % (of.tell(), args.output, args.target_offset)) + + def version(args): print(__version__) @@ -2615,19 +4226,25 @@ def version(args): # -def main(custom_commandline=None): +def main(argv=None, esp=None): """ Main function for esptool - custom_commandline - Optional override for default arguments parsing (that uses sys.argv), can be a list of custom arguments + argv - Optional override for default arguments parsing (that uses sys.argv), can be a list of custom arguments as strings. Arguments and their values need to be added as individual items to the list e.g. "-b 115200" thus becomes ['-b', '115200']. + + esp - Optional override of the connected device previously returned by get_default_connected_device() """ + + external_esp = esp is not None + parser = argparse.ArgumentParser(description='esptool.py v%s - ESP8266 ROM Bootloader Utility' % __version__, prog='esptool') parser.add_argument('--chip', '-c', help='Target chip type', - choices=['auto', 'esp8266', 'esp32'], + type=lambda c: c.lower().replace('-', ''), # support ESP32-S2, etc. + choices=['auto'] + SUPPORTED_CHIPS, default=os.environ.get('ESPTOOL_CHIP', 'auto')) parser.add_argument( @@ -2644,13 +4261,13 @@ def main(custom_commandline=None): parser.add_argument( '--before', help='What to do before connecting to the chip', - choices=['default_reset', 'no_reset', 'no_reset_no_sync'], + choices=['default_reset', 'usb_reset', 'no_reset', 'no_reset_no_sync'], default=os.environ.get('ESPTOOL_BEFORE', 'default_reset')) parser.add_argument( '--after', '-a', help='What to do after esptool.py is finished', - choices=['hard_reset', 'soft_reset', 'no_reset'], + choices=['hard_reset', 'soft_reset', 'no_reset', 'no_reset_stub'], default=os.environ.get('ESPTOOL_AFTER', 'hard_reset')) parser.add_argument( @@ -2669,12 +4286,19 @@ def main(custom_commandline=None): choices=ESP32ROM.OVERRIDE_VDDSDIO_CHOICES, nargs='?') + parser.add_argument( + '--connect-attempts', + help=('Number of attempts to connect, negative or 0 for infinite. ' + 'Default: %d.' % DEFAULT_CONNECT_ATTEMPTS), + type=int, + default=os.environ.get('ESPTOOL_CONNECT_ATTEMPTS', DEFAULT_CONNECT_ATTEMPTS)) + subparsers = parser.add_subparsers( dest='operation', help='Run esptool {command} -h for additional help') def add_spi_connection_arg(parent): - parent.add_argument('--spi-connection', '-sc', help='ESP32-only argument. Override default SPI Flash connection. ' + + parent.add_argument('--spi-connection', '-sc', help='ESP32-only argument. Override default SPI Flash connection. ' 'Value can be SPI, HSPI or a comma-separated list of 5 I/O numbers to use for SPI flash (CLK,Q,D,HD,CS).', action=SpiConnectionAction) @@ -2700,28 +4324,31 @@ def add_spi_connection_arg(parent): help='Read-modify-write to arbitrary memory location') parser_write_mem.add_argument('address', help='Address to write', type=arg_auto_int) parser_write_mem.add_argument('value', help='Value', type=arg_auto_int) - parser_write_mem.add_argument('mask', help='Mask of bits to write', type=arg_auto_int) + parser_write_mem.add_argument('mask', help='Mask of bits to write', type=arg_auto_int, nargs='?', default='0xFFFFFFFF') - def add_spi_flash_subparsers(parent, is_elf2image): + def add_spi_flash_subparsers(parent, allow_keep, auto_detect): """ Add common parser arguments for SPI flash properties """ - extra_keep_args = [] if is_elf2image else ['keep'] - auto_detect = not is_elf2image + extra_keep_args = ['keep'] if allow_keep else [] - if auto_detect: + if auto_detect and allow_keep: extra_fs_message = ", detect, or keep" + elif auto_detect: + extra_fs_message = ", or detect" + elif allow_keep: + extra_fs_message = ", or keep" else: extra_fs_message = "" parent.add_argument('--flash_freq', '-ff', help='SPI Flash frequency', choices=extra_keep_args + ['40m', '26m', '20m', '80m'], - default=os.environ.get('ESPTOOL_FF', '40m' if is_elf2image else 'keep')) + default=os.environ.get('ESPTOOL_FF', 'keep' if allow_keep else '40m')) parent.add_argument('--flash_mode', '-fm', help='SPI Flash mode', choices=extra_keep_args + ['qio', 'qout', 'dio', 'dout'], - default=os.environ.get('ESPTOOL_FM', 'qio' if is_elf2image else 'keep')) + default=os.environ.get('ESPTOOL_FM', 'keep' if allow_keep else 'qio')) parent.add_argument('--flash_size', '-fs', help='SPI Flash size in MegaBytes (1MB, 2MB, 4MB, 8MB, 16M)' ' plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)' + extra_fs_message, action=FlashSizeAction, auto_detect=auto_detect, - default=os.environ.get('ESPTOOL_FS', 'detect' if auto_detect else '1MB')) + default=os.environ.get('ESPTOOL_FS', 'keep' if allow_keep else '1MB')) add_spi_connection_arg(parent) parser_write_flash = subparsers.add_parser( @@ -2734,18 +4361,24 @@ def add_spi_flash_subparsers(parent, is_elf2image): help='Erase all regions of flash (not just write areas) before programming', action="store_true") - add_spi_flash_subparsers(parser_write_flash, is_elf2image=False) + add_spi_flash_subparsers(parser_write_flash, allow_keep=True, auto_detect=True) parser_write_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true") - parser_write_flash.add_argument('--verify', help='Verify just-written data on flash ' + + parser_write_flash.add_argument('--verify', help='Verify just-written data on flash ' '(mostly superfluous, data is read back during flashing)', action='store_true') - parser_write_flash.add_argument('--encrypt', help='Encrypt before write ', + parser_write_flash.add_argument('--encrypt', help='Apply flash encryption when writing data (required correct efuse settings)', action='store_true') + # In order to not break backward compatibility, our list of encrypted files to flash is a new parameter + parser_write_flash.add_argument('--encrypt-files', metavar='
', + help='Files to be encrypted on the flash. Address followed by binary filename, separated by space.', + action=AddrFilenamePairAction) parser_write_flash.add_argument('--ignore-flash-encryption-efuse-setting', help='Ignore flash encryption efuse settings ', action='store_true') compress_args = parser_write_flash.add_mutually_exclusive_group(required=False) - compress_args.add_argument('--compress', '-z', help='Compress data in transfer (default unless --no-stub is specified)',action="store_true", default=None) - compress_args.add_argument('--no-compress', '-u', help='Disable data compression during transfer (default if --no-stub is specified)',action="store_true") + compress_args.add_argument('--compress', '-z', help='Compress data in transfer (default unless --no-stub is specified)', + action="store_true", default=None) + compress_args.add_argument('--no-compress', '-u', help='Disable data compression during transfer (default if --no-stub is specified)', + action="store_true") subparsers.add_parser( 'run', @@ -2769,13 +4402,19 @@ def add_spi_flash_subparsers(parent, is_elf2image): help='Create an application image from ELF file') parser_elf2image.add_argument('input', help='Input ELF file') parser_elf2image.add_argument('--output', '-o', help='Output filename prefix (for version 1 image), or filename (for version 2 single image)', type=str) - parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1','2'], default='1') - parser_elf2image.add_argument('--min-rev', '-r', help='Minimum chip revision', choices=['0','1','2','3'], default='0') - parser_elf2image.add_argument('--secure-pad', action='store_true', help='Pad image so once signed it will end on a 64KB boundary. For ESP32 images only.') + parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1', '2', '3'], default='1') + parser_elf2image.add_argument('--min-rev', '-r', help='Minimum chip revision', choices=['0', '1', '2', '3'], default='0') + parser_elf2image.add_argument('--secure-pad', action='store_true', + help='Pad image so once signed it will end on a 64KB boundary. For Secure Boot v1 images only.') + parser_elf2image.add_argument('--secure-pad-v2', action='store_true', + help='Pad image to 64KB, so once signed its signature sector will start at the next 64K block. ' + 'For Secure Boot v2 images only.') parser_elf2image.add_argument('--elf-sha256-offset', help='If set, insert SHA256 hash (32 bytes) of the input ELF file at specified offset in the binary.', type=arg_auto_int, default=None) + parser_elf2image.add_argument('--use_segments', help='If set, ELF segments will be used instead of ELF sections to genereate the image.', + action='store_true') - add_spi_flash_subparsers(parser_elf2image, is_elf2image=True) + add_spi_flash_subparsers(parser_elf2image, allow_keep=False, auto_detect=False) subparsers.add_parser( 'read_mac', @@ -2795,7 +4434,7 @@ def add_spi_flash_subparsers(parent, is_elf2image): help='Read SPI flash status register') add_spi_connection_arg(parser_read_status) - parser_read_status.add_argument('--bytes', help='Number of bytes to read (1-3)', type=int, choices=[1,2,3], default=2) + parser_read_status.add_argument('--bytes', help='Number of bytes to read (1-3)', type=int, choices=[1, 2, 3], default=2) parser_write_status = subparsers.add_parser( 'write_flash_status', @@ -2803,7 +4442,7 @@ def add_spi_flash_subparsers(parent, is_elf2image): add_spi_connection_arg(parser_write_status) parser_write_status.add_argument('--non-volatile', help='Write non-volatile bits (use with caution)', action='store_true') - parser_write_status.add_argument('--bytes', help='Number of status bytes to write (1-3)', type=int, choices=[1,2,3], default=2) + parser_write_status.add_argument('--bytes', help='Number of status bytes to write (1-3)', type=int, choices=[1, 2, 3], default=2) parser_write_status.add_argument('value', help='New value', type=arg_auto_int) parser_read_flash = subparsers.add_parser( @@ -2822,7 +4461,7 @@ def add_spi_flash_subparsers(parent, is_elf2image): action=AddrFilenamePairAction) parser_verify_flash.add_argument('--diff', '-d', help='Show differences', choices=['no', 'yes'], default='no') - add_spi_flash_subparsers(parser_verify_flash, is_elf2image=False) + add_spi_flash_subparsers(parser_verify_flash, allow_keep=True, auto_detect=True) parser_erase_flash = subparsers.add_parser( 'erase_flash', @@ -2836,17 +4475,34 @@ def add_spi_flash_subparsers(parent, is_elf2image): parser_erase_region.add_argument('address', help='Start address (must be multiple of 4096)', type=arg_auto_int) parser_erase_region.add_argument('size', help='Size of region to erase (must be multiple of 4096)', type=arg_auto_int) + parser_merge_bin = subparsers.add_parser( + 'merge_bin', + help='Merge multiple raw binary files into a single file for later flashing') + + parser_merge_bin.add_argument('--output', '-o', help='Output filename', type=str, required=True) + parser_merge_bin.add_argument('--format', '-f', help='Format of the output file', choices='raw', default='raw') # for future expansion + add_spi_flash_subparsers(parser_merge_bin, allow_keep=True, auto_detect=False) + + parser_merge_bin.add_argument('--target-offset', '-t', help='Target offset where the output file will be flashed', + type=arg_auto_int, default=0) + parser_merge_bin.add_argument('--fill-flash-size', help='If set, the final binary file will be padded with FF ' + 'bytes up to this flash size.', action=FlashSizeAction) + parser_merge_bin.add_argument('addr_filename', metavar='
', + help='Address followed by binary filename, separated by space', + action=AddrFilenamePairAction) + subparsers.add_parser( 'version', help='Print esptool version') + subparsers.add_parser('get_security_info', help='Get some security-related data') + # internal sanity check - every operation matches a module function of the same name for operation in subparsers.choices.keys(): assert operation in globals(), "%s should be a module function" % operation - expand_file_arguments() - - args = parser.parse_args(custom_commandline) + argv = expand_file_arguments(argv or sys.argv[1:]) + args = parser.parse_args(argv) print('esptool.py v%s' % __version__) # operation function can take 1 arg (args), 2 args (esp, arg) @@ -2856,6 +4512,13 @@ def add_spi_flash_subparsers(parent, is_elf2image): parser.print_help() sys.exit(1) + # Forbid the usage of both --encrypt, which means encrypt all the given files, + # and --encrypt-files, which represents the list of files to encrypt. + # The reason is that allowing both at the same time increases the chances of + # having contradictory lists (e.g. one file not available in one of list). + if args.operation == "write_flash" and args.encrypt and args.encrypt_files is not None: + raise FatalError("Options --encrypt and --encrypt-files must not be specified at the same time.") + operation_func = globals()[args.operation] if PYTHON2: @@ -2871,42 +4534,34 @@ def add_spi_flash_subparsers(parent, is_elf2image): initial_baud = args.baud if args.port is None: - ser_list = sorted(ports.device for ports in list_ports.comports()) + ser_list = get_port_list() print("Found %d serial ports" % len(ser_list)) else: ser_list = [args.port] - esp = None - for each_port in reversed(ser_list): - print("Serial port %s" % each_port) - try: - if args.chip == 'auto': - esp = ESPLoader.detect_chip(each_port, initial_baud, args.before, args.trace) - else: - chip_class = { - 'esp8266': ESP8266ROM, - 'esp32': ESP32ROM, - }[args.chip] - esp = chip_class(each_port, initial_baud, args.trace) - esp.connect(args.before) - break - except (FatalError, OSError) as err: - if args.port is not None: - raise - print("%s failed to connect: %s" % (each_port, err)) - esp = None + esp = esp or get_default_connected_device(ser_list, port=args.port, connect_attempts=args.connect_attempts, + initial_baud=initial_baud, chip=args.chip, trace=args.trace, + before=args.before) + if esp is None: raise FatalError("Could not connect to an Espressif device on any of the %d available serial ports." % len(ser_list)) - print("Chip is %s" % (esp.get_chip_description())) - - print("Features: %s" % ", ".join(esp.get_chip_features())) - - print("Crystal is %dMHz" % esp.get_crystal_freq()) - - read_mac(esp, args) + if esp.secure_download_mode: + print("Chip is %s in Secure Download Mode" % esp.CHIP_NAME) + else: + print("Chip is %s" % (esp.get_chip_description())) + print("Features: %s" % ", ".join(esp.get_chip_features())) + print("Crystal is %dMHz" % esp.get_crystal_freq()) + read_mac(esp, args) if not args.no_stub: - esp = esp.run_stub() + if esp.secure_download_mode: + print("WARNING: Stub loader is not supported in Secure Download Mode, setting --no-stub") + args.no_stub = True + elif not esp.IS_STUB and esp.stub_is_disabled: + print("WARNING: Stub loader has been disabled for compatibility, setting --no-stub") + args.no_stub = True + else: + esp = esp.run_stub() if args.override_vddsdio: esp.override_vddsdio(args.override_vddsdio) @@ -2928,6 +4583,15 @@ def add_spi_flash_subparsers(parent, is_elf2image): # ROM loader doesn't enable flash unless we explicitly do it esp.flash_spi_attach(0) + # Check flash chip connection + try: + flash_id = esp.flash_id() + if flash_id in (0xffffff, 0x000000): + print('WARNING: Failed to communicate with the flash chip, read/write operations will fail. ' + 'Try checking the chip connections or removing any other hardware connected to IOs.') + except Exception as e: + esp.trace('Unable to verify flash chip connection ({}).'.format(e)) + if hasattr(args, "flash_size"): print("Configuring flash size...") detect_flash_size(esp, args) @@ -2948,41 +4612,51 @@ def add_spi_flash_subparsers(parent, is_elf2image): # the ESP is now running the loaded image, so let it run print('Exiting immediately.') elif args.after == 'hard_reset': - print('Hard resetting via RTS pin...') esp.hard_reset() elif args.after == 'soft_reset': print('Soft resetting...') # flash_finish will trigger a soft reset esp.soft_reset(False) - else: + elif args.after == 'no_reset_stub': + print('Staying in flasher stub.') + else: # args.after == 'no_reset' print('Staying in bootloader.') if esp.IS_STUB: esp.soft_reset(True) # exit stub back to ROM loader - esp._port.close() + if not external_esp: + esp._port.close() else: operation_func(args) -def expand_file_arguments(): +def get_port_list(): + if list_ports is None: + raise FatalError("Listing all serial ports is currently not available. Please try to specify the port when " + "running esptool.py or update the pyserial package to the latest version") + return sorted(ports.device for ports in list_ports.comports()) + + +def expand_file_arguments(argv): """ Any argument starting with "@" gets replaced with all values read from a text file. Text file arguments can be split by newline or by space. Values are added "as-is", as if they were specified in this order on the command line. """ new_args = [] expanded = False - for arg in sys.argv: + for arg in argv: if arg.startswith("@"): expanded = True - with open(arg[1:],"r") as f: + with open(arg[1:], "r") as f: for line in f.readlines(): new_args += shlex.split(line) else: new_args.append(arg) if expanded: print("esptool.py %s" % (" ".join(new_args[1:]))) - sys.argv = new_args + return new_args + return argv class FlashSizeAction(argparse.Action): @@ -3034,17 +4708,17 @@ def __call__(self, parser, namespace, value, option_string=None): if len(values) != 5: raise argparse.ArgumentError(self, '%s is not a valid list of comma-separate pin numbers. Must be 5 numbers - CLK,Q,D,HD,CS.' % value) try: - values = tuple(int(v,0) for v in values) + values = tuple(int(v, 0) for v in values) except ValueError: raise argparse.ArgumentError(self, '%s is not a valid argument. All pins must be numeric values' % values) if any([v for v in values if v > 33 or v < 0]): raise argparse.ArgumentError(self, 'Pin numbers must be in the range 0-33.') # encode the pin numbers as a 32-bit integer with packed 6-bit values, the same way ESP32 ROM takes them # TODO: make this less ESP32 ROM specific somehow... - clk,q,d,hd,cs = values + clk, q, d, hd, cs = values value = (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk else: - raise argparse.ArgumentError(self, '%s is not a valid spi-connection value. ' + + raise argparse.ArgumentError(self, '%s is not a valid spi-connection value. ' 'Values are SPI, HSPI, or a sequence of 5 pin numbers CLK,Q,D,HD,CS).' % value) setattr(namespace, self.dest, value) @@ -3057,23 +4731,23 @@ def __init__(self, option_strings, dest, nargs='+', **kwargs): def __call__(self, parser, namespace, values, option_string=None): # validate pair arguments pairs = [] - for i in range(0,len(values),2): + for i in range(0, len(values), 2): try: - address = int(values[i],0) + address = int(values[i], 0) except ValueError: - raise argparse.ArgumentError(self,'Address "%s" must be a number' % values[i]) + raise argparse.ArgumentError(self, 'Address "%s" must be a number' % values[i]) try: argfile = open(values[i + 1], 'rb') except IOError as e: raise argparse.ArgumentError(self, e) except IndexError: - raise argparse.ArgumentError(self,'Must be pairs of an address and the binary filename to write there') + raise argparse.ArgumentError(self, 'Must be pairs of an address and the binary filename to write there') pairs.append((address, argfile)) # Sort the addresses and check for overlapping end = 0 - for address, argfile in sorted(pairs): - argfile.seek(0,2) # seek to end + for address, argfile in sorted(pairs, key=lambda x: x[0]): + argfile.seek(0, 2) # seek to end size = argfile.tell() argfile.seek(0) sector_start = address & ~(ESPLoader.FLASH_SECTOR_SIZE - 1) @@ -3087,105 +4761,373 @@ def __call__(self, parser, namespace, values, option_string=None): # Binary stub code (see flasher_stub dir for source & details) ESP8266ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" -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\ +5dwuzr3/OyU1/H+n+L8kk1ilxnz4f3giyVw=\ """))) ESP32ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" -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\ +"""))) +ESP32S2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ ++rqe/Hebvj3Y4S8jQkWaGpP//j8geRE3\ +"""))) +ESP32S3BETA2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32S3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32C3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +xp4M93cxNksKk1z9D2WAtrU=\ +"""))) +ESP32C6BETAROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32H2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +eNrFWmtbG8cV/isYMCRunnZG2ts4MUi2QFyM66SOKa7cend2lzoXnoBFjNvov3fec9GsAMn+1g8CaXZ35sw573nPZfa/29PmZrr9eK3antwYO7mx4VPl4Ts+5t3R5MYX4Vt/clO6yU1Bo1thsHwR/mQ/hD9JGMrC\ +/2Y9/PHydEJPT27a+iynOXbDH/M8zN+fhlFcbidXk5vGhF+9QTXaCAsUmyxD1Tuc3NS90dPD9fCsScuwcC98wr1FMQh/+pPtyQVWwHzXYYaU5qO7XD4Lo2GBJshsXfjShis+CF+1+WSb5PrjONxXh/srfrZt83zJ\ +BV16yKqhnYZPXee0U54Pwmeis95ceeETVOfCx/fx/9lMZClOsccBhN+L65jwv3BDVsH9i7rdmSzNKww6MuhK8bf1z2AAnj2sd3dqWCzJR1BPsI/r87zBosFGzh/ryrB3eKgMD/v2bW5ZnbbPdubbbXEQFqnHYW47\ +Ciiow5ONqiEZ4Rqsx6tUCZvImBOsm+5C2FFXcZbnd1ifxsKUedhGk/ENBLKMMYHfzjGWdJwnUV28ZBNgSfyv8q25yp6SUYZBbmd5CpOOcSu2UwJOkI1hjkWKRuYpSKfrYWeuN1pQrahGt+Khy2H4awOompRHC8vq\ +KbCYw/z2ATwASDKMBdzWutMd3BSnDtJ59kPVbuH2MRLUW8tIW5+y4aEN6NvmMqWNslUG/uetTNZnqEG+tsGkY3WaDjDCI9hHJh5nBHM23qI7bm1cHfaoaTUSjJYKu6mxlL87BQT7lReu8j3CpvgB7ijSPcCNgfn9\ +rj+UnWW4e9xBMnblVUX9joMKndW4Gz86DPIUtwz5fiuSk+4MrmS40tuAwWXqIgNANniRUrffndzZ8ygOLZ/ERaDLoOtWaQNzElOqjVT+ErMoGs2tzSjkaefLdUC/hUhMgqXn7iT0xVsSBNb6A5vlpR7wlYBJNqIR\ +HMCIt+lFpqy7UzYicC3I5HBDUeNeDqIwg69jjTAUSV7wHrr0A8CBLQSUJQAcLlWOMBs09vVIriCUuC12Zds/Y4xTHPEH/scwjdGfI/8v7BWmQRx5Qsx7MNgSLTbslk60CzoAzZJPGyaSxt8KT9iwT3WTA/KUCT07\ +GbC+wnQTknkCxuzzNlw+GDEBUaSU0Chhshsdr9KG/UYDeScuX38rYdk3u79o6BTfkRi5bC+0n0q4yqZMElVvzBsh9OZrO2yVxn7bDUBjhefg/iXBAUUrDAvf7+0tk2MtfAEfhQDHFKmZgxBkRxFDTWPUF4jSmTI2\ +mMIVix0vmbJ4gKzvshtx5/mMmQAJDkKIlysASFE/ZuwBXfhfWSb4AjdXG3yB2XGIcOKY8FgZL5kw743PMfQvRhQAy0gw6UWtMqUvMyPG6n6829PdcV0r3MLPnXfIrHc7ahCFFzFarFzSxiU5CkC7VR4TEEkveKuP\ +xHXFu6tUgj+kc4kE/2Vx1yCfMnIX42wPwY0I5/oS9GFnH3DpPVZOt8SYYQt1wxutJXepS5EonwEYJecHQk9TVkah+YEIC8HbcocFZapZZ3AqIy2D9vbtqU36U5yfiKAcD5nROEc578Rc+kBMJ3pmQZYI6ETAxcc7\ +q8Gg6foqfnu3KZe93cMXuBi+mdRHXVXl4BGsVI4mF4Go2uysfQPdvjmcOyrI1PkPuElt9v6ICRY2KNIXq1m2nOPrFss29cI065+fBtEQ0cY5Jd7N4XtmL0YvpMt+ZH6d05hkO/e6DiHLXl7OPrwGDbwBF/0TgKqE\ +YCg+roMEt+CZR9gLgluLqqqGPP2fmCkaAWgpNlUVL8jR+zPXLRVUW2VMP6AH3/vM1sFh6bnH6EeetEhrxVOh5dxqwK/GC+hi9p/lExIW/WPmUwKgv3tzWPWKHyHW1XTIHu5I0ZbezrXyWz5EmYSVfLAeYSKkDklI\ +N03lL9m/iESglzLNNdADqOkQnpXmLxhZ5Zcgy0vMwywOJbNz/vf3DJZKKdWhOGh7Ivf8yp+GbPSCZOx/lNqYRhZkeP35VMQvOskJJ2FfqjBEMC9Bu0BoO0TQC5OUuMfplfbzclS9mSRVUEqa8I+m5Ccbt+P/GhGN\ +KNP0nkgVvlxWGiQk/MDp+AUn4yRrCkbVamSeuqKC09TV2jEPQqNIMag78f8l2MFk+mMaOfb628snt719taKpBl5k45ePXgGEryYXZxD+8CcQTnl0dIyLx4+e4+LzycUJqPrtSafnUuWnw0PQxGW0ALI+bDbQ/o44\ +SSYcJUG0lCAKUq0LCbKZBNmk872kvICexT1Nj7MslCZ4FuDADsGBPluda5Res75iF1S2UAhp+qSpDMaqUlbCLDZ5CIPYvX9zbliwwlPGzrw4WyysHpzvEOjWJBPWgiwtBtpIw7euEMicjN3XelaNOu+T2XSPppOY\ +uDQdbn3s000m47sAqWhK1QFtMafMccLSrwJQW64gc1OJnrmJgkqJ9iLmFi1fzHt00liAfPA6khvAtDSY8hdui+ytFqukVsl5fiwbQ5fD5ov5MVLjIm2HrZoi9pQoSwmiThGR8beof86fq2GXbbeUOOAqjVQM0Xlr\ +IZd9F7w/xG4aT+J4KJ64Jr6QihgGAi/hP272ufbEJlPcih01ch08273fkA+dUCeokmIm+55+Ov35NX66ptMnokWSmNf7fFPcRkr82kk1ZunZND7rPDI5/2yHfdeijijt3gGU/UTpXOcuunM7265j9rc89dwPwE+o\ ++lqV8AvrmAF3JBaLmFWVazdPImZsVodrBy5KLTcsq1wbcqoIS6GACxPv1FBsmGsG3KI1U+rOIZvLvwEY2owRarltB1bLM1/xhYCUKxoWL7GF/0WuJJMpXYGY+VeU98S4Q22LVmM8d9fDBnZ0xPFIme6cOYmARp68\ +A78nEVYj/ytoLVNIgPGb/BWEH4uUTf56ciUB1EmTp1UQpPchwgrrcfgQ9KWSkdSSq6QPv5M+A1Ey7RytI0zi0n1ZL+VyHVzlEcVNuueZTFomOgZuXRxsbCqhbqxzTDHmmtcuU9VrnW8hHLSicYTYmq6hJoTOS6sr\ +t7Jy1u3m9LygIefGnumpu4yUepZhGhqiO5o6eQAFAGjqYXSgYXdmYmcY3tiRxHWYtdMB3kRmq1L2REqYjQwI0lXpao+QXzyHjAcw3gFYbQRFj1YXDIVHle5A3NyJkaLdSqisbdQoHM5zCS4wDrCgy/P2oGeDqM6r\ +Olqbm81+rnaZktSQjrr9U+qyQ1v23P+MMdT2Bn0zjIYtSTOtEK4x6Xec1k61A5pI+WTYnbEzz4qtOqLMdSvyNX6OvVHE3kVU5XWCcqLQVvgatdDnuwNkKNwq4Prz84srofO606oyTB5Xi/aF9kqyLwKzpiHBGcQu\ +ZdqVQGZGCoa2Kw8O5nMmMidiVn/McVIYTJOX4FTDWPSWyaFwOdNDJ8tKYolc2HHcAtmvkShKTzQaQ151GWOkkfuwG0P4Wi347N25MpXy4VZUa4R3oOomiRJaldBJuykdARRN+5ukEtTZUO+1hnvGi4nJlJ4AHVH2\ +yM25nrgpHPZqPvevsdnaGmkiLsz1qZ90OpziUqToWkjTCLSr/jm7vSrcUU5mH3I73NdoQrb1EM0uV8+40m/rmRYx9YCHmkI2WFGdJN3LZRIagyYB/Lm+YLw0ImTV//iQOZ1qq3z4Pq7kL6RUNLNPcfQTyPkz6xVm\ +KpK7I16v1Qbi8scGHyTMyVqtIWtvDqXFkJPHyDmGZadpkltRlbGLtlslTu5rZlz1+Hhw1mo08AfdGNJEApoVbKwql1qznedi+zxArYlUugKWkwPaa93xRcaBbzuppxxhgGOb/oZ4UxWPusRDn+ixCx0b7LEROb6O\ +ZWReGwurUCiYJ+X3qPk3fi+Aj/o+Cvl0XMT23447YT+JCCYx2votFr3mWer6YLRspXcoWqwf/R2YTQYz+LQchOpZksMpnZNcppWnQeVFq9lnIYSRTqZDtnGtZ5zQQSk/iv7DI+rWS99Z6hc6v2kXQPJOqoFaLIY3\ +E6gOoqZ1oyk0pQkfNYeUI3nT/0rvlYIwkKTilcpHBYo2wRsXCZaQq6erGbKamvObTTlvMLKf6hbcrHmheSxHVNbIvpyvtOIZEQ9G8EBQ3YwiWxcbXNT8IHcJ619InkX/kSXMzwJqaW5FBa7J8Ucqzbeu/xV6fH7f\ +RWc6T9rDbvFjvdc4mOxpbipFflWsqoQWQGrvVkZUvtQ8xu9sHN72Q86Lr+JrCb7vR2sgCX/KL0potfobqYut1bhoVfT7iAI6PSTe7RlLxI9LJqH1n9fxGMrlHQPMQ7tM7qbjiKvJW05xl4ek0/gGEG8SjpH4D/S+\ +zroovk81xpQzqEg7hkjbCpezqqaUMLyOx62qQo4Q/iWh9UJ4VGobX14zgmjuVOcusthIUbMtoZC/Ud2/eAAr+m9dV5BDIbFSjveh9MU7VBHcAN0eUm3/9arVEdO/l3cqQi3XdNo16WVHv2ojM1oVgp8tvKWFKHp8\ +++UXqaRIPafS+LaE52sGcyUvOim4K67Nt4VXKCHoyxltHluO90PkWdxPYXZWKWGXGbDy9xceanIQLZoDSF6BYB0vW1ZSYfUdkd/1lFdBlPHLEiu6dvIuT7sjJ0T6JOX/KO/DhBfcl+RTh0Dweh6SxBVMeiCvHrSc\ +hV0dPEWX8jGLyCL/BYZYH/cO5GUWhlOR/iEVqnC4NpSsfvrCwYjky7Tpofeix/65LfkFVWgaPihPG3Csp/0igap6pwgTdC5KxZueuqRyaG06b2L4GLWI+uQlBnonhWim5AjVZHIAoJbC/7onDRSnrik5B0UWogtq\ +v+pBQh6Pfkw+DymLPkohuye1vRKdPmmWJyv4ZHc9bzlSlwJ+LfvEAKA0vCGEUEwqPiOAMckX+clAW/yJ9KGoPn7ZvgJeX+2HOFBlZ1tvENbeAD7/wOXjFrV89nz/BJdPJtNOD58owWx/s0bve/7rw7S8wluf1uR5\ +Ym2RmHCluZhefZoP9vu9IgzW5bTU10MBquBM2zLcncXYLHEmmf0PTRUSUg==\ +"""))) +ESP8684ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ """))) @@ -3198,4 +5140,4 @@ def _main(): if __name__ == '__main__': - _main() \ No newline at end of file + _main() diff --git a/images/icon-512.icns b/images/icon-512.icns new file mode 100644 index 0000000..d1213d8 Binary files /dev/null and b/images/icon-512.icns differ diff --git a/requirements.txt b/requirements.txt index e5df225..f92a451 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -esptool==2.6 +esptool==3.2 wxPython # Previously PyInstaller 3.4 was used - this has security vulnerabilities per GitHub -PyInstaller>=3.6 +PyInstaller>=4.7 requests diff --git a/update_pyinstaller.sh b/update_pyinstaller.sh new file mode 100755 index 0000000..8747971 --- /dev/null +++ b/update_pyinstaller.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# This script rebuilds the bootloaders so as to not have differing signatures from the system that is doing the main +# application build. This is required for notarization to work on MacOS (and may prevent security warnings on Windows) + +# Note - this command (at least the "pip install" bit) needs to be done from within the virtualenv +git clone https://github.com/pyinstaller/pyinstaller.git +cd pyinstaller +git checkout v4.7 +cd bootloader +python ./waf all +pip install .. +