import io import struct import esptool from esphomeflasher.const import HTTP_REGEX from esphomeflasher.helpers import prevent_print class EsphomeflasherError(Exception): pass class MockEsptoolArgs(object): def __init__(self, flash_size, addr_filename): self.compress = True self.no_compress = False self.flash_size = flash_size self.addr_filename = addr_filename self.flash_mode = 'keep' self.flash_freq = 'keep' self.no_stub = False self.verify = False class ChipInfo(object): def __init__(self, family, model, mac): self.family = family self.model = model self.mac = mac self.is_esp32 = None def as_dict(self): return { 'family': self.family, 'model': self.model, 'mac': self.mac, 'is_esp32': self.is_esp32, } class ESP32ChipInfo(ChipInfo): def __init__(self, model, mac, num_cores, cpu_frequency, has_bluetooth, has_embedded_flash, has_factory_calibrated_adc): super(ESP32ChipInfo, self).__init__("ESP32", model, mac) self.num_cores = num_cores self.cpu_frequency = cpu_frequency self.has_bluetooth = has_bluetooth self.has_embedded_flash = has_embedded_flash self.has_factory_calibrated_adc = has_factory_calibrated_adc def as_dict(self): data = ChipInfo.as_dict(self) data.update({ 'num_cores': self.num_cores, 'cpu_frequency': self.cpu_frequency, 'has_bluetooth': self.has_bluetooth, 'has_embedded_flash': self.has_embedded_flash, 'has_factory_calibrated_adc': self.has_factory_calibrated_adc, }) return data class ESP8266ChipInfo(ChipInfo): def __init__(self, model, mac, chip_id): super(ESP8266ChipInfo, self).__init__("ESP8266", model, mac) self.chip_id = chip_id def as_dict(self): data = ChipInfo.as_dict(self) data.update({ 'chip_id': self.chip_id, }) return data def read_chip_property(func, *args, **kwargs): try: return prevent_print(func, *args, **kwargs) except esptool.FatalError as err: raise EsphomeflasherError("Reading chip details failed: {}".format(err)) def read_chip_info(chip): mac = ':'.join('{:02X}'.format(x) for x in read_chip_property(chip.read_mac)) if isinstance(chip, esptool.ESP32ROM): model = read_chip_property(chip.get_chip_description) features = read_chip_property(chip.get_chip_features) num_cores = 2 if 'Dual Core' in features else 1 frequency = next((x for x in ('160MHz', '240MHz') if x in features), '80MHz') has_bluetooth = 'BT' in features has_embedded_flash = 'Embedded Flash' in features has_factory_calibrated_adc = 'VRef calibration in efuse' in features return ESP32ChipInfo(model, mac, num_cores, frequency, has_bluetooth, has_embedded_flash, has_factory_calibrated_adc) elif isinstance(chip, esptool.ESP8266ROM): model = read_chip_property(chip.get_chip_description) chip_id = read_chip_property(chip.chip_id) return ESP8266ChipInfo(model, mac, chip_id) raise EsphomeflasherError("Unknown chip type {}".format(type(chip))) def chip_run_stub(chip): try: return chip.run_stub() except esptool.FatalError as err: raise EsphomeflasherError("Error putting ESP in stub flash mode: {}".format(err)) def detect_flash_size(stub_chip): flash_id = read_chip_property(stub_chip.flash_id) return esptool.DETECTED_FLASH_SIZES.get(flash_id >> 16, '4MB') def read_firmware_info(firmware): header = firmware.read(4) firmware.seek(0) magic, _, flash_mode_raw, flash_size_freq = struct.unpack("BBBB", header) if magic != esptool.ESPLoader.ESP_IMAGE_MAGIC: raise EsphomeflasherError( "The firmware binary is invalid (magic byte={:02X}, should be {:02X})" "".format(magic, esptool.ESPLoader.ESP_IMAGE_MAGIC)) flash_freq_raw = flash_size_freq & 0x0F flash_mode = {0: 'qio', 1: 'qout', 2: 'dio', 3: 'dout'}.get(flash_mode_raw) flash_freq = {0: '40m', 1: '26m', 2: '20m', 0xF: '80m'}.get(flash_freq_raw) return flash_mode, flash_freq def open_downloadable_binary(path): if hasattr(path, 'seek'): path.seek(0) return path if HTTP_REGEX.match(path) is not None: import requests try: response = requests.get(path) response.raise_for_status() except requests.exceptions.Timeout as err: raise EsphomeflasherError( "Timeout while retrieving firmware file '{}': {}".format(path, err)) except requests.exceptions.RequestException as err: raise EsphomeflasherError( "Error while retrieving firmware file '{}': {}".format(path, err)) binary = io.BytesIO() binary.write(response.content) binary.seek(0) return binary try: return open(path, 'rb') except IOError as err: raise EsphomeflasherError("Error opening binary '{}': {}".format(path, err)) def format_bootloader_path(path, flash_mode, flash_freq): return path.replace('$FLASH_MODE$', flash_mode).replace('$FLASH_FREQ$', flash_freq) def configure_write_flash_args(info, firmware_path, flash_size, bootloader_path, partitions_path, otadata_path): addr_filename = [] firmware = open_downloadable_binary(firmware_path) flash_mode, flash_freq = read_firmware_info(firmware) if isinstance(info, ESP32ChipInfo): if flash_freq in ('26m', '20m'): raise EsphomeflasherError( "No bootloader available for flash frequency {}".format(flash_freq)) bootloader = open_downloadable_binary( format_bootloader_path(bootloader_path, flash_mode, flash_freq)) partitions = open_downloadable_binary(partitions_path) otadata = open_downloadable_binary(otadata_path) addr_filename.append((0x1000, bootloader)) addr_filename.append((0x8000, partitions)) addr_filename.append((0xE000, otadata)) addr_filename.append((0x10000, firmware)) else: addr_filename.append((0x0, firmware)) return MockEsptoolArgs(flash_size, addr_filename) def detect_chip(port, force_esp8266=False, force_esp32=False): if force_esp8266 or force_esp32: klass = esptool.ESP32ROM if force_esp32 else esptool.ESP8266ROM chip = klass(port) else: try: chip = esptool.ESPLoader.detect_chip(port) except esptool.FatalError as err: raise EsphomeflasherError("ESP Chip Auto-Detection failed: {}".format(err)) try: chip.connect() except esptool.FatalError as err: raise EsphomeflasherError("Error connecting to ESP: {}".format(err)) return chip