508 lines
22 KiB
Python
508 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
|
# action_replicate_layout.py
|
|
#
|
|
# Copyright (C) 2019-2022 Mitja Nemec
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
# MA 02110-1301, USA.
|
|
#
|
|
#
|
|
import wx
|
|
import pcbnew
|
|
import os
|
|
import logging
|
|
import sys
|
|
import time
|
|
from .replicate_layout_GUI import ReplicateLayoutGUI
|
|
from .error_dialog_GUI import ErrorDialogGUI
|
|
from .replicate_layout import Replicator
|
|
from .replicate_layout import Settings
|
|
from .conn_issue_GUI import ConnIssueGUI
|
|
|
|
|
|
class ConnIssueDialog(ConnIssueGUI):
|
|
def SetSizeHints(self, sz1, sz2):
|
|
# DO NOTHING
|
|
pass
|
|
|
|
def __init__(self, parent, replicator):
|
|
super(ConnIssueDialog, self).__init__(parent)
|
|
|
|
self.list.InsertColumn(0, 'Footprint', width=100)
|
|
self.list.InsertColumn(1, 'Pad', width=100)
|
|
|
|
index = 0
|
|
for issue in replicator.connectivity_issues:
|
|
self.list.InsertItem(index, issue[0])
|
|
self.list.SetItem(index, 1, issue[1])
|
|
index = index + 1
|
|
|
|
|
|
class ErrorDialog(ErrorDialogGUI):
|
|
def SetSizeHints(self, sz1, sz2):
|
|
# DO NOTHING
|
|
pass
|
|
|
|
def __init__(self, parent):
|
|
super(ErrorDialog, self).__init__(parent)
|
|
|
|
|
|
class ReplicateLayoutDialog(ReplicateLayoutGUI):
|
|
def SetSizeHints(self, sz1, sz2):
|
|
# DO NOTHING
|
|
pass
|
|
|
|
def __init__(self, parent, replicator, fp_ref, logger):
|
|
super(ReplicateLayoutDialog, self).__init__(parent)
|
|
|
|
self.logger = logger
|
|
|
|
self.replicator = replicator
|
|
self.src_anchor_fp = self.replicator.get_fp_by_ref(fp_ref)
|
|
self.levels = self.src_anchor_fp.filename
|
|
|
|
# clear levels
|
|
self.list_levels.Clear()
|
|
self.list_levels.AppendItems(self.levels)
|
|
|
|
self.sheet_selection = None
|
|
|
|
self.src_footprints = []
|
|
self.hl_fps = []
|
|
self.hl_items = []
|
|
|
|
# select the bottom most level
|
|
nr_levels = self.list_levels.GetCount()
|
|
self.list_levels.SetSelection(nr_levels - 1)
|
|
self.level_changed(None)
|
|
|
|
def __del__(self):
|
|
self.replicator.highlight_clear_level(self.hl_fps, self.hl_items)
|
|
|
|
def group_layout_changed( self, event ):
|
|
# when enabled, they should be checked by default
|
|
if self.chkbox_group_layouts.GetValue():
|
|
self.chkbox_group_footprints.Enable(True)
|
|
self.chkbox_group_footprints.SetValue(True)
|
|
self.chkbox_group_tracks.Enable(True)
|
|
self.chkbox_group_tracks.SetValue(True)
|
|
self.chkbox_group_zones.Enable(True)
|
|
self.chkbox_group_zones.SetValue(True)
|
|
self.chkbox_group_text.Enable(True)
|
|
self.chkbox_group_text.SetValue(True)
|
|
self.chkbox_group_drawings.Enable(True)
|
|
self.chkbox_group_drawings.SetValue(True)
|
|
else:
|
|
self.chkbox_group_footprints.Disable()
|
|
self.chkbox_group_footprints.SetValue(False)
|
|
self.chkbox_group_tracks.Disable()
|
|
self.chkbox_group_tracks.SetValue(False)
|
|
self.chkbox_group_zones.Disable()
|
|
self.chkbox_group_zones.SetValue(False)
|
|
self.chkbox_group_text.Disable()
|
|
self.chkbox_group_text.SetValue(False)
|
|
self.chkbox_group_drawings.Disable()
|
|
self.chkbox_group_drawings.SetValue(False)
|
|
if event is not None:
|
|
event.Skip()
|
|
|
|
def level_changed(self, event):
|
|
index = self.list_levels.GetSelection()
|
|
list_sheets_choices = self.replicator.get_sheets_to_replicate(self.src_anchor_fp,
|
|
self.src_anchor_fp.sheet_id[index])
|
|
|
|
# show/hide checkbox
|
|
if self.chkbox_group.GetValue():
|
|
self.chkbox_include_group_items.Disable()
|
|
self.chkbox_include_group_items.SetValue(False)
|
|
self.chkbox_intersecting.Disable()
|
|
else:
|
|
self.chkbox_include_group_items.Enable(True)
|
|
self.chkbox_intersecting.Enable(True)
|
|
if self.chkbox_intersecting.GetValue():
|
|
self.chkbox_include_group_items.Enable(True)
|
|
else:
|
|
self.chkbox_include_group_items.Disable()
|
|
self.chkbox_include_group_items.SetValue(False)
|
|
|
|
# clear highlight on all footprints on selected level
|
|
self.replicator.highlight_clear_level(self.hl_fps, self.hl_items)
|
|
self.hl_fps = []
|
|
self.hl_items = []
|
|
pcbnew.Refresh()
|
|
|
|
# get anchor footprints
|
|
anchor_footprints = self.replicator.get_list_of_footprints_with_same_id(self.src_anchor_fp.fp_id)
|
|
# find matching anchors to matching sheets
|
|
ref_list = []
|
|
for sheet in list_sheets_choices:
|
|
for pf in anchor_footprints:
|
|
if "/".join(sheet) in "/".join(pf.sheet_id):
|
|
ref_list.append(pf.ref)
|
|
break
|
|
|
|
sheets_for_list = ['/'.join(x[0]) + " (" + x[1] + ")" for x in zip(list_sheets_choices, ref_list)]
|
|
# clear levels
|
|
self.sheet_selection = self.list_sheets.GetSelections()
|
|
|
|
self.list_sheets.Clear()
|
|
self.list_sheets.AppendItems(sheets_for_list)
|
|
|
|
# if none is selected, select all
|
|
if len(self.sheet_selection) == 0:
|
|
number_of_items = self.list_sheets.GetCount()
|
|
for i in range(number_of_items):
|
|
self.list_sheets.Select(i)
|
|
else:
|
|
for n in range(len(sheets_for_list)):
|
|
if n in self.sheet_selection:
|
|
self.list_sheets.Select(n)
|
|
else:
|
|
self.list_sheets.Deselect(n)
|
|
|
|
# parse the settings
|
|
settings = Settings(rep_tracks=self.chkbox_tracks.GetValue(), rep_zones=self.chkbox_zones.GetValue(),
|
|
rep_text=self.chkbox_text.GetValue(), rep_drawings=self.chkbox_drawings.GetValue(),
|
|
group_layouts=self.chkbox_group_layouts.GetValue(), group_footprints=self.chkbox_group_footprints.GetValue(),
|
|
group_tracks=self.chkbox_group_tracks.GetValue(), group_zones=self.chkbox_group_zones.GetValue(),
|
|
group_text=self.chkbox_group_text.GetValue(), group_drawings=self.chkbox_group_drawings.GetValue(),
|
|
rep_locked_tracks=self.chkbox_locked_tracks.GetValue(), rep_locked_zones=self.chkbox_locked_zones.GetValue(),
|
|
rep_locked_text=self.chkbox_locked_text.GetValue(), rep_locked_drawings=self.chkbox_locked_drawings.GetValue(),
|
|
intersecting=self.chkbox_intersecting.GetValue(), group_items=self.chkbox_include_group_items.GetValue(),
|
|
group_only=self.chkbox_group.GetValue(), locked_fps=self.chkbox_locked.GetValue(),
|
|
remove=self.chkbox_remove.GetValue())
|
|
|
|
# highlight all footprints on selected level
|
|
(self.hl_fps, self.hl_items) = self.replicator.highlight_set_level(self.src_anchor_fp.sheet_id[0:self.list_levels.GetSelection() + 1],
|
|
settings)
|
|
pcbnew.Refresh()
|
|
|
|
if event is not None:
|
|
event.Skip()
|
|
|
|
def on_ok(self, event):
|
|
# clear highlight on all footprints on selected level
|
|
# so that duplicated tracks don't remain selected
|
|
self.replicator.highlight_clear_level(self.hl_fps, self.hl_items)
|
|
self.hl_fps = []
|
|
self.hl_items = []
|
|
|
|
selected_items = self.list_sheets.GetSelections()
|
|
selected_names = []
|
|
for item in selected_items:
|
|
selected_names.append(self.list_sheets.GetString(item))
|
|
|
|
# grab checkboxes
|
|
remove_existing_nets_zones = self.chkbox_remove.GetValue()
|
|
remove_duplicates = self.chkbox_remove_duplicates.GetValue()
|
|
rep_locked = self.chkbox_locked.GetValue()
|
|
group_only = self.chkbox_group.GetValue()
|
|
|
|
# parse the settings
|
|
settings = Settings(rep_tracks=self.chkbox_tracks.GetValue(), rep_zones=self.chkbox_zones.GetValue(),
|
|
rep_text=self.chkbox_text.GetValue(), rep_drawings=self.chkbox_drawings.GetValue(),
|
|
group_layouts=self.chkbox_group_layouts.GetValue(), group_footprints=self.chkbox_group_footprints.GetValue(),
|
|
group_tracks=self.chkbox_group_tracks.GetValue(), group_zones=self.chkbox_group_zones.GetValue(),
|
|
group_text=self.chkbox_group_text.GetValue(), group_drawings=self.chkbox_group_drawings.GetValue(),
|
|
rep_locked_tracks=self.chkbox_locked_tracks.GetValue(), rep_locked_zones=self.chkbox_locked_zones.GetValue(),
|
|
rep_locked_text=self.chkbox_locked_text.GetValue(), rep_locked_drawings=self.chkbox_locked_drawings.GetValue(),
|
|
intersecting=self.chkbox_intersecting.GetValue(), group_items=self.chkbox_include_group_items.GetValue(),
|
|
group_only=self.chkbox_group.GetValue(), locked_fps=self.chkbox_locked.GetValue(),
|
|
remove=self.chkbox_remove.GetValue())
|
|
|
|
# failsafe sometimes on my machine wx does not generate a listbox event
|
|
level = self.list_levels.GetSelection()
|
|
selection_indices = self.list_sheets.GetSelections()
|
|
sheets_on_a_level = self.replicator.get_sheets_to_replicate(self.src_anchor_fp,
|
|
self.src_anchor_fp.sheet_id[level])
|
|
dst_sheets = [sheets_on_a_level[i] for i in selection_indices]
|
|
|
|
# check if all the destination anchor footprints are on the same layer as source anchor footprint
|
|
# first get all the anchor footprints
|
|
all_dst_footprints = []
|
|
for sheet in dst_sheets:
|
|
all_dst_footprints.extend(self.replicator.get_footprints_on_sheet(sheet))
|
|
dst_anchor_footprints = [x for x in all_dst_footprints if x.fp_id == self.src_anchor_fp.fp_id]
|
|
|
|
# replicate now
|
|
self.logger.info("Replicating layout")
|
|
|
|
self.start_time = time.time()
|
|
self.last_time = self.start_time
|
|
self.progress_dlg = wx.ProgressDialog("Preparing for replication", "Starting plugin", maximum=100)
|
|
self.progress_dlg.Show()
|
|
self.progress_dlg.ToggleWindowStyle(wx.STAY_ON_TOP)
|
|
self.Hide()
|
|
|
|
try:
|
|
# update progress dialog
|
|
self.replicator.update_progress = self.update_progress
|
|
self.replicator.replicate_layout(self.src_anchor_fp, self.src_anchor_fp.sheet_id[0:level + 1],
|
|
dst_sheets,
|
|
settings, remove_duplicates)
|
|
|
|
self.logger.info("Replication complete")
|
|
|
|
if self.replicator.connectivity_issues:
|
|
self.logger.info("Letting the user know there are some issues with replicated design")
|
|
report_string = ""
|
|
for item in self.replicator.connectivity_issues:
|
|
report_string = report_string + f"Footprint {item[0]}, pad {item[1]}\n"
|
|
self.logger.info(f"Looks like the design has an exotic connectivity that the plugin might not"
|
|
f" handle properly\n "
|
|
f"Make sure that you check the connectivity around:\n" + report_string)
|
|
# show dialog
|
|
issue_dlg = ConnIssueDialog(self, self.replicator)
|
|
issue_dlg.ShowModal()
|
|
issue_dlg.Destroy()
|
|
|
|
# clear highlight on all footprints on selected level
|
|
self.replicator.highlight_clear_level(self.hl_fps, self.hl_items)
|
|
self.hl_fps = []
|
|
self.hl_items = []
|
|
pcbnew.Refresh()
|
|
|
|
logging.shutdown()
|
|
self.progress_dlg.Destroy()
|
|
event.Skip()
|
|
self.EndModal(True)
|
|
except LookupError as exception:
|
|
# clear highlight on all footprints on selected level
|
|
self.replicator.highlight_clear_level(self.hl_fps, self.hl_items)
|
|
self.hl_fps = []
|
|
self.hl_items = []
|
|
pcbnew.Refresh()
|
|
|
|
caption = 'Replicate Layout'
|
|
message = str(exception)
|
|
dlg = wx.MessageDialog(self, message, caption, wx.OK | wx.ICON_ERROR)
|
|
dlg.ShowModal()
|
|
dlg.Destroy()
|
|
logging.shutdown()
|
|
self.progress_dlg.Destroy()
|
|
event.Skip()
|
|
self.EndModal(False)
|
|
return
|
|
except Exception:
|
|
# clear highlight on all footprints on selected level
|
|
self.replicator.highlight_clear_level(self.hl_fps, self.hl_items)
|
|
self.hl_fps = []
|
|
self.hl_items = []
|
|
pcbnew.Refresh()
|
|
|
|
self.logger.exception("Fatal error when running Replicate layout plugin")
|
|
e_dlg = ErrorDialog(self)
|
|
e_dlg.ShowModal()
|
|
e_dlg.Destroy()
|
|
logging.shutdown()
|
|
self.progress_dlg.Destroy()
|
|
event.Skip()
|
|
self.Destroy()
|
|
|
|
def on_cancel(self, event):
|
|
# clear highlight on all footprints on selected level
|
|
self.replicator.highlight_clear_level(self.hl_fps, self.hl_items)
|
|
self.hl_fps = []
|
|
self.hl_items = []
|
|
pcbnew.Refresh()
|
|
|
|
self.logger.info("User canceled the dialog")
|
|
logging.shutdown()
|
|
event.Skip()
|
|
|
|
self.Destroy()
|
|
|
|
def update_progress(self, stage, percentage, message=None):
|
|
current_time = time.time()
|
|
# update GUI only every 10 ms
|
|
i = int(percentage * 100)
|
|
if message is not None:
|
|
logging.info("updating GUI message: " + repr(message))
|
|
self.progress_dlg.Update(i, message)
|
|
if (current_time - self.last_time) > 0.01:
|
|
self.last_time = current_time
|
|
delta_time = self.last_time - self.start_time
|
|
logging.info("updating GUI with: " + repr(i))
|
|
self.progress_dlg.Update(i)
|
|
|
|
|
|
class ReplicateLayout(pcbnew.ActionPlugin):
|
|
def __init__(self):
|
|
super(ReplicateLayout, self).__init__()
|
|
|
|
self.frame = None
|
|
|
|
self.name = "Replicate layout"
|
|
self.category = "Replicate layout"
|
|
self.description = "Replicates layout of one hierarchical sheet to other copies of the same sheet."
|
|
self.icon_file_name = os.path.join(
|
|
os.path.dirname(__file__), 'replicate_layout_light.png')
|
|
self.dark_icon_file_name = os.path.join(
|
|
os.path.dirname(__file__), 'replicate_layout_dark.png')
|
|
|
|
self.debug_level = logging.INFO
|
|
|
|
# plugin paths
|
|
self.plugin_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
|
self.version_file_path = os.path.join(self.plugin_folder, 'version.txt')
|
|
|
|
# load the plugin version
|
|
with open(self.version_file_path) as fp:
|
|
self.version = fp.readline()
|
|
|
|
def defaults(self):
|
|
pass
|
|
|
|
def Run(self):
|
|
# grab PCB editor frame
|
|
self.frame = wx.FindWindowByName("PcbFrame")
|
|
|
|
# load board
|
|
board = pcbnew.GetBoard()
|
|
pass
|
|
|
|
# go to the project folder - so that log will be in proper place
|
|
os.chdir(os.path.dirname(os.path.abspath(board.GetFileName())))
|
|
|
|
# Remove all handlers associated with the root logger object.
|
|
for handler in logging.root.handlers[:]:
|
|
logging.root.removeHandler(handler)
|
|
|
|
file_handler = logging.FileHandler(filename='replicate_layout.log', mode='w')
|
|
handlers = [file_handler]
|
|
|
|
# set up logger
|
|
logging.basicConfig(level=logging.INFO,
|
|
format='%(asctime)s %(name)s %(lineno)d:%(message)s',
|
|
datefmt='%m-%d %H:%M:%S',
|
|
handlers=handlers)
|
|
logger = logging.getLogger(__name__)
|
|
logger.info("Plugin executed on: " + repr(sys.platform))
|
|
logger.info("Plugin executed with python version: " + repr(sys.version))
|
|
logger.info("KiCad build version: " + str(pcbnew.GetBuildVersion()))
|
|
logger.info("Plugin version: " + self.version)
|
|
logger.info("Frame repr: " + repr(self.frame))
|
|
|
|
# check if there is exactly one footprints selected
|
|
selected_footprints = [x.GetReference() for x in board.GetFootprints() if x.IsSelected()]
|
|
|
|
# if more or less than one show only a message box
|
|
if len(selected_footprints) != 1:
|
|
caption = 'Replicate layout'
|
|
message = "More or less than 1 footprint selected. Please select exactly one footprint " \
|
|
"and run the script again"
|
|
dlg = wx.MessageDialog(self.frame, message, caption, wx.OK | wx.ICON_INFORMATION)
|
|
dlg.ShowModal()
|
|
dlg.Destroy()
|
|
return
|
|
|
|
# this is the source anchor footprint reference
|
|
src_anchor_fp_reference = selected_footprints[0]
|
|
|
|
# search for the Replicate.Layout user layer where replication rooms can be defined
|
|
|
|
if 'Replicate.Layout' in [board.GetLayerName(x) for x in board.GetEnabledLayers().Users()]:
|
|
pass
|
|
|
|
# prepare the replicator
|
|
logger.info("Preparing replicator with " + src_anchor_fp_reference + " as a reference")
|
|
|
|
# TODO return if replication is not possible at all
|
|
try:
|
|
replicator = Replicator(board, src_anchor_fp_reference)
|
|
except LookupError as exception:
|
|
logger.exception("Fatal error when making an instance of replicator")
|
|
caption = 'Replicate Layout'
|
|
message = str(exception)
|
|
dlg = wx.MessageDialog(self.frame, message, caption, wx.OK | wx.ICON_ERROR)
|
|
dlg.ShowModal()
|
|
dlg.Destroy()
|
|
logging.shutdown()
|
|
return
|
|
except Exception:
|
|
logger.exception("Fatal error when making an instance of replicator")
|
|
e_dlg = ErrorDialog(self.frame)
|
|
e_dlg.ShowModal()
|
|
e_dlg.Destroy()
|
|
logging.shutdown()
|
|
return
|
|
|
|
src_anchor_fp = replicator.get_fp_by_ref(src_anchor_fp_reference)
|
|
|
|
# check if source anchor footprint is on root level
|
|
if len(src_anchor_fp.filename) == 0:
|
|
caption = 'Replicate layout'
|
|
message = "Selected anchor footprint is on the root schematic sheet. Replication is not possible."
|
|
dlg = wx.MessageDialog(self.frame, message, caption, wx.OK | wx.ICON_INFORMATION)
|
|
dlg.ShowModal()
|
|
dlg.Destroy()
|
|
return
|
|
|
|
# check if there are at least two sheets pointing to same hierarchical file that the source anchor footprint belongs to
|
|
count = 0
|
|
for filename in replicator.dict_of_sheets.values():
|
|
# filename contain sheet name and sheet filename, check only sheet filename.
|
|
if filename[1] in src_anchor_fp.filename:
|
|
count = count + 1
|
|
if count < 2:
|
|
caption = 'Replicate layout'
|
|
message = "Selected anchor footprint is on the schematic sheet which does not have multiple instances." \
|
|
" Replication is not possible."
|
|
dlg = wx.MessageDialog(self.frame, message, caption, wx.OK | wx.ICON_INFORMATION)
|
|
dlg.ShowModal()
|
|
dlg.Destroy()
|
|
return
|
|
|
|
logger.info(f'source anchor footprint is {repr(src_anchor_fp.ref)}\n'
|
|
f'Located on: {repr(src_anchor_fp.sheet_id)}\n'
|
|
f'With filenames: {repr(src_anchor_fp.filename)}\n'
|
|
f'With sheet_id:{repr(src_anchor_fp.sheet_id)}')
|
|
|
|
list_of_footprints = replicator.get_list_of_footprints_with_same_id(src_anchor_fp.fp_id)
|
|
nice_list = [(x.ref, x.sheet_id) for x in list_of_footprints]
|
|
logger.info(f'Corresponding footprints are \n{repr(nice_list)}')
|
|
|
|
if not list_of_footprints:
|
|
caption = 'Replicate Layout'
|
|
message = "Selected footprint is unique in the pcb (only one footprint with this ID)"
|
|
dlg = wx.MessageDialog(self.frame, message, caption, wx.OK | wx.ICON_ERROR)
|
|
dlg.ShowModal()
|
|
dlg.Destroy()
|
|
logging.shutdown()
|
|
return
|
|
|
|
# show dialog
|
|
logger.info("Showing dialog")
|
|
try:
|
|
dlg = ReplicateLayoutDialog(self.frame, replicator, src_anchor_fp_reference, logger)
|
|
dlg.CenterOnParent()
|
|
# find position of right toolbar
|
|
toolbar_pos = self.frame.FindWindowById(pcbnew.ID_V_TOOLBAR).GetScreenPosition()
|
|
logger.info("Toolbar position: " + repr(toolbar_pos))
|
|
# find site of dialog
|
|
size = dlg.GetSize()
|
|
# place the dialog by the right toolbar
|
|
dialog_position = wx.Point(toolbar_pos[0] - size[0], toolbar_pos[1])
|
|
logger.info("Dialog position: " + repr(dialog_position))
|
|
dlg.SetPosition(dialog_position)
|
|
dlg.Show()
|
|
except Exception:
|
|
logger.exception("Fatal error when making an instance of replicator")
|
|
e_dlg = ErrorDialog(self.frame)
|
|
e_dlg.ShowModal()
|
|
e_dlg.Destroy()
|
|
logging.shutdown()
|
|
return
|