299 lines
10 KiB
Python
Raw Normal View History

2024-09-11 09:18:07 +02:00
# Using plotly
import plotly.graph_objects as go
import random, argparse, os, datetime
2024-09-11 09:18:07 +02:00
import pandas as pd
2024-09-14 16:40:44 +02:00
"""
This script reads a CSV file with the following columns: start,end,name,order,comment,X0,LR
- name: Name of the function or location (Required)
- start: Start address of the function or location (Required)
- end: End address of the function or location (Required)
Then it generates a memory map of the regions, and outputs an HTML file with the memory map.
"""
class MemoryDrawer():
2024-09-11 09:18:07 +02:00
def __init__(self, input):
"""
If this file is run manually, will take an input .csv path and output a memory map in .html format.
Args:
(Required) input (str): Path to the input .csv file
(Optional) output (str): Path to the output .html file
"""
if isinstance(input, str):
if os.path.isfile(input):
output = f'{os.path.splitext(os.path.basename(input))[0]}_memory_drawer'
data = MemoryDrawer.read_data(pd.read_csv(input))
2024-09-11 09:18:07 +02:00
else:
raise ValueError('Input string must be a path to a .csv file')
elif isinstance(input, pd.DataFrame):
now = datetime.datetime.now()
output = f'{now.strftime("%Y-%m-%d_%H-%M-%S")}_memory_drawer'
data = MemoryDrawer.read_data(input)
else:
raise ValueError('Input must be a path to a .csv file or a pandas DataFrame')
fig = MemoryDrawer.draw_diagram(data)
MemoryDrawer.write_output(fig, output)
def read_data(data):
def _convert_to_int(value):
try:
if isinstance(value, str) and value.startswith('0x'):
return int(value, 16)
else:
return int(value)
except ValueError:
return value
data['start'] = data['start'].apply(_convert_to_int)
data['end'] = data['end'].apply(_convert_to_int)
data['size'] = data['end'] - data['start']
2025-01-03 20:31:34 +01:00
# Sort start on ascending order, and size on descending order
data.sort_values(by=['start', 'size'], inplace=True, ascending=[True, False])
# Inverse the order of the data
data.reset_index(drop=True, inplace=True)
data['overlap'] = False
data['index'] = data.index
for i, row in data.iterrows():
data.at[i, 'overlap'] = False
data.at[i, 'partial_overlap'] = False
# Annotate rows that fully overlap the current row
temp = data.loc[(data['start'] <= row['start']) & (data['end'] >= row['end'])]
if temp.shape[0] > 1:
data.at[i, 'overlap'] = True
data.at[i, 'overlapped_by'] = ','.join(temp['index'].astype(str).to_list())
# Annotate rows that partially overlap the current row (from start, but not to end)
2025-01-03 20:31:34 +01:00
temp = data.loc[(data['start'] <= row['start']) & (data['end'] < row['end']) & (data['end'] > row['start'])]
if temp.shape[0] > 1:
data.at[i, 'partial_overlap'] = "Bottom"
data.at[i, 'partial_overlapped_by'] = ','.join(temp['index'].astype(str).to_list())
# Annotate rows that partially overlap the current row (from end, but not to start)
2025-01-03 20:31:34 +01:00
temp = data.loc[(data['start'] > row['start']) & (data['end'] >= row['end']) & (data['start'] < row['end'])]
if temp.shape[0] > 1:
data.at[i, 'partial_overlap'] = "Top"
data.at[i, 'partial_overlapped_by'] = ','.join(temp['index'].astype(str).to_list())
# Also annotate which regions this row is overlapping
temp = data.loc[(data['start'] >= row['start']) & (data['end'] <= row['end'])]
if temp.shape[0] > 1:
data.at[i, 'overlap'] = True
data.at[i, 'overlapping'] = ','.join(temp['index'].astype(str).to_list())
# Send warnings if sizes are negative
if (data['size'] < 0).any():
print(f'Warning: Negative sizes detected at indices {data[data["size"] < 0].index}')
2025-01-03 20:31:34 +01:00
data["n_overlaps"] = data["overlapped_by"].apply(lambda x: len(str(x).split(',')) if pd.notna(x) else pd.NA)
2024-09-11 09:18:07 +02:00
return data
2024-09-14 16:40:44 +02:00
def draw_diagram(data, vertical_gap_percentage=0.12, horizontal_gap=0.1):
tickpointers = []
labels = pd.DataFrame()
2024-09-14 16:40:44 +02:00
def random_color():
return f'#{random.randint(0, 0xFFFFFF):06x}'
2024-09-11 09:18:07 +02:00
fig = go.Figure()
fig.update_layout(font=dict(family="Courier New, monospace"))
2024-09-11 09:18:07 +02:00
fig.update_layout(
plot_bgcolor='#FFFFFF',
)
2024-09-14 16:40:44 +02:00
for i, d in data.iterrows():
fillcolor = random_color()
data.at[i, 'fillcolor'] = fillcolor
# Set base x values. Width of the rectangle.
x0 = 1
x1 = 6
# Set base y values. Height of the rectangle.
y0 = d['index']
y1 = d['index']+1
if d['overlap'] == True:
2025-01-03 20:31:34 +01:00
n_overlaps = str(d['overlapped_by']).split(',')
# Row is overlapping the current row
if pd.notna(d['overlapping']):
y0 = sorted(map(int, d['overlapping'].split(',')))[0]
y1 = sorted(map(int, d['overlapping'].split(',')))[-1] + 1
if pd.notna(d['overlapped_by']):
y0 = y0
y1 = y1
2025-01-03 20:31:34 +01:00
x0 = x0 + (horizontal_gap*d['n_overlaps'])
x1 = x1 - (horizontal_gap*d['n_overlaps'])
if d['partial_overlap'] == "Bottom":
if pd.notna(d['partial_overlapped_by']) and isinstance(d['partial_overlapped_by'], int):
y0 = y0 + 0.25 + (0.6**n_overlaps)
if d['partial_overlap'] == "Top":
if pd.notna(d['partial_overlapped_by']) and isinstance(d['partial_overlapped_by'], int):
y1 = y1 - (0.6**n_overlaps)
# Adjust y axis if there are multiple regions with the same end (otherwise would be drawn on top of each other)
if i < data.size - 1:
unique_end = data.loc[data['end'] == d['end']].reset_index(drop=True)
print(d['name'])
if len(unique_end) > 1:
y1 = y1 - vertical_gap_percentage*(unique_end.loc[unique_end['name'] == d['name']].index[0]+1)
fig.add_shape(
type="rect",
x0=x0,
x1=x1,
y0=y0+vertical_gap_percentage,
y1=y1-vertical_gap_percentage,
line=dict(width=1),
fillcolor=fillcolor,
opacity=0.4,
layer="below",
)
### Add middle text
2024-09-14 16:40:44 +02:00
fig.add_trace(go.Scatter
(
x=[(x0+x1)/2],
2025-01-03 20:31:34 +01:00
y=[y0+0.5],
text=d['name'],
2024-09-14 16:40:44 +02:00
mode="text",
textposition="middle center",
name=d['name'],
2024-09-14 16:40:44 +02:00
marker=dict(
color=fillcolor,
),
))
### Add top-left text with d['end']
# Overlapped to the right, to make it more readable
if pd.notna(d['overlapped_by']):
fig.add_trace(go.Scatter
(
x=[(x1-0.24+horizontal_gap)],
y=[y1-0.16],
text=hex(d['end']),
mode="text",
textposition="middle center",
marker=dict(
color=fillcolor,
),
showlegend=False,
))
# Add bottom-left text with d['end']
fig.add_trace(go.Scatter
(
x=[(x1-0.24+horizontal_gap)],
y=[y0+0.14],
text=hex(d['start']),
mode="text",
textposition="middle center",
marker=dict(
color=fillcolor,
),
showlegend=False,
))
else:
fig.add_trace(go.Scatter
(
x=[(x0+0.14+horizontal_gap)],
y=[y1-0.16],
text=hex(d['end']),
mode="text",
textposition="middle center",
marker=dict(
color=fillcolor,
),
showlegend=False,
))
### Add bottom-left text with d['end']
fig.add_trace(go.Scatter
(
x=[(x0+0.14+horizontal_gap)],
y=[y0+0.14],
text=hex(d['start']),
mode="text",
textposition="middle center",
marker=dict(
color=fillcolor,
),
showlegend=False,
))
fig.update_xaxes(
range=[0, 7],
tickvals=[0, 1, 2, 3, 4, 5, 6, 7],
)
2024-09-11 09:18:07 +02:00
start_values = data['start'].sort_values()
end_values = data['end'].sort_values()
2024-09-11 09:18:07 +02:00
labels = []
2024-09-11 09:18:07 +02:00
for i, d in data.iterrows():
if i == 0:
labels.append(f'{hex(start_values.iloc[i])}')
elif i == len(data)-1:
labels.append(f'{hex(end_values.iloc[i])}')
else:
labels.append(f'{hex(start_values.iloc[i])}<br>{hex(end_values.iloc[i-1])}')
tickpointers = [i for i in range(len(data))]
fig.update_yaxes(
tickvals = tickpointers,
griddash="longdashdot",
gridwidth=0,
gridcolor="black",
showgrid=False,
showticklabels=False,
autorange='reversed',
)
fig.update_xaxes(
showgrid=False,
showticklabels=False,
)
fig.update_layout(
width=1200,
height=1200,
autosize=True,
margin=dict(l=200, r=20, t=20, b=20),
font=dict(
size=18,
),
legend_title_text="Function/Locations",
)
return fig
def write_output(fig, output_file):
fig.write_html(f'{output_file}.html')
2024-09-11 09:18:07 +02:00
if __name__ == '__main__':
argparser = argparse.ArgumentParser()
argparser.add_argument('--input', help='Input CSV file path', required=True, type=str)
2024-09-11 09:18:07 +02:00
args = argparser.parse_args()
MemoryDrawer(args.input)