#!/usr/bin/env python # # SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD # # SPDX-License-Identifier: Apache-2.0 import sys import struct import argparse import binascii import hashlib import os import subprocess import shutil import json import lzma # src_file = 'hello-world.bin' # compressed_file = 'hello-world.bin.xz' # packed_compressed_file = 'hello-world.bin.xz.packed' # siged_packed_compressed_file = 'hello-world.bin.xz.packed.signed' binary_compress_type = {'none': 0, 'xz':1} header_version = {'v1': 1, 'v2': 2, 'v3': 3} SCRIPT_VERSION = '1.0.0' ORIGIN_APP_IMAGE_HEADER_LEN = 288 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t). See esp_app_format.h # At present, we calculate the checksum of the first 4KB data of the old app. OLD_APP_CHECK_DATA_SIZE = 4096 # v1 compressed data header: # Note: Encryption_type field is deprecated, the field is reserved for compatibility. # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------| # | | Magic | header | Compress | delta | Encryption | Reserved | Firmware | The length of | The MD5 of | The CRC32 for| compressed | # | | number | version | type | type | type | | version | compressed data| compressed data| the header | data | # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------| # | Size | 4 bytes | 1 byte | 4 bits | 4 bits | 1 bytes | 1 bytes | 32 bytes | 4 bytes | 32 bytes | 4 bytes | | # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------| # | Data | String | | | | | | String | little-endian | byte string | little-endian| | # | type | ended | | | | | | ended | integer | | integer | | # | |with ‘\0’| | | | | | with ‘\0’| | | | Binary data| # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------| # | Data | “ESP” | 1 | 0/1 | 0/1 | | | | | | | | # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------| # v2 compressed data header # Note: Encryption_type field is deprecated, the field is reserved for compatibility. # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------| # | | Magic | header | Compress | delta | Encryption | Reserved | Firmware | The length of | The MD5 of | base app check| The CRC32 for | The CRC32 for| compressed | # | | number | version | type | type | type | | version | compressed data| compressed data| data len | base app data | the header | data | # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------| # | Size | 4 bytes | 1 byte | 4 bits | 4 bits | 1 bytes | 1 bytes | 32 bytes | 4 bytes | 32 bytes | 4 bytes | 4 bytes | 4 bytes | | # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------| # | Data | String | | | | | | String | little-endian | byte string | little-endian | little-endian | little-endian| | # | type | ended | | | | | | ended | integer | | integer | integer | integer | | # | |with ‘\0’| | | | | | with ‘\0’| | | | | | Binary data| # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------| # | Data | “ESP” | 1 | 0/1 | 0/1 | | | | | | | | | | # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------| # v3 compressed data header: # |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------| # | | Magic | header | Compress | Reserved | Reserved | The length of | The MD5 of | The CRC32 for| compressed | # | | number | version | type | | | compressed data| compressed data| the header | data | # |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------| # | Size | 4 bytes | 1 byte | 4 bits | 4 bits | 8 bytes | 4 bytes | 16 bytes | 4 bytes | | # |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------| # | Data | String | integer | | | | little-endian | byte string | little-endian| | # | type | ended | | | | | integer | | integer | | # | |with ‘\0’| | | | | | | | Binary data| # |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------| # | Data | “ESP” | 3 | 0/1 | | | | | | | # |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------| def xz_compress(store_directory, in_file): compressed_file = ''.join([in_file,'.xz']) if(os.path.exists(compressed_file)): os.remove(compressed_file) xz_compressor_filter = [ {"id": lzma.FILTER_LZMA2, "preset": 6, "dict_size": 64*1024}, ] with open(in_file, 'rb') as src_f: data = src_f.read() with lzma.open(compressed_file, "wb", format=lzma.FORMAT_XZ, check=lzma.CHECK_CRC32, filters=xz_compressor_filter) as f: f.write(data) f.close() if not os.path.exists(os.path.join(store_directory, os.path.split(compressed_file)[1])): shutil.copy(compressed_file, store_directory) print('copy xz file done') def secure_boot_sign(sign_key, in_file, out_file): ret = subprocess.call('espsecure.py sign_data --version 2 --keyfile {} --output {} {}'.format(sign_key, out_file, in_file), shell = True) if ret: raise Exception('sign failed') def get_app_name(): with open('flasher_args.json') as f: try: flasher_args = json.load(f) return flasher_args['app']['file'] except Exception as e: print(e) return '' def get_script_version(): return SCRIPT_VERSION def main(): parser = argparse.ArgumentParser() parser.add_argument('-hv', '--header_ver', nargs='?', choices = ['v1', 'v2', 'v3'], default='v3', help='the version of the packed file header [default:v3]') parser.add_argument('-c', '--compress_type', nargs= '?', choices = ['none', 'xz'], default='xz', help='compressed type [default:xz]') parser.add_argument('-i', '--in_file', nargs = '?', default='', help='the new app firmware') parser.add_argument('--sign_key', nargs = '?', default='', help='the sign key used for secure boot') parser.add_argument('-fv', '--fw_ver', nargs='?', default='', help='the version of the compressed data(this field is deprecated in v3)') parser.add_argument('--add_app_header', action="store_true", help='add app header to use native esp_ota_* & esp_https_ota_* APIs') parser.add_argument('-v', '--version', action='version', version=get_script_version(), help='the version of the script') args = parser.parse_args() compress_type = args.compress_type firmware_ver = args.fw_ver src_file = args.in_file sign_key = args.sign_key header_ver = args.header_ver add_app_header = args.add_app_header if src_file == '': origin_app_name = get_app_name() if(origin_app_name == ''): print('get origin app name fail') return if os.path.exists(origin_app_name): src_file = origin_app_name else: print('origin app.bin not found') return print('src file is: {}'.format(src_file)) # rebuild the cpmpressed_app directroy cpmoressed_app_directory = 'custom_ota_binaries' if os.path.exists(cpmoressed_app_directory): shutil.rmtree(cpmoressed_app_directory) os.mkdir(cpmoressed_app_directory) print('The compressed file will store in {}'.format(cpmoressed_app_directory)) #step1: compress if compress_type == 'xz': xz_compress(cpmoressed_app_directory, os.path.abspath(src_file)) origin_app_name = os.path.split(src_file)[1] compressed_file_name = ''.join([origin_app_name, '.xz']) compressed_file = os.path.join(cpmoressed_app_directory, compressed_file_name) else: compressed_file = ''.join(src_file) print('compressed file is: {}'.format(compressed_file)) #step2: packet the compressed image header with open(src_file, 'rb') as s_f: src_image_header = bytearray(s_f.read(ORIGIN_APP_IMAGE_HEADER_LEN)) src_image_header[1] = 0x00 packed_file = ''.join([compressed_file,'.packed']) with open(compressed_file, 'rb') as src_f: data = src_f.read() f_len = src_f.tell() # magic number bin_data = struct.pack('4s', b'ESP') # header version bin_data += struct.pack('B', header_version[header_ver]) # Compress type bin_data += struct.pack('B', binary_compress_type[compress_type]) print('compressed type: {}'.format(binary_compress_type[compress_type])) # in header v1/v2, there is a field "Encryption type", this field has been deprecated in v3 if (header_version[header_ver] < 3): bin_data += struct.pack('B', 0) # Reserved bin_data += struct.pack('?', 0) # Firmware version bin_data += struct.pack('32s', firmware_ver.encode()) else: # Reserved bin_data += struct.pack('10s', b'0') # The length of the compressed data bin_data += struct.pack('