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.

## 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'
# ...
```

> 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:

> 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:

> 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

> 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']
```

> 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