forked from skarrmann/horizon
-
Notifications
You must be signed in to change notification settings - Fork 0
/
horizon-board-producer-plugin.py
287 lines (243 loc) · 12.6 KB
/
horizon-board-producer-plugin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# Copyright (c) 2023 Steven Karrmann
# SPDX-License-Identifier: MIT
# Horizon Board Producer Plugin Usage:
# 1. Copy this Python file (or create a symbolic link) in the KiCad plugins directory
# * See: https://dev-docs.kicad.org/en/python/pcbnew/#_typical_plugin_structure
# 2. Open KiCad Pcbnew, and open the main board .kicad_pcb file
# 3. In Pcbnew, click "Tools > External Plugins > Horizon Board Producer"
# 4. If successful, 3 gerber zip files are created in the gerbers folder: main board, top plate, and bottom plate
import pcbnew, wx, os, shutil, re
class HorizonBoardProducerPlugin(pcbnew.ActionPlugin):
def defaults(self):
self.name = "Horizon Board Producer Rev2.4 (KiCad 7)"
self.category = "Gerbers, plates, generator"
self.description = "Generates top plate and bottom plate PCBs, and then creates gerber files for the main, top plate, and bottom plate PCBs"
def Run(self):
HorizonBoardProducerPlugin.produce()
@staticmethod
def __create_gerbers(board, path):
"""
Creates Gerber files for a KiCad board and outputs them to the specified path.
Args:
board (pcbnew.BOARD): The board for which to generate Gerber files.
path (str): File system path where Gerber files will be created.
"""
plot_controller = pcbnew.PLOT_CONTROLLER(board)
plot_options = plot_controller.GetPlotOptions()
# Set General Options:
plot_options.SetOutputDirectory(path)
plot_options.SetPlotFrameRef(False) # "plot border and title block"
plot_options.SetPlotValue(True)
plot_options.SetPlotReference(True)
plot_options.SetPlotInvisibleText(False)
plot_options.SetPlotOnAllLayersSelection(pcbnew.LSET())
plot_options.SetSketchPadsOnFabLayers(False)
plot_options.SetPlotViaOnMaskLayer(False) # "do not tent vias"
plot_options.SetDrillMarksType(pcbnew.DRILL_MARKS_NO_DRILL_SHAPE)
plot_options.SetScale(1.0)
plot_options.SetPlotMode(pcbnew.FILLED) # Filled
plot_options.SetUseAuxOrigin(False)
plot_options.SetMirror(False)
plot_options.SetNegative(False)
# Note: "check zone fills before plotting" does not seem to be available in API
plot_options.SetUseGerberProtelExtensions(True)
plot_options.SetCreateGerberJobFile(False)
plot_options.SetSubtractMaskFromSilk(True)
plot_options.SetGerberPrecision(6) # "coordinate format: 4.6, unit mm"
plot_options.SetUseGerberX2format(False)
plot_options.SetIncludeGerberNetlistInfo(False)
layers = [
( 'F.Cu', pcbnew.F_Cu, 'Front Copper' ),
( 'B.Cu', pcbnew.B_Cu, 'Back Copper' ),
( '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' )
]
for layer in layers:
plot_controller.SetLayer(layer[1])
plot_controller.OpenPlotfile(layer[0], pcbnew.PLOT_FORMAT_GERBER, layer[2])
plot_controller.PlotLayer()
plot_controller.ClosePlot()
@staticmethod
def __create_drill_file(board, path):
"""
Creates drill files for a KiCad board and outputs them to the specified path.
Args:
board (pcbnew.BOARD): The board for which to generate Gerber files.
path (str): File system path where drill files will be created.
"""
format = {
'metric': True,
'zero_format': pcbnew.GENDRILL_WRITER_BASE.DECIMAL_FORMAT,
'left_digits': 3,
'right_digits': 3
}
options = {
'mirror_y_axis': False,
'minimal_header': False,
'offset': pcbnew.VECTOR2I(0,0),
'pth_npth_single_file': False
}
drill_writer = pcbnew.EXCELLON_WRITER(board)
drill_writer.SetFormat(format['metric'], format['zero_format'], format['left_digits'], format['right_digits'])
drill_writer.SetOptions(options['mirror_y_axis'], options['minimal_header'], options['offset'], options['pth_npth_single_file'])
drill_writer.SetRouteModeForOvalHoles(False)
drill_writer.CreateDrillandMapFilesSet(path, True, False)
@staticmethod
def __create_zip(zip_file_path, source_folder):
"""
Creates a zip file containing all files in the source folder.
Args:
zip_file_path (str): The full path and file name for the new zip file.
source_folder (str): The source folder containing the files to include in the zip.
"""
return shutil.make_archive(zip_file_path, 'zip', source_folder)
@staticmethod
def __create_plate_pcb_from_layer(board, layer_name):
"""
Converts the provided board into a new cutout plate PCB, using layer data from the specified source board.
All tracks and zones are removed from the plate, and only the footprint's cutouts are retained.
These items within the bounds of the plate's cutout will be preserved:
* Silkscreen directly on the board (but not in footprints)
* LOGO footprints (silkscreen images)
* H footprints (mounting holes)
For all other footprints within the bounds of the plate's cutout:
* SMD pads on the target layer are converted to non-plated through-hole pads.
* Graphic lines on the target layer are converted to edge cuts.
* All other pads and graphics are deleted.
Args:
board (pcbnew.BOARD): The board to convert into a plate
layer_name (str): The layer name which represents the edge cutouts for the plate.
"""
remove_board_items = []
for track in board.GetTracks():
remove_board_items.append(track)
for zone in board.Zones():
remove_board_items.append(zone)
for drawing in board.GetDrawings():
if drawing.IsOnLayer(board.GetLayerID(layer_name)) and drawing.GetClass() == 'PCB_SHAPE':
# Preserve graphic lines on target layer, and move them to layer edge cuts
drawing.SetLayer(board.GetLayerID('Edge.Cuts'))
elif (drawing.IsOnLayer(board.GetLayerID('F.Silkscreen')) or drawing.IsOnLayer(board.GetLayerID('B.Silkscreen'))) and drawing.GetClass() == 'PCB_TEXT':
# Preserve graphics text on silkscreen
continue
else:
# Delete all other graphics
remove_board_items.append(drawing)
# Remove the tracks/zones/drawings now that we are done iterating
for remove_board_item in remove_board_items:
board.Remove(remove_board_item)
platebounds = board.GetBoardEdgesBoundingBox()
# Convert footprints to NPTH pads and edge cuts
for footprint in board.GetFootprints():
if footprint.GetBoundingBox().Intersects(platebounds):
footprint.Reference().SetVisible(False)
footprint.Value().SetVisible(False)
if re.match(r'^(H|LOGO)\d+$', footprint.GetReference()):
# Preserve pads on 'H' (mounting hole) and 'LOGO' (graphics) footprints
continue
else:
remove_footprint_items = []
for pad in footprint.Pads(): # Convert SMD circle/oval pads on target layer to NPTH pads, and remove all other pads
if pad.IsOnLayer(board.GetLayerID(layer_name)) and pad.GetShape() in [pcbnew.PAD_SHAPE_CIRCLE, pcbnew.PAD_SHAPE_OVAL]:
drill_shape = pcbnew.PAD_DRILL_SHAPE_CIRCLE if pad.GetShape() == pcbnew.PAD_SHAPE_CIRCLE else pcbnew.PAD_DRILL_SHAPE_OBLONG
pad.SetAttribute(pcbnew.PAD_ATTRIB_NPTH)
pad.SetDrillShape(drill_shape)
pad.SetDrillSize(pad.GetSize())
pad.SetLayerSet(pad.UnplatedHoleMask())
pad.SetPosition(pad.GetPosition())
else:
remove_footprint_items.append(pad)
for graphic in footprint.GraphicalItems(): # Convert graphics on target layer to edge cuts, and remove all other graphics
if graphic.IsOnLayer(board.GetLayerID(layer_name)):
graphic.SetLayer(board.GetLayerID('Edge.Cuts')) # Move target layer graphics to edge cuts
else:
remove_footprint_items.append(graphic)
# Remove the pads/graphics now that we are done iterating
for remove_footprint_item in remove_footprint_items:
footprint.Remove(remove_footprint_item)
if len(footprint.Pads()) == 0 and len(footprint.GraphicalItems()) == 0:
remove_board_items.append(footprint)
else:
remove_board_items.append(footprint)
# Remove the footprints now that we are done iterating
for remove_board_item in remove_board_items:
board.Remove(remove_board_item)
board.Save(board.GetFileName())
@staticmethod
def __copy_board(board_source_path, board_destination_path):
"""
Creates a copy of the board at the source path and saves it to the destination path.
Args:
board_source_path (str): Path to the source .kicad_pcb file
board_destination_path (str): Path to the destination .kicad_pcb file
Returns:
pcbnew.BOARD: The board object for the newly-copied board
"""
shutil.copy(board_source_path, board_destination_path)
return pcbnew.LoadBoard(board_destination_path)
@staticmethod
def __generate_board_fabrication_files(board, temp_path, output_path):
"""
Generates the board's fabrication .zip file, containing gerber, drill, and drill map files
Args:
board (pcbnew.BOARD): The board
temp_path (str): Path to the temporary location of generated files
output_path (str): Path to the output fabrication .zip file
Returns:
str: Generated file name if successful
"""
if board.GetBoardEdgesBoundingBox().GetArea() > 0:
(board_folder, board_filename) = os.path.split(board.GetFileName())
(board_name, _) = os.path.splitext(board_filename)
gerber_output_path = os.path.join(temp_path, board_name)
archive_file_path = os.path.join(output_path, board_name)
HorizonBoardProducerPlugin.__create_gerbers(board, gerber_output_path)
HorizonBoardProducerPlugin.__create_drill_file(board, gerber_output_path)
generated_file = HorizonBoardProducerPlugin.__create_zip(archive_file_path, gerber_output_path)
return generated_file
else:
return None
@staticmethod
def produce():
"""
Executes the board production on the currently-opened board.
"""
current_board_path = pcbnew.GetBoard().GetFileName()
try:
relative_output_path = '../../gerbers'
relative_temp_path = '../../temp'
(board_folder, board_filename) = os.path.split(current_board_path)
temp_path = os.path.normpath(os.path.join(board_folder, relative_temp_path))
output_path = os.path.normpath(os.path.join(board_folder, relative_output_path))
if os.path.exists(temp_path):
shutil.rmtree(temp_path)
os.makedirs(temp_path)
if not os.path.exists(output_path):
os.makedirs(output_path)
main_board = HorizonBoardProducerPlugin.__copy_board(current_board_path, os.path.join(temp_path, board_filename))
main_board_output_file = HorizonBoardProducerPlugin.__generate_board_fabrication_files(main_board, temp_path, output_path)
bottom_plate = HorizonBoardProducerPlugin.__copy_board(current_board_path, os.path.join(temp_path, board_filename.replace('.kicad_pcb', '-bottom-plate.kicad_pcb')))
HorizonBoardProducerPlugin.__create_plate_pcb_from_layer(bottom_plate, 'B.Adhesive')
bottom_plate_output_file = HorizonBoardProducerPlugin.__generate_board_fabrication_files(bottom_plate, temp_path, output_path)
top_plate = HorizonBoardProducerPlugin.__copy_board(current_board_path, os.path.join(temp_path, board_filename.replace('.kicad_pcb', '-top-plate.kicad_pcb')))
HorizonBoardProducerPlugin.__create_plate_pcb_from_layer(top_plate, 'F.Adhesive')
top_plate_output_file = HorizonBoardProducerPlugin.__generate_board_fabrication_files(top_plate, temp_path, output_path)
# Reload the current board and its project
pcbnew.LoadBoard(current_board_path)
settings_manager = pcbnew.GetSettingsManager()
settings_manager.LoadProject(current_board_path.replace('.kicab_pcb', '.kicad_pro'), False)
generated_file_list = filter(lambda f: f is not None, [main_board_output_file, bottom_plate_output_file, top_plate_output_file])
complete_dialog = wx.MessageDialog(
None,
'Boards produced successfully.\n\nGenerated files:\n' + '\n'.join(generated_file_list) + '\n\nIMPORTANT: KiCad may be in a bad application state since this plugin uses unsupported board loading functionality.\n\nTO BE SAFE, KICAD WILL NOW FORCEFULLY EXIT. GOODBYE!',
'Horizon Board Producer - Complete',
wx.OK
)
complete_dialog.ShowModal()
complete_dialog.Destroy()
finally:
wx.Abort() # Kill the application to ensure users do not accidentally lose their work by interacting with bad application state.
HorizonBoardProducerPlugin().register()