Script that can find programmer and flash firmware via it. (#2193)
* Init * Fallback to networked interface * remove unneeded cmsis_dap_backend * serial number * windows :( * remove jlink, fix path handling * scripts: program: path normalization * scripts: program: path normalization: second encounter Co-authored-by: hedger <hedger@nanode.su> Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
		
							parent
							
								
									9f279ac872
								
							
						
					
					
						commit
						6e179bda1f
					
				
							
								
								
									
										459
									
								
								scripts/program.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										459
									
								
								scripts/program.py
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,459 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import typing | ||||
| import subprocess | ||||
| import logging | ||||
| import time | ||||
| import os | ||||
| import socket | ||||
| 
 | ||||
| from abc import ABC, abstractmethod | ||||
| from dataclasses import dataclass | ||||
| from flipper.app import App | ||||
| 
 | ||||
| 
 | ||||
| class Programmer(ABC): | ||||
|     @abstractmethod | ||||
|     def flash(self, bin: str) -> bool: | ||||
|         pass | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     def probe(self) -> bool: | ||||
|         pass | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     def get_name(self) -> str: | ||||
|         pass | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     def set_serial(self, serial: str): | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class OpenOCDInterface: | ||||
|     name: str | ||||
|     file: str | ||||
|     serial_cmd: str | ||||
|     additional_args: typing.Optional[list[str]] = None | ||||
| 
 | ||||
| 
 | ||||
| class OpenOCDProgrammer(Programmer): | ||||
|     def __init__(self, interface: OpenOCDInterface): | ||||
|         self.interface = interface | ||||
|         self.logger = logging.getLogger("OpenOCD") | ||||
|         self.serial: typing.Optional[str] = None | ||||
| 
 | ||||
|     def _add_file(self, params: list[str], file: str): | ||||
|         params.append("-f") | ||||
|         params.append(file) | ||||
| 
 | ||||
|     def _add_command(self, params: list[str], command: str): | ||||
|         params.append("-c") | ||||
|         params.append(command) | ||||
| 
 | ||||
|     def _add_serial(self, params: list[str], serial: str): | ||||
|         self._add_command(params, f"{self.interface.serial_cmd} {serial}") | ||||
| 
 | ||||
|     def set_serial(self, serial: str): | ||||
|         self.serial = serial | ||||
| 
 | ||||
|     def flash(self, bin: str) -> bool: | ||||
|         i = self.interface | ||||
| 
 | ||||
|         if os.altsep: | ||||
|             bin = bin.replace(os.sep, os.altsep) | ||||
| 
 | ||||
|         openocd_launch_params = ["openocd"] | ||||
|         self._add_file(openocd_launch_params, i.file) | ||||
|         if self.serial: | ||||
|             self._add_serial(openocd_launch_params, self.serial) | ||||
|         if i.additional_args: | ||||
|             for a in i.additional_args: | ||||
|                 self._add_command(openocd_launch_params, a) | ||||
|         self._add_file(openocd_launch_params, "target/stm32wbx.cfg") | ||||
|         self._add_command(openocd_launch_params, "init") | ||||
|         self._add_command(openocd_launch_params, f"program {bin} reset exit 0x8000000") | ||||
| 
 | ||||
|         # join the list of parameters into a string, but add quote if there are spaces | ||||
|         openocd_launch_params_string = " ".join( | ||||
|             [f'"{p}"' if " " in p else p for p in openocd_launch_params] | ||||
|         ) | ||||
| 
 | ||||
|         self.logger.debug(f"Launching: {openocd_launch_params_string}") | ||||
| 
 | ||||
|         process = subprocess.Popen( | ||||
|             openocd_launch_params, | ||||
|             stdout=subprocess.PIPE, | ||||
|             stderr=subprocess.STDOUT, | ||||
|         ) | ||||
| 
 | ||||
|         while process.poll() is None: | ||||
|             time.sleep(0.25) | ||||
|             print(".", end="", flush=True) | ||||
|         print() | ||||
| 
 | ||||
|         success = process.returncode == 0 | ||||
| 
 | ||||
|         if not success: | ||||
|             self.logger.error("OpenOCD failed to flash") | ||||
|             if process.stdout: | ||||
|                 self.logger.error(process.stdout.read().decode("utf-8").strip()) | ||||
| 
 | ||||
|         return success | ||||
| 
 | ||||
|     def probe(self) -> bool: | ||||
|         i = self.interface | ||||
| 
 | ||||
|         openocd_launch_params = ["openocd"] | ||||
|         self._add_file(openocd_launch_params, i.file) | ||||
|         if self.serial: | ||||
|             self._add_serial(openocd_launch_params, self.serial) | ||||
|         if i.additional_args: | ||||
|             for a in i.additional_args: | ||||
|                 self._add_command(openocd_launch_params, a) | ||||
|         self._add_file(openocd_launch_params, "target/stm32wbx.cfg") | ||||
|         self._add_command(openocd_launch_params, "init") | ||||
|         self._add_command(openocd_launch_params, "exit") | ||||
| 
 | ||||
|         self.logger.debug(f"Launching: {' '.join(openocd_launch_params)}") | ||||
| 
 | ||||
|         process = subprocess.Popen( | ||||
|             openocd_launch_params, | ||||
|             stderr=subprocess.STDOUT, | ||||
|             stdout=subprocess.PIPE, | ||||
|         ) | ||||
| 
 | ||||
|         # Wait for OpenOCD to end and get the return code | ||||
|         process.wait() | ||||
|         found = process.returncode == 0 | ||||
| 
 | ||||
|         if process.stdout: | ||||
|             self.logger.debug(process.stdout.read().decode("utf-8").strip()) | ||||
| 
 | ||||
|         return found | ||||
| 
 | ||||
|     def get_name(self) -> str: | ||||
|         return self.interface.name | ||||
| 
 | ||||
| 
 | ||||
| def blackmagic_find_serial(serial: str): | ||||
|     import serial.tools.list_ports as list_ports | ||||
| 
 | ||||
|     if serial and os.name == "nt": | ||||
|         if not serial.startswith("\\\\.\\"): | ||||
|             serial = f"\\\\.\\{serial}" | ||||
| 
 | ||||
|     ports = list(list_ports.grep("blackmagic")) | ||||
|     if len(ports) == 0: | ||||
|         return None | ||||
|     elif len(ports) > 2: | ||||
|         if serial: | ||||
|             ports = list( | ||||
|                 filter( | ||||
|                     lambda p: p.serial_number == serial | ||||
|                     or p.name == serial | ||||
|                     or p.device == serial, | ||||
|                     ports, | ||||
|                 ) | ||||
|             ) | ||||
|             if len(ports) == 0: | ||||
|                 return None | ||||
| 
 | ||||
|         if len(ports) > 2: | ||||
|             raise Exception("More than one Blackmagic probe found") | ||||
| 
 | ||||
|     # If you're getting any issues with auto lookup, uncomment this | ||||
|     # print("\n".join([f"{p.device} {vars(p)}" for p in ports])) | ||||
|     port = sorted(ports, key=lambda p: f"{p.location}_{p.name}")[0] | ||||
| 
 | ||||
|     if serial: | ||||
|         if ( | ||||
|             serial != port.serial_number | ||||
|             and serial != port.name | ||||
|             and serial != port.device | ||||
|         ): | ||||
|             return None | ||||
| 
 | ||||
|     if os.name == "nt": | ||||
|         port.device = f"\\\\.\\{port.device}" | ||||
|     return port.device | ||||
| 
 | ||||
| 
 | ||||
| def _resolve_hostname(hostname): | ||||
|     try: | ||||
|         return socket.gethostbyname(hostname) | ||||
|     except socket.gaierror: | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
| def blackmagic_find_networked(serial: str): | ||||
|     if not serial: | ||||
|         serial = "blackmagic.local" | ||||
| 
 | ||||
|     # remove the tcp: prefix if it's there | ||||
|     if serial.startswith("tcp:"): | ||||
|         serial = serial[4:] | ||||
| 
 | ||||
|     # remove the port if it's there | ||||
|     if ":" in serial: | ||||
|         serial = serial.split(":")[0] | ||||
| 
 | ||||
|     if not (probe := _resolve_hostname(serial)): | ||||
|         return None | ||||
| 
 | ||||
|     return f"tcp:{probe}:2345" | ||||
| 
 | ||||
| 
 | ||||
| class BlackmagicProgrammer(Programmer): | ||||
|     def __init__( | ||||
|         self, | ||||
|         port_resolver,  # typing.Callable[typing.Union[str, None], typing.Optional[str]] | ||||
|         name: str, | ||||
|     ): | ||||
|         self.port_resolver = port_resolver | ||||
|         self.name = name | ||||
|         self.logger = logging.getLogger("BlackmagicUSB") | ||||
|         self.port: typing.Optional[str] = None | ||||
| 
 | ||||
|     def _add_command(self, params: list[str], command: str): | ||||
|         params.append("-ex") | ||||
|         params.append(command) | ||||
| 
 | ||||
|     def _valid_ip(self, address): | ||||
|         try: | ||||
|             socket.inet_aton(address) | ||||
|             return True | ||||
|         except: | ||||
|             return False | ||||
| 
 | ||||
|     def set_serial(self, serial: str): | ||||
|         if self._valid_ip(serial): | ||||
|             self.port = f"{serial}:2345" | ||||
|         elif ip := _resolve_hostname(serial): | ||||
|             self.port = f"{ip}:2345" | ||||
|         else: | ||||
|             self.port = serial | ||||
| 
 | ||||
|     def flash(self, bin: str) -> bool: | ||||
|         if not self.port: | ||||
|             if not self.probe(): | ||||
|                 return False | ||||
| 
 | ||||
|         # We can convert .bin to .elf with objcopy: | ||||
|         # arm-none-eabi-objcopy -I binary -O elf32-littlearm --change-section-address=.data=0x8000000 -B arm -S app.bin app.elf | ||||
|         # But I choose to use the .elf file directly because we are flashing our own firmware and it always has an elf predecessor. | ||||
|         elf = bin.replace(".bin", ".elf") | ||||
|         if not os.path.exists(elf): | ||||
|             self.logger.error( | ||||
|                 f"Sorry, but Blackmagic can't flash .bin file, and {elf} doesn't exist" | ||||
|             ) | ||||
|             return False | ||||
| 
 | ||||
|         # arm-none-eabi-gdb build/f7-firmware-D/firmware.bin | ||||
|         # -ex 'set pagination off' | ||||
|         # -ex 'target extended-remote /dev/cu.usbmodem21201' | ||||
|         # -ex 'set confirm off' | ||||
|         # -ex 'monitor swdp_scan' | ||||
|         # -ex 'attach 1' | ||||
|         # -ex 'set mem inaccessible-by-default off' | ||||
|         # -ex 'load' | ||||
|         # -ex 'compare-sections' | ||||
|         # -ex 'quit' | ||||
| 
 | ||||
|         gdb_launch_params = ["arm-none-eabi-gdb", elf] | ||||
|         self._add_command(gdb_launch_params, f"target extended-remote {self.port}") | ||||
|         self._add_command(gdb_launch_params, "set pagination off") | ||||
|         self._add_command(gdb_launch_params, "set confirm off") | ||||
|         self._add_command(gdb_launch_params, "monitor swdp_scan") | ||||
|         self._add_command(gdb_launch_params, "attach 1") | ||||
|         self._add_command(gdb_launch_params, "set mem inaccessible-by-default off") | ||||
|         self._add_command(gdb_launch_params, "load") | ||||
|         self._add_command(gdb_launch_params, "compare-sections") | ||||
|         self._add_command(gdb_launch_params, "quit") | ||||
| 
 | ||||
|         self.logger.debug(f"Launching: {' '.join(gdb_launch_params)}") | ||||
| 
 | ||||
|         process = subprocess.Popen( | ||||
|             gdb_launch_params, | ||||
|             stdout=subprocess.PIPE, | ||||
|             stderr=subprocess.STDOUT, | ||||
|         ) | ||||
| 
 | ||||
|         while process.poll() is None: | ||||
|             time.sleep(0.5) | ||||
|             print(".", end="", flush=True) | ||||
|         print() | ||||
| 
 | ||||
|         if not process.stdout: | ||||
|             return False | ||||
| 
 | ||||
|         output = process.stdout.read().decode("utf-8").strip() | ||||
|         flashed = "Loading section .text," in output | ||||
| 
 | ||||
|         # Check flash verification | ||||
|         if "MIS-MATCHED!" in output: | ||||
|             flashed = False | ||||
| 
 | ||||
|         if "target image does not match the loaded file" in output: | ||||
|             flashed = False | ||||
| 
 | ||||
|         if not flashed: | ||||
|             self.logger.error("Blackmagic failed to flash") | ||||
|             self.logger.error(output) | ||||
| 
 | ||||
|         return flashed | ||||
| 
 | ||||
|     def probe(self) -> bool: | ||||
|         if not (port := self.port_resolver(self.port)): | ||||
|             return False | ||||
| 
 | ||||
|         self.port = port | ||||
|         return True | ||||
| 
 | ||||
|     def get_name(self) -> str: | ||||
|         return self.name | ||||
| 
 | ||||
| 
 | ||||
| programmers: list[Programmer] = [ | ||||
|     OpenOCDProgrammer( | ||||
|         OpenOCDInterface( | ||||
|             "cmsis-dap", | ||||
|             "interface/cmsis-dap.cfg", | ||||
|             "cmsis_dap_serial", | ||||
|             ["transport select swd"], | ||||
|         ), | ||||
|     ), | ||||
|     OpenOCDProgrammer( | ||||
|         OpenOCDInterface( | ||||
|             "stlink", "interface/stlink.cfg", "hla_serial", ["transport select hla_swd"] | ||||
|         ), | ||||
|     ), | ||||
|     BlackmagicProgrammer(blackmagic_find_serial, "blackmagic_usb"), | ||||
| ] | ||||
| 
 | ||||
| network_programmers = [ | ||||
|     BlackmagicProgrammer(blackmagic_find_networked, "blackmagic_wifi") | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
| class Main(App): | ||||
|     def init(self): | ||||
|         self.subparsers = self.parser.add_subparsers(help="sub-command help") | ||||
|         self.parser_flash = self.subparsers.add_parser("flash", help="Flash a binary") | ||||
|         self.parser_flash.add_argument( | ||||
|             "bin", | ||||
|             type=str, | ||||
|             help="Binary to flash", | ||||
|         ) | ||||
|         interfaces = [i.get_name() for i in programmers] | ||||
|         interfaces.extend([i.get_name() for i in network_programmers]) | ||||
|         self.parser_flash.add_argument( | ||||
|             "--interface", | ||||
|             choices=interfaces, | ||||
|             type=str, | ||||
|             help="Interface to use", | ||||
|         ) | ||||
|         self.parser_flash.add_argument( | ||||
|             "--serial", | ||||
|             type=str, | ||||
|             help="Serial number or port of the programmer", | ||||
|         ) | ||||
|         self.parser_flash.set_defaults(func=self.flash) | ||||
| 
 | ||||
|     def _search_interface(self, serial: typing.Optional[str]) -> list[Programmer]: | ||||
|         found_programmers = [] | ||||
| 
 | ||||
|         for p in programmers: | ||||
|             name = p.get_name() | ||||
|             if serial: | ||||
|                 p.set_serial(serial) | ||||
|                 self.logger.debug(f"Trying {name} with {serial}") | ||||
|             else: | ||||
|                 self.logger.debug(f"Trying {name}") | ||||
| 
 | ||||
|             if p.probe(): | ||||
|                 self.logger.debug(f"Found {name}") | ||||
|                 found_programmers += [p] | ||||
|             else: | ||||
|                 self.logger.debug(f"Failed to probe {name}") | ||||
| 
 | ||||
|         return found_programmers | ||||
| 
 | ||||
|     def _search_network_interface( | ||||
|         self, serial: typing.Optional[str] | ||||
|     ) -> list[Programmer]: | ||||
|         found_programmers = [] | ||||
| 
 | ||||
|         for p in network_programmers: | ||||
|             name = p.get_name() | ||||
| 
 | ||||
|             if serial: | ||||
|                 p.set_serial(serial) | ||||
|                 self.logger.debug(f"Trying {name} with {serial}") | ||||
|             else: | ||||
|                 self.logger.debug(f"Trying {name}") | ||||
| 
 | ||||
|             if p.probe(): | ||||
|                 self.logger.debug(f"Found {name}") | ||||
|                 found_programmers += [p] | ||||
|             else: | ||||
|                 self.logger.debug(f"Failed to probe {name}") | ||||
| 
 | ||||
|         return found_programmers | ||||
| 
 | ||||
|     def flash(self): | ||||
|         start_time = time.time() | ||||
|         bin_path = os.path.abspath(self.args.bin) | ||||
| 
 | ||||
|         if not os.path.exists(bin_path): | ||||
|             self.logger.error(f"Binary file not found: {bin_path}") | ||||
|             return 1 | ||||
| 
 | ||||
|         if self.args.interface: | ||||
|             i_name = self.args.interface | ||||
|             interfaces = [p for p in programmers if p.get_name() == i_name] | ||||
|             if len(interfaces) == 0: | ||||
|                 interfaces = [p for p in network_programmers if p.get_name() == i_name] | ||||
|         else: | ||||
|             self.logger.info(f"Probing for interfaces...") | ||||
|             interfaces = self._search_interface(self.args.serial) | ||||
| 
 | ||||
|             if len(interfaces) == 0: | ||||
|                 # Probe network blackmagic | ||||
|                 self.logger.info(f"Probing for network interfaces...") | ||||
|                 interfaces = self._search_network_interface(self.args.serial) | ||||
| 
 | ||||
|             if len(interfaces) == 0: | ||||
|                 self.logger.error("No interface found") | ||||
|                 return 1 | ||||
| 
 | ||||
|             if len(interfaces) > 1: | ||||
|                 self.logger.error("Multiple interfaces found: ") | ||||
|                 self.logger.error( | ||||
|                     f"Please specify '--interface={[i.get_name() for i in interfaces]}'" | ||||
|                 ) | ||||
|                 return 1 | ||||
| 
 | ||||
|         interface = interfaces[0] | ||||
| 
 | ||||
|         if self.args.serial: | ||||
|             interface.set_serial(self.args.serial) | ||||
|             self.logger.info( | ||||
|                 f"Flashing {bin_path} via {interface.get_name()} with {self.args.serial}" | ||||
|             ) | ||||
|         else: | ||||
|             self.logger.info(f"Flashing {bin_path} via {interface.get_name()}") | ||||
| 
 | ||||
|         if not interface.flash(bin_path): | ||||
|             self.logger.error(f"Failed to flash via {interface.get_name()}") | ||||
|             return 1 | ||||
| 
 | ||||
|         flash_time = time.time() - start_time | ||||
|         bin_size = os.path.getsize(bin_path) | ||||
|         self.logger.info(f"Flashed successfully in {flash_time:.2f}s") | ||||
|         self.logger.info(f"Effective speed: {bin_size / flash_time / 1024:.2f} KiB/s") | ||||
|         return 0 | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     Main()() | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Sergey Gavrilov
						Sergey Gavrilov