Skip to content

Instantly share code, notes, and snippets.

@aster94
Last active April 15, 2023 09:07
Show Gist options
  • 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()
@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