image loader improvements
All checks were successful
Rust/va416xx-rs/pipeline/pr-main This commit looks good

This commit is contained in:
Robin Müller 2024-09-23 11:10:58 +02:00
parent 9c01c10d74
commit edd5d73b5b
Signed by: muellerr
GPG Key ID: A649FB78196E3849

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from typing import List from typing import List, Tuple
from spacepackets.ecss.defs import PusService from spacepackets.ecss.defs import PusService
from spacepackets.ecss.tm import PusTm from spacepackets.ecss.tm import PusTm
from tmtccmd.com import ComInterface from tmtccmd.com import ComInterface
@ -60,6 +60,7 @@ class ActionId(enum.IntEnum):
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SEQ_PROVIDER = SeqCountProvider(bit_width=14)
@dataclasses.dataclass @dataclasses.dataclass
@ -70,7 +71,174 @@ class LoadableSegment:
data: bytes data: bytes
SEQ_PROVIDER = SeqCountProvider(bit_width=14) class Target(enum.Enum):
BOOTLOADER = 0
APP_A = 1
APP_B = 1
class ImageLoader:
def __init__(self, com_if: ComInterface, verificator: PusVerificator) -> None:
self.com_if = com_if
self.verificator = verificator
def handle_ping_cmd(self):
_LOGGER.info("Sending ping command")
ping_tc = PusTc(
apid=0x00,
service=PusService.S17_TEST,
subservice=1,
seq_count=SEQ_PROVIDER.get_and_increment(),
app_data=bytes(PING_PAYLOAD_SIZE),
)
self.verificator.add_tc(ping_tc)
self.com_if.send(bytes(ping_tc.pack()))
data_available = self.com_if.data_available(0.4)
if not data_available:
_LOGGER.warning("no ping reply received")
for reply in self.com_if.receive():
result = self.verificator.add_tm(
Service1Tm.from_tm(PusTm.unpack(reply, 0), UnpackParams(0))
)
if result is not None and result.completed:
_LOGGER.info("received ping completion reply")
def handle_corruption_cmd(self, target: Target):
if target == Target.BOOTLOADER:
_LOGGER.error("can not corrupt bootloader")
if target == Target.APP_A:
self.send_tc(
PusTc(
apid=0,
service=ACTION_SERVICE,
subservice=ActionId.CORRUPT_APP_A,
),
)
if target == Target.APP_B:
self.send_tc(
PusTc(
apid=0,
service=ACTION_SERVICE,
subservice=ActionId.CORRUPT_APP_B,
),
)
def handle_flash_cmd(self, target: Target, file_path: Path) -> int:
loadable_segments = []
_LOGGER.info("Parsing ELF file for loadable sections")
total_size = 0
loadable_segments, total_size = create_loadable_segments(target, file_path)
segments_info_str(target, loadable_segments, total_size, file_path)
result = self._perform_flashing_algorithm(loadable_segments)
if result != 0:
return result
self._crc_and_app_size_postprocessing(target, total_size, loadable_segments)
return 0
def _perform_flashing_algorithm(
self,
loadable_segments: List[LoadableSegment],
) -> int:
# Perform the flashing algorithm.
for segment in loadable_segments:
segment_end = segment.offset + segment.size
current_addr = segment.offset
pos_in_segment = 0
while pos_in_segment < segment.size:
next_chunk_size = min(segment_end - current_addr, CHUNK_SIZE)
data = segment.data[pos_in_segment : pos_in_segment + next_chunk_size]
next_packet = pack_memory_write_command(current_addr, data)
_LOGGER.info(
f"Sending memory write command for address {current_addr:#08x} and data with "
f"length {len(data)}"
)
self.verificator.add_tc(next_packet)
self.com_if.send(bytes(next_packet.pack()))
current_addr += next_chunk_size
pos_in_segment += next_chunk_size
start_time = time.time()
while True:
if time.time() - start_time > 1.0:
_LOGGER.error("Timeout while waiting for reply")
return -1
data_available = self.com_if.data_available(0.1)
done = False
if not data_available:
continue
replies = self.com_if.receive()
for reply in replies:
tm = PusTm.unpack(reply, 0)
if tm.service != 1:
continue
service_1_tm = Service1Tm.from_tm(tm, UnpackParams(0))
check_result = self.verificator.add_tm(service_1_tm)
# We could send after we have received the step reply, but that can
# somehow lead to overrun errors. I think it's okay to do it like
# this as long as the flash loader only uses polling..
if (
check_result is not None
and check_result.status.completed == StatusField.SUCCESS
):
done = True
# This is an optimized variant, but I think the small delay is not an issue.
"""
if (
check_result is not None
and check_result.status.step == StatusField.SUCCESS
and len(check_result.status.step_list) == 1
):
done = True
"""
self.verificator.remove_completed_entries()
if done:
break
return 0
def _crc_and_app_size_postprocessing(
self,
target: Target,
total_size: int,
loadable_segments: List[LoadableSegment],
):
if target == Target.BOOTLOADER:
_LOGGER.info("Blanking the bootloader checksum")
# Blank the checksum. For the bootloader, the bootloader will calculate the
# checksum itself on the initial run.
checksum_write_packet = pack_memory_write_command(
BOOTLOADER_CRC_ADDR, bytes([0x00, 0x00, 0x00, 0x00])
)
self.send_tc(checksum_write_packet)
else:
crc_addr = None
size_addr = None
if target == Target.APP_A:
crc_addr = APP_A_CRC_ADDR
size_addr = APP_A_SIZE_ADDR
elif target == Target.APP_B:
crc_addr = APP_B_CRC_ADDR
size_addr = APP_B_SIZE_ADDR
assert crc_addr is not None
assert size_addr is not None
_LOGGER.info(f"Writing app size {total_size} at address {size_addr:#08x}")
size_write_packet = pack_memory_write_command(
size_addr, struct.pack("!I", total_size)
)
self.com_if.send(bytes(size_write_packet.pack()))
time.sleep(0.2)
crc_calc = PredefinedCrc("crc-32")
for segment in loadable_segments:
crc_calc.update(segment.data)
checksum = crc_calc.digest()
_LOGGER.info(
f"Writing checksum 0x[{checksum.hex(sep=',')}] at address {crc_addr:#08x}"
)
self.send_tc(pack_memory_write_command(crc_addr, checksum))
def send_tc(self, tc: PusTc):
self.com_if.send(bytes(tc.pack()))
def main() -> int: def main() -> int:
@ -110,262 +278,133 @@ def main() -> int:
verificator = PusVerificator() verificator = PusVerificator()
com_if = SerialCobsComIF(serial_cfg) com_if = SerialCobsComIF(serial_cfg)
com_if.open() com_if.open()
target = None
if args.target == "bl":
target = Target.BOOTLOADER
elif args.target == "a":
target = Target.APP_A
elif args.target == "b":
target = Target.APP_B
image_loader = ImageLoader(com_if, verificator)
file_path = None file_path = None
result = -1
if args.ping: if args.ping:
_LOGGER.info("Sending ping command") image_loader.handle_ping_cmd()
ping_tc = PusTc( result = 0
apid=0x00, if target:
service=PusService.S17_TEST,
subservice=1,
seq_count=SEQ_PROVIDER.get_and_increment(),
app_data=bytes(PING_PAYLOAD_SIZE),
)
verificator.add_tc(ping_tc)
com_if.send(ping_tc.pack())
data_available = com_if.data_available(0.4)
if not data_available:
_LOGGER.warning("no ping reply received")
for reply in com_if.receive():
result = verificator.add_tm(
Service1Tm.from_tm(PusTm.unpack(reply, 0), UnpackParams(0))
)
if result is not None and result.completed:
_LOGGER.info("received ping completion reply")
if not args.target:
return 0
if args.target:
if not args.corrupt: if not args.corrupt:
if not args.path: if not args.path:
_LOGGER.error("App Path needs to be specified for the flash process") _LOGGER.error("App Path needs to be specified for the flash process")
return -1
file_path = Path(args.path) file_path = Path(args.path)
if not file_path.exists(): if not file_path.exists():
_LOGGER.error("File does not exist") _LOGGER.error("File does not exist")
return -1
if args.corrupt: if args.corrupt:
if not args.target: if target:
image_loader.handle_corruption_cmd(target)
result = 0
else:
_LOGGER.error("target for corruption command required") _LOGGER.error("target for corruption command required")
return -1
if args.target == "bl":
_LOGGER.error("can not corrupt bootloader")
if args.target == "a":
packet = PusTc(
apid=0,
service=ACTION_SERVICE,
subservice=ActionId.CORRUPT_APP_A,
)
com_if.send(packet.pack())
if args.target == "b":
packet = PusTc(
apid=0,
service=ACTION_SERVICE,
subservice=ActionId.CORRUPT_APP_B,
)
com_if.send(packet.pack())
else: else:
assert file_path is not None assert file_path is not None
loadable_segments = [] assert target is not None
_LOGGER.info("Parsing ELF file for loadable sections") result = image_loader.handle_flash_cmd(target, file_path)
total_size = 0
with open(file_path, "rb") as app_file:
elf_file = ELFFile(app_file)
for (idx, segment) in enumerate(elf_file.iter_segments("PT_LOAD")):
if segment.header.p_filesz == 0:
continue
# Basic validity checks of the base addresses.
if idx == 0:
if (
args.target == "bl"
and segment.header.p_paddr != BOOTLOADER_START_ADDR
):
raise ValueError(
f"detected possibly invalid start address {segment.header.p_paddr:#08x} for "
f"bootloader, expected {BOOTLOADER_START_ADDR}"
)
if (
args.target == "a"
and segment.header.p_paddr != APP_A_START_ADDR
):
raise ValueError(
f"detected possibly invalid start address {segment.header.p_paddr:#08x} for "
f"App A, expected {APP_A_START_ADDR}"
)
if (
args.target == "b"
and segment.header.p_paddr != APP_B_START_ADDR
):
raise ValueError(
f"detected possibly invalid start address {segment.header.p_paddr:#08x} for "
f"App B, expected {APP_B_START_ADDR}"
)
name = None
for section in elf_file.iter_sections():
if (
section.header.sh_offset == segment.header.p_offset
and section.header.sh_size > 0
):
name = section.name
if name is None:
_LOGGER.warning("no fitting section found for segment")
continue
# print(f"Segment Addr: {segment.header.p_paddr}")
# print(f"Segment Offset: {segment.header.p_offset}")
# print(f"Segment Filesize: {segment.header.p_filesz}")
loadable_segments.append(
LoadableSegment(
name=name,
offset=segment.header.p_paddr,
size=segment.header.p_filesz,
data=segment.data(),
)
)
total_size += segment.header.p_filesz
context_str = None
# Set context string and perform basic sanity checks.
if args.target == "bl":
if total_size > BOOTLOADER_MAX_SIZE:
_LOGGER.error(
f"provided bootloader app larger than allowed {total_size} bytes"
)
return -1
context_str = "Bootloader"
elif args.target == "a":
if total_size > APP_A_MAX_SIZE:
_LOGGER.error(
f"provided App A larger than allowed {total_size} bytes"
)
return -1
context_str = "App Slot A"
elif args.target == "b":
if total_size > APP_B_MAX_SIZE:
_LOGGER.error(
f"provided App B larger than allowed {total_size} bytes"
)
return -1
context_str = "App Slot B"
_LOGGER.info(
f"Flashing {context_str} with image {file_path} (size {total_size})"
)
for idx, segment in enumerate(loadable_segments):
_LOGGER.info(
f"Loadable section {idx} {segment.name} with offset {segment.offset:#08x} and "
f"size {segment.size}"
)
result = perform_flashing_algorithm(com_if, loadable_segments, verificator)
if result != 0:
return result
crc_and_app_size_postprocessing(
com_if, args.target, total_size, loadable_segments
)
com_if.close() com_if.close()
return 0 return result
def perform_flashing_algorithm( def create_loadable_segments(
com_if: ComInterface, target: Target, file_path: Path
loadable_segments: List[LoadableSegment], ) -> Tuple[List[LoadableSegment], int]:
verificator: PusVerificator, loadable_segments = []
) -> int: total_size = 0
# Perform the flashing algorithm. with open(file_path, "rb") as app_file:
for segment in loadable_segments: elf_file = ELFFile(app_file)
segment_end = segment.offset + segment.size
current_addr = segment.offset for idx, segment in enumerate(elf_file.iter_segments("PT_LOAD")):
pos_in_segment = 0 if segment.header.p_filesz == 0:
while pos_in_segment < segment.size: continue
next_chunk_size = min(segment_end - current_addr, CHUNK_SIZE) # Basic validity checks of the base addresses.
data = segment.data[pos_in_segment : pos_in_segment + next_chunk_size] if idx == 0:
next_packet = pack_memory_write_command(current_addr, data) if (
_LOGGER.info( target == Target.BOOTLOADER
f"Sending memory write command for address {current_addr:#08x} and data with " and segment.header.p_paddr != BOOTLOADER_START_ADDR
f"length {len(data)}" ):
raise ValueError(
f"detected possibly invalid start address {segment.header.p_paddr:#08x} for "
f"bootloader, expected {BOOTLOADER_START_ADDR}"
)
if (
target == Target.APP_A
and segment.header.p_paddr != APP_A_START_ADDR
):
raise ValueError(
f"detected possibly invalid start address {segment.header.p_paddr:#08x} for "
f"App A, expected {APP_A_START_ADDR}"
)
if (
target == Target.APP_B
and segment.header.p_paddr != APP_B_START_ADDR
):
raise ValueError(
f"detected possibly invalid start address {segment.header.p_paddr:#08x} for "
f"App B, expected {APP_B_START_ADDR}"
)
name = None
for section in elf_file.iter_sections():
if (
section.header.sh_offset == segment.header.p_offset
and section.header.sh_size > 0
):
name = section.name
if name is None:
_LOGGER.warning("no fitting section found for segment")
continue
# print(f"Segment Addr: {segment.header.p_paddr}")
# print(f"Segment Offset: {segment.header.p_offset}")
# print(f"Segment Filesize: {segment.header.p_filesz}")
loadable_segments.append(
LoadableSegment(
name=name,
offset=segment.header.p_paddr,
size=segment.header.p_filesz,
data=segment.data(),
)
) )
verificator.add_tc(next_packet) total_size += segment.header.p_filesz
com_if.send(next_packet.pack()) return loadable_segments, total_size
current_addr += next_chunk_size
pos_in_segment += next_chunk_size
start_time = time.time()
while True:
if time.time() - start_time > 1.0:
_LOGGER.error("Timeout while waiting for reply")
return -1
data_available = com_if.data_available(0.1)
done = False
if not data_available:
continue
replies = com_if.receive()
for reply in replies:
tm = PusTm.unpack(reply, 0)
if tm.service != 1:
continue
service_1_tm = Service1Tm.from_tm(tm, UnpackParams(0))
check_result = verificator.add_tm(service_1_tm)
# We could send after we have received the step reply, but that can
# somehow lead to overrun errors. I think it's okay to do it like
# this as long as the flash loader only uses polling..
if (
check_result is not None
and check_result.status.completed == StatusField.SUCCESS
):
done = True
# This is an optimized variant, but I think the small delay is not an issue.
"""
if (
check_result is not None
and check_result.status.step == StatusField.SUCCESS
and len(check_result.status.step_list) == 1
):
done = True
"""
verificator.remove_completed_entries()
if done:
break
return 0
def crc_and_app_size_postprocessing( def segments_info_str(
com_if: ComInterface, target: Target,
target: str,
total_size: int,
loadable_segments: List[LoadableSegment], loadable_segments: List[LoadableSegment],
total_size: int,
file_path: Path,
): ):
if target == "bl": # Set context string and perform basic sanity checks.
_LOGGER.info("Blanking the bootloader checksum") if target == Target.BOOTLOADER:
# Blank the checksum. For the bootloader, the bootloader will calculate the if total_size > BOOTLOADER_MAX_SIZE:
# checksum itself on the initial run. _LOGGER.error(
checksum_write_packet = pack_memory_write_command( f"provided bootloader app larger than allowed {total_size} bytes"
BOOTLOADER_CRC_ADDR, bytes([0x00, 0x00, 0x00, 0x00]) )
) return -1
com_if.send(checksum_write_packet.pack()) context_str = "Bootloader"
else: elif target == Target.APP_A:
crc_addr = None if total_size > APP_A_MAX_SIZE:
size_addr = None _LOGGER.error(f"provided App A larger than allowed {total_size} bytes")
if target == "a": return -1
crc_addr = APP_A_CRC_ADDR context_str = "App Slot A"
size_addr = APP_A_SIZE_ADDR elif target == Target.APP_B:
elif target == "b": if total_size > APP_B_MAX_SIZE:
crc_addr = APP_B_CRC_ADDR _LOGGER.error(f"provided App B larger than allowed {total_size} bytes")
size_addr = APP_B_SIZE_ADDR return -1
assert crc_addr is not None context_str = "App Slot B"
assert size_addr is not None _LOGGER.info(f"Flashing {context_str} with image {file_path} (size {total_size})")
_LOGGER.info(f"Writing app size {total_size} at address {size_addr:#08x}") for idx, segment in enumerate(loadable_segments):
size_write_packet = pack_memory_write_command(
size_addr, struct.pack("!I", total_size)
)
com_if.send(size_write_packet.pack())
time.sleep(0.2)
crc_calc = PredefinedCrc("crc-32")
for segment in loadable_segments:
crc_calc.update(segment.data)
checksum = crc_calc.digest()
_LOGGER.info( _LOGGER.info(
f"Writing checksum 0x[{checksum.hex(sep=',')}] at address {crc_addr:#08x}" f"Loadable section {idx} {segment.name} with offset {segment.offset:#08x} and "
f"size {segment.size}"
) )
checksum_write_packet = pack_memory_write_command(crc_addr, checksum)
com_if.send(checksum_write_packet.pack())
def pack_memory_write_command(addr: int, data: bytes) -> PusTc: def pack_memory_write_command(addr: int, data: bytes) -> PusTc:
@ -381,7 +420,7 @@ def pack_memory_write_command(addr: int, data: bytes) -> PusTc:
service=MEMORY_SERVICE, service=MEMORY_SERVICE,
subservice=RAW_MEMORY_WRITE_SUBSERVICE, subservice=RAW_MEMORY_WRITE_SUBSERVICE,
seq_count=SEQ_PROVIDER.get_and_increment(), seq_count=SEQ_PROVIDER.get_and_increment(),
app_data=app_data, app_data=bytes(app_data),
) )