 b9a766d909
			
		
	
	
		b9a766d909
		
	
	
	
	
		
			
			* Added support for running applications from SD card (FAPs - Flipper Application Packages) * Added plugin_dist target for fbt to build FAPs * All apps of type FlipperAppType.EXTERNAL and FlipperAppType.PLUGIN are built as FAPs by default * Updated VSCode configuration for new fbt features - re-deploy stock configuration to use them * Added debugging support for FAPs with fbt debug & VSCode * Added public firmware API with automated versioning Co-authored-by: hedger <hedger@users.noreply.github.com> Co-authored-by: SG <who.just.the.doctor@gmail.com> Co-authored-by: あく <alleteam@gmail.com>
		
			
				
	
	
		
			158 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			158 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from dataclasses import dataclass
 | |
| from typing import Tuple, Dict
 | |
| import struct
 | |
| import posixpath
 | |
| import os
 | |
| import zlib
 | |
| 
 | |
| import gdb
 | |
| 
 | |
| 
 | |
| def get_file_crc32(filename):
 | |
|     with open(filename, "rb") as f:
 | |
|         return zlib.crc32(f.read())
 | |
| 
 | |
| 
 | |
| @dataclass
 | |
| class AppState:
 | |
|     name: str
 | |
|     text_address: int = 0
 | |
|     entry_address: int = 0
 | |
|     other_sections: Dict[str, int] = None
 | |
|     debug_link_elf: str = ""
 | |
|     debug_link_crc: int = 0
 | |
| 
 | |
|     def __post_init__(self):
 | |
|         if self.other_sections is None:
 | |
|             self.other_sections = {}
 | |
| 
 | |
|     def get_original_elf_path(self, elf_path="build/latest/.extapps") -> str:
 | |
|         return (
 | |
|             posixpath.join(elf_path, self.debug_link_elf)
 | |
|             if elf_path
 | |
|             else self.debug_link_elf
 | |
|         )
 | |
| 
 | |
|     def is_debug_available(self) -> bool:
 | |
|         have_debug_info = bool(self.debug_link_elf and self.debug_link_crc)
 | |
|         if not have_debug_info:
 | |
|             print("No debug info available for this app")
 | |
|             return False
 | |
|         debug_elf_path = self.get_original_elf_path()
 | |
|         debug_elf_crc32 = get_file_crc32(debug_elf_path)
 | |
|         if self.debug_link_crc != debug_elf_crc32:
 | |
|             print(
 | |
|                 f"Debug info ({debug_elf_path}) CRC mismatch: {self.debug_link_crc:08x} != {debug_elf_crc32:08x}, rebuild app"
 | |
|             )
 | |
|             return False
 | |
|         return True
 | |
| 
 | |
|     def get_gdb_load_command(self) -> str:
 | |
|         load_path = self.get_original_elf_path()
 | |
|         print(f"Loading debug information from {load_path}")
 | |
|         load_command = (
 | |
|             f"add-symbol-file -readnow {load_path} 0x{self.text_address:08x} "
 | |
|         )
 | |
|         load_command += " ".join(
 | |
|             f"-s {name} 0x{address:08x}"
 | |
|             for name, address in self.other_sections.items()
 | |
|         )
 | |
|         return load_command
 | |
| 
 | |
|     def get_gdb_unload_command(self) -> str:
 | |
|         return f"remove-symbol-file -a 0x{self.text_address:08x}"
 | |
| 
 | |
|     def is_loaded_in_gdb(self, gdb_app) -> bool:
 | |
|         # Avoid constructing full app wrapper for comparison
 | |
|         return self.entry_address == int(gdb_app["entry"])
 | |
| 
 | |
|     @staticmethod
 | |
|     def parse_debug_link_data(section_data: bytes) -> Tuple[str, int]:
 | |
|         # Debug link format: a null-terminated string with debuggable file name
 | |
|         # Padded with 0's to multiple of 4 bytes
 | |
|         # Followed by 4 bytes of CRC32 checksum of that file
 | |
|         elf_name = section_data[:-4].decode("utf-8").split("\x00")[0]
 | |
|         crc32 = struct.unpack("<I", section_data[-4:])[0]
 | |
|         return (elf_name, crc32)
 | |
| 
 | |
|     @staticmethod
 | |
|     def from_gdb(gdb_app: "AppState") -> "AppState":
 | |
|         state = AppState(str(gdb_app["manifest"]["name"].string()))
 | |
|         state.entry_address = int(gdb_app["entry"])
 | |
| 
 | |
|         app_state = gdb_app["state"]
 | |
|         if debug_link_size := int(app_state["debug_link_size"]):
 | |
|             debug_link_data = (
 | |
|                 gdb.selected_inferior()
 | |
|                 .read_memory(int(app_state["debug_link"]), debug_link_size)
 | |
|                 .tobytes()
 | |
|             )
 | |
|             state.debug_link_elf, state.debug_link_crc = AppState.parse_debug_link_data(
 | |
|                 debug_link_data
 | |
|             )
 | |
| 
 | |
|         for idx in range(app_state["mmap_entry_count"]):
 | |
|             mmap_entry = app_state["mmap_entries"][idx]
 | |
|             section_name = mmap_entry["name"].string()
 | |
|             section_addr = int(mmap_entry["address"])
 | |
|             if section_name == ".text":
 | |
|                 state.text_address = section_addr
 | |
|             else:
 | |
|                 state.other_sections[section_name] = section_addr
 | |
| 
 | |
|         return state
 | |
| 
 | |
| 
 | |
| class FlipperAppDebugHelper:
 | |
|     def __init__(self):
 | |
|         self.app_ptr = None
 | |
|         self.app_type_ptr = None
 | |
|         self.current_app: AppState = None
 | |
| 
 | |
|     def attach_fw(self) -> None:
 | |
|         self.app_ptr = gdb.lookup_global_symbol("last_loaded_app")
 | |
|         self.app_type_ptr = gdb.lookup_type("FlipperApplication").pointer()
 | |
|         self._check_app_state()
 | |
| 
 | |
|     def _check_app_state(self) -> None:
 | |
|         app_ptr_value = self.app_ptr.value()
 | |
|         if not app_ptr_value and self.current_app:
 | |
|             # There is an ELF loaded in GDB, but nothing is running on the device
 | |
|             self._unload_debug_elf()
 | |
|         elif app_ptr_value:
 | |
|             # There is an app running on the device
 | |
|             loaded_app = app_ptr_value.cast(self.app_type_ptr).dereference()
 | |
| 
 | |
|             if self.current_app and not self.current_app.is_loaded_in_gdb(loaded_app):
 | |
|                 # Currently loaded ELF is not the one running on the device
 | |
|                 self._unload_debug_elf()
 | |
| 
 | |
|             if not self.current_app:
 | |
|                 # Load ELF for the app running on the device
 | |
|                 self._load_debug_elf(loaded_app)
 | |
| 
 | |
|     def _unload_debug_elf(self) -> None:
 | |
|         try:
 | |
|             gdb.execute(self.current_app.get_gdb_unload_command())
 | |
|         except gdb.error as e:
 | |
|             print(f"Failed to unload debug ELF: {e} (might not be an error)")
 | |
|         self.current_app = None
 | |
| 
 | |
|     def _load_debug_elf(self, app_object) -> None:
 | |
|         self.current_app = AppState.from_gdb(app_object)
 | |
| 
 | |
|         if self.current_app.is_debug_available():
 | |
|             gdb.execute(self.current_app.get_gdb_load_command())
 | |
| 
 | |
|     def handle_stop(self, event) -> None:
 | |
|         self._check_app_state()
 | |
| 
 | |
| 
 | |
| helper = FlipperAppDebugHelper()
 | |
| try:
 | |
|     helper.attach_fw()
 | |
|     print("Support for Flipper external apps debug is enabled")
 | |
|     gdb.events.stop.connect(helper.handle_stop)
 | |
| except gdb.error as e:
 | |
|     print(f"Support for Flipper external apps debug is not available: {e}")
 |