Path: blob/master/Tools/scripts/firmware_version_decoder.py
9398 views
#!/usr/bin/env python312# flake8: noqa34import enum5import io6import sys7import struct8from argparse import ArgumentParser9from dataclasses import dataclass10from elftools.elf.elffile import ELFFile11from typing import Any121314class FirmwareVersionType(enum.Enum):15Dev = 016Alpha = 6417Beta = 12818RC = 19219Official = 25520EnumEnd = 2562122@staticmethod23def get_release(version: int) -> str:24"""25Return the closest release type for a given version type, going down.26This is required because it is common in ardupilot to increase the version type27for successive betas, such as here:28https://github.com/ArduPilot/ardupilot/blame/8890c44370a7cf27d5efc872ef6da288ae3bc41f/ArduCopter/version.h#L1229"""30for release in reversed(FirmwareVersionType):31if version >= release.value:32return release33return "Unknown"3435class VehicleType(enum.Enum):36Rover = 137ArduCopter = 238ArduPlane = 339AntennaTracker = 440UNKNOWN = 541Replay = 642ArduSub = 743iofirmware = 844AP_Periph = 945NONE = 256464748class BoardType(enum.Enum):49SITL = 350SMACCM = 451PX4 = 552LINUX = 753VRBRAIN = 854CHIBIOS = 1055F4LIGHT = 1156EMPTY = 99575859class BoardSubType(enum.Enum):60NONE = 655356162LINUX_NONE = 100063LINUX_ERLEBOARD = 100164LINUX_PXF = 100265LINUX_NAVIO = 100366LINUX_ZYNQ = 100467LINUX_BBBMINI = 100568LINUX_BEBOP = 100669LINUX_ERLEBRAIN2 = 100970LINUX_BH = 101071LINUX_PXFMINI = 101272LINUX_NAVIO2 = 101373LINUX_DISCO = 101474LINUX_AERO = 101575LINUX_DARK = 101676LINUX_BLUE = 101877LINUX_OCPOC_ZYNQ = 101978LINUX_EDGE = 102079LINUX_RST_ZYNQ = 102180LINUX_POCKET = 102281LINUX_NAVIGATOR = 102382LINUX_VNAV = 102483LINUX_OBAL = 102584LINUX_CANZERO = 102685LINUX_T3_GEM_O1 = 102986CHIBIOS_SKYVIPER_F412 = 500087CHIBIOS_FMUV3 = 500188CHIBIOS_FMUV4 = 500289CHIBIOS_GENERIC = 500990CHIBIOS_FMUV5 = 501391CHIBIOS_VRBRAIN_V51 = 501692CHIBIOS_VRBRAIN_V52 = 501793CHIBIOS_VRUBRAIN_V51 = 501894CHIBIOS_VRCORE_V10 = 501995CHIBIOS_VRBRAIN_V54 = 5020969798@dataclass99class FWVersion:100header: int = 0x61706677766572FB101header_version: bytes = bytes([0, 0])102pointer_size: int = 0103vehicle_type: VehicleType = VehicleType.NONE104board_type: BoardType = BoardType.EMPTY105board_subtype: BoardSubType = BoardSubType.NONE106major: int = 0107minor: int = 0108patch: int = 0109firmware_type: FirmwareVersionType = FirmwareVersionType.EnumEnd110os_software_version: int = 0111firmware_string: str = ""112firmware_hash_string: str = ""113firmware_hash: int = 0114middleware_name: str = ""115middleware_hash_string: str = ""116os_name: str = ""117os_hash_string: str = ""118119def __str__(self):120header = self.header.to_bytes(8, byteorder="big")121header_version = self.header_version.to_bytes(2, byteorder="big")122firmware_day = self.os_software_version % 100123firmware_month = self.os_software_version % 10000 - firmware_day124firmware_year = self.os_software_version - firmware_month - firmware_day125firmware_month = int(firmware_month / 100)126firmware_year = int(firmware_year / 10000)127return f"""128{self.__class__.__name__}:129header:130magic: {header[0:7].decode("utf-8")}131checksum: {hex(header[-1])}132version: {header_version[0]}.{header_version[1]}133pointer_size: {self.pointer_size}134firmware:135string: {self.firmware_string}136vehicle: {self.vehicle_type.name}137board: {self.board_type.name}138board subtype: {self.board_subtype.name}139hash: {self.firmware_hash_string}140hash integer: 0x{self.firmware_hash:02x}141version: {self.major}.{self.minor}.{self.patch}142type: {self.firmware_type.name}143os:144name: {self.os_name}145hash: {self.os_hash_string}146software_version: {firmware_day}/{firmware_month}/{firmware_year}147middleware:148name: {self.middleware_name}149hash: {self.middleware_hash_string}150"""151152153class Decoder:154def __init__(self) -> None:155self.bytesio = io.BytesIO()156self.fwversion = FWVersion()157self.byteorder = ""158self.pointer_size = 0159self.elffile = None160161def unpack(self, struct_format: str) -> Any:162struct_format = f"{self.byteorder}{struct_format}"163size = struct.calcsize(struct_format)164return struct.unpack(struct_format, self.bytesio.read(size))[0]165166def unpack_string_from_pointer(self) -> str:167pointer_format = "Q" if self.pointer_size == 8 else "I"168address = self.unpack(pointer_format)169170# nullptr, return empty string171if address == 0:172return ""173174# Calculate address offset for PIE (Position Independent Executables) binaries175address = next(self.elffile.address_offsets(address))176177current_address = self.bytesio.seek(0, io.SEEK_CUR)178self.bytesio.seek(address)179string = []180while True:181string += self.bytesio.read(1)182if string[-1] == 0:183string = string[0 : len(string) - 1]184break185self.bytesio.seek(current_address)186return bytes(string).decode("UTF-8")187188@staticmethod189def locate_header(data: bytes, byteorder: str) -> int:190return data.find(struct.pack(f"{byteorder}Q", FWVersion.header))191192def unpack_fwversion(self) -> None:193assert self.bytesio.read(8) == struct.pack(194f"{self.byteorder}Q", FWVersion.header195)196197self.fwversion.header_version = self.unpack("H")198major_version = self.fwversion.header_version >> 8199200self.pointer_size = self.unpack("B")201self.fwversion.pointer_size = self.pointer_size202self.unpack("B") # reserved203self.fwversion.vehicle_type = VehicleType(self.unpack("B"))204self.fwversion.board_type = BoardType(self.unpack("B"))205self.fwversion.board_subtype = BoardSubType(self.unpack("H"))206207self.fwversion.major = self.unpack("B")208self.fwversion.minor = self.unpack("B")209self.fwversion.patch = self.unpack("B")210self.fwversion.firmware_type = FirmwareVersionType.get_release(self.unpack("B"))211self.fwversion.os_software_version = self.unpack("I")212213self.fwversion.firmware_string = self.unpack_string_from_pointer()214self.fwversion.firmware_hash_string = self.unpack_string_from_pointer()215if major_version >= 2:216self.fwversion.firmware_hash = self.unpack("I")217218self.fwversion.middleware_name = self.unpack_string_from_pointer()219self.fwversion.middleware_hash_string = self.unpack_string_from_pointer()220self.fwversion.os_name = self.unpack_string_from_pointer()221self.fwversion.os_hash_string = self.unpack_string_from_pointer()222223def process(self, filename) -> FWVersion:224# We need the file open for ELFFile225file = open(filename, "rb")226data = file.read()227self.elffile = ELFFile(file)228229if not data:230raise RuntimeError("Failed to find FWVersion.")231232# Detect endianness233for order in [">", "<"]:234position = Decoder.locate_header(data, order)235if position != -1:236self.byteorder = order237self.bytesio = io.BytesIO(data)238self.bytesio.seek(position)239break240else:241raise RuntimeError("Failed to find FWVersion.")242243# Unpack struct and print it244self.unpack_fwversion()245return self.fwversion246247248if __name__ == "__main__":249assert (250sys.version_info.major >= 3 and sys.version_info.minor >= 7251), "Python version should be at least 3.7"252253# Parse arguments254parser = ArgumentParser(description=__doc__)255parser.add_argument(256"-f",257dest="file",258required=True,259help="File that contains a valid ardupilot firmware in ELF format.",260)261parser.add_argument(262"--expected-hash",263dest="expected_hash",264help="Expected git hash. The script fails if this doesn't match the git hash in the binary file. Used in CI",265)266args = parser.parse_args()267268decoder = Decoder()269try:270firmware_data = decoder.process(args.file)271except Exception as e:272print(f"Error decoding FWVersion: {type(e)}")273exit(-1)274275print(firmware_data)276if args.expected_hash and args.expected_hash != firmware_data.firmware_hash_string:277print(f"Git hashes don't match! expected: {args.expected_hash}, got {firmware_data.firmware_hash_string}")278exit(-1)279280281