Pan, Zoom, Refresh from python in pcbnew

So you’ve written a cool new layout modification utility in python. It sure would be nice to see the results on the screen. This is an issue I faced with my placement replicator script. It does what I want except that the user has to click the refresh button to see the results.

Now there’s a way to trigger zoom, pan, and refresh from python. As of this writing (March 20, 2017), you need to use one of the nightlies or build yourself.

To refresh the screen:

pcbnew.Refresh()

To pan and zoom, you need to supply x,y, width, height:

x = pcbnew.FromMM(10)
y = pcbnew.FromMM(10)
width  = pcbnew.FromMM(20)
height = pcbnew.FromMM(20)
pcbnew.WindowZoom(x, y, width, height)

Contributing to Kicad is painless

I initially searched and searched in pcbnew’s C code for python APIs like these. Eventually, I came to realize that they didn’t exist yet in pcbnew. So finally, I have my very first code contribution to an open source project.

I’ve been paid to write software for many years but it’s all been in-house stuff. My professional environment was pretty loosey-goosey, with very few formal constraints. 1 Over the years, I’ve read lots of stories about how open source projects can be a pain to deal with. Lots of coding standards, lots of requirements,… lots of hoops to jump through.2

This was not my experience with proposing these code changes to Kicad. I read the developer wiki, and attempted with follow the directions. I mailed the patch file to the developer email list, along with an introduction of who I am and what I’m trying to accomplish.

It was committed the same day.

Wayne, the kicad project lead, was very welcoming and encouraging. Although there were a couple things I missed, he went ahead and did the tweaks and pushed the change. Next time, I’ll know.

Most important to me, I’m excited to do more.

 

 


  1. This was mostly fine. On the one hand, a lot of bugs slipped through that shouldn’t have, but on the other, it meant that a lot of what we did was very much co-development/co-design with the users.

  2. sometimes protection of empires, but probably also a lot critical code. I’d hope that mysql, Apache, linux kernel, g++,… are strict. Hopefully, they’ve also found ways to be welcoming. I don’t know. I guess it’s often the negative news that we remember.

Adding your own command buttons to the pcbnew GUI

Kicad has three rows of command buttons with predefined functionality. This post is about how to add your own button.

The sample code can be found here.

Perliminaries

Before getting into the code, there are a couple things that are weird about kicad/wxPython’s behavior1

Getting proper aui pointers

The first thing is that if you simply open the python scripting window in pcbnew, find the top level window, and look for the command bars, you’ll likely get a pointer of type wxControl. That type doesn’t have the APIs needed to look at, amend, and change the command buttons. The real type of these is wx.aui.AuiToolBar but unless the pointer is cast correctly, you’re out of luck.

After a bunch of poking around, running in the debugger, trying to recompile (unsuccessfully) wxPython with debug symbols turned on, I stumbled upon the easy answer. You have to do this before you run any of the wx scripting apis.

import wx
import wx.aui # this is the key one

After running these, you’ll get the correct pointer types. 2

Getting a proper WxApp pointer

The second funny thing isn’t really needed to add a button, but it’s super helpful in looking at the structure of the GUI yourself. In this post, I’m not just trying to help you add a button in a specific place, but rather to help you learn more about how pcbnew is put together. With this understanding you’ll hopefully be able to do even more.

When you first open the python interpreter in pcbnew, if you ask for the wxApp pointer, you’ll get a pointer of type wxEvtHandler. While this is correct, it’s incomplete and also unhelpful if you need a wxApp pointer. If you do this, wx.GetApp() will return a usable wxApp pointer3

import wx
wx.App()

Note that if you do this more than once in a given session, it’ll crash pcbnew.4

So why do we care? Well, if you have a proper wxApp pointer you can do this (remember, only one call to wx.App() per session):

import wx
wx.App()
import wx.aui
import wx.lib.inspection
wx.lib.inspection.InspectionTool().Show()

Which will give you a Widget Inspection Tool window like the one below. It enables you to click on different parts of the pcbnew GUI and see what’s what. Note that it doesn’t give me information about the individual buttons. It’s a helpful learning/debug aid nonetheless.

So let’s add a button

There are a couple things you need to add a button

  • The toolbar you want to add the button to (top, left, or right)
  • A callback function to be called when the button is clicked
  • A picture/bitmap to use as your button’s icon.

Getting the toolbar to add to

To lookup the toolbar window, you start by finding the main pcbnew window and then asking it for one of its children by id number. How do you know the number? The current, bad, answer is to either use the window inspector I mentioned earlier in this post or for me to tell you.5 Here are the numbers:

  • 6038 is the value that H_TOOLBAR from kicad/include/id.h happens to get. It’s the one on the top with buttons like “new board”, “open existing board”
  • 6041 is AUX_TOOLBAR. That’s the second row of stuff in the pcbnew gui. It contains things like track width, via size, grid
  • 6039 is V_TOOLBAR, the right commands window. zoom to selection, highlight net.
  • 6040 is OPT_TOOLBAR, the left commands window. disable drc, hide grid, display polar
import pcbnew
import wx
import wx.aui

def findPcbnewWindow():
    windows = wx.GetTopLevelWindows()
    pcbnew = [w for w in windows if w.GetTitle() == "Pcbnew"]
    if len(pcbnew) != 1:
        raise Exception("Cannot find pcbnew window from title matching!")
    return pcbnew[0]

pcbwin = findPcbnewWindow()
top_tb = pcbwin.FindWindowById(6038)

Callback function

This one’s easy. It’s just a function that takes the event as an argument. If you print from this function, the output will appear in the terminal used to invoke pcbnew, not the python window. The event will be of type wx.CommandEvent which is useful to know if you want to have one callback for a variety of buttons.

def MyButtonsCallback(event):
    # when called as a callback, the output of this print
    # will appear in your xterm or wherever you invoked pcbnew.
    print("got a click on my new button {}".format(str(event)))

icon image

If you were to look in the kicad source code for bitmaps_png/CMakeLists.txt you’d find this:

# Plan for three sizes of bitmaps:
# SMALL – for menus – 16 x 16
# MID – for toolbars – 26 x 26
# BIG – for program icons – 48 x 48

The rest of that file handles automagic conversion from svg files into a format that can be consumed by the kicad code. Things are easier for us. You just need a 26×26 image file. In this example, I use png format, but you can use any format listed here on the wxPython page

# assuming you're creating your button from a script, one logical place to keep
# the image is in the same directory. Here's a snippet for how to get the
# path of the script.
import inspect
import os
filename = inspect.getframeinfo(inspect.currentframe()).filename
path = os.path.dirname(os.path.abspath(filename))

# here we actually load the icon file
bm = wx.Bitmap(path + '/hello.png', wx.BITMAP_TYPE_PNG)

Putting it all together

Now we have top_tb holding the toolbar window we want to add to, bm holding a bitmap pointer, and MyButtonsCallback to do the work. Now we just need to add a new tool button, bind it to the callback and tell the system to make it real.

itemid = wx.NewId()
top_tb.AddTool(itemid, "mybutton", bm, "this is my button", wx.ITEM_NORMAL)
top_tb.Bind(wx.EVT_TOOL, MyButtonsCallback, id=itemid)
top_tb.Realize()

And that’s it. Hope you enjoyed the post. Please comment. Again, the sample code can be found here.


  1. Things are only weird if you don’t know the explanation. In this case, I can guess at the reasons, but I don’t know for sure.

  2. if you already have the pointer with the wrong type, subsequent searches for windows will yield the same unhelpful type. My theory on why this is so is a matter of caching (why it persists) and swig symbol tables. Before you do import wx.aui, these types are not yet known to python and swig, so they return a type that is known. wxControl

  3. probably incorrect, but usable for what I’m about to talk about.

  4. again, this is a bad solution, but it’s only needed for debugging and learning about pcbnew’s structure.

  5. The better answer is for the kicad enum that holds these number to be exposed in python. Easy to add, but it’s not there now.

How to add mounting holes

Most of the time, the modules in your design will be introduced via netlist import from eeschema. An important exception to this is mounting holes. Your design may need some holes to enable you to screw the resulting board onto some sort of case/enclosure. In the GUI, you’d do this via the “Add footprints” command. This works, but what if you want to ensure that the resulting holes end up in specific locations? Script it!

Compared to other scripting tasks in pcbnew, figuring out how to add footprints to a design was a pain. In the end, it’s pretty easy1

My designs are pretty simple2, amounting to rectangular boards. My “enclosures” tend to be a piece of wood to which I mount a board with some drywall screws. Because of this simplicity, it doesn’t really matter where my mounting holes are. Still, I would like for them to at least be in a regular pattern. So I put together a script to put one in each corner of the board’s boundary.

First off, while pcbnew’s add footprint command can access kicad’s footprint libraries on GITHUB, this is not something I’ve achieved yet.3 For the code in this post to work, you’ll need to clone at least one of kicad’s footprint repos. For example, something like this:

git clone https://github.com/KiCad/Connectors.pretty.git

Don’t forget where you put it. In the case of the script, I have a variable footprint_lib. You’ll want to change this variable4. While we’re at it, let’s add some other boilerplate.

footprint_lib = '/bubba/electronicsDS/kicad/development/footprints/Connectors.pretty'

board = pcbnew.GetBoard()

# the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm)
# the coordinate 121550000 corresponds to 121.550000 

SCALE = 1000000.0

Since I want to put my mounting holes in the four corners of the board, I have to compute the four corners:

rect = None
for d in board.GetDrawings():
  if (d.GetLayerName() != "Edge.Cuts"):
    continue
  if (rect == None):
    rect = d.GetBoundingBox()
  else:
    rect.Merge(d.GetBoundingBox())

While the module class has a GetBoundingBox function, that box includes stuff like reference designators. So I have a function to compute the bounding box of metals, solder masks and such.

def GetModBBox(mod):
  modbox = None
  for pad in mod.Pads():
    if (modbox == None):
      modbox = pad.GetBoundingBox()
    else:
      modbox.Merge(pad.GetBoundingBox())
  for gi in mod.GraphicalItems():
    if (modbox == None):
      modbox = gi.GetBoundingBox()
    else:
      modbox.Merge(gi.GetBoundingBox())
 
  return modbox

Since I want to put a mounting hole in each corner, I generate a list of points from a bounding rectangle. Much better than cut/pasting a bunch of module code.

def GetRectCorners(rect):
  return [pcbnew.wxPoint(rect.Centre().x-rect.GetWidth()/2, rect.Centre().y-rect.GetHeight()/2),
          pcbnew.wxPoint(rect.Centre().x-rect.GetWidth()/2, rect.Centre().y+rect.GetHeight()/2),
          pcbnew.wxPoint(rect.Centre().x+rect.GetWidth()/2, rect.Centre().y+rect.GetHeight()/2),
          pcbnew.wxPoint(rect.Centre().x+rect.GetWidth()/2, rect.Centre().y-rect.GetHeight()/2)]

How to actually add a footprint.

Now we finally get to the “hard” part, adding the footprint. The ability to add a footprint is exposed in an API in the PCB_IO plugin. I don’t yet understand the role that plugin’s play in the context of pcbnew; most plugins are python scripts. PCB_IO is one written in C++.

It comes down to two easy lines. One to construct an instance of the PCB_IO plugin, and the second to instantiate the footprint. As I mentioned earlier in this post footprint_lib contains a path to a directory where you’ve put your kicad_mod files.

io = pcbnew.PCB_IO()
mod = io.FootprintLoad(footprint_lib, "1pin")

Everything after is easy.

board = pcbnew.GetBoard()
board.Add(mod)

I want to put four of these in the four corners of the boundary bounding box defined by rect above. First I shrink rect by the size of the footprint module. 5. I have to do a little bit of math to size the box. SetWidth/SetHeight are relative to the bottom left corner, something I’ve had to get used to. I wanted it to be relative to the center.

mod = io.FootprintLoad(footprint_lib, "1pin")

modbox = GetModBBox(mod);
rect.SetWidth(rect.GetWidth() - modbox.GetWidth())
rect.SetHeight(rect.GetHeight() - modbox.GetHeight())
rect.SetX(rect.GetX() + modbox.GetWidth()/2)
rect.SetY(rect.GetY() + modbox.GetHeight()/2)

And now I finally create and place the holes:

for point in GetRectCorners(rect):
  mod = io.FootprintLoad(footprint_lib, "1pin")
  modbox = GetModBBox(mod)
 
  point.x = point.x - modbox.Centre().x + mod.GetPosition().x
  point.y = point.y - modbox.Centre().y + mod.GetPosition().y
  mod.SetPosition(point)
  point.y - modbox.Centre().y))
 
  board.Add(mod)

Again, the script can be found on my github.

 

 

 


  1. Makes me think of times in my career when I spent a couple full days debugging a problem that was fixed with a single character. I’m sure most C/C++ programmers have experienced this with “=” vs “==” in an if statement. In this case, it’s not a matter of bugs, but that I don’t know the code that well.I should probably try to interact some with the kicad developers.

  2. I’m a software guy. I entered college intending to major EE, but my aptitude for programming eclipsed circuit stuff. Still, I pine for the ability to design circuits.

  3. I didn’t actually try, but I did keep and eye out for relevant code while tracking down the APIs needed to add a module

  4. “bubba”, in the path below, is one of my drives. I think of it as a name that only big guys have. As disks go and as technology has progressed, it really not that big anymore.

  5. EDA_RECT is nice enough to have an API called inflate. Sadly, it takes wxCoord as arguments. I haven’t found a way to create one or these via the existing python APIs.

adding GUI elements in pcbnew

In my last post, I talked about how you can run pcbnew headless. In many cases you actually want more GUI. For example, in my code sample for replicating module placement across multiple sheet instances, I have the pivot instances hard coded in the script. Why not do it in a GUI?

Adding GUI elements to pcbnew is what this post is about.1. The code I’m talking about can be found here.

The pcbnew GUI is written using wxWidgets/wxPython. If you’re not familiar with wxPython, here is my favorite tutorial so far. All of its python APIs work fine for me in pcbnew so far. I’ve found there are two important things to keep in mind:

  • You don’t need to do the normal app = MyApp(0); app.MainLoop() 2
  • If you do print from callback (for debugging) the text will not be in the python window. Instead, you can find it in the terminal where you invoked kicad. Keep this in mind if you usually invoke from the kicad project manager or from you OS task launcher.

At the end of this post, we’ll have a new, not particularly attractive, window in pcbnew like this:

As usual, you’ll need some imports:

import wx 
import pcbnew

To create a new window, you’ll want to create a new class derived from wx.Frame3. The initializer/constructor will create the new GUI elements and you’ll want some methods to use as event callbacks. To determine the placement of the new widgets, I’m using BoxSizer, but there are a bunch of other options.

Here are some of the high level points for when your reading the code below:

  • All widgets automagically get a unique negative id. You can pick your own numbers if you’d like. You’ll need the id when binding callbacks, given a widget pointer, just call GetId().
  • wx.StaticText is a simple text label.
  • wx.Button is a button. duh
  • wx.ComboBox gives you a scrollable list selection. I use it to pick a net or module.
  • wx.BoxSizer is just for layout. Line things up vertically or horizontally
  • To tell wx about your callback functions, use the bind method

Given that, the code below should be easy to follow, even if you’ve never done anything with wxWidgets/wxPython

class SimpleGui(wx.Frame):
    def __init__(self, parent, board):
        wx.Frame.__init__(self, parent, title="this is the title")
        self.panel = wx.Panel(self) 
        label = wx.StaticText(self.panel, label = "Hello World")
        button = wx.Button(self.panel, label="Button label", id=1)
        
        nets = board.GetNetsByName()
        self.netnames = []
        for netname, net in nets.items():
            if (str(netname) == ""):
                continue
            self.netnames.append(str(netname))
        
        netcb = wx.ComboBox(self.panel, choices=self.netnames)
        netcb.SetSelection(0)

        netsbox = wx.BoxSizer(wx.HORIZONTAL)
        netsbox.Add(wx.StaticText(self.panel, label="Nets:"))
        netsbox.Add(netcb, proportion=1)
        
        modules = board.GetModules()
        self.modulenames = []
        for mod in modules:
            self.modulenames.append("{}({})".format(mod.GetReference(), mod.GetValue()))
        modcb = wx.ComboBox(self.panel, choices=self.modulenames)
        modcb.SetSelection(0)

        modsbox = wx.BoxSizer(wx.HORIZONTAL)
        modsbox.Add(wx.StaticText(self.panel, label="Modules:"))
        modsbox.Add(modcb, proportion=1)
        
        box = wx.BoxSizer(wx.VERTICAL)
        box.Add(label,   proportion=0)
        box.Add(button,  proportion=0)
        box.Add(netsbox, proportion=0)
        box.Add(modsbox, proportion=0)
        
        self.panel.SetSizer(box)
        self.Bind(wx.EVT_BUTTON, self.OnPress, id=1)
        self.Bind(wx.EVT_COMBOBOX, self.OnSelectNet, id=netcb.GetId())
        self.Bind(wx.EVT_COMBOBOX, self.OnSelectMod, id=modcb.GetId())
        
    def OnPress(self, event):
        print("in OnPress")

    def OnSelectNet(self, event):
        item = event.GetSelection()
        print("Net {} was selected".format(self.netnames[item]))

    def OnSelectMod(self, event):
        item = event.GetSelection()
        print("Module {} was selected".format(self.modulenames[item]))

Now that we have the derived GUI class, it’s a simple matter of instantiating it.

def InitSimpleGui(board):
  sg = SimpleGui(None, board)
  sg.Show(True)
  return sg


sg = InitSimpleGui(pcbnew.GetBoard())

And that’s it. If you find this useful and if you end up creating something for pcbnew, I’d love to hear about it.


  1. I haven’t updated the replicate script yet, but I plan on it.

  2. You actually can run them, but you’ll get some undesirable behavior. In particular, the mouse icon will be stuck in “busy” mode, and the normal interactive commands won’t work

  3. There are other options, like dialogs

pcbnew scripting doesn’t require the GUI

If you just want to process something in the pcbnew data model, you don’t have to bring up the GUI. You can just start a python job directly from the commandline. 1

Why would you do this?

In my previous life working for a large semiconductor company, we had many, many quality checks and progress trackers. Now that experience was in VLSI, but surely there are parallels in the PCB world.

Management needs a way to track the progress of the project. Engineering needs a way to enforce layout standards. An obvious example of this is design rule checking. How many opens, shorts and spacing violations are there currently? There are many other things that one could check. Maybe you have rules about the clock network. Maybe you want to check that the power grid is strong enough to handle the current its being asked to carry. Maybe the sub-design you’re working on has some restrictions on it to ensure it’ll fit into the larger design.

At the beginning of a project, we had many millions of such violations. For a while the number wouldn’t change since we were still deciding on the bigger picture stuff. Eventually, we’d change gears and want to move towards tapeout.

Of course, these quality checks don’t run in a GUI 2. You want something that can be run from the command line every night in some automated fashion.

Coming back to Kicad, there’s already such a check in the scripts directory: ddr3_length_match.py.3

From the script’s header:

Report any length problems pertaining to a SDRAM DDR3 T topology using 4 memory chips: a T into 2 Ts routing strategy from the CPU.

Once you have kicad installed, pcbnew’s python library is available to any python script. 4

python
>>> import pcbnew
>>> print pcbnew.__file__
/usr/lib/python2.7/dist-packages/pcbnew.py
>>> import sys
>>> print sys.path
['', '/usr/lib/python2.7', <a bunch of others deleted>]

What if you compiled kicad yourself?

python
>>> import sys
>>> sys.path.insert(0, "<path to your build>/kicad/install/lib/python2.7/dist-packages")
>>> import pcbnew
>>> print pcbnew.__file__
<path to your build>/kicad/install/lib/python2.7/dist-packages/pcbnew.pyc

In the case of ddr3_length_match, it simply keeps its eye on a file5. Once a second, it looks at the time stamp of the file. If it’s been updated, the script will load it and run the required checks.

Here’s the relevant code for loading the file:

pcb = pcbnew.LoadBoard(filename)
pcb.BuildListOfNets() # required so 'pcb' contains valid netclass data

There are some other interesting APIs used in there as well. For example, to see if two pads are connected only with wires and vias:

try:
  tracks = pcb.TracksInNetBetweenPoints(start_pad.GetPosition(), end_pad.GetPosition(), netcode)
except IOError as ioe:
  return False
return True

Either way, the biggest point I’m trying to make is that when working on a design, there may be some stuff you want to keep an eye on. Timing critical nets, noise safeguards, power rail loading…

Write a script that loads your design and does the checks. Maybe it continually monitors your saves, maybe you have a checkin trigger 6, nightly, or maybe you just run it by hand.


  1. Another popular method is to parse the kicad files directly, bypassing kicad. While that works, part of the point of this blog is to reduce the need for that.

  2. well, maybe they could, but not for tracking purposes

  3. this script was written by and brought to my attention by Dick Hollenbeck. He was also instrumental in making pcbnew’s python interface as useful as it is. He added a bunch of the APIs without which scripting is kinda limited

  4. my experience is limited to linux. I imagine things will be similar for Mac users. Windows always seems to be a mystery.

  5. The file name is an argument to the script

  6. in SVN or GIT, for example

Zones, boundaries, and silkscreen

In previous posts, I’ve talked about the main layout components. Tracks, modules, pads. In this one, I’m focusing on lines. The board boundary, zones, and silkscreens.

At the end of this post, I’ll have a script (you can find the full script here) that redraws the board boundary to shrink wrap around all of the existing components. It’ll also generate a zone on the clk net (of course, it’s more common to use power/gnd). The zone will have the same outline as the board.

Most of the code is pretty straightforward, but I do recommend looking closely at the notes about zones at the end of this post. There are a couple things that are not immediately obvious. 1

As a reminder, I have a uml diagram in my github area of useful, pcbnew python APIs (click for a bigger view)

Some preliminaries

First, it’s helpful to have a layername->layernumber lookup table. Later on, I’ll need to know the layers for backside copper (B.Cu) and the board boundary (Edge.Cuts)

layertable = {}

numlayers = pcbnew.LAYER_ID_COUNT
for i in range(numlayers):
 layertable[board.GetLayerName(i)] = i

Next, I’ll want to compute the bounding box by starting with an empty box and adding to it. So I’ll create a bbox class. 2. Also, I have variations on min and max. Mine are different in that the value None is supported the way I want. The standard ones treat None as -inf.

def mymin(a,b):
 if (a == None):
 return b
 if (b == None):
 return a
 if (a<b):
 return a
 return b

def mymax(a,b):
 if (a == None):
 return b
 if (b == None):
 return a
 if (a>b):
 return a
 return b


class BBox:
 def __init__(self, xl=None, yl=None, xh=None, yh=None):
 self.xl = xl
 self.xh = xh
 self.yl = yl
 self.yh = yh

 def __str__(self):
 return "({},{} {},{})".format(self.xl, self.yl, self.xh, self.yh)
 
 def addPoint(self, pt):
 self.xl = mymin(self.xl, pt.x)
 self.xh = mymax(self.xh, pt.x)
 self.yl = mymin(self.yl, pt.y)
 self.yh = mymax(self.yh, pt.y)

 def addPointBloatXY(self, pt, x, y):
 self.xl = mymin(self.xl, pt.x-x)
 self.xh = mymax(self.xh, pt.x+x)
 self.yl = mymin(self.yl, pt.y-y)
 self.yh = mymax(self.yh, pt.y+y)

Computing the BBox

Start with a null bbox:

boardbbox = BBox();

Adding tracks

alltracks = board.GetTracks() 
for track in alltracks:
  boardbbox.addPoint(track.GetStart())
  boardbbox.addPoint(track.GetEnd())

Adding Pads

allpads = board.GetPads()
for pad in allpads:
  if (pad.GetShape() == pcbnew.PAD_SHAPE_RECT): 
    if ((pad.GetOrientationDegrees()==270) | (pad.GetOrientationDegrees()==90)):
      boardbbox.addPointBloatXY(pad.GetPosition(), pad.GetSize().y/2, pad.GetSize().x/2)
    else:
      boardbbox.addPointBloatXY(pad.GetPosition(), pad.GetSize().x/2, pad.GetSize().y/2)

  elif (pad.GetShape() == pcbnew.PAD_SHAPE_CIRCLE):
    boardbbox.addPointBloatXY(pad.GetPosition(), pad.GetSize().x/2, pad.GetSize().y/2)
 
  elif (pad.GetShape() == pcbnew.PAD_SHAPE_OVAL):
    boardbbox.addPointBloatXY(pad.GetPosition(), pad.GetSize().x/2, pad.GetSize().y/2)
 
  else:
    print("unknown pad shape {}({})".format(pad.GetShape(), padshapes[pad.GetShape()]))

Adding Module silkscreens and such

for mod in board.GetModules():
  for gi in mod.GraphicalItems():
    bbox = gi.GetBoundingBox()
    boardbbox.addPointBloatXY(bbox.Centre(), bbox.GetWidth()/2, bbox.GetHeight()/2)

Generating the new data

Now we have a bounding box of everything, let’s remove the old boundary before creating a new one.

for d in board.GetDrawings():
  board.Remove(d)

A new boundary

There’s be a better way to do this. I could modify the bbox class to produce a list of line segment, but I’m lazy.

edgecut = layertable['Edge.Cuts']

seg1 = pcbnew.DRAWSEGMENT(board)
board.Add(seg1)
seg1.SetStart(pcbnew.wxPoint(boardbbox.xl, boardbbox.yl))
seg1.SetEnd( pcbnew.wxPoint(boardbbox.xl, boardbbox.yh))
seg1.SetLayer(edgecut)

seg1 = pcbnew.DRAWSEGMENT(board)
board.Add(seg1)
seg1.SetStart(pcbnew.wxPoint(boardbbox.xl, boardbbox.yh))
seg1.SetEnd( pcbnew.wxPoint(boardbbox.xh, boardbbox.yh))
seg1.SetLayer(edgecut)

seg1 = pcbnew.DRAWSEGMENT(board)
board.Add(seg1)
seg1.SetStart(pcbnew.wxPoint(boardbbox.xh, boardbbox.yh))
seg1.SetEnd( pcbnew.wxPoint(boardbbox.xh, boardbbox.yl))
seg1.SetLayer(edgecut)

seg1 = pcbnew.DRAWSEGMENT(board)
board.Add(seg1)
seg1.SetStart(pcbnew.wxPoint(boardbbox.xh, boardbbox.yl))
seg1.SetEnd( pcbnew.wxPoint(boardbbox.xl, boardbbox.yl))
seg1.SetLayer(edgecut)

A new zone

Zones are a little tricky in the kicad model. There are several classes involved. To begin, here’s an interesting comment from PolyLine.h:

// A polyline contains one or more contours, where each contour
// is defined by a list of corners and side-styles
// There may be multiple contours in a polyline.
// The last contour may be open or closed, any others must be closed.
// All of the corners and side-styles are concatenated into 2 arrays,
// separated by setting the end_contour flag of the last corner of
// each contour.
//
// When used for copper (or technical layers) areas, the first contour is the outer edge
// of the area, subsequent ones are "holes" in the copper.

I want to create my zone on the clk net on the backside, so I need a pointer to the net and the layer (using the layer table generated in preliminaries

nets = board.GetNetsByName()
clknet = nets.find("/clk").value()[1]
backlayer = layertable['B.Cu']

Now let’s create the zone. It’s a little different from creating the board boundary. If you’ve used the zone creation GUI command, the order of events should make sense.

newarea = board.InsertArea(clknet.GetNet(), 0, backlayer, boardbbox.xl, boardbbox.yl, pcbnew.CPolyLine.DIAGONAL_EDGE)

newoutline = newarea.Outline()
newoutline.AppendCorner(boardbbox.xl, boardbbox.yh);
newoutline.AppendCorner(boardbbox.xh, boardbbox.yh);
newoutline.AppendCorner(boardbbox.xh, boardbbox.yl);

This next line shouldn’t really be necessary but without it, saving to file will yield a file that won’t load.3

newoutline.CloseLastContour()

I don’t know why this is necessary. When calling InsertArea above, DIAGONAL_EDGE was passed. If you save/restore the file, the zone will come back hatched. Before then, the zone boundary will just be a line. Omit this if you are using pcbnew.CPolyLine.NO_HATCH

newoutline.Hatch()

Please leave a comment with questions or if you’d like me cover some other topic.


  1. I had to read the C++ code to figure it out

  2. wxBox probably already has something for this, but I’m not familiar enough with it. So I’m writing one

  3. The parenthesis won’t line up; there won’t be enough closing parens. I’d argue that AppendCorner should automatically do it.

Replicating pcbnew new for arrayed sheets

Many circuit designs can have repeated structures. Sadly, pcbnew doesn’t have any features to make the placement and routing any more productive.

Here is a demo of a script I wrote to help:

If you have X copies of the same sheet in eeschema, the script will allow you to place and route one of them and them apply that to the other sheets.

How’s it work?

If you call GetPath() on the modules in your design, you’ll get values like these (note the the path is only the /number/number part):

mod Q1 path /587DA765/58758821
mod Q2 path /5875D13C/58758821
mod Q3 path /5875D13D/58758821
mod Q4 path /5875D13E/58758821
mod R1 path /587DA765/5875882F
mod R2 path /587DA765/5875883D
mod R3 path /5875D13C/5875882F
mod R4 path /5875D13C/5875883D

The first number tells you which sheet instance the module belongs to. The second is an identifier for the module. So from the information above we can tell that these are all in the same sheet:

mod Q1 path /587DA765/58758821
mod R1 path /587DA765/5875882F
mod R2 path /587DA765/5875883D

Similarly, we know that these are the same transistor, just in different instances of the sheet:

mod Q1 path /587DA765/58758821
mod Q2 path /5875D13C/58758821
mod Q3 path /5875D13D/58758821
mod Q4 path /5875D13E/58758821

So if I have placed and routed sheet 587DA765, the script just needs to find all of the members of 5875D13C and simply adjust their positions by some constant increment.

The script can be found here in my github repository for kicad

Querying pcbnew to generate a basic svg file

In a previous post, I gave some basic query capabilies in pcbnew. In this post, I’ll use that to generate a simple svg file.

The result looks like this:

Not the most interesting layout and a bunch of detail is missing, but I was happy to get this far. The code is hopefully self-explanatory. After the last post, I don’t really know what I can add.

The code can be found in my github

Preliminaries

I need to get each of pcbnew’s layer colors. Some of the color names are modified to match svr colors. The real answer is to get the rgb values from colorrefs, but those aren’t exposed in python today

colornames = {
 pcbnew.BLACK: 'BLACK', 
 pcbnew.DARKDARKGRAY: 'DARKSLATEGRAY', # 'DARKDARKGRAY', 
 pcbnew.DARKGRAY: 'DARKGRAY', 
 pcbnew.LIGHTGRAY: 'LIGHTGRAY', 
 pcbnew.WHITE: 'WHITE', 
 pcbnew.LIGHTYELLOW: 'LIGHTYELLOW', 
 pcbnew.DARKBLUE: 'DARKBLUE', 
 pcbnew.DARKGREEN: 'DARKGREEN', 
 pcbnew.DARKCYAN: 'DARKCYAN', 
 pcbnew.DARKRED: 'DARKRED', 
 pcbnew.DARKMAGENTA: 'DARKMAGENTA', 
 pcbnew.DARKBROWN: 'MAROON', # 'DARKBROWN', 
 pcbnew.BLUE: 'BLUE', 
 pcbnew.GREEN: 'GREEN', 
 pcbnew.CYAN: 'CYAN', 
 pcbnew.RED: 'RED', 
 pcbnew.MAGENTA: 'MAGENTA', 
 pcbnew.BROWN: 'BROWN', 
 pcbnew.LIGHTBLUE: 'LIGHTBLUE', 
 pcbnew.LIGHTGREEN: 'LIGHTGREEN', 
 pcbnew.LIGHTCYAN: 'LIGHTCYAN', 
 pcbnew.LIGHTRED: 'INDIANRED', # 'LIGHTRED', 
 pcbnew.LIGHTMAGENTA: 'LIGHTPINK', # 'LIGHTMAGENTA', 
 pcbnew.YELLOW: 'YELLOW', 
 pcbnew.PUREBLUE: 'MEDIUMBLUE', # 'PUREBLUE', 
 pcbnew.PUREGREEN: 'LAWNGREEN', # 'PUREGREEN', 
 pcbnew.PURECYAN: 'DARKTURQUOISE', # 'PURECYAN', 
 pcbnew.PURERED: 'FIREBRICK', # 'PURERED', 
 pcbnew.PUREMAGENTA: 'DARKORCHID', # PUREMAGENTA', 
 pcbnew.PUREYELLOW: 'KHAKI', # PUREYELLOW' 
}

padshapes = {
 pcbnew.PAD_SHAPE_CIRCLE: "PAD_SHAPE_CIRCLE",
 pcbnew.PAD_SHAPE_OVAL: "PAD_SHAPE_OVAL",
 pcbnew.PAD_SHAPE_RECT: "PAD_SHAPE_RECT",
 pcbnew.PAD_SHAPE_TRAPEZOID: "PAD_SHAPE_TRAPEZOID" 
}
# new in the most recent kicad code
if hasattr(pcbnew, 'PAD_SHAPE_ROUNDRECT'):
 padshapes[pcbnew.PAD_SHAPE_ROUNDRECT] = "PAD_SHAPE_ROUNDRECT",

Now we can get to it. Need to get the board, get the boundary coordinates, and set the scale factor

board = pcbnew.GetBoard()
boardbbox = board.ComputeBoundingBox()
boardxl = boardbbox.GetX()
boardyl = boardbbox.GetY()
boardwidth = boardbbox.GetWidth()
boardheight = boardbbox.GetHeight()

# the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm)
# the coordinate 121550000 corresponds to 121.550000

SCALE = 1000000.0

Writing an SVG

Here is where things happen. I’m sure if kicad include the svgwrite package or if it picks it up from my python install area. I did need to install it to python to do some experiments before doing anything in Kicad.

import sys, os
import svgwrite

print("working in the dir " + os.getcwd())
name = "output.svg"
# A4 is approximately 21x29
dwg = svgwrite.Drawing(name, size=('21cm', '29cm'), profile='full', debug=True)

dwg.viewbox(width=boardwidth, height=boardheight, minx=boardxl, miny=boardyl)
background = dwg.add(dwg.g(id='bg', stroke='white'))
background.add(dwg.rect(insert=(boardxl, boardyl), size=(boardwidth, boardheight), fill='white'))

svglayers = {}
for colorcode, colorname in colornames.items():
 layer = dwg.add(dwg.g(id='layer_'+colorname, stroke=colorname.lower(), stroke_linecap="round"))
 svglayers[colorcode] = layer

alltracks = board.GetTracks() 
for track in alltracks:
 # print("{}->{}".format(track.GetStart(), track.GetEnd()))
 # print("{},{}->{},{} width {} layer {}".format(track.GetStart().x/SCALE, track.GetStart().y/SCALE,
 # track.GetEnd().x/SCALE, track.GetEnd().y/SCALE,
 # track.GetWidth()/SCALE,
 # track.GetLayer()) 
 # )
 layercolor = board.GetLayerColor(track.GetLayer())
 svglayers[layercolor].add(dwg.line(start=(track.GetStart().x,
 track.GetStart().y),
 end=(track.GetEnd().x,
 track.GetEnd().y),
 stroke_width=track.GetWidth()
 ))


svgpads = dwg.add(dwg.g(id='pads', stroke='red',fill='orange'))
allpads = board.GetPads()

for pad in allpads:
 mod = pad.GetParent()
 name = pad.GetPadName()
 if (0):
 print("pad {}({}) on {}({}) at {},{} shape {} size {},{}"
 .format(name,
 pad.GetNet().GetNetname(),
 mod.GetReference(),
 mod.GetValue(),
 pad.GetPosition().x, pad.GetPosition().y,
 padshapes[pad.GetShape()],
 pad.GetSize().x, pad.GetSize().y
 ))
 if (pad.GetShape() == pcbnew.PAD_SHAPE_RECT):
 if ((pad.GetOrientationDegrees()==270) | (pad.GetOrientationDegrees()==90)):
 svgpads.add(dwg.rect(insert=(pad.GetPosition().x-pad.GetSize().x/2,
 pad.GetPosition().y-pad.GetSize().y/2),
 size=(pad.GetSize().y, pad.GetSize().x)))
 else:
 svgpads.add(dwg.rect(insert=(pad.GetPosition().x-pad.GetSize().x/2,
 pad.GetPosition().y-pad.GetSize().y/2),
 size=(pad.GetSize().x, pad.GetSize().y)))
 elif (pad.GetShape() == pcbnew.PAD_SHAPE_CIRCLE):
 svgpads.add(dwg.circle(center=(pad.GetPosition().x, pad.GetPosition().y),
 r=pad.GetSize().x))
 elif (pad.GetShape() == pcbnew.PAD_SHAPE_OVAL):
 svgpads.add(dwg.ellipse(center=(pad.GetPosition().x, pad.GetPosition().y),
 r=(pad.GetSize().x/2, pad.GetSize().y/2)))
 else:
 print("unknown pad shape {}({})".format(pad.GetShape(), padshapes[pad.GetShape()]))


 
dwg.save()

 

modifying pcbnew layout from python

I my previous post, I talked about querying for information about your layout. In this one I’ll show you how to create your own wires/vias from python. I also cover moving modules. Most of the code is self-explanatory but I find it helpful to have sample “cookbook” code.

The main thing to keep in mind when creating new objects is that even though you have to pass a board pointer to the constructors, you still have to call board.Add(obj). Also, you have to add it before setting the net. If you try to set the net before, it’ll do nothing.

Remember that the units are 1E-6mm. So if you have a mm value multiply it by a million.

Layers

First, let’s generate our layer mapping

layertable = {}

numlayers = pcbnew.LAYER_ID_COUNT
for i in range(numlayers):
 layertable[i] = board.GetLayerName(i)
 print("{} {}".format(i, board.GetLayerName(i)))

Add a track

track = pcbnew.TRACK(board)
track.SetStart(pcbnew.wxPoint(136144000, 95504000))
track.SetEnd(pcbnew.wxPoint(176144000, 95504000))
track.SetWidth(1614400)
track.SetLayer(layertable["F.Cu"])

board.Add(track)
track.SetNet(clknet)

Add a via

In this case, I’m going to copy an existing via. Note that there is also the clone method, but doing it this way you’ll know how to generate a via from scratch.

There isn’t yet a direct way to query a via for its layers. The way I work around this is by looping through all layers and calling IsOnLayer This is one of the reasons I’m showing how to copy an existing via.

The via types will be one of these:

  • pcbnew.VIA_THROUGH
  • pcbnew.VIA_BLIND_BURIED
  • pcbnew.VIA_MICROVIA

The width if the via is the diameter. If you forget to set this, you’ll get funny behavior where via disappears from the display when you zoom in.

newvia = pcbnew.VIA(board)
# need to add before SetNet will work, 
# so just doing it first
board.Add(newvia)

toplayer=-1
bottomlayer=pcbnew.LAYER_ID_COUNT
for l in range(pcbnew.LAYER_ID_COUNT):
   if not track.IsOnLayer(l):
      continue
   toplayer = max(toplayer, l)
   bottomlayer = min(bottomlayer, l)

# now that I have the top and bottom layers, I tell the new
# via
newvia.SetLayerPair(toplayer, bottomlayer)
newvia.SetPosition(pcbnew.wxPoint(track.GetPosition().x+offset[0],
                                  track.GetPosition().y+offset[1]))
newvia.SetViaType(oldvia.GetViaType())
newvia.SetWidth(oldvia.GetWidth())
newvia.SetNet(tonet)

Moving a module

I haven’t tried creating a new module yet. I prefer to let the netlist importer do this for me. I do, however, find it useful to be able to move modules. They all come in on top of each other. There are a variety of placement algorithms one might want to implement. 1

Note that the orientation is degrees*10.0

peer.SetPosition(pcbnew.wxPoint(newx, newy))
peer.SetOrientation(180*10)

Class diagram

I’ve created a class diagram to help me remember. Click to enlarge. Also available in my github


  1. In my previous professional life, one of the more interesting ones I saw was using linear programming. You start with everything in the middle. You create a set of equations representing net connectivity as well as cell overlaps. Solve the equations. Repeat. It was good for a couple hundred thousand cells. Much more than what PCB requires. Simulated Annealing would likely be easier here.

The basics of scripting in pcbnew

I’ve found that, so far, 1 I’m able to do all of the layout queries and manipulations I’ve wanted to do.

Note that these examples don’t work on the latest release as of Feb 2017 (4.0.5). You want one of the nightly builds, also in the kicad download area. Or you can build it

The interface is lacking some consistency but it’s fine if you have a map of the classes (click for a larger version or download it from here):

 

In this post, I’ll focus on querying a board for information about it’s contents. The code can be found in this github repo.

Getting started

To invoke any of these examples, you’ll want pcbnew’s scripting window. Tools->scripting console

You’ll probably want these in your scripts

import pcbnew

# most queries start with a board
board = pcbnew.GetBoard()

Nets

Want to know all of the nets in your board?
Nets can be looked up in two ways:

  • by name
  • by netcode – a unique integer identifier for your net.

If you run this code:

# returns a dictionary netcode:netinfo_item
netcodes = board.GetNetsByNetcode()

# list off all of the nets in the board.
for netcode, net in netcodes.items():
    print("netcode {}, name {}".format(netcode, net.GetNetname()))

# here's another way of doing the same thing.
print("here's the other way to do it")
nets = board.GetNetsByName()
for netname, net in nets.items():
    print("method2 netcode {}, name{}".format(net.GetNet(), netname))

# maybe you just want a single net
# the find method returns an iterator to all matching nets.
# the value of an iterator is a tuple: name, netinfo
clknet = nets.find("/clk").value()[1]
clkclass = clknet.GetNetClass()

print("net {} is on netclass {}".format(clknet.GetNetname(),
 clkclass))

You’ll get something like this:

netcode 49, name /ihg
netcode 50, name /ihh
netcode 51, name /data_in
netcode 52, name /data_out
netcode 53, name /clk
here's the other way to do it
method2 netcode 0, name
method2 netcode 23, name+5V
method2 netcode 53, name/clk
method2 netcode 55, name/data_contd
method2 netcode 51, name/data_in

Physical dimensions

The coordinate space of kicad_pcb is in mm. At the beginning of this wiki about Kicad’s Board_File_Format

“All physical units are in mils (1/1000th inch) unless otherwise noted.”

Then later in historical notes, it says,

“As of 2013, the PCBnew application creates ‘.kicad_pcb’ files that begin with (kicad_pcb (version 3)”. All distances are in millimeters.

In short, for the data that I’ve recently created, the internal coordinate space of pcbnew is 10E-6 mm. (a millionth of a mm)
For example, the coordinate 121550000 corresponds to 121.550000mm

SCALE = 1000000.0

boardbbox = board.ComputeBoundingBox()
boardxl = boardbbox.GetX()
boardyl = boardbbox.GetY()
boardwidth = boardbbox.GetWidth()
boardheight = boardbbox.GetHeight()

print("this board is at position {},{} {} wide and {} high".format(boardxl,
    boardyl,
    boardwidth,
    boardheight))

Modules/Pads

Each of your placed modules can be found with its reference name. The module connection points are pads, of course.

# generate a LUT with shape integers to a string
padshapes = {
    pcbnew.PAD_SHAPE_CIRCLE: "PAD_SHAPE_CIRCLE",
    pcbnew.PAD_SHAPE_OVAL: "PAD_SHAPE_OVAL",
    pcbnew.PAD_SHAPE_RECT: "PAD_SHAPE_RECT",
    pcbnew.PAD_SHAPE_TRAPEZOID: "PAD_SHAPE_TRAPEZOID"
}
# new in the most recent kicad code
if hasattr(pcbnew, 'PAD_SHAPE_ROUNDRECT'):
    padshapes[pcbnew.PAD_SHAPE_ROUNDRECT] = "PAD_SHAPE_ROUNDRECT",

modref = "U1"
mod = board.FindModuleByReference(modref)
for pad in mod.Pads():
    print("pad {}({}) on {}({}) at {},{} shape {} size {},{}"
        .format(pad.GetPadName(),
                pad.GetNet().GetNetname(),
                mod.GetReference(),
                mod.GetValue(),
                pad.GetPosition().x, pad.GetPosition().y,
                padshapes[pad.GetShape()],
                pad.GetSize().x, pad.GetSize().y
    ))

Gives you this:

pad 1(/ilb) on U1(74HC595) at 127635000,106520000 shape PAD_SHAPE_RECT size 1500000,600000
pad 2(/ilc) on U1(74HC595) at 126365000,106520000 shape PAD_SHAPE_RECT size 1500000,600000
pad 3(/ild) on U1(74HC595) at 125095000,106520000 shape PAD_SHAPE_RECT size 1500000,600000

Layer

Most of the pcb data is on a layer. pcbnew stores layers as numbers. Here we can print them all out

layertable = {}

numlayers = pcbnew.LAYER_ID_COUNT
   for i in range(numlayers):
       layertable[i] = board.GetLayerName(i)
       print("{} {}".format(i, board.GetLayerName(i)))

Which gives you this:

0 F.Cu
1 In1.Cu
2 In2.Cu
3 In3.Cu
...

Tracks (wires and vias)

A wire is stored in a TRACK object. Vias are in a class derived from TRACK. Let’s list some of them.

# clk net was defined above as was SCALE
clktracks = board.TracksInNet(clknet.GetNet())
   for track in clktracks:
       print("{},{}->{},{} width {} layer {}".format(track.GetStart().x/SCALE,
             track.GetStart().y/SCALE,
             track.GetEnd().x/SCALE,
             track.GetEnd().y/SCALE,
             track.GetWidth()/SCALE,
             layertable[track.GetLayer()]))

And here are the wires

121.92,97.79->126.492,97.79 width 0.25 layer F.Cu
127.762,100.33->125.984,100.33 width 0.25 layer F.Cu
127.762,99.314->127.762,100.33 width 0.25 layer B.Cu

So that’s how to query data. In my next post, I’ll talk about making changes


  1. thanks to recent work by Kicad contributor/developer Dick Hollenbeck