#!/usr/bin/env python """ rpi-eeprom-config """ import argparse import atexit import os import subprocess import struct import sys import tempfile import time IMAGE_SIZE = 512 * 1024 MAX_BOOTCONF_SIZE = 2024 # Each section starts with a magic number followed by a 32 bit offset to the # next section (big-endian). # The number, order and size of the sections depends on the bootloader version # but the following mask can be used to test for section headers and skip # unknown data. MAGIC = 0x55aaf00f MAGIC_MASK = 0xfffff00f FILE_MAGIC = 0x55aaf11f # id for modifiable file, currently only bootconf.txt FILE_HDR_LEN = 20 FILENAME_LEN = 12 TEMP_DIR = None def exit_handler(): """ Delete any temporary files. """ if TEMP_DIR is not None and os.path.exists(TEMP_DIR): tmp_image = '%s/pieeprom.upd' % TEMP_DIR if os.path.exists(tmp_image): os.remove(tmp_image) os.rmdir(TEMP_DIR) def exit_error(msg): """ Trapped a fatal arror, output message to stderr and exit with non-zero return code. """ sys.stderr.write("ERROR: %s\n" % msg) sys.exit(1) def shell_cmd(args): start = time.time() arg_str = ' '.join(args) result = subprocess.Popen(args, stdout=subprocess.PIPE, stdin=subprocess.PIPE) while time.time() - start < 5: if result.poll() is not None: break if result.poll is None: exit_error("%s timeout" % arg_str) if result.returncode != 0: exit_error("%s failed: %d\n %s\n %s\n" % (arg_str, result.returncode, result.stdout.read(), result.stderr.read())) else: return result.stdout.read() def get_latest_eeprom(): """ Returns the path of the latest EEPROM image file if it exists. """ latest = shell_cmd(['rpi-eeprom-update', '-l']).rstrip() if not os.path.exists(latest): exit_error("EEPROM image '%s' not found" % latest) return latest def apply_update(config): """ Applies the config file to the latest available EEPROM image and spawns rpi-eeprom-update to schedule the update at the next reboot. """ global TEMP_DIR latest = get_latest_eeprom() TEMP_DIR = tempfile.mkdtemp() tmp_update = "%s/%s" % (TEMP_DIR, 'pieeprom.upd') image = BootloaderImage(latest, tmp_update) image.write(config) sys.stdout.write("Updating bootloader EEPROM\n image: %s\nconfig: %s\n" % (latest, config)) args = ['sudo', 'rpi-eeprom-update', '-d', '-f', tmp_update] resp = shell_cmd(args) sys.stdout.write(resp) def read_current_config(): """ Reads the configuration used by the current bootloader. """ result = shell_cmd(['vcgencmd', 'bootloader_config']) if result is None: exit_error('Failed to read the current bootloader configuration') return result class BootloaderImage(object): def __init__(self, filename, output): """ Instantiates a Bootloader image writer with a source eeprom (filename) and optionally an output filename. """ self._filename = filename try: self._bytes = bytearray(open(filename, 'rb').read()) except IOError as err: exit_error("Failed to read \'%s\'\n%s\n" % (filename, str(err))) self._out = None if output is not None: self._out = open(output, 'wb') if len(self._bytes) != IMAGE_SIZE: exit_error("%s: Expected size %d bytes actual size %d bytes" % (filename, IMAGE_SIZE, len(self._bytes))) def find_config(self): offset = 0 magic = 0 while offset < IMAGE_SIZE: magic, length = struct.unpack_from('>LL', self._bytes, offset) if (magic & MAGIC_MASK) != MAGIC: raise Exception('EEPROM is corrupted') if magic == FILE_MAGIC: # Found a file name = self._bytes[offset + 8: offset + FILE_HDR_LEN] if name.decode('utf-8') == 'bootconf.txt': return (offset, length) offset += 8 + length # length + type offset = (offset + 7) & ~7 raise Exception('EEPROM parse error: Bootloader config not found') def write(self, new_config): hdr_offset, length = self.find_config() new_config_bytes = open(new_config, 'rb').read() new_len = len(new_config_bytes) + FILENAME_LEN + 4 if len(new_config_bytes) > MAX_BOOTCONF_SIZE: raise Exception("Config is too large (%d bytes). The maximum size is %d bytes." % (len(new_config_bytes), MAX_BOOTCONF_SIZE)) if hdr_offset + len(new_config_bytes) + FILE_HDR_LEN > IMAGE_SIZE: raise Exception('EEPROM image size exceeded') struct.pack_into('>L', self._bytes, hdr_offset + 4, new_len) struct.pack_into(("%ds" % len(new_config_bytes)), self._bytes, hdr_offset + 4 + FILE_HDR_LEN, new_config_bytes) # If the new config is smaller than the old config then set any old # data which is now unused to all ones (erase value) pad_start = hdr_offset + 4 + FILE_HDR_LEN + len(new_config_bytes) pad = 0 while pad < (length - len(new_config_bytes)): struct.pack_into('B', self._bytes, pad_start + pad, 0xff) pad = pad + 1 if self._out is not None: self._out.write(self._bytes) self._out.close() else: if hasattr(sys.stdout, 'buffer'): sys.stdout.buffer.write(self._bytes) else: sys.stdout.write(self._bytes) def read(self): hdr_offset, length = self.find_config() offset = hdr_offset + 4 + FILE_HDR_LEN config_bytes = self._bytes[offset:offset+length-FILENAME_LEN-4] if self._out is not None: self._out.write(config_bytes) self._out.close() else: if hasattr(sys.stdout, 'buffer'): sys.stdout.buffer.write(config_bytes) else: sys.stdout.write(config_bytes) def main(): """ Utility for reading and writing the configuration file in the Raspberry Pi 4 bootloader EEPROM image. """ description = """\ Bootloader EEPROM configuration tool for the Raspberry Pi 4. Operating modes: 1. Output the current bootloader to configuration STDOUT if no arguments are specified OR the given output file if --out is specified. rpi-eeprom-config (--out boot.conf) 2. Extracts the configuration file from the given \'eeprom\' file and outputs the result to STDOUT or the output file if --output is specified. rpi-eeprom-config pieeprom.bin (--out boot.conf) 3. Writes a new EEPROM image replacing the configuration file with the contents of the file specified by --config. rpi-eeprom-config --config boot.conf --out newimage.bin pieeprom.bin The new image file can be installed via rpi-eeprom-update rpi-eeprom-update -d -f newimage.bin 4. Applies a given config file to the latest available EEPROM image and invokes rpi-eeprom-update to update the bootloader when the system is rebooted. rpi-eeprom-config --apply boot.conf The latest available image is determined by querying \'rpi-eeprom-update -l\' and depends on the rpi-eeprom-update configuration. Bootloader EEPROM images are contained in the \'rpi-eeprom-images\' package, which installs them to the /lib/firmware/raspberrypi/bootloader directory.' """ parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=description) parser.add_argument('-a', '--apply', required=False, help='Updates the bootloader to the given config plus latest available EEPROM release.') parser.add_argument('-c', '--config', help='Name of bootloader configuration file', required=False) parser.add_argument('-o', '--out', help='Name of output file', required=False) parser.add_argument('eeprom', nargs='?', help='Name of EEPROM file to use as input') args = parser.parse_args() if args.apply is not None: if not os.path.exists(args.apply): exit_error("config file '%s' not found" % args.apply) apply_update(args.apply) elif args.eeprom is not None: image = BootloaderImage(args.eeprom, args.out) if args.config is not None: if not os.path.exists(args.config): exit_error("config file '%s' not found" % args.config) image.write(args.config) else: image.read() elif args.config is None and args.eeprom is None: sys.stdout.write(read_current_config()) if __name__ == '__main__': atexit.register(exit_handler) main()