Files
rpi-eeprom/rpi-eeprom-config
Tim Gover 1fe54409b8 rpi-eeprom-config: Add new option to apply config and update in one operation
* Add -a/--apply parameter which provides a one shot image for applying
  a new configuration to the latest bootloader image and installs it
  via rpi-eeprom-update.
* Print the live configuration if no arguments are specified.
* Add short flags instead of requiring verbose names.
* Make the errors more human readable instead of Exceptions
2020-09-28 10:41:20 +01:00

251 lines
8.6 KiB
Python
Executable File

#!/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()