Python Extension to Pick all components in pcb edit from the same original schematic hierarchical sheet

Working on an interactive Python Extension Script which leverages kicad hashes to identify all components and nets which are coming to layout from the same hierarchical schematic. The envisioned function is you click a footprint in pcb editor, envoke script, and the script causes all nets and components from the same hierarchical script to be highlighted so that they can be manipulated as a group.

So, we have studied the hashtags and the python interface extensively. We have written a python script (following) which through debug we have verified is choosing exactly what we want, all of the items we want highlighted are highlighted, and then the script ends and it drops all chosen items. We seem to be 99.9% there, but are missing something. We realize that:
a) The problem might be because we are using an older version of kicad (caused by our OS distributions having long integration cycles which also tends to make them more stable. However, this may be a bug which is already fixed though upgrading right now would cause our workstations to completely crash. If this is the case though it would make sense that we need to setup a dedicated workstation for this purpose… Just need to know. We are using kicad 5.0.2 by the way)
b) The problem might be we are misreading and/or misunderstanding something about the mechanisms behind the kicad extensions. Here the right person telling us the right tip could literally save us months of the most infuriating guessing.
c) Don’t even know… Guessing. Everybody with my team though is completely puzzled by this action. With this, if this tool were to work, it would absolutely be something of general interest and we are completely open as is the idea with open software to allow others to use the extension.

Without further stuff… here it is… Any thoughts greatly appreciated (including tips for attaching documents instead of just pasting inline…)

 # This is an action plugin script for kicad
 # Copy it into /usr/share/kicad/scripting/plugins/
 # Then start kicad, pcbnew, and go to the Tools->External Plugins menu.
 # It should be visible as menu item "panel selector" with a hammer icon.
 import pcbnew
 
 def isRouted(pad):
    loc=pad.GetPosition()
    for i in pad.GetBoard().TracksInNet( pad.GetNetCode() ):
        if i.GetStart()==loc or i.GetEnd()==loc : return True 
    return False

 def isAnchor(module):
    ref=module.GetReference()
    if ref and ord( ref[0] )==0x2693: return True
    return False

class PanelSelectPlugin(pcbnew.ActionPlugin):
     def defaults(self):
         self.name="panel selector"
         self.category="panelization"
         self.description="""Select panel anchor(s) and sheet contents from a pre-selected modules.
         If incompatible anchors are selected, deselect incompatible panels with less modules.
         If Panels are already fully selected, deselect everything except anchors.
         If only a single anchor is left selected, move the cursor to it.
 """
 #       layertable={}
 #       for i in range( pcbnew.PCB_LAYER_ID_COUNT ):
 #           layertable[board.GetLayerName(i)]=i
         #

     def Run(self):
         # The entry function that is executed on user action
         board=pcbnew.GetBoard()
         # Find selected items, sort into sheets (panels) they belong to.
         # A 'panelID' is path code[:-1].
         # An 'anchorID' is the minimumID code[-1] for a given sheetID.
         # For every panelID, locate an anchorID and anchorModule.
         print("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n")
         all_panel={}
         for module in board.GetModules():
             path=module.GetPath().split('/')
             if len(path)==2: continue  # Don't catalog root sheet, no point...
             panelID='/'.join(path[:-1])
             partID=path[-1]
             try:
                 panel=all_panel[panelID]
             except: 
                 #panel=[ selected, footprints, anchID, anchM, searchForAnch ]
                 panel=[0,0,partID,module,True]
                 all_panel[panelID]=panel
             panel[0]+=module.IsSelected()
             if module.IsSelected(): print( "Sel=",partID,module.GetReference())
             panel[1]+=1 # Count footprints 
                  if panel[4] and (partID<panel[2]): panel[2:5]=( partID,module,True)
             if isAnchor(module): panel[2:5]=( partID,module,False )
         # All panel instances are tabulated
         board.ClearSelected() # Prepare for panel-wide selection.
         # If any two panels are compatible, they will have the same anchorID
         anchorID, allSelect=False,set()
         # Go through all panels, keeping only paths that have the anchor.
         for panelID,panel in all_panel.items():
             if not panel[0]: continue
             if not anchorID: anchorID=panel[2]
             if anchorID != panel[2]:
                 if panel[0]: print("Warn: Deselecting ",panel[3].GetReference())
                 continue
             panel[3].SetSelected() # Anchor is always on if any selection 
              if panel[0]!=panel[1]: allSelect.add( panelID )
         for module in board.GetModules():
             path=module.GetPath().split('/')
             panelID='/'.join(path[:-1])
             if panelID in allSelect:
                 module.SetSelected()
                 print("+",module.GetReference() )
         
         return
1 Like

Are you aware of the replicate_layout script?
First getting the layout of one section right before doing the others seems more logical to me.

Also, why use the hashtags for this? KiCad V5 uses a “unique Id” which was called “timestamp” in V4. V6 is going to use UUID’s.
During annotation of the schematic there is an option to add offsets of 100 or 1000 to the RefDes values. This is probably more logical to use than the (mostly) undocumented hashes.

1 Like

This may be happening because pcbnew refreshes internal state after running the plugin. There were some changes around that code in 5.1 so updating kicad may help but it may not.
Workaround is to not let the action plugin function end. Add a window to your dialog with “select” and “close” buttons. Let select do it’s thing, then move components/nets around as you need. When you don’t need the selection anymore press close and then return from the main plugin function.

1 Like

As an author of replicate_layout action plugin, you really might want to look at it. I have two API compatibility fixes so that the plugin can run in 5.1.x and current 5.99 branch. Furthermore, you might want to look at the get_modules_on_sheet method.

As for selection, Qu1ck is right. In order to keep selection the plugin must not exit. Though maybe for your use case using SetBrightened/ClearBrightened method might work for you as Brighten status remains even when plugin exits.

Thank you Qu1ck and MitiaN. Will do some hacking over the next couple of days to look into several of these suggestions. Will get back as soon as I have some solid results one way or another…

Look at my length_stats plugin for inspiration how to make a plugin where plugin window stays open

Tried several of the options with regards to keeping the window open, however there seems to be something else wrong as well. The debug script seems to be indicating that items have been selected, though these items aren’t highlighted on the main screen. Here is the description from some of us over here, and we have cleaned up the script as well and I will try and repost it so that it is cleaner.

 #Here's the sScenario. ..  my dialog is up, and the plugin has already
 #exited after pressing "refresh plugins."
 #
 #A dialog, however, stays on screen and does not exit.  So, you can click
 #on a button in the dialog to run a method but kicad doesn't know the
 #plugin has run again; and there is no cleanup of data structures.  (Half
 #the battle is won, right there....)
 #
 #Manually, using the mouse, on the PCB screen... I click on a first
 #footprint ... it highlights just fine.
 #I move the mouse over to my dialog, and click "Debug."
 #In the debug window (eg:the bash termial I launched kicad from) it
 #prints the reference of a single footprint.
 #In the dialog I press "Select".
 #The old part is still highlighted on the screen but, but nothing
 #*appears* to select.
 #In the debug screen, the script claims  have selected MANY parts which
 #are not highlighting.
 #In the dialog I press "Debug."
 #It lists all the selected parts, both the lit up ones and the ones the
 #script supposedly selected but which aren't highlighted.
 #
 #If I then move the mouse back to the pcb screen....
 #I can SHIFT click on a blank PCB item not related to the selected
 #sheet.  This should manually EXPAND a selection set (add one item to all
 #selected). ... the new part highlights and the 1st part is still
 #highlighted.  But none of the parts chosen by the script light up ; so
 #it's not just a redrawing problem.  Kicad really doesn't know a bunch of
 #parts have been selected by the script.
 #
 #eg: NONE of pads that I did module.SetSelected()   or
 #module.SetBrightened()  light up even when adding to the selection.
 #
 #Kicad PCBNEW Gui doesn't recognize them as selected, even though
 #SetSelected() has been run.
 #To prove this point, I choose Move from the mouse menu.
 #Only the *visibly* highlighted parts begin to move with the mouse; all
 #the parts selected by the script DO NOT MOVE.  I drop the selected items
 #at some random spot ... and they stay highlighted.
 #
 #If I go to my dialog and press "Debug" , the script still lists ALL the
 #items as selected .... even the ones that weren't moved.  So they aren't
 #"dropped" by ki-cad, they are just ignored!!!!
 #
 #Next, I shift-click on an item that isn't highlighted on the screen but
 #which the script DID select.
 #The part DOES NOT highlight or select on the screen, but stays dark.
 #If I press the "DEBUG" button on the dialog again, the dark item just
 #clicked on is NO LONGER in the printout.
 #
 #So, kicad is only using the GetSelected() property to decide whether to
 #select or deselect a part; but the property itself ISN'T kicad's idea of
 #a "selection." That's apparently a totally different (undocumented) data
 #structure.
 #
 #So, the question is ... what IS the selection data structure, and how
 #can I access it from python ?

And now getting to the updated script where we attempted to clean things up a bit, add a box to keep it open, and tried several of the wonderful suggestions of which we are greatful, we have

 # This is an action plugin script for kicad
 # Copy it into /usr/share/kicad/scripting/plugins/
 # Then start kicad, pcbnew, and go to the Tools->External Plugins menu.
 # It should be visible as menu item "panel selector" with a hammer icon.
 import pcbnew
 import wx
 import wx.xrc
 #wx.App()
 #import wx.aui
 #import wx.lib.inspection
 #wx.lib.inspection.InspectionTool().Show()
 
 def isRouted(pad):
         loc=pad.GetPosition()
         for i in pad.GetBoard().TracksInNet( pad.GetNetCode() ):
             if i.GetStart()==loc or i.GetEnd()==loc : return True 
         return False
 
 def isAnchor(module):
         ref=module.GetReference()
         if ref and ord( ref[0] )==0x2693: return True
         return False
   
 #----------------------------------------------------------------------------
 class ActionDialog(wx.Frame):
     fieldHeight=45
     buttonHeight=28
     buttonWidth=90
     # 
     def OnClose(self,e):
         print("OnClose() panel_select deactivated")
         self.Destroy()
         e.Skip()
     #
     def OnSkip(self,e):
         print("OnSkip()",e)
         print()
         e.Skip()
     #
     def __init__(self, parent, board):
         wx.Frame.__init__(self,None,title=parent.name,
             size=(int(2.5*self.buttonWidth),3*self.fieldHeight)
         )
         panel=wx.Panel(self)
         panel.Bind( wx.EVT_CLOSE, self.OnClose )
         sizer=wx.GridBagSizer(1,2)
         currentPos=1
         buttonAction = wx.Button(panel, label="SELECT", size=(self.buttonWidth, self.buttonHeight))
         buttonClose = wx.Button(panel, label="DEBUG", size=(self.buttonWidth, self.buttonHeight))
         sizer.Add(buttonAction, pos=(currentPos, 2),    flag=wx.LEFT|wx.BOTTOM, border=5)
         sizer.Add(buttonClose, pos=(currentPos, 1), flag=wx.RIGHT|wx.BOTTOM, border=5)
 
         buttonAction.Bind( wx.EVT_BUTTON, parent.PanelSelect )
         buttonClose.Bind( wx.EVT_BUTTON, parent.PrintSelected )
         panel.SetSizerAndFit(sizer)
         self.Layout()
         self.Centre()
     
 # -------------------------------------------------------------------------
 
 class PanelSelectPlugin(pcbnew.ActionPlugin):
     def defaults(self):
         self.name="panel selector"
         self.category="panelization"
         self.description="""Select panel anchor(s) and sheet contents from a pre-selected modules.
         If incompatible anchors are selected, deselect incompatible panels with less modules.
         If Panels are already fully selected, deselect everything except anchors.
         If only a single anchor is left selected, move the cursor to it.
 """
 #       layertable={}
 #       for i in range( pcbnew.PCB_LAYER_ID_COUNT ):
 #           layertable[board.GetLayerName(i)]=i
         #
 
     def Run(self):
         # The entry function that is executed on user action
         board=pcbnew.GetBoard()
         gui=ActionDialog(self,board)
         gui.Show()
 
     def PrintSelected(self,ev):
         print("printing selected footprint references.")
         board=pcbnew.GetBoard()
         for module in board.GetModules():
             if module.IsSelected(): print( module.GetReference() )
         ev.Skip()
 
     def PanelSelect(self,ev):
         # Find selected items, sort into sheets (panels) they belong to.
         # A 'panelID' is path code[:-1].
         # An 'anchorID' is the minimumID code[-1] for a given sheetID.
         # For every panelID, locate an anchorID and anchorModule.
         print("\n\n\n\n\nPanelSelect() called")
         board=pcbnew.GetBoard()
         all_panel={}
         for module in board.GetModules():
             path=module.GetPath().split('/')
             if len(path)==2: continue  # Don't catalog root sheet, no point...
             panelID='/'.join(path[:-1])
             partID=path[-1]
             try:
                 panel=all_panel[panelID]
             except: 
                 #panel=[ selected, footprints, anchID, anchM, searchForAnch ]
                 panel=[0,0,partID,module,True]
                 all_panel[panelID]=panel
             panel[0]+=module.IsSelected()
             if module.IsSelected(): print( "Sel=",partID,module.GetReference())
             panel[1]+=1 # Count footprints 
             if panel[4] and (partID<panel[2]): panel[2:5]=( partID,module,True)
             if isAnchor(module): panel[2:5]=( partID,module,False )
         # All panel instances are tabulated
         board.ClearSelected() # Prepare for panel-wide selection.
         # If any two panels are compatible, they will have the same anchorID
         anchorID, allSelect=False,set()
         # Go through all panels, keeping only paths that have the anchor.
         for panelID,panel in all_panel.items():
             if not panel[0]: continue
             if not anchorID: anchorID=panel[2]
             if anchorID != panel[2]:
                 if panel[0]: print("Warn: Deselecting ",panel[3].GetReference())
                 continue
             panel[3].SetSelected() # Anchor is always on if any selection 
             if panel[0]!=panel[1]: allSelect.add( panelID )
         for module in board.GetModules():
             path=module.GetPath().split('/')
             panelID='/'.join(path[:-1])
             if panelID in allSelect:
                 module.SetSelected()
                 module.SetBrightened()
                 print("+",module.GetReference() )
         ev.Skip()
 
 PanelSelectPlugin().register()
 PanelSelectPlugin().Run()  # Run an instance during plugin refresh.

There do seem still to be some things which are blocking us. I appreciate all of the comments and those questions that I haven’t answered, like why did we target the hashtags, and why did we not just use the replicate panel script, I will attempt to answer fully soon. I will say that we did look through and learn from the replicate panel script and are appreciative of the work that obviously went into this piece of work. If it was not for that piece of work, it would have been much harder to create this piece of work. These 2 are not separate but budding from each other, and I do wish to mention that this script help

You do call the pcbnew.refresh() after you highlight module? And as for SetBrightened / ClearBrightened acting on module (footprint), what I found out in order to brighten module, you have to brighten each pad and drawing item. Invoking the methods on module will not do anything. Might be the same for SetSelected

Look at set_highlight_on_module in "action_replicate_layout.py

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.