-
-
Save aster94/bd52972ab6dbf13a44fc046b4222f7e7 to your computer and use it in GitHub Desktop.
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() |
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).
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
The GUI could have checkbox and some active/disable some output, also export and import the settings.
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!! :-)
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
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!
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.
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! :-)
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?
... 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
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.
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
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.
I see it can generate also BOM and pick and place files, very nice!
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.
The main problem (at least for windows user) is to download cairo