Repository: raulgotor/linkerscope Branch: main Commit: ba13813088f8 Files: 30 Total size: 104.6 KB Directory structure: gitextract_tz97aatf/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── area_view.py ├── examples/ │ ├── break_config.yaml │ ├── break_map.yaml │ ├── labels_config.yaml │ ├── labels_map.yaml │ ├── link_config.yaml │ ├── link_map.yaml │ ├── sample_config.yaml │ ├── stack_config.yaml │ ├── stack_map.yaml │ ├── stm32f103_config.yaml │ └── stm32f103_map.yaml ├── gnu_linker_map_parser.py ├── helpers.py ├── labels.py ├── linkerscope.py ├── links.py ├── logger.py ├── map_file_loader.py ├── map_render.py ├── requirements.txt ├── section.py ├── sections.py └── style.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: raulgotor tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/workflows/main.yml ================================================ name: Pylint on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install pylint - name: Analysing the code with pylint run: | pylint $(git ls-files '*.py') --disable=C0114,R0902,C0116 --fail-under=9 ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # IDE .idea # OS .DS_Store # Artifacts *.svg # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added * `auto` property to `hide-*` attributes so LinkerScope can explicitly decide whether the attribute should be hidden due to overlapping issues * `hidden` property to sections to allow hiding one section while still computing its properties ## [0.3.1] - 2024-02-03 ### Added * `--convert` flag to simply convert `.map` files to `.yaml` files ## [0.3.0] - 2024-02-03 ### Added * Title feature and property (`title`) for the different areas * `hide-name`, `hide-address` and `hide-size` style properties to hide specific visual elements * `flags` can be specified at map file as well * `size` property at root level to modify the document size * `labels` property can go on the left side as well * Friendly name field (`name`) for sections at the yaml map file to be used instead of ID ### Fixed * Design issue that was hindering creating linked sections if the area contained breaks * Bug that repeated the title of the area if the area contained breaks * Breaks on areas that had empty regions would not perform correctly ## [0.2.0] - 2023-12-14 ### Added * Style overriding by section: each section can have its own style * Added `links/sections`, which links a section or group of between main and secondary area * `flags` property for each section * `opacity` style property that controls the background opacity of a linked section * `background` style property that controls the document's background * `grows-up` and `grows-down` flags for sections that draw an arrow indicating the growth direction of the section * `growth-arrow-weight`, `-stroke-color` and `-fill-color` style properties for the sections growth arrows * Method at `Style` class to easily override properties from another object: `override_properties_from` * Method at `Style` class to get a default initialized object: `get_default` * Property names in yaml also accept `-` instead of underscore * `style` property to `links`, `area`, and `area/sections` * flags are specified per section at `area/sections/` * custom labels at specific memory addresses ### Changed * Naming of the memory domain: Area instead of Map * Refactored initialization of `AreaView` to pass parameters via `kwargs` * Refactored out parameter `dwg` at `map_drawer.py` * `size-x` and `size-y` moved to `[size]`, `x` and `y` to `[pos]`, `addresses` to `[range]` * overridden style for areas is constructed before class declaration, and not before drawing ## [0.1.0] - 2023-11-24 * Initial release ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Raúl Gotor Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # LinkerScope ## Project summary LinkerScope is a memory map diagram generator. It can be fed either with a GNU Linker map file or with a custom `yaml` file and generate beautiful and detailed diagrams of the different areas and sections such as the one below. ![Example of_STM32F103 Memory Map generated with LinkerScope](docs/assets/stm32f103_map.svg) ko-fi ## Installing LinkerScope Optionally create and activate an environment for LinkerScope: ```bash python3 -m venv venv source env/bin/activate ``` Install the needed requirements by executing: ```bash pip3 install -r requirements.txt ``` ## Usage ### Execution LinkerScope needs a file with memory information for basic functionality: that could be either GNU Linker map files (`.map`) or custom structured YAML file (`.yaml`). ```bash ./linkerscope.py linker.map [] ``` This will output memory map diagram to a default `map.svg` file. While this enables a quick generation of memory map, one might want to carefully tune the different properties of the diagram such as visual and style properties, memory regions, names, links, ... For that, a [configuration file](#creating-a-configuration-file) with the desired characteristics has to be specified: ```bash ./linkerscope.py linker.map --config config.yaml --output map.svg ``` where: - First parameter specifies the path to the input file, where LinkerScope should get the data to represent from. It can come from a GNU Linker map file `.map` or from an already parsed or hand-crafted `.yaml` file. Check [Manually crafting input file](#Manually crafting input file) section for learning how to do this. - `-c, --config` [OPTIONAL] specifies the path to the configuration file. This file contains all the custom information to tell LinkerScope what to and how to draw the memory maps. While it is optional, the default parameters will most likely not apply to a given use case. - `-o, --output` [OPTIONAL] specifies the path to the output file, which will be a newly generated SVG. - `--convert` [OPTIONAL] tells LinkerScope to perform a conversion from a `.map` file to `.yaml` file containing memory information. After conversion, proqram will quit. ### Input files LinkerScope can use two types of input files: GNU linker map files (`.map`) or custom defined yaml files (`.yaml`). #### Using .map files Under the hood, LinkerScope will convert `.map` files to custom `.yaml` ones, and will work from there. Since this operation is time-consuming and makes no sense to do it multiple times, two strategies can be performed when using `.map` files: - Convert `.map` files to `.yaml` file and then use the `.yaml` file as an input to LinkerScope > This is specially useful if you plan to execute LinkerScope multiple times, since this conversion is time-consuming. Therefore better doing the conversion step once, right? Execute the example below: > ```shell > # Conversion step > ./linkerscope.py examples/sample_map.map --convert > > # Map diagram generation. You can execute multiple times without having to do the conversion again > ./linkerscope.py map.yaml -c examples/sample_config.yaml -o sample_map.svg >``` - Directly use the `.map` files to output a memory map diagram. > Use this strategy when you already have a configuration file, and you know that LinkerScope will produce the expected result. Execute the example below: > ```shell > ./linkerscope.py examples/sample_map.map -c examples/sample_config.yaml -o sample_map.svg > ``` #### Manually crafted memory map files Custom memory map files can be manually crafted and can run from a couple of memory sections up to very complex memory schemes with hundreds of sections. Normally you would do that when representing simple memory maps. For making a memory map file, one has to specify at least a single section. Each section must include an `id`, an `address` and a `size`. While these three are needed, there are other possible attributes that are optional: - `name`: Friendly text name that would be used instead of the `id` - `type`: Section type, which can be used for different purposes. Current possibilities are `section` (default) and `area`. The input file should contain the `map` keyword whose value is an array of sections. Below an example of how an input file should look like: ```yaml - address: 0x80045D4 size: 0x0000F00 id: .rodata name: Read Only Data type: area - address: 0x8002C3C id: SysTick_Handler size: 0x0000008 - ... ``` In order to use this file, invoke LinkerScope and specify the yaml map file as input: ```bash ./linkerscope.py memory_map.yaml -o memory_map.svg -c config.yaml ``` ### Creating a configuration file The configuration file is a `.yaml` file containing all the required information to tell LinkerScope what and how to draw the memory map. All information there is optional, including the file itself. If this information is not provided, the default values will be used. Normally, a configuration file contains **areas**, **links** and **style** information. **Areas** provides a list of memory areas to be displayed, with information regarding its position and size on the map, memory range to include or specific drawing style. Additionally, it can contain a **sections** sub-property where specific sections can be added to modify desired properties **Links** provides a simple way of graphically relating same memory addresses across different areas **Style** defines the parent drawing style properties that will be used at the document. Additionally, each area can override specific style properties at its style section. Lastly, sections can override parent (area) style as well ```yaml style: background: '#99B898' stroke: 'black' # ... areas: - area: style: # ... title: Register Map range: [0x0, 0x100000000] sections: - names: [USART1, USART2] style: hide-name: true # ... - area: # ... links: addresses: [0x80045d4] sections: [__malloc_av_, [TIM2, TIM3]] ``` #### Areas The diagram can have one or multiple areas. When multiple areas are declared, first area has a special status since all links will start on it and go to the corresponding sections on the other areas The areas are declared at root level under `areas`. Then each area must use the key `area`. Areas can be placed at any position in the document, and some of its properties can be tuned.elements such as labels, memory addresses, For instance, some elements such as the name / id, size, address, can be show or hidden, labels at specific memory regions can be configured, sections can be marked as break sections, and the visual style of the area and its sections can be modified independently from the others. For each area, the following characteristics of an area can be defined: - `title`: **[Optional, none]** - The title of the area, which will appear on top of it - `pos`: **[Optional, (50, 50)]** **[x, y]** - absolute position of the area's top-left corner in pixels - `size`: **[Optional, (300, 600)]** **[width, height]** - area size in pixels - `range`: **[Optional, (0, no limit)]** **[min, max]** - range of addresses that will be taken into account in this area. - `start`: **[start, end]** force area to start in to a given address - `section-size`: **[Optional, (0, no limit)]** **[min, max]** - size range of the sections that should be shown. Exclude others. - `style`: **[Optional, default: parent style]** - specific style for current area. See [Styles](####Styles) section. - `sections`: **[Optional, none]** - specify or modify a section or group of sections property such as `style`, `flags`,... - `names`: - list of one or more sections to modify with the parameters below - `flags`: **[Optional, none]** - flags to append to the specified section/s. See [Flags](#### Section flags) section. - `style`: **[Optional, parent style]** - style properties to or modify to the specified section/s - `labels`: **[Optional, none]** - Add text labels to specific memory positions of the current area - `address`: - Memory address where the label should be placed - `text`: - Text to display for this label - `length`: **[Optional, none]** - length of the label line in pixels - `directions`: **[Optional, none]** - direction or list of directions for the arrow head. Possible values are none, `in`, `out` or both. - `side`: **[Optional, `right`]** - Area side at which the label should be placed - `style`: **[Optional, parent style]** - style properties to or modify to the specified section/s Below an example of area definition: ```yaml areas: - area: title: 'Full Memory Map' pos: [30, 50] size: [300, 900] range: [0x0, 0x100000000] section-size: [0x02F00000] style: fill: *pastel_green sections: - names: [ External Device ] flags: grows-up style: hide-size: true ``` #### Labels Labels can bind a text string with a specific memory position. This property falls inside `area`. An extract from the `labels` example can be found below: ```yaml areas: - area: labels: - address: 0xe8043800 text: In direction length: 150 directions: in style: stroke-dasharray: '5,1,3,1,3' # ... ``` ![Example of different labels](docs/assets/labels_map.svg) > The example can be executed at the root folder by running: > ```bash > ./linkerscope.py examples/labels_map.yaml -c examples/labels_config.yaml > ``` #### Section flags Section flags allows flagging specified sections with special properties. These properties are `hidden`, `break`, `grows-up` and `grows-down`. Flags are listed under property `flags` and can be specified both at the map files under each section ```yaml # flags can be defined at map.yaml files under each section map: - address: 0x20000000 id: stack # ... flags: grows-down, break ``` or at the configuration files, with the possibility to specify multiple sections at the same time: ```yaml # flags can be defined at config.yaml files under each section or group of sections areas: - area: # ... sections: - names: [ROM Table, TPIU] flags: break ``` ##### `hidden` This flag marks a section or sections to be hidden. Its properties are still computed, the space that occupies in the graph is still accounted for, but it is not shown. This could be useful, for instance, to remove irrelevant sections while to some other more important. Another quite useful use case is doing side by side memory maps: ![Example for side by side maps](docs/assets/side_by_side_map.svg) > The example can be executed at the root folder by running: > ```bash > ./linkerscope.py examples/side_by_side_map.yaml -c examples/side_by_side_config.yaml > ``` ##### Growths These flags specify the section as growing section, for instance, if the section is meant to grow into one direction, such as the stack. When flagging a section with `grows-down`, an arrow pointing downwards will be appended to the bottom of the section indicating that the section is growing into that direction: ![Example of the different breaks](docs/assets/stack_map.svg) > The example can be executed at the root folder by running: > ```bash > ./linkerscope.py examples/stack_map.yaml -c examples/stack_config.yaml > ``` ##### `break` A break or discontinuous section shortens a sized section to a fixed size by drawing a symbol representing a discontinuity across it. This is specially useful when wanting to include several sections of considerable different sizes in one diagram. Reducing the size of the biggest one helps to visually simplify the diagram and evenly distribute the space. There are four different break styles, which can be defined by the 'break-type' style property: `~`: Wave, `≈`: Double wave, `/`: Diagonal, `...`: Dots ![Example of the different breaks](docs/assets/break_map.svg) > The example can be executed at the root folder by running: > ```bash > ./linkerscope.py examples/break_map.yaml -c examples/break_config.yaml > ``` #### Links Links establish a connection between same addresses or sections at different areas. While address links are represented with a finite line between the addresses, section link drawing cover the whole region space. These can be used, for instance, to represent a _zoom_ in effects from one overview area to other area with more detail. > When drawing a section link, Linkerscope expects both start and end section addresses to be visible at both intended areas. If any of those is not present, the link will not be drawn Links are defined at root level of the configuration file under the `links` tag. Links must have either `addresses` or `sections` tags or both. - `addresses` is a list of integers representing memory addresses - `sections` is a list whose elements can be either single section id's, section id's pairs (as a sublist) or both. When using pairs, the first element should be the one with the lowest memory address. Additionally, specific styles can be specified under the `style` tag. ```yaml links: style: stroke: 'lightgray' stroke-width: 1 fill: 'gray' addresses: [ 0xe8040000, 0xe8042000 ] sections: [['Bit band region', 'Bit band alias'],'ITM'] ``` ![Example of linked sections](docs/assets/link_map.svg) > The example can be executed at the root folder by running: > ```bash > ./linkerscope.py examples/link_map.yaml -c examples/link_config.yaml > ``` > #### Styles The style can be defined at document level, where it will be applied to all areas, but also at area or even at section level. Specifying a style at area level will override the specified properties for the whole area where it was defined. Specifying it at section level, it will override style only for the specified section or group of sections. ```yaml style: # This is a style defined at document level text-fill: 'lightgrey' background: 'black' stroke: '#99B898' stroke-width: 1 font-type: 'Helvetica' areas: - area: title: 'Internal Memory' pos: [30, 50] style: # This is a style defined at area level, which will override fill property only applied at Main Memory area fill: 'blue' sections: - names: [ SRAM, Flash ] style: # This is a style defined at section level, and will be applied only to SRAM and Flash sections fill: 'green' hide-address: true - area: title: 'External Memory' # ... ``` Below a list of style properties with general use and specific for sections: ##### General - `background`, `fill`, `font-size`, `font-type`, `opacity`, `size`, `stroke`, `stroke_dasharray`, `stroke-width`, `text-stroke`,`text-fill`,`text-stroke-width`, `weight` ##### Section properties: - `break-type`: specify memory break type. See [`break`](#break) section - `break-size`: specify memory break size in pixels. See [`break`](#break) section - `growth-arrow-size`: size of the direction growth arrow. See [`Growths`](#growths) section - `growth-arrow-fill`: color for the direction growth arrow. See [`Growths`](#growths) section - `growth-arrow-stroke`: stroke color for the direction growth arrow. See [`Growths`](#growths) section - `hide-size`: hides the size label of a section [`true | auto | false` ] - `hide-name`: hides the name label of a section [`true | auto | false` ] - `hide-address`: hides the address label of a section [`true | auto | false` ] #### Other properties _Document size_ The generated SVG document has a fixed size. If you want to adjust it, use the `size` property at root level to pass desired document width and height in pixels. ## Run some examples with LinkerScope At the folder examples, there are a series of configurations and map `.yaml` files you can use to get a preview of what LinkerScope can do. ## Roadmap - [x] Labels at specific memory addresses - [x] Area titles - [x] Links across specific areas - [x] Choose side of the labels - [x] Memory direction - [x] Hide specific elements - [ ] Memory size in bytes - [x] Section links across breaks - [x] Friendly name and identifier - [ ] Legend - [ ] Area representation different from section - [ ] Make `type` default to `section` - [x] Bug: title appears at top of each break section, if long enough ## References - [YAML cheatsheet](https://quickref.me/yaml) - [MapViewer](https://github.com/govind-mukundan/MapViewer) - [LinkerMapViz](https://github.com/PromyLOPh/linkermapviz) ## License Distributed under the MIT License. See `LICENSE` for more information. ================================================ FILE: area_view.py ================================================ import copy from helpers import safe_element_list_get, safe_element_dict_get, DefaultAppValues from labels import Labels from logger import logger from style import Style class AreaView: """ AreaView provides the container for a given set of sections and the methods to process and transform the information they contain into useful data for graphical representation """ pos_y: int pos_x: int zoom: int address_to_pxl: float total_height_pxl: int start_address: int end_address: int def __init__(self, sections, style, area_config=[], labels=None, is_subarea = False): self.sections = sections self.processed_section_views = [] self.is_subarea = is_subarea self.area = area_config self.style = style self.start_address = safe_element_dict_get(self.area, 'start', self.sections.lowest_memory) self.end_address = safe_element_dict_get(self.area, 'end', self.sections.highest_memory) self.pos_x = safe_element_list_get( safe_element_dict_get(self.area, 'pos'), 0, default=DefaultAppValues.POSITION_X) self.pos_y = safe_element_list_get( safe_element_dict_get(self.area, 'pos'), 1, default=DefaultAppValues.POSITION_Y) self.size_x = safe_element_list_get( safe_element_dict_get(self.area, 'size'), 0, default=DefaultAppValues.SIZE_X) self.size_y = safe_element_list_get( safe_element_dict_get(self.area, 'size'), 1, default=DefaultAppValues.SIZE_Y) self.labels = Labels(safe_element_dict_get(self.area, 'labels', []), style) self.title = safe_element_dict_get(self.area, 'title', DefaultAppValues.TITLE) self.address_to_pxl = (self.end_address - self.start_address) / self.size_y if not self.is_subarea: self._process() def get_split_area_views(self): """ Get current area view split in multiple area views around break sections :return: List of AreaViews """ return self.processed_section_views def to_pixels(self, value) -> float: """ Convert a given address to pixels in an absolute manner, according to the address / pixel size ratio of current area :param value: Address to be converted to pixels :return: Conversion result """ return value / self.address_to_pxl def to_pixels_relative(self, value) -> float: """ Convert a given address to pixels in a relative manner, according to the address / pixel size ratio of current area Relative in this context means relative to the start address of the Area view. If Area View starts at 0x20000 and ends at 0x30000, passing these values to this function for an area with a height of 1000 pixels, will result in 0 and 1000 respectively :param value: Address to be converted to pixels :return: Conversion result """ return self.size_y - ((value - self.start_address) / self.address_to_pxl) def _overwrite_sections_info(self): """ Override default style with section specific style Overrides default style (normally style defined by the area it is at) and flags information on a section given a new definition is provided for an specific section at the map or configuration files """ for section in self.sections.get_sections(): section_style = copy.deepcopy(self.style) section.style = section_style inner_sections = safe_element_dict_get(self.area, 'sections', []) if inner_sections is None: logger.warning( "'sections' property is declared but is empty. Field has been ignored") inner_sections = [] for element in inner_sections: section_names = safe_element_dict_get(element, 'names', []) if section_names is None: logger.warning( "'sections' property is declared but is empty. Field has been ignored") section_names = [] for item in section_names: if item == section.id: # OVERWRITE style, address, size and type if needed section_style.override_properties_from(Style(style=element.get('style'))) section.address = element.get('address', section.address) section.type = element.get('type', section.type) section.size = element.get('size', section.size) # As flags can be defined previously at map file, APPEND whatever is new section.flags += element.get('flags', section.flags) def _process(self): def recalculate_subarea_size_y(start_mem_addr, end_mem_addr): """ Recalculates the size of the current sub-area, provided the maximum and minimum memory that this area has to show For that, it makes a relation between the memory that needs to be displayed and the total non-break memory available :param start_mem_addr: minimum memory address that the new sub-area must show :param end_mem_addr: maximum memory address that the new sub-area must show :return: Recalculated size for this area """ return (self.to_pixels(end_mem_addr - start_mem_addr) / total_non_breaks_size_y_px) * \ (total_non_breaks_size_y_px + expandable_size_px) def area_config_clone(configuration, pos_y_px, size_y_px, start_mem_addr, end_mem_addr): """ Clones an area configuration and changes position and size :param configuration: Area configuration to clone :param pos_y_px: Position in pixels for the new cloned configuration :param size_y_px: Size y in pixels of the new cloned configuration :param start_mem_addr: minimum memory address that the new sub-area must show :param end_mem_addr: maximum memory address that the new sub-area must show :return: A new area configuration with the provided configuration and provided parameters """ new_configuration = copy.deepcopy(configuration) new_configuration['size'] = [DefaultAppValues.SIZE_X, DefaultAppValues.SIZE_Y] if new_configuration.get('pos') is None: new_configuration['pos'] = [DefaultAppValues.POSITION_X, DefaultAppValues.POSITION_Y] new_configuration['size'][1] = size_y_px new_configuration['pos'][1] = pos_y_px - size_y_px new_configuration['start'] = start_mem_addr new_configuration['end'] = end_mem_addr return new_configuration self._overwrite_sections_info() if len(self.sections.get_sections()) == 0: print("Filtered sections produced no results") return split_section_groups = self.sections.split_sections_around_breaks() breaks_count = len(self.sections.filter_breaks().get_sections()) area_has_breaks = breaks_count >= 1 breaks_section_size_y_px = self.style.break_size if self.style is not None else 20 if not area_has_breaks: if len(self.sections.get_sections()) == 0: logger.error(f"An area view without sections made its through the process. " f"This shouldn't be happening") exit(-1) self.processed_section_views.append(self) return total_breaks_size_y_px = self._get_break_total_size_before_transform_px() total_non_breaks_size_y_px = self._get_non_breaks_total_size_px(total_breaks_size_y_px) # Size gained at the area after flagged sections transformed to breaks expandable_size_px = total_breaks_size_y_px - (breaks_section_size_y_px * breaks_count) last_area_pos = self.pos_y + self.size_y for i, section_group in enumerate(split_section_groups): # TODO: ideally, instead of doing this, we should have previously obtained an array # of sub-areas with already start and end values set. That method should live # at area class and not at sections. That is, kill the # `split_sections_around_breaks` method if section_group is split_section_groups[0]: start_addr = self.start_address end_addr = split_section_groups[1].lowest_memory elif section_group is split_section_groups[-1]: end_addr = self.end_address if self.end_address > section_group.highest_memory else section_group.highest_memory start_addr = split_section_groups[-2].highest_memory elif section_group.is_break_section_group(): start_addr = section_group.lowest_memory end_addr = section_group.highest_memory else: start_addr = split_section_groups[i - 1].highest_memory end_addr = split_section_groups[i + 1].lowest_memory # Assign new calculated size y corrected_size_y_px = breaks_section_size_y_px \ if section_group.is_break_section_group() \ else recalculate_subarea_size_y(start_addr, end_addr) subconfig = area_config_clone(self.area, last_area_pos, corrected_size_y_px, start_addr, end_addr) last_area_pos = subconfig['pos'][1] self.processed_section_views.append(AreaView( sections=section_group, area_config=subconfig, labels=self.labels, style=self.style, is_subarea=True) ) def _get_break_total_size_before_transform_px(self): """ Compute the sum of pixels that the break sections would occupy if they wouldn't be break sections :return: Computed size in pixels """ total_breaks_size_px = 0 for _break in self.sections.filter_breaks().get_sections(): total_breaks_size_px += self.to_pixels(_break.size) return total_breaks_size_px def _get_non_breaks_total_size_px(self, breaks_size_y_sum_px): """ Get pixel count at y of displayed memory that is not break-flagged section before transformation into a break section. That takes into account both normal sections and empty memory :param breaks_size_y_sum_px: Total pixel count at y axis occupied by break-flagged sections before transformation into break sections. :return: """ highest_mem = self.end_address if self.end_address > self.sections.highest_memory else self.sections.highest_memory lowest_mem = self.start_address if self.start_address < self.sections.lowest_memory else self.sections.lowest_memory return self.to_pixels(highest_mem - lowest_mem) - breaks_size_y_sum_px ================================================ FILE: examples/break_config.yaml ================================================ size: [950, 570] variables: graphite: &graphite '#2A363B' pastel_green: &pastel_green '#99B898' style: text-fill: 'black' break-size: 60 #background: *graphite fill: *pastel_green hide-address: true hide-size: true stroke: *graphite stroke-width: 0 font_type: 'Helvetica' areas: - area: title: 'Wave' pos: [30, 50] style: break-type: '~' sections: - names: [ External RAM ] flags: break - names: [ External Device, Peripheral, SRAM Area, External RAM ] style: stroke-width: 1 - area: title: 'Double-wave' pos: [260, 50] style: break-type: '≈' stroke-width: 1 sections: - names: [ External RAM ] flags: break - area: title: 'Diagonal' pos: [490, 50] style: break-type: '/' sections: - names: [ External RAM ] flags: break - names: [ External Device, Peripheral, SRAM Area, External RAM ] style: stroke-width: 1 - area: title: 'Dots' pos: [720, 50] style: break-type: '...' stroke-width: 1 sections: - names: [ External RAM ] flags: break ================================================ FILE: examples/break_map.yaml ================================================ map: - address: 0x20000000 size: 0x20000000 id: SRAM Area type: area - address: 0x40000000 size: 0x20000000 id: Peripheral type: area - address: 0x60000000 size: 0x40000000 id: External RAM type: area - address: 0xA0000000 size: 0x40000000 id: External Device type: area ================================================ FILE: examples/labels_config.yaml ================================================ size: [800, 300] variables: graphite: &graphite '#2A363B' pastel_green: &pastel_green '#99B898' pastel_yellow: &pastel_yellow '#FECEA8' pastel_orange: &pastel_orange '#FF847C' pastel_red: &pastel_red '#E84A5F' style: text-fill: lightgrey background: *graphite hide-size: true areas: - area: title: 'Address Link Example' size: [ 200, 200 ] pos: [200] range: [0xE8040000, 0xE8044000] style: stroke: grey stroke-width: 1 weight: 1 text_fill: lightgrey stroke-dasharray: '1' labels: - address: 0xe8043800 text: In direction length: 150 directions: in style: stroke-dasharray: '5,1,3,1,3' - address: 0xe8042800 text: No direction length: 150 side: right style: stroke-dasharray: '2,2' - address: 0xe8041800 text: Out direction length: 150 directions: out - address: 0xe8041800 text: Label at left side length: 40 directions: out side: left - address: 0xe8040800 text: In and Out directions length: 150 directions: [in, out] sections: - names: [ ROM Table ] style: fill: *pastel_green - names: [ External PPB ] style: fill: *pastel_yellow text_fill: grey - names: [ ETM, Peripheral ] style: fill: *pastel_orange - names: [ TPIU ] style: fill: *pastel_red ================================================ FILE: examples/labels_map.yaml ================================================ map: - address: 0xE8040000 size: 0x00001000 id: TPIU type: section - address: 0xE8041000 size: 0x00001000 id: ETM type: section - address: 0xE8042000 size: 0x00001000 id: External PPB type: section - address: 0xE8043000 size: 0x00001000 id: ROM Table type: section ================================================ FILE: examples/link_config.yaml ================================================ size: [750, 650] variables: graphite: &graphite '#2A363B' pastel_green: &pastel_green '#99B898' pastel_yellow: &pastel_yellow '#FECEA8' pastel_orange: &pastel_orange '#FF847C' pastel_red: &pastel_red '#E84A5F' style: text-fill: 'lightgrey' background: *graphite fill: '#99B898' hide-size: true stroke: *graphite stroke-width: 1 text-stroke: 'black' text-stroke-width: 0 font-type: 'Helvetica' break-type: '~' break-size: 100 areas: - area: title: 'Full Memory Map' size: [200, 500] range: [0x0, 0x100000000] section-size: [0x02F00000] style: hide-address: true fill: *pastel_green sections: - names: [ External Device, External Ram ] fill: '#FF847C' style: hide-size: true hide-address: true - area: title: 'Address Link Example' pos: [ 400, 50 ] size: [ 200, 200 ] range: [0xE8040000, 0xE8044000] sections: # Adding the 'regions' sub-level under 'style' - names: [ ROM Table ] style: fill: *pastel_green - names: [ External PPB ] style: fill: *pastel_yellow - names: [ ETM, Peripheral ] flags: break style: fill: *pastel_orange - names: [ TPIU ] style: fill: *pastel_red - area: title: pos: [ 400, 330 ] size: [ 200, 300 ] range: [0x40000000, 0x43000000] sections: # Adding the 'regions' sub-level under 'style' - names: [ Bit ] flags: break style: fill: *pastel_yellow - names: [ Bit band region ] style: fill: *pastel_orange - names: [ Reserved ] flags: grows-down links: style: stroke: 'lightgray' stroke-width: 1 fill: 'gray' addresses: [ 0xe8040000 ] sections: [['Bit band region', 'Bit band alias'], ['TPIU', 'External PPB']] ================================================ FILE: examples/link_map.yaml ================================================ map: - address: 0x20000000 size: 0x20000000 id: SRAM Area type: area - address: 0x40000000 size: 0x20000000 id: Peripheral type: area - address: 0x60000000 size: 0x40000000 id: External RAM type: area - address: 0xA0000000 size: 0x40000000 id: External Device type: area - address: 0xE0000000 size: 0x08000000 id: Private Periph. Bus - Internal type: area - address: 0xE8000000 size: 0x08000000 id: Private Periph. Bus - External type: area - address: 0xF0000000 size: 0x08000000 id: Vendor Specific type: area - address: 0xE8040000 size: 0x00001000 id: TPIU type: section - address: 0xE8041000 size: 0x00001000 id: ETM type: section - address: 0xE8042000 size: 0x00001000 id: External PPB type: section - address: 0xE8043000 size: 0x00001000 id: ROM Table type: section - address: 0xE0000000 size: 0x00010000 id: ITM type: section - address: 0xE0010000 size: 0x00010000 id: DWT type: section - address: 0xE0020000 size: 0x00010000 id: FPB type: section - address: 0xE0030000 size: 0x000B0000 id: Reserved type: section - address: 0xE00E0000 size: 0x00010000 id: NVIC type: section - address: 0xE00F0000 size: 0x00010000 id: NVIC type: section - address: 0x20000000 size: 0x00100000 id: Bit band region type: section - address: 0x20100000 size: 0x01F00000 id: Bit type: section - address: 0x22000000 size: 0x01000000 id: Bit band alias type: section - address: 0x40000000 size: 0x00100000 id: Bit band region type: section - address: 0x40100000 size: 0x01F00000 id: Bit type: section - address: 0x42000000 size: 0x01000000 id: Bit band alias type: section ================================================ FILE: examples/sample_config.yaml ================================================ size: [500,1100] variables: graphite: &graphite '#212b38' cleargraphite: &cleargraphite '#37465b' mydarkgreen: &mydarkgreen '#08c6ab' style: text-fill: 'white' background: *graphite fill: *mydarkgreen hide-address: false stroke: *cleargraphite stroke-width: 0.5 text-stroke-width: 0.1 font-size: 12 font_type: 'Helvetica' areas: - area: title: ESP32 app space (extract) range: [0x400d0000, 0x400da000] section-size: [0x000010,0x0008000] pos: [ 100, 50 ] size: [200, 1000] ================================================ FILE: examples/stack_config.yaml ================================================ size: [300, 600] variables: green: &green '#47b39d' yellow: &yellow '#ffc153' orange: &orange '#eb6b56' granat: &granat '#b05f6d' purple: &purple '#462446' graphite: &graphite '#313b48' style: hide-size: true hide-address: true growth-arrow-size: 2 stroke-width: 0 areas: - area: style: background: *graphite title: 'Full Memory Map' sections: - names: [Text] style: fill: *green - names: [ Initialized data ] style: fill: *yellow - names: [Uninitialized data] style: fill: *orange - names: [ Heap ] flags: grows-up style: fill: *granat - names: [ Stack ] flags: grows-down style: fill: *purple text-fill: white ================================================ FILE: examples/stack_map.yaml ================================================ map: - address: 0x20000000 size: 0x10000000 id: Text type: area - address: 0x30000000 size: 0x10000000 id: Initialized data type: area - address: 0x40000000 size: 0x10000000 id: Uninitialized data type: area - address: 0x50000000 size: 0x10000000 id: Heap type: area # flags: grows-up - address: 0xA0000000 size: 0x10000000 id: Stack type: area #flags: grows-down ================================================ FILE: examples/stm32f103_config.yaml ================================================ size: [1100,1100] variables: graphite: &graphite '#212b38' cleargraphite: &cleargraphite '#37465b' myturqoise: &myturqoise '#4aefd7' mydarkgreen: &mydarkgreen '#08c6ab' mypurple: &mypurple '#726eff' #mylightblue: &mylightblue '#7bd5f5' #myblue: &myblue '#1ca7ec' #mydarkblue: &mydarkblue '#1f2f98' mylightgrey: &mylightgrey '#F6F6F6' style: text-fill: 'white' break-size: 25 break-type: '≈' growth-arrow-size: 2 growth-arrow-fill: 'white' growth-arrow-stroke: 'black' background: *graphite fill: *mydarkgreen hide-size: true # stroke: *mydarkblue stroke-width: 0 text-stroke: 'black' text-stroke-width: 0.1 font-size: 12 font_type: 'Helvetica' areas: - area: title: STM32F103 Memory Space range: [0x0, 0x100000000] pos: [ 100, 50 ] size: [200, 700] style: hide-size: true sections: - names: [none] style: hide-name: true fill: *cleargraphite - area: range: [0x08000000, 0x0801FFFF] size: [200, 100] pos: [450, 700] style: hide-size: false fill: *myturqoise sections: - names: [Flash Memory] style: fill: *mypurple - area: range: [0x1FFFF000, 0x1FFFF80F] size: [200, 100] pos: [450, 550] style: hide-size: false sections: - names: [System Memory] style: fill: *myturqoise - area: range: [0xE0000000, 0xE1000000] size: [200, 200] pos: [450, 50] style: hide-size: false sections: - names: [ M3 Cortex Internal Peripherals ] - area: title: APB memory space range: [0x40000000, 0x40030000] pos: [ 750, 50 ] size: [ 200, 1000 ] sections: - names: [Reserved1, Reserved2, Reserved3, Reserved4, Reserved5, Reserved6, Reserved7, Reserved8, Reserved9, Reserved10, Reserved11] flags: break style: fill: *cleargraphite - names: [AFIO, EXTI, PORT A, PORT B, PORT C, PORT D, PORT E ] style: fill: *mypurple links: style: opacity: 0.2 fill: *mylightgrey stroke: lightgrey sections: [[TIM2, CRC], M3 Cortex Internal Peripherals, Flash Memory, System Memory] ================================================ FILE: examples/stm32f103_map.yaml ================================================ map: - id: Aliased address: 0x00000000 size: 0x08000000 type: area - id: Flash Memory address: 0x08000000 size: 0x0001FFFF type: area - id: Reserved0 address: 0x0801FFFF size: 0x1F7FD001 type: area - id: System Memory address: 0x1FFFF000 size: 0x00000800 type: area - id: Option Bytes address: 0x1FFFF800 size: 0x0000000F type: area - id: Reserved00 address: 0x1FFFF80F size: 0x00000800 type: area - id: none address: 0x00000000 size: 0x20000000 type: area - id: none address: 0x20000000 size: 0x20000000 type: area - id: none address: 0x40000000 size: 0x20000000 type: area - id: none address: 0x60000000 size: 0x20000000 type: area - id: none address: 0x80000000 size: 0x20000000 type: area - id: none address: 0xA0000000 size: 0x20000000 type: area - id: none address: 0xC0000000 size: 0x20000000 type: area - id: none address: 0xE0100000 size: 0x0FE00000 type: area - id: SRAM address: 0x20000000 size: 0x08000000 type: area - id: Peripherals address: 0x40000000 size: 0x08000000 type: area - id: M3 Cortex Internal Peripherals address: 0xE0000000 size: 0x01000000 type: area - id: TIM2 address: 0x40000000 size: 0x00000400 type: area - id: TIM3 address: 0x40000400 size: 0x00000400 type: area - id: TIM4 address: 0x40000800 size: 0x00000400 type: area - id: Reserved1 address: 0x40000C00 size: 0x1c00 type: area - id: RTC address: 0x40002800 size: 0x00000400 type: area - id: WWDG address: 0x40002C00 size: 0x00000400 type: area - id: IWDG address: 0x40003000 size: 0x00000400 type: area - id: Reserved2 address: 0x40003400 size: 0x00000400 type: area - id: SPI2 address: 0x40003800 size: 0x00000400 type: area - id: Reserved3 address: 0x40003C00 size: 0x00000800 type: area - id: USART2 address: 0x40004400 size: 0x00000400 type: area - id: USART3 address: 0x40004800 size: 0x00000400 type: area - id: Reserved4 address: 0x40004C00 size: 0x00000800 type: area - id: I2C1 address: 0x40005400 size: 0x00000400 type: area - id: I2C2 address: 0x40005800 size: 0x00000400 type: area - id: USB Registers address: 0x40005C00 size: 0x00000400 type: area - id: USB/CAN SRAM address: 0x40006000 size: 0x00000400 type: area - id: BXCAN address: 0x40006400 size: 0x00000400 type: area - id: Reserved5 address: 0x40006800 size: 0x00000400 type: area - id: BKP address: 0x40006C00 size: 0x00000400 type: area - id: PWR address: 0x40007000 size: 0x00000400 type: area - id: Reserved6 address: 0x40007400 size: 0x00008C00 type: area - id: AFIO address: 0x40010000 size: 0x00000400 type: area - id: EXTI address: 0x40010400 size: 0x00000400 type: area - id: PORT A address: 0x40010800 size: 0x00000400 type: area - id: PORT B address: 0x40010C00 size: 0x00000400 type: area - id: PORT C address: 0x40011000 size: 0x00000400 type: area - id: PORT D address: 0x40011400 size: 0x00000400 type: area - id: PORT E address: 0x40011800 size: 0x00000400 type: area - id: Reserved7 address: 0x40011C00 size: 0x00000800 type: area - id: ADC1 address: 0x40012400 size: 0x00000400 type: area - id: ADC2 address: 0x40012800 size: 0x00000400 type: area - id: TIM1 address: 0x40012C00 size: 0x00000400 type: area - id: SPI1 address: 0x40013000 size: 0x00000400 type: area - id: Reserved7 address: 0x40013400 size: 0x00000400 type: area - id: USART1 address: 0x40013800 size: 0x00000400 type: area - id: Reserved8 address: 0x40013C00 size: 0x0000C400 type: area - id: DMA address: 0x40020000 size: 0x00000400 type: area - id: Reserved9 address: 0x40020400 size: 0x00000C00 type: area - id: RCC address: 0x40021000 size: 0x00000400 type: area - id: Reserved10 address: 0x40021400 size: 0x00000C00 type: area - id: Flash Interface address: 0x40022000 size: 0x00000400 type: area - id: Reserved11 address: 0x40022400 size: 0x00000C00 type: area - id: CRC address: 0x40023000 size: 0x00000400 type: area ================================================ FILE: gnu_linker_map_parser.py ================================================ import re import yaml from section import Section class GNULinkerMapParser: """ Parse a GNU linker map file and convert it to a yaml file for further processing """ def __init__(self, input_filename, output_filename): self.sections = [] self.subsections = [] self.input_filename = input_filename self.output_filename = output_filename def parse(self): with open(self.input_filename, 'r', encoding='utf8') as file: file_iterator = iter(file) prev_line = next(file_iterator) for line in file_iterator: self.process_areas(prev_line) multiple_line = prev_line + line self.process_sections(multiple_line) prev_line = line my_dict = {'map': []} for section in self.sections: my_dict['map'].append({ 'type': 'area', 'address': section.address, 'size': section.size, 'id': section.id, 'flags': section.flags }) for subsection in self.subsections: my_dict['map'].append({ 'type': 'section', 'parent': subsection.parent, 'address': subsection.address, 'size': subsection.size, 'id': subsection.id, 'flags': subsection.flags }) with open(self.output_filename, 'w', encoding='utf8') as file: yaml_string = yaml.dump(my_dict) file.write(yaml_string) def process_areas(self, line): pattern = r'([.][a-z]{1,})[ ]{1,}(0x[a-fA-F0-9]{1,})[ ]{1,}(0x[a-fA-F0-9]{1,})\n' p = re.compile(pattern) result = p.search(line) if result is not None: self.sections.append(Section(parent=None, id=result.group(1), address=int(result.group(2), 0), size=int(result.group(3), 0), _type='area' ) ) def process_sections(self, line): pattern = r'\s(.[^.]+).([^. \n]+)[\n\r]\s+(0x[0-9a-fA-F]{16})\s+' \ r'(0x[0-9a-fA-F]+)\s+[^\n]+[\n\r]{1}' p = re.compile(pattern) result = p.search(line) if result is not None: self.subsections.append(Section(parent=result.group(1), id=result.group(2), address=int(result.group(3), 0), size=int(result.group(4), 0), _type='section' ) ) ================================================ FILE: helpers.py ================================================ from logger import logger class DefaultAppValues: DOCUMENT_SIZE = (400, 700) POSITION_X = 50 POSITION_Y = 50 SIZE_X = 200 SIZE_Y = 500 TITLE = '' def safe_element_list_get(_list: [], index: int, default=None) -> int: """ Get an element from a list checking if both the list and the element exist :param default: Default value to return if list or element are non existent :param _list: List to extract the element from :param index: Index of the element in the list :return: The expected element if exists, None if it doesn't """ return _list[index] if _list is not None and len(_list) > index else default def safe_element_dict_get(_dict: {}, key: str, default=None) -> int: """ Get an element from a dict checking if both the dict and the element exist :param default: Default value to return if list or element are non existent :param _dict: Dict to extract the element from :param key: Key of the key - value pair to extract :return: The expected element if exists, None if it doesn't """ return _dict[key] if _dict is not None and key in _dict else default ================================================ FILE: labels.py ================================================ import copy from dataclasses import dataclass from style import Style @dataclass class Labels: """ Container for labels, and methods to build them from a yaml label specification """ sections: [] addresses: [] style: Style def __init__(self, labels, style): self.style = style self.labels = self.build_labels(labels) def build_labels(self, labels_yaml) -> []: """ Build a list of labels (`[Label]`) from a list of labels in a yaml format :param labels_yaml: List of labels in a yaml format :return: list of labels (`[Label]`) """ labels = [] for element in labels_yaml: style = copy.deepcopy(self.style) label = Label(style.override_properties_from(Style(element.get('style')))) for key, value in element.items(): if key != 'style': setattr(label, key.replace('-','_'), value) labels.append(label) return labels class Side: RIGHT = 'right' LEFT = 'left' @dataclass class Label: """ Stores single label information for a given address. Additionally, provides style information for drawing the link """ def __init__(self, style): self.style = style self.address = 0 self.text = 'Label' self.length = 20 self.directions = [] self.side = Side.RIGHT ================================================ FILE: linkerscope.py ================================================ #!/usr/bin/env python3 import argparse import copy import yaml from area_view import AreaView from helpers import safe_element_list_get, safe_element_dict_get, DefaultAppValues from links import Links from logger import logger from map_render import MapRender from style import Style from map_file_loader import MapFileLoader from sections import Sections def parse_arguments(): parser = argparse.ArgumentParser() parser.add_argument('input', help='Name of the map file,' 'can be either linker .map files or .yaml descriptor') parser.add_argument('--output', '-o', help='Name for the generated .svg file', default='map.svg') parser.add_argument('--convert', help='Performs the conversion of a .map file to .yaml if a .map file was passed without any additional step', action='store_true', default=False, required=False ) parser.add_argument('--config', '-c', help='Configuration file (.yml). If not specified,' 'will use config.yaml as default', ) return parser.parse_args() def get_area_views(_raw_sections, _base_style, config=None): """ Get the area view/s with the specified style and properties (if any) Given a list of sections, base style and configuration, this function produce a series of area views with specific properties and styles. If a configuration object is passed, it will be used to retrieve additional configured style and properties for one or more area views. If no configuration is passed, only one area view will be generated with the default style and properties :param _raw_sections: A list of unprocessed sections to be selected from and displayed :param _base_style: Base / default style to build child styles from :param config: Optional, configuration object indicating number of areas, style, properties,... :return: A list of configured area views """ def get_default_area_view(sections, style): """ Get an area view configured with default parameters :param sections: A list of unprocessed sections to be selected from and displayed :param style: Base / default style to build child styles from :return: List of one element corresponding to a default area view """ return [AreaView( sections=(Sections(sections=sections)), style=copy.deepcopy(style) )] def get_custom_area_views(sections, style): """ Get a list of area views configured according to passed parameters :param sections: A list of unprocessed sections to be selected from and displayed :param style: Base / default style to build child styles from :return: List of one or various custom area views """ area_views = [] for i, area_element in enumerate(area_configurations): area_config = safe_element_dict_get(area_element, 'area') section_size = safe_element_dict_get(area_config, 'section-size', None) memory_range = safe_element_dict_get(area_config, 'range', None) area_style = copy.deepcopy(style) filtered_sections = (Sections(sections=copy.deepcopy(sections)) .filter_address_min(safe_element_list_get(memory_range, 0)) .filter_address_max(safe_element_list_get(memory_range, 1)) .filter_size_min(safe_element_list_get(section_size, 0)) .filter_size_max(safe_element_list_get(section_size, 1)) ) if len(filtered_sections.get_sections()) == 0: logger.warning(f"Filter for area view with index {i} doesn't result in any" f"section. Try re-adjusting memory range, size, ... This area " f"will be omitted") continue area_views.append( AreaView( sections=filtered_sections, area_config=area_config, style=area_style.override_properties_from( Style(style=safe_element_dict_get(area_config, 'style', None))) ) ) return area_views area_configurations = safe_element_dict_get(config, 'areas', []) or [] if len(area_configurations) == 0: return get_default_area_view(_raw_sections, _base_style) else: return get_custom_area_views(_raw_sections, _base_style) arguments = parse_arguments() raw_sections = MapFileLoader(arguments.input, arguments.convert).parse() base_style = Style().get_default() links = None document_size = DefaultAppValues.DOCUMENT_SIZE configuration = {} # Apply custom configuration if configuration file is available if arguments.config: with open(arguments.config, 'r', encoding='utf-8') as file: configuration = yaml.safe_load(file) if configuration is None: configuration = {} base_style_cpy = copy.deepcopy(base_style) style_config = safe_element_dict_get(configuration, 'style', None) base_style.override_properties_from(Style(style=style_config)) yaml_links = safe_element_dict_get(configuration, 'links', None) links_style = base_style_cpy.override_properties_from( Style(style=safe_element_dict_get(yaml_links, 'style', None))) links = Links(yaml_links, style=links_style) document_size = safe_element_dict_get(configuration, 'size', DefaultAppValues.DOCUMENT_SIZE) MapRender(area_view=get_area_views(raw_sections, base_style, configuration), links=links, style=base_style, file=arguments.output, size=document_size ).draw() ================================================ FILE: links.py ================================================ import logging from helpers import safe_element_dict_get from style import Style from logger import CustomFormatter, logger class Links: """ Stores the link information between given section or address Additionally, provides style information for drawing the link """ sections: [] addresses: [] style: Style def __init__(self, links=None, style=None): self.links = links self.addresses = safe_element_dict_get(self.links, 'addresses', []) self.sections = safe_element_dict_get(self.links, 'sections', []) self.style = style self.configuration_validator() def configuration_validator(self): if self.addresses is None: logger.warning("'addresses' property is declared but is empty. Field has been ignored") self.addresses = [] if self.sections is None: logger.warning("'sections' property is declared but is empty. Field has been ignored") self.sections = [] for address in self.addresses: if not isinstance(address, int): self.addresses.remove(address) logger.warning(f"Link address '{address}' is incorrect: can only be of the type " f"integer. It will be ignored") for section_address in self.sections: if not isinstance(section_address, str) and not isinstance(section_address, list): self.sections.remove(section_address) logger.warning(f"Section link '{section_address}' is incorrect: can only be of the " f"type integer or list. It will be ignored") elif isinstance(section_address, list) and len(section_address) != 2: self.sections.remove(section_address) logger.warning(f"Section link list '{section_address}' can only have exactly two " f"sections. It will be ignored") elif isinstance(section_address, list) and \ (not isinstance(section_address[0], str) or not isinstance(section_address[1], str)): self.sections.remove(section_address) logger.warning(f"Section link list elements'{section_address}' must be strings. " f"They will be ignored") ================================================ FILE: logger.py ================================================ import logging # Adaptation from https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output class CustomFormatter(logging.Formatter): grey = "\x1b[38;20m" yellow = "\x1b[33;20m" red = "\x1b[31;20m" bold_red = "\x1b[31;1m" reset = "\x1b[0m" format = "%(asctime)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" FORMATS = { logging.DEBUG: grey + format + reset, logging.INFO: grey + format + reset, logging.WARNING: yellow + format + reset, logging.ERROR: red + format + reset, logging.CRITICAL: bold_red + format + reset } def format(self, record): log_fmt = self.FORMATS.get(record.levelno) formatter = logging.Formatter(log_fmt) return formatter.format(record) logger = logging.getLogger() logger.setLevel(logging.DEBUG) handler = logging.StreamHandler() handler.setLevel(logging.DEBUG) handler.setFormatter(CustomFormatter()) logger.addHandler(handler) ================================================ FILE: map_file_loader.py ================================================ import os import sys import yaml from logger import logger from section import Section from gnu_linker_map_parser import GNULinkerMapParser class MapFileLoader: """ Takes input file provided by user and loads it in memory for further processing. Depending on the type of file (.map or .yaml) will include an additional conversion step and create a temporary .yaml file """ def __init__(self, file, convert): self.input_filename = file self.convert = convert def parse(self): _, file_extension = os.path.splitext(self.input_filename) if file_extension == '.map': self.parse_map(self.input_filename) if self.convert: logger.info(".map file converted and saved as map.yaml") exit(0) return self.parse_yaml('map.yaml') if file_extension in ['.yaml', '.yml']: if self.convert: logger.error("--convert flag requires a .map file") exit(-1) return self.parse_yaml(self.input_filename) logger.error(f"Wrong map file extension: '{file_extension}'. Use .map or .yaml files") sys.exit(-1) @staticmethod def parse_yaml(filename): sections = [] with open(filename, 'r', encoding='utf-8') as file: y = yaml.safe_load(file) for element in y['map']: sections.append(Section(address=element['address'], size=element['size'], id=element['id'], name=element.get('name'), parent=element.get('parent', 'none'), _type=element.get('type', 'area'), flags=element.get('flags', '') ) ) return sections @staticmethod def parse_map(input_filename): GNULinkerMapParser(input_filename=input_filename, output_filename='map.yaml').parse() ================================================ FILE: map_render.py ================================================ from math import cos from svgwrite import Drawing import svgwrite from helpers import DefaultAppValues from labels import Side from logger import logger from section import Section from style import Style class MapRender: """ This class does the actual rendering of the map. Takes all the graphical information stored at the different sections and areas, together with their style and configuration, and convert them to SVG objects (see `draw()` function) """ dwg: Drawing pointer_y: int def __init__(self, area_view, links, file='map.svg', size=DefaultAppValues.DOCUMENT_SIZE, **kwargs): self.style = kwargs.get('style') self.type = type self.area_views = area_view self.current_style = Style() self.links = links self.links_sections = self._get_valid_linked_sections(links.sections) if links is not None else [] self.file = file self.size = size self.dwg = svgwrite.Drawing(file, profile='full', size=self.size ) def _get_valid_linked_sections(self, linked_sections): """ Get a valid list of linked sections to draw, given a list of wished sections to be linked For a link to be valid, the starting and ending addresses of the linked section/s must be visible and available inside of at least one single area :param linked_sections: List of sections or pair of sections to be linked :return: List of valid (start, end) addresses for sections """ l_sections = [] # Iterate through all linked sections for linked_section in linked_sections: appended = False multi_section = False # Check if we are dealing with a link for a single section or for many of them. # That is, user passed a string or a list of two strings if isinstance(linked_section, list): multi_section = True # Iterate through all available areas checking if this is a valid link: i.e, the # starting and ending addresses of the linked section/s is visible and available # inside of a single area for area in self.area_views: start = None end = None # Exit loop if we found that the link is valid if appended: break for section in area.sections.get_sections(): # If single section, the start and end address of the linked section equals # those of the section if not multi_section: if section.id == linked_section: l_sections.append([section.address, section.address + section.size]) appended = True break # If multiple section, the start and end address of the linked section are the # start of the first provided section and the end of the second provided section # respectively else: if section.id == linked_section[0]: start = section.address elif section.id == linked_section[1]: end = section.address + section.size # If before finishing the iteration on this area, we found a valid start and # end address, we can append this linked section to the list if start is not None and end is not None: l_sections.append([start, end]) appended = True break # If we finish iterating the area, and we have a valid start (or end) address but # the section was not appended, means that the other end of the section is at # another area, and that is not valid if multi_section and not appended and (start is not None or end is not None): logger.warning("A multisection zoom region was specified for two sections" f"of different areas, which is not supported: " f"{linked_section[0]}, {linked_section[1]}") break return l_sections def draw(self): dwg = self.dwg def _draw_area(area) -> svgwrite.container.Group: """ Draw given area Draw the title for the area, then, for each subarea proceed to draw the different elements. Those are the frame and sections, with its information such as labels, name, memory, etc... :param area: Area to be drawn :return Container with an area to be drawn """ area_group = dwg.g() title = self._make_title(area) title.translate(area.pos_x, area.pos_y) area_group.add(title) for sub_area in area.get_split_area_views(): subarea_group = dwg.g() subarea_group.add(self._make_main_frame(sub_area)) for section in sub_area.sections.get_sections(): if section.is_hidden(): continue self._make_section(subarea_group, section, sub_area) subarea_group.translate(sub_area.pos_x, sub_area.pos_y) area_group.add(subarea_group) return area_group def draw_section_links() -> svgwrite.container.Group: linked_sections_group = dwg.g() for section_link in self.links_sections: is_drawn = False for _area_view in self.area_views[1:]: if section_link[0] >= _area_view.sections.lowest_memory and \ section_link[1] <= _area_view.sections.highest_memory and \ section_link[0] >= self.area_views[0].sections.lowest_memory and \ section_link[1] <= self.area_views[0].sections.highest_memory: linked_sections_group.add(self._make_poly(_area_view, section_link[0], section_link[1], self.links.style)) is_drawn = True break if not is_drawn: logger.warning(f"Starting or ending point of the zoom region is outside the " f"shown areas for the link with addresses " f"[{hex(section_link[0])}, {hex(section_link[1])}]") return linked_sections_group def draw_labels() -> svgwrite.container.Group: global_labels = dwg.g() for area in self.area_views: for subarea in area.get_split_area_views(): g = dwg.g() if subarea.labels is not None: for label in subarea.labels.labels: if subarea.sections.has_address(label.address): g.add(self._make_label(label, subarea)) g.translate(subarea.pos_x, subarea.pos_y) global_labels.add(g) return global_labels def draw_growths() -> svgwrite.container.Group: # We need to do another pass once all areas are drawn in order to be able to properly # draw the growth arrows without the break areas hiding them. Also, as we do stuff # outside the loop where the areas are drawn, we loose the reference for translation, # and we have to manually translate the grows here for _area_view in self.area_views: for subarea in _area_view.get_split_area_views(): area_growth = dwg.g() for section in subarea.sections.get_sections(): if section.is_hidden(): continue area_growth.add(self._make_growth(section)) area_growth.translate(subarea.pos_x, subarea.pos_y) growths_group.add(area_growth) return growths_group def draw_links() -> svgwrite.container.Group: lines_group = dwg.g() for address in self.links.addresses: lines_group.add(self._make_link(address, self.links.style)) return lines_group dwg.add(dwg.rect(insert=(0, 0), size=('100%', '100%'), rx=None, ry=None, fill=self.style.background)) growths_group = dwg.g() dwg.add(draw_section_links()) if self.links_sections is not None else None dwg.add(draw_links()) if self.links is not None else None for area_view in self.area_views: dwg.add(_draw_area(area_view)) dwg.add(draw_labels()) dwg.add(draw_growths()) dwg.save() def _make_title(self, area_view): title_pos_x = area_view.size_x / 2 title_pos_y = -20 return self._make_text(area_view.title, (title_pos_x, title_pos_y), style=area_view.style, anchor='middle', text_type='title' ) def _make_growth(self, section: Section) -> svgwrite.container.Group: """ Make the growth arrows for the sections that have it :param section: Section for which to draw the arrow :return: A SVG group containing the new arrows """ group = self.dwg.g() # Why grows doesn't draw on a break section? multiplier = section.style.growth_arrow_size mid_point_x = (section.pos_x + section.size_x) / 2 arrow_head_width = 5 * multiplier arrow_head_height = 10 * multiplier arrow_length = 10 * multiplier arrow_tail_width = 1 * multiplier def _make_growth_arrow_generic(arrow_start_y, direction): points_list = [(mid_point_x - arrow_tail_width, arrow_start_y), (mid_point_x - arrow_tail_width, arrow_start_y - direction * arrow_length), (mid_point_x - arrow_head_width, arrow_start_y - direction * arrow_head_height), (mid_point_x, arrow_start_y - direction * (arrow_length + arrow_head_height)), (mid_point_x + arrow_head_width, arrow_start_y - direction * arrow_head_height), (mid_point_x + arrow_tail_width, arrow_start_y - direction * arrow_length), (mid_point_x + arrow_tail_width, arrow_start_y)] group.add(self.dwg.polyline(points_list, stroke=section.style.growth_arrow_stroke, stroke_width=1, fill=section.style.growth_arrow_fill)) if section.is_grow_up(): _make_growth_arrow_generic(section.pos_y, 1) if section.is_grow_down(): _make_growth_arrow_generic(section.pos_y + section.size_y, -1) return group def _make_main_frame(self, area_view): return self.dwg.rect((0, 0), (area_view.size_x, area_view.size_y), fill=area_view.style.background, stroke=area_view.style.stroke, stroke_width=area_view.style.stroke_width) def _make_box(self, section: Section): return self.dwg.rect((section.pos_x, section.pos_y), (section.size_x, section.size_y), fill=section.style.fill, stroke=section.style.stroke, stroke_width=section.style.stroke_width) def _make_break(self, section: Section) -> svgwrite.container.Group: """ Make a break representation for a given section. Depending on the selected break type (at style/break_type), break can be wave (~), double wave(≈), diagonal(/) or dots(...) :param section: Section for which the break wants to be created :return: SVG group container with the breaks graphics """ group = self.dwg.g() mid_point_x = (section.pos_x + section.size_x) / 2 mid_point_y = (section.pos_y + section.size_y) / 2 style = section.style def _make_break_dots(_section: Section) -> svgwrite.container.Group: """ Make a break representation using dot style :param _section: Section for which the break wants to be created :return: SVG group container with the breaks graphics """ rectangle = self.dwg.rect((_section.pos_x, _section.pos_y), (_section.size_x, _section.size_y)) rectangle.fill(style.fill) rectangle.stroke(style.stroke, width=style.stroke_width) group.add(rectangle) points_list = [ (mid_point_x, mid_point_y), (mid_point_x, mid_point_y + 12), (mid_point_x, mid_point_y - 12), ] for points_set in points_list: group.add(self.dwg.circle(points_set, 3, fill=style.text_fill)) return group def _make_break_wave(_section: Section) -> svgwrite.container.Group: """ Make a break representation using wave style :param _section: Section for which the break wants to be created :return: SVG group container with the breaks graphics """ wave_len = _section.size_x + 1 shifts = [(-5, 2/5, 0), (5, 3 / 5, _section.size_y), ] for shift in shifts: points = [(i, mid_point_y + shift[0] + 2 * cos(i / 24)) for i in range(wave_len)] points.extend( [ (_section.pos_x + _section.size_x, (_section.pos_y + _section.size_y) * shift[1]), (_section.pos_x + _section.size_x, _section.pos_y + shift[2]), (_section.pos_x, _section.pos_y + shift[2]), (_section.pos_x, mid_point_y + shift[0] + 2 * cos(_section.pos_x / 24)), ] ) group.add(self.dwg.polyline(points, stroke=style.stroke, stroke_width=style.stroke_width, fill=style.fill)) return group def _make_break_double_wave(_section: Section) -> svgwrite.container.Group: """ Make a break representation using double wave style :param _section: Section for which the break wants to be created :return: SVG group container with the breaks graphics """ points_list = [[ (_section.pos_x, (_section.pos_y + _section.size_y) * 2 / 5), (_section.pos_x, _section.pos_y), (_section.pos_x + _section.size_x, _section.pos_y), (_section.pos_x + _section.size_x, (_section.pos_y + _section.size_y) * 2 / 5), ], [ (_section.pos_x, (_section.pos_y + _section.size_y) * 3 / 5), (_section.pos_x, _section.pos_y + _section.size_y), (_section.pos_x + _section.size_x, _section.pos_y + _section.size_y), (_section.pos_x + _section.size_x, (_section.pos_y + _section.size_y) * 3 / 5), ] ] rectangle = self.dwg.rect((_section.pos_x, _section.pos_y), (_section.size_x, _section.size_y)) rectangle.fill(section.style.fill) group.add(rectangle) for points_set in points_list: group.add(self.dwg.polyline(points_set, stroke=style.stroke, stroke_width=style.stroke_width, fill='none')) wave_length = 20 shifts = [(0, -5), (0, +5), (_section.size_x, -5), (_section.size_x, +5), ] for shift in shifts: points = [(i - wave_length / 2 + shift[0], mid_point_y + shift[1] + cos(i / 2)) for i in range(wave_length)] group.add(self.dwg.polyline(points, stroke=style.stroke, stroke_width=style.stroke_width, fill='none')) return group def _make_break_diagonal(_section: Section) -> svgwrite.container.Group: """ Make a break representation using diagonal style :param _section: Section for which the break wants to be created :return: SVG group container with the breaks graphics """ points_list = [[(_section.pos_x, _section.pos_y), (_section.pos_x + _section.size_x, _section.pos_y), (_section.pos_x + _section.size_x, (_section.pos_y + _section.size_y) * 3 / 10), (_section.pos_x, (_section.pos_y + _section.size_y) * 5 / 10), (_section.pos_x, _section.pos_y) ], [(_section.pos_x, _section.pos_y + _section.size_y), (_section.pos_x + _section.size_x, _section.pos_y + _section.size_y), (_section.pos_x + _section.size_x, (_section.pos_y + _section.size_y) * 5 / 10), (_section.pos_x, (_section.pos_y + _section.size_y) * 7 / 10), (_section.pos_x, _section.pos_y + _section.size_y), ]] for points_set in points_list: group.add(self.dwg.polyline(points_set, stroke=style.stroke, stroke_width=style.stroke_width, fill=style.fill)) return group breaks = [('/', _make_break_diagonal), ('≈', _make_break_double_wave), ('~', _make_break_wave), ('...', _make_break_dots), ] for _break in breaks: if style.break_type == _break[0]: return _break[1](section) def _make_text(self, text, position, style, text_type='normal', **kwargs): if text_type == 'title': size = '24px' elif text_type == 'small': size = '12px' else: size = style.font_size return self.dwg.text(text, insert=(position[0], position[1]), stroke=style.text_stroke, # focusable='true', fill=style.text_fill, stroke_width=style.text_stroke_width, font_size=size, font_weight="normal", font_family=style.font_type, text_anchor=kwargs.get('anchor', 'middle'), alignment_baseline=kwargs.get('baseline', 'middle') ) def _make_name(self, section): name = section.name if section.name is not None else section.id return self._make_text(name, (section.name_label_pos_x, section.name_label_pos_y), style=section.style, anchor='middle', ) def _make_size_label(self, section): return self._make_text(hex(section.size), (section.size_label_pos[0], section.size_label_pos[1]), section.style, anchor='start', baseline='hanging', text_type='small' ) def _make_address(self, section): return self._make_text(hex(section.address), (section.addr_label_pos_x, section.addr_label_pos_y), anchor='start', style=section.style) def _make_section(self, group, section: Section, area_view): section.size_x = area_view.size_x section.size_y = area_view.to_pixels(section.size) section.pos_y = area_view.to_pixels(area_view.end_address - section.size - section.address) section.pos_x = 0 if section.is_break(): group.add(self._make_break(section)) else: group.add(self._make_box(section)) if not section.is_name_hidden(): group.add(self._make_name(section)) if not section.is_address_hidden(): group.add(self._make_address(section)) if not section.is_size_hidden(): group.add(self._make_size_label(section)) return group def _get_points_for_address(self, address, area_view): left_block_view = self.area_views[0] right_block_view = area_view left_block_x = left_block_view.size_x + left_block_view.pos_x left_block_x2 = left_block_x + 30 left_block_y = left_block_view.pos_y + left_block_view.to_pixels_relative(address) right_block_x = area_view.pos_x right_block_x2 = right_block_x - 30 right_block_y = right_block_view.pos_y + right_block_view.to_pixels_relative(address) return [(left_block_x, left_block_y), (left_block_x2, left_block_y), (right_block_x2, right_block_y), (right_block_x, right_block_y), ] def _make_poly(self, area_view, start_address, end_address, style): def find_right_subarea_view(address, area): """ Given an area, find the subarea where the provided address is :param address: Address to look for :param area: Area that contains the subarea to be found :return: Found subarea, if not found, parent area """ for subarea in area.get_split_area_views(): if subarea.start_address <= address <= subarea.end_address: return subarea return area points = [] end_subarea = find_right_subarea_view(end_address, area_view) start_subarea = find_right_subarea_view(start_address, area_view) _reversed = self._get_points_for_address(end_address, end_subarea) _reversed.reverse() points.extend(self._get_points_for_address(start_address, start_subarea)) points.extend(_reversed) return self.dwg.polyline(points, stroke=style.stroke, stroke_width=style.stroke_width, fill=style.fill, opacity=style.opacity) def _make_arrow_head(self, label, direction='down'): if direction == 'left': angle = 90 elif direction == 'right': angle = 270 elif direction == 'up': angle = 0 else: angle = 180 arrow_head_width = 5 * label.style.weight arrow_head_height = 10 * label.style.weight group = self.dwg.g() points_list = [(0, 0 - arrow_head_height), (0 - arrow_head_width, 0 - arrow_head_height), (0, 0), (0 + arrow_head_width, 0 - arrow_head_height), (0, 0 - arrow_head_height), ] poly = self.dwg.polyline(points_list, stroke=label.style.stroke, stroke_width=1, fill=label.style.stroke) poly.rotate(angle, center=(0, 0)) group.add(poly) return group def _make_label(self, label, area_view): line_label_spacer = 3 g = self.dwg.g() address = label.address text = label.text label_length = label.length if address is None: raise KeyError("A label without address was found") if label.side == Side.RIGHT: pos_x_d = area_view.size_x direction = 1 anchor = 'start' else: pos_x_d = 0 direction = -1 anchor = 'end' pos_y = area_view.to_pixels_relative(address) points = [(0 + pos_x_d, pos_y), (direction*(label_length + pos_x_d), pos_y)] def add_arrow_head(_direction): arrow_direction = 'right' if 'in' == _direction: if label.side == Side.LEFT: arrow_direction = 'right' elif label.side == Side.RIGHT: arrow_direction = 'left' arrow_head_x = pos_x_d elif 'out' == _direction: if label.side == Side.LEFT: arrow_direction = 'left' elif label.side == Side.RIGHT: arrow_direction = 'right' arrow_head_x = direction * (label_length + pos_x_d) else: logger.warning(f"Invalid direction {_direction} provided") return g.add(self._make_arrow_head(label, direction=arrow_direction))\ .translate(arrow_head_x, pos_y) if type(label.directions) == str: add_arrow_head(label.directions) elif type(label.directions) == list: for head_direction in label.directions: add_arrow_head(head_direction) g.add(self._make_text(text, (direction*(pos_x_d + label_length + line_label_spacer), pos_y), label.style, anchor=anchor)) g.add(self.dwg.polyline(points, stroke=label.style.stroke, stroke_dasharray=label.style.stroke_dasharray, stroke_width=label.style.stroke_width )) return g def _make_link(self, address, style): hlines = self.dwg.g(id='hlines', stroke='grey') for area_view in self.area_views[1:]: for subarea in area_view.get_split_area_views(): if not subarea.sections.has_address(address): continue def _make_line(x1, y1, x2, y2): return self.dwg.line(start=(x1, y1), end=(x2, y2), stroke_width=style.stroke_width, stroke=style.stroke) points = self._get_points_for_address(address, subarea) hlines.add(_make_line(x1=points[0][0], y1=points[0][1], x2=points[1][0], y2=points[1][1])) hlines.add(_make_line(x1=points[1][0], y1=points[1][1], x2=points[2][0], y2=points[2][1])) hlines.add(_make_line(x1=points[2][0], y1=points[2][1], x2=points[3][0], y2=points[3][1])) return hlines ================================================ FILE: requirements.txt ================================================ svgwrite==1.4.3 pyyaml==6.0.1 ================================================ FILE: section.py ================================================ from style import Style class Section: """ Holds logical and graphical information for a given section, as well as other properties such as style, visibility, type, etc... """ size: int address: int id: str size_x: int size_y: int pos_x: int pos_y: int label_offset: int = 10 style: Style def __init__(self, size, address, id, _type, parent, flags=[], name=None): self.type = _type self.parent = parent self.size = size self.address = address self.id = id self.name = name self.size_y = 0 self.size_x = 0 self.style = Style() self.flags = flags def is_grow_up(self): return 'grows-up' in self.flags def is_grow_down(self): return 'grows-down' in self.flags def is_break(self): return 'break' in self.flags def is_hidden(self): return 'hidden' in self.flags def _should_element_be_hidden(self, attribute): return True if str(attribute) in ['True', 'yes'] \ else False if str(attribute) in ['False', 'no'] \ else self.size_y < 20 def is_address_hidden(self): return self._should_element_be_hidden(self.style.hide_address) def is_name_hidden(self): return self._should_element_be_hidden(self.style.hide_name) def is_size_hidden(self): return self._should_element_be_hidden(self.style.hide_size) @property def addr_label_pos_x(self): return self.size_x + self.label_offset @property def addr_label_pos_y(self): return self.pos_y + self.size_y @property def name_label_pos_x(self): return self.size_x / 2 @property def size_label_pos(self): return self.pos_x + 2, self.pos_y + 2 @property def name_label_pos_y(self): return self.pos_y + (self.size_y / 2) ================================================ FILE: sections.py ================================================ from section import Section class Sections: """ Provide methods and to select and filter sections according to their base address, size, parent, type,... """ sections: [Section] = [] def __init__(self, sections: [Section]): self.sections = sections def get_sections(self) -> [Section]: return self.sections @property def highest_section(self) -> int: return max(self.sections, key=lambda x: x.address) @property def highest_address(self) -> int: return max(self.sections, key=lambda x: x.address).address @property def highest_memory(self) -> int: section = max(self.sections, key=lambda x: x.address + x.size) return section.address + section.size @property def lowest_memory(self) -> int: return min(self.sections, key=lambda x: x.address).address @property def lowest_size(self) -> int: return min(self.sections, key=lambda x: x.size).size def has_address(self, address: int) -> bool: for section in self.sections: if section.address <= address <= (section.address + section.size): return True return False def is_break_section_group(self): for section in self.get_sections(): if section.is_break(): return True return False def filter_size_min(self, size_bytes: int): return Sections(self.sections) if size_bytes is None \ else Sections(list(filter(lambda item: item.size > size_bytes, self.sections))) def filter_size_max(self, size_bytes: int): return Sections(self.sections) if size_bytes is None \ else Sections(list(filter(lambda item: item.size < size_bytes, self.sections))) def filter_address_max(self, address_bytes: int): return Sections(self.sections) if address_bytes is None \ else Sections(list(filter(lambda item: (item.address + item.size) <= address_bytes, self.sections))) def filter_address_min(self, address_bytes: int): return Sections(self.sections) if address_bytes is None \ else Sections(list(filter(lambda item: item.address >= address_bytes, self.sections))) def filter_type(self, _type: str): return Sections(self.sections) if _type is None \ else Sections(list(filter(lambda item: item.filter_type == _type, self.sections))) def filter_parent(self, parent: str): return Sections(self.sections) if parent is None \ else Sections(list(filter(lambda item: item.filter_parent == parent, self.sections))) def filter_breaks(self): return Sections(list(filter(lambda item: item.is_break(), self.sections))) def split_sections_around_breaks(self) -> []: """ Split a Sections object into different Sections objects having a break section as delimiter :return: A list of Section objects """ split_sections = [] previous_break_end_address = self.lowest_memory breaks = self.filter_breaks().get_sections() for _break in breaks: # Section that covers from previous break till start of this break # If it was the first break, will cover from begining of the whole area to this break. # Only append if search returns more than 0 counts s = Sections(sections=self.sections) \ .filter_address_max(_break.address) \ .filter_address_min(previous_break_end_address) if len(s.get_sections()) > 0: split_sections.append(s) # This section covers the break itself split_sections.append(Sections(sections=[_break])) previous_break_end_address = _break.address + _break.size # Section that covers from the last break end address to the end of the whole area. Only # append if search returns more than 0 counts last_group = Sections(sections=self.sections) \ .filter_address_max(self.highest_memory) \ .filter_address_min(previous_break_end_address) if len(last_group.sections) > 0: split_sections.append(last_group) return split_sections ================================================ FILE: style.py ================================================ class Style: """ Holds style for different rendering objects """ # Non SVG background: str break_type: str break_size: int growth_arrow_size: float growth_arrow_stroke: str growth_arrow_fill: str stroke_dasharray: str hide_size: str hide_name: str hide_address: str # SVG fill: str stroke: str stroke_width: int size: int font_size: int font_type: str weight: int opacity: int text_stroke: str text_stroke_width: int text_fill: str weight: int def __init__(self, style=None): if style is not None: for key, value in style.items(): setattr(self, key.replace('-', '_'), style.get(key, value)) def override_properties_from(self, style): """ Modify self by adding additional members available at the provided style :param style: Style whose members wants to be added :return: New merged styl """ members = [attr for attr in dir(style) if not callable(getattr(style, attr)) and not attr.startswith("__") and getattr( style, attr) is not None] for member in members: value = getattr(style, member) setattr(self, member, value) return self @staticmethod def get_default(): """ Get an initialized default Style instance :return: A default initialized Style instance """ default_style = Style() default_style.break_type = '≈' default_style.break_size = 20 default_style.growth_arrow_size = 1 default_style.background = 'white' default_style.stroke = 'black' default_style.stroke_width = 1 default_style.size = 2 default_style.font_size = 16 default_style.font_type = 'Helvetica' default_style.weight = 1 default_style.opacity = 1 default_style.text_stroke = 'black' default_style.text_fill = 'black' default_style.text_stroke_width = 0 default_style.fill = 'lightgrey' default_style.growth_arrow_fill = 'white' default_style.growth_arrow_stroke = 'black' default_style.stroke_dasharray = '3,2' default_style.weight = 2 default_style.hide_size = 'auto' default_style.hide_name = 'auto' default_style.hide_address = 'auto' return default_style