Skip to content

Instantly share code, notes, and snippets.

@aster94
Last active April 15, 2023 09:07
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aster94/bd52972ab6dbf13a44fc046b4222f7e7 to your computer and use it in GitHub Desktop.
Save aster94/bd52972ab6dbf13a44fc046b4222f7e7 to your computer and use it in GitHub Desktop.
KiCad plugin to automatically generate Gerber files, drill file and an image of the PCB, all in the project's folder
import pcbnew
import os
import shutil
import subprocess
# SETTINGS:
# Gerber
# Drill
METRIC = True
ZERO_FORMAT = pcbnew.GENDRILL_WRITER_BASE.DECIMAL_FORMAT
INTEGER_DIGITS = 3
MANTISSA_DIGITS = 3
MIRROR_Y_AXIS = False
HEADER = True
OFFSET = pcbnew.wxPoint(0,0)
MERGE_PTH_NPTH = True
DRILL_FILE = True
MAP_FILE = False
REPORTER = None
def generate_gerbers(pcb, path):
plot_controller = pcbnew.PLOT_CONTROLLER(pcb)
plot_options = plot_controller.GetPlotOptions()
# Set General Options:
plot_options.SetOutputDirectory(path)
plot_options.SetPlotFrameRef(False)
plot_options.SetPlotValue(True)
plot_options.SetPlotReference(True)
plot_options.SetPlotInvisibleText(True)
plot_options.SetPlotViaOnMaskLayer(True)
plot_options.SetExcludeEdgeLayer(False)
#plot_options.SetPlotPadsOnSilkLayer(PLOT_PADS_ON_SILK_LAYER)
#plot_options.SetUseAuxOrigin(PLOT_USE_AUX_ORIGIN)
plot_options.SetMirror(False)
#plot_options.SetNegative(PLOT_NEGATIVE)
#plot_options.SetDrillMarksType(PLOT_DRILL_MARKS_TYPE)
#plot_options.SetScale(PLOT_SCALE)
plot_options.SetAutoScale(True)
#plot_options.SetPlotMode(PLOT_MODE)
#plot_options.SetLineWidth(pcbnew.FromMM(PLOT_LINE_WIDTH))
# Set Gerber Options
#plot_options.SetUseGerberAttributes(GERBER_USE_GERBER_ATTRIBUTES)
#plot_options.SetUseGerberProtelExtensions(GERBER_USE_GERBER_PROTEL_EXTENSIONS)
#plot_options.SetCreateGerberJobFile(GERBER_CREATE_GERBER_JOB_FILE)
#plot_options.SetSubtractMaskFromSilk(GERBER_SUBTRACT_MASK_FROM_SILK)
#plot_options.SetIncludeGerberNetlistInfo(GERBER_INCLUDE_GERBER_NETLIST_INFO)
plot_plan = [
( 'F.Cu', pcbnew.F_Cu, 'Front Copper' ),
( 'B.Cu', pcbnew.B_Cu, 'Back Copper' ),
( 'F.Paste', pcbnew.F_Paste, 'Front Paste' ),
( 'B.Paste', pcbnew.B_Paste, 'Back Paste' ),
( 'F.SilkS', pcbnew.F_SilkS, 'Front SilkScreen' ),
( 'B.SilkS', pcbnew.B_SilkS, 'Back SilkScreen' ),
( 'F.Mask', pcbnew.F_Mask, 'Front Mask' ),
( 'B.Mask', pcbnew.B_Mask, 'Back Mask' ),
( 'Edge.Cuts', pcbnew.Edge_Cuts, 'Edges' ),
( 'Eco1.User', pcbnew.Eco1_User, 'Eco1 User' ),
( 'Eco2.User', pcbnew.Eco2_User, 'Eco1 User' ),
]
for layer_info in plot_plan:
plot_controller.SetLayer(layer_info[1])
plot_controller.OpenPlotfile(layer_info[0], pcbnew.PLOT_FORMAT_GERBER, layer_info[2])
plot_controller.PlotLayer()
plot_controller.ClosePlot()
def detect_blind_buried_or_micro_vias(pcb):
through_vias = 0
micro_vias = 0
blind_or_buried_vias = 0
for track in pcb.GetTracks():
if track.Type() != pcbnew.PCB_VIA_T:
continue
if track.GetShape() == pcbnew.VIA_THROUGH:
through_vias += 1
elif track.GetShape() == pcbnew.VIA_MICROVIA:
micro_vias += 1
elif track.GetShape() == pcbnew.VIA_BLIND_BURIED:
blind_or_buried_vias += 1
if micro_vias or blind_or_buried_vias:
return True
else:
return False
def generate_drill_file(pcb, path):
#if detect_blind_buried_or_micro_vias(pcb):
# return
drill_writer = pcbnew.EXCELLON_WRITER(pcb)
drill_writer.SetFormat(METRIC, ZERO_FORMAT, INTEGER_DIGITS, MANTISSA_DIGITS)
drill_writer.SetOptions(MIRROR_Y_AXIS, HEADER, OFFSET, MERGE_PTH_NPTH)
drill_writer.CreateDrillandMapFilesSet(path, DRILL_FILE, MAP_FILE, REPORTER)
class SimplePlugin(pcbnew.ActionPlugin):
def defaults(self):
self.name = 'Gerber Plot'
self.category = 'Gerber'
self.description = 'Generate Gerber files, drill holes, see the result and send to a compressed folder'
self.show_toolbar_button = True
self.icon_file_name = os.path.join(os.path.dirname(__file__), 'gerber_plot_icon.png')
def Run(self):
# The entry function of the plugin that is executed on user action
try:
cwd_path = os.getcwd()
pcb = pcbnew.GetBoard()
project_path, project_name = os.path.split(pcb.GetFileName())
project_name = os.path.splitext(project_name)[0]
output_path = os.path.join(project_path, project_name + '-Gerber').replace('\\','/')
tmp_path = os.path.join(project_path, 'tmp').replace('\\','/')
log_file = os.path.join(project_path, 'log.txt').replace('\\','/')
if os.path.exists(log_file):
os.remove(log_file)
except Exception as err:
with open(log_file, 'a') as file:
file.write('Startup error\nError:{}\n'.format(err))
# Create a temp folder
try:
os.mkdir(tmp_path)
except Exception as err:
with open(log_file, 'a') as file:
file.write('tmp folder not created\nError:{}\n'.format(err))
# Generate Gerber and drill files
try:
generate_gerbers(pcb, tmp_path)
except Exception as err:
with open(log_file, 'a') as file:
file.write('Gerbers not plotted\nError:{}\n'.format(err))
try:
generate_drill_file(pcb, tmp_path)
except Exception as err:
with open(log_file, 'a') as file:
file.write('Drill file not plotted\nError:{}\n'.format(err))
# Render an image: we need to call an external script that uses python 3
try:
subprocess.check_call(['powershell','render_pcb', tmp_path, os.path.join(project_path, project_name + '.png').replace('\\','/')], shell=True)
# if you don't wish to have it as a exe file you could use:
#subprocess.check_call(['powershell', 'path_to_python3', 'path_to_render_pcb', tmp_path, os.path.join(project_path, project_name + '.png').replace('\\','/')], shell=True)
except Exception as err:
with open(log_file, 'a') as file:
file.write('PCB not rendered\nError:{}\n'.format(err))
# Create compressed file from tmp
try:
os.chdir(tmp_path)
shutil.make_archive(output_path, 'zip', tmp_path)
os.chdir(cwd_path)
except Exception as err:
with open(log_file, 'a') as file:
file.write('ZIP file not created\nError:{}\n'.format(err))
# Remove temp folder
try:
shutil.rmtree(tmp_path, ignore_errors=True)
except Exception as err:
with open(log_file, 'a') as file:
file.write('temp folder not deleted\nError:{}\n'.format(err))
SimplePlugin().register() # Instantiate and register to Pcbnew
import os, shutil
from gerber import common
from gerber.layers import PCBLayer, DrillLayer
from gerber.render import RenderSettings
from gerber.render.cairo_backend import GerberCairoContext
from PIL import Image
import click
# Render
SCALE = 25
OFFSET = 20
@click.command()
@click.argument('input_path')
def render_pcb(input_path):
"""Render Gerber Files into a PNG Image
INPUT_PATH - Could be a folder or a zip file containing the Gerber Files
"""
del_tmp_folder = False
extract_dir = ''
if os.path.isfile(input_path):
if not input_path.endswith('.zip'):
click.BadParameter('Wrong INPUT_PATH') # exit
extract_dir = os.path.join(os.path.dirname(input_path), 'tmp')
shutil.unpack_archive(input_path, extract_dir, 'zip')
input_path = extract_dir
del_tmp_folder = True
output_path = os.path.join(os.path.dirname(input_path), 'pcb.png')
img_front_path = os.path.join(input_path, 'front.png')
img_bottom_path = os.path.join(input_path, 'bottom.png')
for file in os.listdir(input_path):
real_path = os.path.join(input_path, file)
if not os.path.isfile(real_path):
continue
# Drill
if file.endswith('.drl'):
drill = DrillLayer(real_path, common.read(real_path))
# Front
elif file.endswith('-F_Cu.gbr'):
copper_front = PCBLayer(real_path, 'top', common.read(real_path))
elif file.endswith('-F_Mask.gbr'):
mask_front = PCBLayer(real_path, 'topmask', common.read(real_path))
elif file.endswith('-F_SilkS.gbr'):
silk_front = PCBLayer(real_path, 'topsilk', common.read(real_path))
# Bottom
elif file.endswith('-B_Cu.gbr'):
copper_bottom = PCBLayer(real_path, 'bottom', common.read(real_path))
elif file.endswith('-B_Mask.gbr'):
mask_bottom = PCBLayer(real_path, 'bottommask', common.read(real_path))
elif file.endswith('-B_SilkS.gbr'):
silk_bottom = PCBLayer(real_path, 'bottomsilk', common.read(real_path))
else:
continue
# Create a new drawing context
ctx = GerberCairoContext(scale=SCALE)
ctx.render_layer(copper_front)
ctx.render_layer(mask_front)
ctx.render_layer(silk_front)
ctx.render_layer(drill)
# Write png file
ctx.dump(img_front_path)
# Clear the drawing
ctx.clear()
# Render bottom layers
ctx.render_layer(copper_bottom)
ctx.render_layer(mask_bottom)
ctx.render_layer(silk_bottom)
ctx.render_layer(drill, settings=RenderSettings(mirror=True))
# Write png file
ctx.dump(img_bottom_path)
ctx.clear()
# Concatenate
front = Image.open(img_front_path)
bottom = Image.open(img_bottom_path)
render = Image.new('RGB', (front.width, front.height * 2 + OFFSET))
render.paste(front, (0, 0))
render.paste(bottom, (0, front.height + OFFSET))
render.save(output_path)
render.show()
if del_tmp_folder:
shutil.rmtree(extract_dir, ignore_errors=True)
if __name__ == "__main__":
render_pcb()
@aster94
Copy link
Author

aster94 commented Dec 28, 2019

The main problem (at least for windows user) is to download cairo

@hildogjr
Copy link

It will be nice create a GUI to manage the configuration used and also export / import some file file those (we could use to export different projects with the same pattern).

@aster94
Copy link
Author

aster94 commented Dec 28, 2019

hi, i don't feel that a gui is really needed since there is already the kicad gui. The purpose is mnore like a single button that does all you need

@hildogjr
Copy link

The GUI could have checkbox and some active/disable some output, also export and import the settings.

@hoijui
Copy link

hoijui commented Aug 28, 2020

great, thank you @aster94!
As it is now, I must consider this "All rights reserved" (= closed source), because you did not yet specify a license...
would you be willing to put it under an open source license? (GPL 2/3, BSD, MIT, APL, ... or even CC0 or public-domain)

... ouh I saw, that you like the GPL 3 ... that wold be great!! :-)

@aster94
Copy link
Author

aster94 commented Aug 28, 2020

Hello @hoijui consider this as open source, feel free to use it, just let me know about your creation I am interested
I will update the licence in the following days

@hoijui
Copy link

hoijui commented Aug 28, 2020

cool, thank you! :-)
I imagine this becoming an important part of the future of electronic Open Source Hardware...
My current idea is, to make a full fledged project out of it, where I would like to give you access too.
I am working for OSE Germany (sister organization of the much more famous OSE US), where we recently released the first norm for Open Source Hardware (OSH), and where we are generally working on improving the state of OSH, especially in terms of documentation.
having an automated way of generating gerbers and even renders is an important part of the puzzle, in my eyes (I am responsible for improving the technical aspects of documenting OSH projects).
auto-generating gerbers means, they do not have to be stored in the repo its self, which makes the repo much leaner and cleaner, and it means that when generated in a CI job on github, for example, one always has the latest gerbers available, without chance of someone forgetting to generate them... I imagine you though about similar things when you did this.. let's try to spread it!

... will keep you posted!

@hoijui
Copy link

hoijui commented Aug 28, 2020

ah wait... now I see ... the gerber part needs to be used from within kicad, right?
could it be adjusted tot be used from the command-line?
sorry, I never coded anything for kicad, I have no idea.

@hoijui
Copy link

hoijui commented Aug 28, 2020

ahh.. it was easy!

I just replaced the last line:

SimplePlugin().register() # Instantiate and register to Pcbnew

with these:

pcb = pcbnew.LoadBoard("base.kicad_pcb")
gerbers_path = "build/gerbers/base/"
generate_gerbers(pcb, gerbers_path)
generate_drill_file(pcb, gerbers_path)
shutil.make_archive("build/myProject-base-gerbers", 'zip', gerbers_path)

great, thank you again! :-)

@hoijui
Copy link

hoijui commented Sep 1, 2020

I put something together that basically seems to work now:
https://github.com/osegermany/kicad-pcb-generate-doc-tool
... GPL v3 is OK, right? or did you want an other licence?

@aster94
Copy link
Author

aster94 commented Sep 1, 2020

... GPL v3 is OK, right? or did you want an other licence?

whatever license fits better for your organization

could it be adjusted tot be used from the command-line?

I am happy you made it work!

Keep in mind this: the two files (render_pcb.py and plot_gerber.py) are split for a reason. Kicad uses an old version of python (2.7) and Cairo (a library needed to generate the png image) is not well supported by this old python. So I had to call a newer version of python (which support cairo) from the main file
here the code: https://gist.github.com/aster94/bd52972ab6dbf13a44fc046b4222f7e7#file-gerber_plot-py-L151-L155

once the kicad team will move to the newer python you will be able to merge the two files in one (here a better explanation of the issue https://forum.kicad.info/t/run-kicads-python-pip/20381/4)

@hoijui check also the idea of @hildogjr https://gist.github.com/aster94/bd52972ab6dbf13a44fc046b4222f7e7#gistcomment-3122252

@hoijui
Copy link

hoijui commented Sep 1, 2020

ouhh thank you for that explanation!
I will add this as docu into the file.
That only seems to affect the gerber_plot.py when used as a plugin in KiCad though (which I did not try). When using from the command line, I ran both the files with my default python (3.8.5), and it works fine.
I left the plugin part in there anyway, even though we do not need it; I will add this comment of yours to that part of the code.

@aster94
Copy link
Author

aster94 commented Sep 5, 2020

IMHO you should merge the two files and remove the kicad part, it will make life for future contributor easier

you just need to bring the function render_pcb in the main file and instead of this (https://gist.github.com/aster94/bd52972ab6dbf13a44fc046b4222f7e7#file-gerber_plot-py-L153) call the new function

@hoijui
Copy link

hoijui commented Sep 7, 2020

I personally would not merge the files, because they do different, potentially independent usable things. So to keep it modular, having them separate makes sense.
For example, I just got to know about KiBot, which seems to be a much more sophisticated and further developed way to auto-generate Gerbers from KiCad files. If I see it right though, it does not do 2D rendering of Gerbers, so we could use KiBot for gerbers and your render_pcb.py for 2D rendering.

@aster94
Copy link
Author

aster94 commented Sep 7, 2020

I see it can generate also BOM and pick and place files, very nice!

@hoijui
Copy link

hoijui commented Sep 9, 2020

yeah!
It can also render in 2D, I saw

I did not yet get it to work for me. it is made up of quite a few sub-tools, and one needs to find out how to install each one first. I will try to help make the documentation more easy to get into. But the end result is really great! especially the interactive BOM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment