├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── desc.png ├── gerber_drill.py ├── holes_with_ref.png ├── kisexp.py ├── layout_tool.py ├── loadnet.py ├── mf_dialog_base.py ├── mf_tool.png ├── mf_tool.py ├── qrcode_footprint_wizard.py └── slot_without_ref.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 XToolBox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # KiCad EDA 生产文件生成器 3 | 4 | 现已支持KiCad 6.0. 当直接从PCB更新原理图时,生成的BOM表会缺少Datasheet信息,解决办法,在原理图中生成网表。 5 | 6 | Now support KiCad 6.0. When update PCB from schematic directly, datasheet field will miss. Generate netlist in schematic to solve it. 7 | 8 | 与JLCPCB和JLCKicadTools不同的是,这个工具只根据已有的器件信息来生成生产文件,不会调整器件的角度或是元件号。因此要求在设计原理图和使用封装的时候使用jlc.com上的器件信息,才能保证生成的文件能直接在jlc.com中制作。 9 | * 一些常用的jlc.com器件库[lc_kicad_lib](https://github.com/xtoolbox/lc_kicad_lib) 10 | * 或者是直接从lceda.cn中复制器件和封装库[lckiconverter](https://github.com/xtoolbox/lckiconverter) 11 | 12 | Unlike JLCPCB and JLCKicadTools, this tool only generates fabrication files based on existing device information and does not adjust the angle or part number of the device. Therefore, it is required to use the component and footprint in jlc.com when designing the schematic and PCB to ensure that the generated file can be directly used at jlc.com. 13 | * Some common used jlc.com library[lc_kicad_lib](https://github.com/xtoolbox/lc_kicad_lib) 14 | * Get KiCad symobl/footprint from lceda.cn [lckiconverter](https://github.com/xtoolbox/lckiconverter) 15 | 16 | ## 中文说明 17 | 18 | 本插件可一键生成 PCB 的 Gerber、钻孔、BOM 物料清单、坐标文件。 19 | 20 | ### 安装 21 | 22 | 适用于:KiCad EDA 5.1.0 + 23 | 24 | * Windows 安装命令 25 | ``` 26 | git clone https://github.com/xtoolbox/kicad_tools.git %appdata%/kicad/scripting/plugins/kicad_tools 27 | ``` 28 | * Linux 安装命令 29 | ``` 30 | // TODO 31 | ``` 32 | 33 | ### 使用 34 | 35 | 安装完成即可使用,找到 `工具` -> `外部插件` -> `Gen Manufacture Docs` 打开插件界面,点击插件界面上的 `Gen Manufacture Docs ` 按钮执行命令。 36 | 37 | ![desc](desc.png) 38 | 39 | ### 生成文件 40 | 41 | 当 `BOM List` `Positon File` `Gerber Files` 全选时,点击 `Generate Marnufacture Docs` 按钮后插件会一键生成 BOM 物料清单、坐标文件、Gerber 文件、钻孔文件。 42 | 43 | BOM 文件和坐标文件会以 CSV 格式存放在电路板同级目录下,Gerber 和钻孔文件放在电路板目录下的 gerber 目录中,通过 `Split Slot` 选项生成的钻孔文件中的槽孔会被转换成多个普通孔。 44 | 45 | 生成的文件可以直接在 jlc.com 进行贴装。 46 | 47 | ### 注意事项 48 | 49 | GenMFDoc() 会改变电路板的钻孔原点,建议先用GenMFDoc() 生成 BOM 清单和位置文件,再生成 Gerber 文件。 50 | 51 | ### 参考 52 | 53 | KiCad plot tool is forked from "https://github.com/blairbonnett-mirrors/kicad/blob/master/demos/python_scripts_examples/gen_gerber_and_drill_files_board.py" 54 | 55 | 56 | # Manufacture Tools for kicad 57 | 58 | Usage: 59 | 60 | step 1: Copy the mf_tool.py gerber_drill.py loadnet.py and sexpdata.pyto “[kicad install path]\share\kicad\scripting\plugins” 61 | 62 | step 2: In Python console window, type 63 | ```python 64 | import mf_tool as mf 65 | mf.GenSMTFiles() 66 | ``` 67 | 68 | step 3: or in [tools]->[external tools] menu, invoke the [Gen Manufacture Docs] command. 69 | 70 | step 4: the BOM and Postion CSV file will be generated under the same folder of the board file。 Gerber and drill file is under the "gerber" folder. The slot hole in drill file will split to hole serires. Send them to jlcpcb.com to get a PCBA. 71 | 72 | ## Attention: 73 | 74 | The GenMFDoc() command will change the Aux original point 75 | 76 | ## Preivew 77 | 78 | 79 | ![holes_with_ref](holes_with_ref.png) 80 | 81 | 82 | ![slot_without_ref](slot_without_ref.png) 83 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from . import mf_tool 2 | -------------------------------------------------------------------------------- /desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtoolbox/kicad_tools/7e8321c02ff002d9ef80bfd45d1e86a2656b0b69/desc.png -------------------------------------------------------------------------------- /gerber_drill.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A python script example to create plot files to build a board: 3 | Gerber files 4 | Drill files 5 | Map dril files 6 | This file is forked from "https://github.com/blairbonnett-mirrors/kicad/blob/master/demos/python_scripts_examples/gen_gerber_and_drill_files_board.py" 7 | Important note: 8 | this python script does not plot frame references (page layout). 9 | the reason is it is not yet possible from a python script because plotting 10 | plot frame references needs loading the corresponding page layout file 11 | (.wks file) or the default template. 12 | 13 | This info (the page layout template) is not stored in the board, and therefore 14 | not available. 15 | 16 | Do not try to change SetPlotFrameRef(False) to SetPlotFrameRef(true) 17 | the result is the pcbnew lib will crash if you try to plot 18 | the unknown frame references template. 19 | 20 | Anyway, in gerber and drill files the page layout is not plot 21 | ''' 22 | 23 | import sys 24 | import os 25 | import re 26 | import math 27 | import zipfile as zf 28 | 29 | from pcbnew import * 30 | def def_logger(*args): 31 | r = "" 32 | for t in args: 33 | r = r + str(t) + " " 34 | print(r) 35 | def GenGerberDrill(board = None, split_G85 = 0.2, plotDir = "plot/", plotReference = True, logger = def_logger): 36 | if not board: 37 | board = GetBoard() 38 | 39 | plotFiles = [] 40 | 41 | pctl = PLOT_CONTROLLER(board) 42 | 43 | popt = pctl.GetPlotOptions() 44 | 45 | popt.SetOutputDirectory(plotDir) 46 | 47 | # Set some important plot options: 48 | popt.SetPlotFrameRef(False) #do not change it 49 | if hasattr(popt, "SetLineWidth"): 50 | popt.SetLineWidth(FromMM(0.35)) 51 | 52 | popt.SetAutoScale(False) #do not change it 53 | popt.SetScale(1) #do not change it 54 | popt.SetMirror(False) 55 | popt.SetUseGerberAttributes(True) 56 | popt.SetUseGerberProtelExtensions(False) 57 | popt.SetExcludeEdgeLayer(True) 58 | popt.SetScale(1) 59 | popt.SetUseAuxOrigin(True) 60 | popt.SetPlotReference(plotReference) 61 | 62 | if hasattr(popt, "SetDrillMarksType"): 63 | popt.SetDrillMarksType(0) 64 | 65 | 66 | # This by gerbers only (also the name is truly horrid!) 67 | popt.SetSubtractMaskFromSilk(False) 68 | 69 | # Once the defaults are set it become pretty easy... 70 | # I have a Turing-complete programming language here: I'll use it... 71 | # param 0 is a string added to the file base name to identify the drawing 72 | # param 1 is the layer ID 73 | # param 2 is a comment 74 | plot_plan = [ 75 | ( "Top_Cu", F_Cu, "Top layer" ), 76 | ( "Bottom_Cu", B_Cu, "Bottom layer" ), 77 | ( "Bottom_Paste", B_Paste, "Paste Bottom" ), 78 | ( "Top_Paste", F_Paste, "Paste top" ), 79 | ( "Top_Silk", F_SilkS, "Silk top" ), 80 | ( "Bottom_Silk", B_SilkS, "Silk top" ), 81 | ( "Bottom_Mask", B_Mask, "Mask bottom" ), 82 | ( "Top_Mask", F_Mask, "Mask top" ), 83 | ( "EdgeCuts", Edge_Cuts, "Edges" ), 84 | ] 85 | 86 | 87 | for layer_info in plot_plan: 88 | pctl.SetLayer(layer_info[1]) 89 | pctl.OpenPlotfile(layer_info[0], PLOT_FORMAT_GERBER, layer_info[2]) 90 | logger('plot %s' % pctl.GetPlotFileName()) 91 | if pctl.PlotLayer() == False: 92 | logger("plot error") 93 | 94 | #generate internal copper layers, if any 95 | lyrcnt = board.GetCopperLayerCount(); 96 | 97 | for innerlyr in range ( 1, lyrcnt-1 ): 98 | pctl.SetLayer(innerlyr) 99 | lyrname = 'inner%s' % innerlyr 100 | pctl.OpenPlotfile(lyrname, PLOT_FORMAT_GERBER, "inner") 101 | logger('plot %s' % pctl.GetPlotFileName()) 102 | if pctl.PlotLayer() == False: 103 | logger("plot error") 104 | 105 | 106 | # At the end you have to close the last plot, otherwise you don't know when 107 | # the object will be recycled! 108 | pctl.ClosePlot() 109 | 110 | # Fabricators need drill files. 111 | # sometimes a drill map file is asked (for verification purpose) 112 | drlwriter = EXCELLON_WRITER( board ) 113 | drlwriter.SetMapFileFormat( PLOT_FORMAT_PDF ) 114 | 115 | mirror = False 116 | minimalHeader = False 117 | # offset = wxPoint(0,0) 118 | if hasattr(board, "GetAuxOrigin"): 119 | offset = board.GetAuxOrigin() 120 | else: 121 | offset = board.GetDesignSettings().GetAuxOrigin() 122 | # False to generate 2 separate drill files (one for plated holes, one for non plated holes) 123 | # True to generate only one drill file 124 | mergeNPTH = False 125 | logger("set drill offset ", offset) 126 | drlwriter.SetOptions( mirror, minimalHeader, offset, mergeNPTH ) 127 | 128 | metricFmt = True 129 | drlwriter.SetFormat( metricFmt ) 130 | 131 | genDrl = True 132 | genMap = False 133 | logger('create drill and map files in %s' % pctl.GetPlotDirName()) 134 | drlwriter.CreateDrillandMapFilesSet( pctl.GetPlotDirName(), genDrl, genMap ) 135 | 136 | # One can create a text file to report drill statistics 137 | #rptfn = pctl.GetPlotDirName() + 'drill_report.rpt' 138 | #print('report: %s' % rptfn) 139 | #drlwriter.GenDrillReportFile( rptfn ); 140 | 141 | if split_G85: 142 | logger("Split the slot into holes") 143 | SplitSlotInDrill(pctl.GetPlotDirName(), False, split_G85) 144 | 145 | files = [f for f in os.listdir(pctl.GetPlotDirName()) if f.endswith('.gbr')] 146 | for f in files: 147 | plotFiles.append( pctl.GetPlotDirName() + f ) 148 | 149 | files = [f for f in os.listdir(pctl.GetPlotDirName()) if f.endswith('.drl')] 150 | for f in files: 151 | plotFiles.append( pctl.GetPlotDirName() + f ) 152 | 153 | brdName = board.GetFileName() 154 | s = os.path.split(brdName) 155 | brdName = s[1] 156 | brdName = brdName[0:brdName.rfind('.')] 157 | logger("Board Name:", brdName) 158 | zipName = pctl.GetPlotDirName() + brdName + "_gerber.zip" 159 | logger("Zip them into " + zipName) 160 | 161 | azip = zf.ZipFile(zipName, 'w') 162 | for f in plotFiles: 163 | azip.write(filename=f, arcname = os.path.split(f)[1] , compress_type=zf.ZIP_DEFLATED) 164 | azip.close() 165 | 166 | return pctl.GetPlotDirName() 167 | 168 | def FromGerberPosition(position_str): 169 | s1 = position_str.find('X') 170 | s2 = position_str.find('Y') 171 | if (s1 != -1) and (s2 != -1): 172 | x = float(position_str[s1+1:s2]) 173 | y = float(position_str[s2+1:]) 174 | return [x,y] 175 | 176 | def same(p1,p2,diff = 0.0001): 177 | return (abs(p1[0]-p2[0]) 50: 198 | break 199 | r.append('X%sY%s\n' %(str(float('%.3f'%pt[0])),str(float('%.3f'%pt[1])))) 200 | return r 201 | 202 | def SplitG85(G85Data,step = 0.2): 203 | t = G85Data.split('G85') 204 | r = [] 205 | if len(t) == 2: 206 | p1 = FromGerberPosition(t[0]) 207 | p2 = FromGerberPosition(t[1]) 208 | dx = p2[0] - p1[0] 209 | dy = p2[1] - p1[1] 210 | dist = sqrt(dx*dx+dy*dy) 211 | td = 0 212 | pt = [p1[0],p1[1]] 213 | count = 0 214 | while not same(pt,p2): 215 | r.append('X%sY%s\n' %(str(float('%.3f'%pt[0])),str(float('%.3f'%pt[1])))) 216 | pt[0] = pt[0] + step*dx/dist 217 | pt[1] = pt[1] + step*dy/dist 218 | if dist - td < (step*1.5): 219 | pt[0] = p2[0] 220 | pt[1] = p2[1] 221 | else: 222 | td = td + step 223 | count = count + 1 224 | if count > 50: 225 | break 226 | r.append('X%sY%s\n' %(str(float('%.3f'%pt[0])),str(float('%.3f'%pt[1])))) 227 | return r 228 | else: 229 | return None 230 | 231 | def HoleSize(line): 232 | r = re.compile('(T[0-9]+)C([-0-9.]+)') 233 | m = r.match(line) 234 | if m: 235 | t = m.groups() 236 | return t[0], float(t[1]) 237 | return None, None 238 | 239 | def round(value, round = 0.1): 240 | if value < round: 241 | return round 242 | t = value / round 243 | t = math.ceil(t) 244 | return t * round 245 | 246 | def isG05(line): 247 | return line.find('G05') == 0 248 | 249 | def isG00(line): 250 | return line.find('G00') == 0 251 | def isG01(line): 252 | return line.find('G01') == 0 253 | def isM15(line): 254 | return line.find('M15') == 0 255 | def isM16(line): 256 | return line.find('M16') == 0 257 | 258 | 259 | 260 | def SplitSlotInDrill(drillPath, newfilename = True,step = 0.2): 261 | files = [f for f in os.listdir(drillPath) if f.endswith('.drl')] 262 | for fn in files: 263 | infn = drillPath + fn 264 | outfn = infn 265 | if not fn.endswith('_no_slot.drl'): 266 | with open(infn, "r") as ins: 267 | outline = [] 268 | skip_G05 = False 269 | holes = {} 270 | curHolesSize = None 271 | slot_mode = False 272 | hole_begin = None 273 | hole_end = None 274 | for line in ins: 275 | hn,hs = HoleSize(line) 276 | if hn and hs: 277 | holes[hn+'\n'] = hs 278 | if line in holes: 279 | curHolesSize = holes[line] 280 | step = 0.2 281 | if curHolesSize: 282 | step = round(curHolesSize/3) 283 | r = SplitG85(line, step) 284 | if not r: 285 | if isG00(line): 286 | slot_mode = True 287 | hole_begin = FromGerberPosition(line) 288 | if not slot_mode: 289 | if r: 290 | skip_G05 = True 291 | for l in r: 292 | outline.append(l) 293 | else: 294 | if skip_G05 and isG05(line): 295 | skip_G05 = False 296 | else: 297 | outline.append(line) 298 | else: 299 | if isG01(line): 300 | hole_end = FromGerberPosition(line) 301 | if isG05(line): 302 | slot_mode = False 303 | if hole_begin and hole_end: 304 | r = SplitSlot(hole_begin, hole_end, step) 305 | for l in r: 306 | outline.append(l) 307 | else: 308 | logger("Slot hole format error") 309 | hole_begin = None 310 | hole_end = None 311 | ins.close() 312 | if newfilename: 313 | outfn = infn.replace(".drl", "_no_slot.drl") 314 | fo = open(outfn, "w+") 315 | fo.writelines(outline) 316 | fo.close() 317 | 318 | 319 | -------------------------------------------------------------------------------- /holes_with_ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtoolbox/kicad_tools/7e8321c02ff002d9ef80bfd45d1e86a2656b0b69/holes_with_ref.png -------------------------------------------------------------------------------- /kisexp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue May 12 10:08:59 2020 4 | """ 5 | 6 | import io 7 | 8 | QUOTE = { 9 | "'":"'", 10 | '"':'"', 11 | } 12 | BRACKETS = {'(': ')'} 13 | BRACKETS_END = {')':1} 14 | SPACE = {' ':1, '\t':1, '\r':1, '\n':1} 15 | 16 | def quoteData(content, index): 17 | data = "" 18 | q_e = QUOTE[content[index]] 19 | c_len = len(content) 20 | escape = False 21 | index+=1; 22 | while index < c_len: 23 | c = content[index] 24 | if c == '\\': 25 | escape = True 26 | elif c == q_e: 27 | if(not escape): 28 | return data, index+1 29 | else: 30 | data += c; 31 | else: 32 | escape = False 33 | data += c 34 | index+=1 35 | return data, index+1 36 | 37 | def normalData(content, index): 38 | data = "" 39 | c_len = len(content) 40 | while index < c_len: 41 | c = content[index] 42 | if c in SPACE: 43 | index +=1 44 | break 45 | elif c in BRACKETS: 46 | break 47 | elif c in BRACKETS_END: 48 | break 49 | else: 50 | data += c 51 | index+=1 52 | return data, index 53 | 54 | def parseSexp(content, index = 0): 55 | res = [] 56 | c_len = len(content) 57 | data = "" 58 | bracket_end = "" 59 | while index < c_len: 60 | c = content[index] 61 | if c in BRACKETS: 62 | bracket_end = BRACKETS[c] 63 | data, index = parseSexp(content, index+1) 64 | res.append(data) 65 | elif c in QUOTE: 66 | data, index = quoteData(content, index) 67 | res.append(data) 68 | elif c in SPACE: 69 | index+=1; 70 | elif c in BRACKETS_END: 71 | index+=1 72 | break 73 | else: 74 | data, index = normalData(content, index) 75 | res.append(data) 76 | return res, index 77 | 78 | 79 | def loadKicadNet(filename): 80 | file = io.open(filename, "r", encoding="utf-8") 81 | data = file.read() 82 | res = parseSexp(data) 83 | return res[0][0] 84 | -------------------------------------------------------------------------------- /layout_tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import pcbnew 4 | import csv 5 | import re 6 | import sys 7 | import os 8 | 9 | def MakeRefs(refs): 10 | ''' 11 | Transform reference string 'R1-3,6-8,J1,2,5-7' 12 | to string array ['R1','R2','R3','R6','R7','R8','J1','J2','J5','J6','J7'] 13 | ''' 14 | if type(refs) == list: 15 | return refs 16 | r = refs.upper().split(',') 17 | prefix = 'X' 18 | ref_list = [] 19 | for s in r: 20 | m = re.match('([^0-9]+)',s) 21 | ts = s 22 | if m: 23 | prefix = m.group(0) 24 | ts = s[len(prefix):] 25 | ps = ts.find('-') 26 | if ps != -1: 27 | start_s = ts[:ps] 28 | end_s = ts[ps+1:] 29 | if (not start_s.isdigit()) or (not end_s.isdigit()): 30 | print('Ref not Digital value') 31 | else: 32 | for i in range(int(start_s), int(end_s)+1): 33 | ref_list.append(prefix+str(i)) 34 | else: 35 | if not ts.isdigit(): 36 | print('Ref not Digital value') 37 | else: 38 | ref_list.append(prefix+ts) 39 | return ref_list 40 | 41 | def MoveModules(x,y,refs, brd = None): 42 | if not brd: 43 | brd = pcbnew.GetBoard() 44 | for ref in MakeRefs(refs): 45 | m = brd.FindModuleByReference(ref) 46 | if m: 47 | m.SetPosition(pcbnew.wxPointMM(x,y)) 48 | x = x + m.GetBoundingBox().GetWidth()/1000000 49 | pcbnew.Refresh() 50 | 51 | def CopyModuleCourtyard(module, brd = None, layer = pcbnew.F_Paste): 52 | ''' Copy module courtyard to the destination layer 53 | ''' 54 | if not brd: 55 | brd = pcbnew.GetBoard() 56 | poly = module.GetPolyCourtyardFront() 57 | dw = pcbnew.DRAWSEGMENT() 58 | dw.SetShape(pcbnew.S_POLYGON) 59 | dw.SetLayer(layer) 60 | dw.SetPolyShape(poly) 61 | brd.Add(dw) 62 | 63 | def CopyCourtyard(refs = None, brd = None, layer = pcbnew.F_Paste): 64 | ''' Copy board modules courtyard to the destination layer 65 | ''' 66 | if not brd: 67 | brd = pcbnew.GetBoard() 68 | if not refs: 69 | for m in brd.GetModules(): 70 | if m.GetReference().find('J') == -1: 71 | CopyModuleCourtyard(m, brd, layer) 72 | else: 73 | for ref in MakeRefs(refs): 74 | m = brd.FindModuleByReference(ref) 75 | if m: 76 | CopyModuleCourtyard(m, brd, layer) 77 | pcbnew.Refresh() -------------------------------------------------------------------------------- /loadnet.py: -------------------------------------------------------------------------------- 1 | from . import kisexp as sexp 2 | import pcbnew as pn 3 | import io 4 | import traceback 5 | import os 6 | 7 | def loadNet(brd = None): 8 | if not brd: 9 | brd = pn.GetBoard() 10 | name = brd.GetFileName() 11 | name = name[0:name.rindex('.')] + '.net' 12 | if os.path.exists(name): 13 | return loadNetFile(name) 14 | if hasattr(brd, "GetFootprints"): 15 | print("File not exist, try to get info from footprint", name) 16 | r = {} 17 | for fp in brd.GetFootprints(): 18 | c = parseFootprint(fp) 19 | r[c['value'] + "&" + c['footprint']] = c 20 | return r 21 | return {} 22 | def parseFootprint(fp): 23 | r = {} 24 | prop = fp.GetProperties() 25 | r['value'] = fp.GetValue() 26 | r['footprint'] = str(fp.GetFPID().GetLibItemName()) 27 | if "Datasheet" in prop: 28 | r['datasheet'] = prop["Datasheet"] 29 | if "SuppliersPartNumber" in prop: 30 | r['partNumber'] = prop["SuppliersPartNumber"] 31 | if "Comment" in prop: 32 | if prop["Comment"] != "": 33 | r['comment'] = prop["Comment"] 34 | if "description" in prop: 35 | if prop["description"] != "": 36 | r['description'] = prop["description"] 37 | return r 38 | 39 | def toStr(v): 40 | return v 41 | 42 | def parseComp(comp): 43 | r = {} 44 | if comp[0] != "comp": 45 | print("Parse comp error") 46 | return None 47 | for i in range(1, len(comp)): 48 | key = comp[i][0] 49 | if key == "value": 50 | r['value'] = toStr(comp[i][1]) 51 | if key == "footprint": 52 | fp = toStr(comp[i][1]) 53 | pos = fp.rfind(':') 54 | if pos != -1: 55 | fp = fp[pos+1:] 56 | r['footprint'] = fp 57 | if key == "datasheet": 58 | r['datasheet'] = toStr(comp[i][1]) 59 | if key == "fields": 60 | fields = comp[i] 61 | for j in range(1, len(fields)): 62 | field = fields[j] 63 | fkey = toStr(field[1][1]) 64 | if fkey == "SuppliersPartNumber": 65 | r['partNumber'] = toStr(field[2]) 66 | if fkey == "Comment": 67 | r['comment'] = toStr(field[2]) 68 | if fkey == "description": 69 | r['description'] = toStr(field[2]) 70 | return r 71 | 72 | def loadNetFile(fileName): 73 | try: 74 | nets = sexp.loadKicadNet(fileName) 75 | if nets[3][0] != "components": 76 | return None 77 | comps = nets[3] 78 | r = {} 79 | for i in range(1, len(comps)): 80 | comp = comps[i] 81 | c = parseComp(comp) 82 | r[c['value'] + "&" + c['footprint']] = c 83 | return r 84 | except Exception as e: 85 | print("Fail to load netlist:") 86 | traceback.print_exc() 87 | return None 88 | 89 | 90 | -------------------------------------------------------------------------------- /mf_dialog_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ########################################################################### 4 | ## Python code generated with wxFormBuilder (version 3.10.1-0-g8feb16b3) 5 | ## http://www.wxformbuilder.org/ 6 | ## 7 | ## PLEASE DO *NOT* EDIT THIS FILE! 8 | ########################################################################### 9 | 10 | import wx 11 | import wx.xrc 12 | 13 | ########################################################################### 14 | ## Class MFDialogBase 15 | ########################################################################### 16 | 17 | class MFDialogBase ( wx.Dialog ): 18 | 19 | def __init__( self, parent ): 20 | wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = u"Generate Manufacture Docs", pos = wx.DefaultPosition, size = wx.Size( 736,328 ), style = wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER ) 21 | try: 22 | self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) 23 | except: 24 | self.SetSizeHints( wx.DefaultSize.width, wx.DefaultSize.height, wx.DefaultSize.width, wx.DefaultSize.height ) 25 | 26 | bSizer1 = wx.BoxSizer( wx.VERTICAL ) 27 | 28 | fgSizer3 = wx.FlexGridSizer( 4, 0, 0, 0 ) 29 | fgSizer3.AddGrowableRow( 3 ) 30 | fgSizer3.SetFlexibleDirection( wx.VERTICAL ) 31 | fgSizer3.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED ) 32 | 33 | fgSizer4 = wx.FlexGridSizer( 0, 8, 0, 0 ) 34 | fgSizer4.AddGrowableCol( 6 ) 35 | fgSizer4.SetFlexibleDirection( wx.HORIZONTAL ) 36 | fgSizer4.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED ) 37 | 38 | self.chkBOM = wx.CheckBox( self, wx.ID_ANY, u"BOM List", wx.DefaultPosition, wx.DefaultSize, 0 ) 39 | self.chkBOM.SetValue(True) 40 | self.chkBOM.SetToolTipString( u"Generate BOM" ) 41 | 42 | fgSizer4.Add( self.chkBOM, 0, wx.ALL, 5 ) 43 | 44 | self.chkPos = wx.CheckBox( self, wx.ID_ANY, u"Position File", wx.DefaultPosition, wx.DefaultSize, 0 ) 45 | self.chkPos.SetValue(True) 46 | self.chkPos.SetToolTipString( u"Generate Position file" ) 47 | 48 | fgSizer4.Add( self.chkPos, 0, wx.ALL, 5 ) 49 | 50 | self.chkGerber = wx.CheckBox( self, wx.ID_ANY, u"Gerber File", wx.DefaultPosition, wx.DefaultSize, 0 ) 51 | self.chkGerber.SetValue(True) 52 | self.chkGerber.SetToolTipString( u"Generate Gerber file" ) 53 | 54 | fgSizer4.Add( self.chkGerber, 0, wx.ALL, 5 ) 55 | 56 | self.chkPlotRef = wx.CheckBox( self, wx.ID_ANY, u"Plot Reference", wx.DefaultPosition, wx.DefaultSize, 0 ) 57 | self.chkPlotRef.SetValue(True) 58 | self.chkPlotRef.SetToolTipString( u"Plot reference" ) 59 | 60 | fgSizer4.Add( self.chkPlotRef, 0, wx.ALL, 5 ) 61 | 62 | self.chkSplitSlot = wx.CheckBox( self, wx.ID_ANY, u"Split Slot", wx.DefaultPosition, wx.DefaultSize, 0 ) 63 | self.chkSplitSlot.SetToolTipString( u"Split slot hole into hole series" ) 64 | 65 | fgSizer4.Add( self.chkSplitSlot, 0, wx.ALL, 5 ) 66 | 67 | self.btnGen = wx.Button( self, wx.ID_ANY, u"Gen Manufacture Docs", wx.DefaultPosition, wx.DefaultSize, 0 ) 68 | fgSizer4.Add( self.btnGen, 0, wx.ALL, 5 ) 69 | 70 | self.btnClearLog = wx.Button( self, wx.ID_ANY, u"Clear Log", wx.DefaultPosition, wx.DefaultSize, 0 ) 71 | fgSizer4.Add( self.btnClearLog, 0, wx.ALL, 5 ) 72 | 73 | 74 | fgSizer4.Add( ( 0, 0), 1, wx.EXPAND, 5 ) 75 | 76 | 77 | fgSizer3.Add( fgSizer4, 1, wx.EXPAND, 5 ) 78 | 79 | fgSizer2 = wx.FlexGridSizer( 0, 2, 0, 0 ) 80 | fgSizer2.AddGrowableCol( 1 ) 81 | fgSizer2.AddGrowableRow( 0 ) 82 | fgSizer2.SetFlexibleDirection( wx.HORIZONTAL ) 83 | fgSizer2.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED ) 84 | 85 | self.m_staticText1 = wx.StaticText( self, wx.ID_ANY, u"Exclude Ref:", wx.DefaultPosition, wx.DefaultSize, 0 ) 86 | self.m_staticText1.Wrap( -1 ) 87 | 88 | fgSizer2.Add( self.m_staticText1, 0, wx.ALL, 5 ) 89 | 90 | self.exclude_ref_text = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) 91 | self.exclude_ref_text.SetToolTipString( u"Exclude reference, load from \"exclude.txt\"" ) 92 | self.exclude_ref_text.SetMaxSize( wx.Size( -1,20 ) ) 93 | 94 | fgSizer2.Add( self.exclude_ref_text, 0, wx.ALL|wx.EXPAND, 5 ) 95 | 96 | 97 | fgSizer3.Add( fgSizer2, 1, wx.EXPAND, 5 ) 98 | 99 | self.m_staticText2 = wx.StaticText( self, wx.ID_ANY, u"Log:", wx.DefaultPosition, wx.DefaultSize, 0 ) 100 | self.m_staticText2.Wrap( -1 ) 101 | 102 | fgSizer3.Add( self.m_staticText2, 0, wx.ALL, 5 ) 103 | 104 | self.area_text = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.HSCROLL|wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_WORDWRAP ) 105 | fgSizer3.Add( self.area_text, 0, wx.ALL|wx.EXPAND, 5 ) 106 | 107 | 108 | bSizer1.Add( fgSizer3, 1, wx.EXPAND, 5 ) 109 | 110 | 111 | self.SetSizer( bSizer1 ) 112 | self.Layout() 113 | 114 | self.Centre( wx.BOTH ) 115 | 116 | # Connect Events 117 | self.btnGen.Bind( wx.EVT_BUTTON, self.OnGenBom ) 118 | self.btnClearLog.Bind( wx.EVT_BUTTON, self.ClearLog ) 119 | 120 | def __del__( self ): 121 | pass 122 | 123 | 124 | # Virtual event handlers, override them in your derived class 125 | def OnGenBom( self, event ): 126 | event.Skip() 127 | 128 | def ClearLog( self, event ): 129 | event.Skip() 130 | 131 | 132 | -------------------------------------------------------------------------------- /mf_tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtoolbox/kicad_tools/7e8321c02ff002d9ef80bfd45d1e86a2656b0b69/mf_tool.png -------------------------------------------------------------------------------- /mf_tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import pcbnew 4 | import csv 5 | import re 6 | import sys 7 | import os 8 | from . import gerber_drill as gd 9 | import wx 10 | import io 11 | from . import loadnet 12 | from . import mf_dialog_base 13 | import traceback 14 | 15 | import re 16 | patten = re.compile(r'\d+') 17 | def ref_comp(x): 18 | try: 19 | if type(x) == unicode: 20 | x = x.encode('gbk') 21 | except NameError: 22 | x = x 23 | if type(x) == str: 24 | t = patten.findall(x) 25 | if len(t)>0: 26 | hh = x.replace(t[0],'') 27 | vv = '0'*(6-len(hh)) + hh + '0'*(6-len(t[0])) + t[0] 28 | return vv 29 | else: 30 | print(t) 31 | else: 32 | print(type(x)) 33 | return x 34 | def ref_sorted(iterable, key = None): 35 | return sorted(iterable, key = ref_comp) 36 | 37 | def GetExcludeRefs(): 38 | f = pcbnew.GetBoard().GetFileName() 39 | delimer = '/' 40 | pos = f.rfind('/') 41 | if pos < 0: 42 | delimer = '\\' 43 | pos = f.rfind('\\') 44 | f = f[0:pos] + delimer + "exclude.txt" 45 | if os.path.exists(f): 46 | file = io.open(f, "r") 47 | return file.read() 48 | return "" 49 | 50 | class ExcludeRefClass: 51 | def __init__(self, refs): 52 | self.refNames = {} 53 | self.refPrefix = {} 54 | xx = re.findall(r'([A-Za-z]+[0-9]+)', refs.upper()) 55 | for v in xx: 56 | self.refNames[v] = True 57 | xx = re.findall(r'([A-Za-z]+)\*', refs.upper()) 58 | for v in xx: 59 | self.refPrefix[v] = True 60 | def contains(self, ref): 61 | if self.refNames.get(ref.upper()): 62 | return True 63 | xx = re.findall(r'[A-Za-z_]+', ref) 64 | if len(xx) > 0: 65 | return self.refPrefix.get(xx[0].upper()) 66 | return False 67 | 68 | unusedRef = None 69 | 70 | class RefBuilder: 71 | ''' RefBuilder use to re-build the module referrence number 72 | Step 1: use rb = RefBuilder() to create a RefBuilder object 73 | Step 2: use rb.collect(ref) to collect current exist reference 74 | Step 3: usb newRef = rb.build(oldRef) to build new ref, if oldRef already built 75 | use the last oldRef's new Ref 76 | ''' 77 | def __init__(self, init_ref = None): 78 | self.patten = re.compile(r'([a-zA-Z]+)\s*(\d+)') 79 | self.refMap = {} 80 | self.builtMap = {} 81 | if init_ref: 82 | self.refMap = init_ref 83 | def collect(self, ref): 84 | m = self.patten.match(ref) 85 | if m: 86 | if not (m.group(1) in self.refMap): 87 | self.refMap[m.group(1)] = m.group(2) 88 | else: 89 | max = self.refMap[m.group(1)] 90 | if int(m.group(2)) > int(max): 91 | self.refMap[m.group(1)] = m.group(2) 92 | def collects(self, refs): 93 | for ref in refs: 94 | self.collect(ref) 95 | def build(self, oldRef): 96 | m = re.match(r'([a-zA-Z]+)\s*(\d+)',oldRef) 97 | if not m: 98 | print('Ref is invalid %s'%oldRef) 99 | return None 100 | if oldRef in self.builtMap: 101 | return self.builtMap[oldRef] 102 | newRef = '' 103 | if not (m.group(1) in self.refMap): 104 | self.refMap[m.group(1)] = m.group(2) 105 | newRef = oldRef 106 | else: 107 | max = int(self.refMap[m.group(1)]) 108 | max = max + 1 109 | self.refMap[m.group(1)] = str(max) 110 | newRef = m.group(1) + str(max) 111 | self.builtMap[oldRef] = newRef 112 | return newRef 113 | def Show(self): 114 | print(self.refMap) 115 | 116 | def testRefBuilder(): 117 | rb = RefBuilder() 118 | rb.collects(['R1','R2','R14', 'R10', 'D1', 'D2', 'U3', 'U2', 'U1']) 119 | rb.Show() 120 | print('R1 -> %s'%rb.build('R1')) 121 | print('R2 -> %s'%rb.build('R2')) 122 | print('R3 -> %s'%rb.build('R3')) 123 | print('U1 -> %s'%rb.build('U1')) 124 | print('U2 -> %s'%rb.build('U2')) 125 | print('X2 -> %s'%rb.build('X2')) 126 | print('X1 -> %s'%rb.build('X1')) 127 | print('R? -> %s'%rb.build('R?')) 128 | print('R1 -> %s'%rb.build('R1')) 129 | print('R2 -> %s'%rb.build('R2')) 130 | print('X2 -> %s'%rb.build('X2')) 131 | rb.Show() 132 | 133 | # Get Board Bounding rect by the margin layer element 134 | #def GetBoardArea(brd = None, marginLayer = pcbnew.Margin): 135 | # if not brd: 136 | # brd = pcbnew.GetBoard() 137 | # rect = None 138 | # for dwg in brd.GetDrawings(): 139 | # if dwg.GetLayer() == marginLayer: 140 | # box = dwg.GetBoundingBox() 141 | # if rect: 142 | # rect.Merge(box) 143 | # else: 144 | # rect = box 145 | # rect.SetX(rect.GetX() + 100001) 146 | # rect.SetY(rect.GetY() + 100001) 147 | # rect.SetWidth(rect.GetWidth() - 200002) 148 | # rect.SetHeight(rect.GetHeight() - 200002) 149 | # #print(rect.GetX(), rect.GetY(), rect.GetWidth(), rect.GetHeight()) 150 | # return rect 151 | 152 | def GetBoardBound(brd = None, marginLayer = pcbnew.Edge_Cuts): 153 | ''' Calculate board edge from the margin layer, the default margin layer is Edge_Cuts 154 | enum all the draw segment on the specified layer, and merge their bound rect 155 | ''' 156 | if not brd: 157 | brd = pcbnew.GetBoard() 158 | rect = None 159 | l = None 160 | r = None 161 | t = None 162 | b = None 163 | is_6x = False 164 | for dwg in brd.GetDrawings(): 165 | if dwg.GetLayer() == marginLayer: 166 | if hasattr(dwg, 'Cast_to_DRAWSEGMENT'): 167 | d = dwg.Cast_to_DRAWSEGMENT() 168 | w = d.GetWidth() 169 | elif hasattr(pcbnew, 'Cast_to_DRAWSEGMENT'): 170 | d = pcbnew.Cast_to_DRAWSEGMENT(dwg) 171 | w = d.GetWidth() 172 | else: 173 | is_6x = True 174 | d = pcbnew.Cast_to_BOARD_ITEM(dwg) 175 | w = dwg.GetWidth() 176 | box = d.GetBoundingBox() 177 | box.SetX(int(box.GetX() + w/2)) 178 | box.SetY(int(box.GetY() + w/2)) 179 | box.SetWidth(int(box.GetWidth() - w)) 180 | box.SetHeight(int(box.GetHeight() - w)) 181 | if rect: 182 | rect.Merge(box) 183 | else: 184 | rect = box 185 | w = 0 if is_6x else 2 186 | rect.SetX(int(rect.GetX() + w/2)) 187 | rect.SetY(int(rect.GetY() + w/2)) 188 | rect.SetWidth(int(rect.GetWidth() - w)) 189 | rect.SetHeight(int(rect.GetHeight() - w)) 190 | return rect 191 | 192 | def GetOtherBoard(brd): 193 | r = brd 194 | curbrd = pcbnew.GetBoard() 195 | s = curbrd.GetFileName() 196 | if not brd: 197 | brd = curbrd 198 | elif type(brd) == str: 199 | if os.path.exists(brd): 200 | brd = pcbnew.LoadBoard(brd) 201 | elif os.path.exists(s[0:s.rfind('/')] + '/' + brd): 202 | brd = pcbnew.LoadBoard(s[0:s.rfind('/')] + '/' + brd) 203 | else: 204 | return None 205 | else: 206 | return brd 207 | return brd 208 | 209 | class BoardItems: 210 | ''' Class to hold all interest board items 211 | Use Collect method to get all board items 212 | 213 | ''' 214 | def __init__(self): 215 | self.rb = RefBuilder() 216 | self.orgItems = [] 217 | self.mods = [] 218 | self.rect = None 219 | def ItemValid(self, item): 220 | ''' Check the item is in the rect or not''' 221 | return item.HitTest(self.rect, False) 222 | def Collect(self, brd = None, rect = None): 223 | ''' Collect board items in specify rect''' 224 | brd = GetOtherBoard(brd) 225 | #if not brd: 226 | # brd = pcbnew.GetBoard() 227 | if not rect: 228 | rect = GetBoardBound(brd) 229 | self.rect = rect 230 | for mod in brd.GetModules(): 231 | if self.ItemValid(mod): 232 | self.orgItems.append(mod) 233 | self.mods.append(mod) 234 | self.rb.collect(mod.GetReference()) 235 | for track in brd.GetTracks(): 236 | if self.ItemValid(track): 237 | self.orgItems.append(track) 238 | for dwg in brd.GetDrawings(): 239 | if self.ItemValid(dwg): 240 | self.orgItems.append(dwg) 241 | #print(dwg.GetLayer()) 242 | area_cnt = brd.GetAreaCount() 243 | for i in range(area_cnt): 244 | area = brd.GetArea(i) 245 | if self.ItemValid(area): 246 | self.orgItems.append(area) 247 | self.brd = brd 248 | #self.rb.Show() 249 | def Mirror(self): 250 | rotPt = pcbnew.wxPoint(self.rect.GetX() + self.rect.GetWidth()/2, self.rect.GetY() + self.rect.GetHeight()/2) 251 | for item in self.orgItems: 252 | item.Flip(rotPt) 253 | item.Rotate(rotPt, 1800) 254 | def Rotate(self, angle = 90): 255 | rotPt = pcbnew.wxPoint(self.rect.GetX() + self.rect.GetWidth()/2, self.rect.GetY() + self.rect.GetHeight()/2) 256 | for item in self.orgItems: 257 | item.Rotate(rotPt, angle * 10) 258 | def MoveToMM(self, x, y): 259 | self.MoveTo(pcbnew.wxPointMM(x,y)) 260 | def ShowRect(self): 261 | r = '(' 262 | r += str(self.rect.GetX()/1000000) + ',' 263 | r += str(self.rect.GetY()/1000000) + ',' 264 | r += str(self.rect.GetWidth()/1000000) + ',' 265 | r += str(self.rect.GetHeight()/1000000) + ')' 266 | return r 267 | def MoveTo(self, pos): 268 | off = pcbnew.wxPoint( pos.x - self.rect.GetX(), pos.y - self.rect.GetY() ) 269 | #print('org is:', self.x, ',', self.y) 270 | #print('off is:', off) 271 | for item in self.orgItems: 272 | item.Move(off) 273 | print('Move item in ', self.ShowRect(), 'off = (', off.x/1000000, ',' ,off.y/1000000,')') 274 | self.rect.Move(off) 275 | print('Result is ', self.ShowRect()) 276 | 277 | def Clone(self, brd = None): 278 | if not brd: 279 | brd = self.brd 280 | newBI = BoardItems() 281 | newBI.rect = self.rect 282 | for item in self.orgItems: 283 | newItem = item.Duplicate() 284 | newBI.orgItems.append(newItem) 285 | brd.Add(newItem) 286 | newBI.brd = brd 287 | return newBI 288 | def Remove(self): 289 | for item in self.orgItems: 290 | self.brd.Remove(item) 291 | def UpdateRef(self, rb): 292 | ''' Update items reference with specify ref builder''' 293 | for item in self.orgItems: 294 | if isinstance(item,pcbnew.MODULE): 295 | newRef = rb.build(item.GetReference()) 296 | if newRef: 297 | item.SetReference(newRef) 298 | def ChangeBrd(self, brd = None): 299 | if not brd: 300 | brd = pcbnew.GetBoard() 301 | if brd == self.brd: 302 | print('Same board, do nothing') 303 | for item in self.orgItems: 304 | self.brd.Remove(item) 305 | brd.Add(item) 306 | self.brd = brd 307 | def HideValue(self, hide = True): 308 | for m in self.mods: 309 | if hide: 310 | m.Value().SetVisible(False) 311 | else: 312 | m.Value().SetVisible(True) 313 | 314 | def test2(): 315 | # load board to be panelized 316 | #b1 = pcbnew.LoadBoard(r'test1.kicad_pcb') 317 | b2 = pcbnew.LoadBoard(r'test2.kicad_pcb') 318 | # Get current work borad, must be a empty board 319 | brd = pcbnew.GetBoard() 320 | # Collect items 321 | bi1 = BoardItems() 322 | bi2 = BoardItems() 323 | bi1.Collect(brd) 324 | bi2.Collect(b2) 325 | #bi1 = bi1.Clone(brd) 326 | #bi2 = bi2.Clone(brd) 327 | # Clone items in board 1 328 | bb1 = bi1.Clone() 329 | # Change the module reference 330 | bi2.UpdateRef(bi1.rb) 331 | # Clone items in board 2 332 | bb2 = bi2.Clone() 333 | # Copy board items to current board 334 | #bi1.ChangeBrd(brd) 335 | #bb1.ChangeBrd(brd) 336 | bi2.ChangeBrd(brd) 337 | bb2.ChangeBrd(brd) 338 | # Move them 339 | bi2.MoveToMM(0,0) 340 | bi2.Rotate(180) 341 | 342 | bb1.Mirror() 343 | bb2.Rotate(180) 344 | bb2.Mirror() 345 | 346 | bb1.MoveToMM(54, -59) 347 | bb2.MoveToMM(54, -59) 348 | 349 | def GetPad1(mod): 350 | '''Get the first pad of a module''' 351 | padx = None 352 | for pad in mod.Pads(): 353 | if not padx: 354 | padx = pad 355 | if pad.GetPadName() == '1': 356 | return pad 357 | #print('Pad 1 not found, use the first pad instead') 358 | return padx 359 | def IsSMD(mod): 360 | for pad in mod.Pads(): 361 | attr_smd = pcbnew.PAD_SMD if hasattr(pcbnew,'PAD_SMD') else pcbnew.PAD_ATTRIB_SMD 362 | if pad.GetAttribute() != attr_smd: 363 | return False 364 | return True 365 | def footPrintName(mod): 366 | fid = mod.GetFPID() 367 | f = fid.GetFootprintName().Cast_to_CChar() if hasattr(fid, 'GetFootprintName') else fid.GetLibItemName().Cast_to_CChar() 368 | return f 369 | 370 | class BOMItem: 371 | def __init__(self, ref, footprint, value, pincount, netList = None): 372 | self.refs = [ref] 373 | self.fp = footprint 374 | self.value = value 375 | self.pincount = pincount 376 | kv = value 377 | #if kv.rfind('[') != -1: 378 | # kv = kv[0:kv.rfind('[')] 379 | 380 | self.netKey = kv + "&" + footprint 381 | try: 382 | if not isinstance(self.netKey, unicode): 383 | self.netKey = unicode(self.netKey) 384 | except NameError: 385 | self.netKey = self.netKey 386 | self.partNumber = "" 387 | self.desc = "desc" 388 | self.url = "" 389 | self.libRef = "libref" 390 | if netList: 391 | if self.netKey in netList: 392 | comp = netList[self.netKey] 393 | if 'partNumber' in comp: 394 | self.partNumber = comp['partNumber'] 395 | if 'description' in comp: 396 | self.desc = comp['description'] 397 | if 'datasheet' in comp: 398 | self.url = comp['datasheet'] 399 | if 'comment' in comp: 400 | self.libRef = self.value 401 | self.value = comp['comment'] 402 | else: 403 | print("fail to find ", self.netKey, " in net list") 404 | 405 | def Output(self, out = None): 406 | refs = '' 407 | for r in ref_sorted(self.refs): 408 | refs += r + ',' 409 | if not out: 410 | out = csv.writer(sys.stdout, lineterminator='\n', delimiter=',', quotechar='\"', quoting=csv.QUOTE_ALL) 411 | out.writerow([self.value, self.desc, refs, self.fp, self.libRef, str(self.pincount), str(len(self.refs)), self.partNumber, self.url ]) 412 | def AddRef(self, ref): 413 | self.refs.append(ref) 414 | self.refs = ref_sorted(self.refs) 415 | 416 | def OutputBOMHeader(out = None): 417 | if not out: 418 | out = csv.writer(sys.stdout, lineterminator='\n', delimiter=',', quotechar='\"', quoting=csv.QUOTE_ALL) 419 | out.writerow(['Comment','Description','Designator','Footprint','LibRef','Pins','Quantity','PartNumber','url']) 420 | 421 | def IsModExclude(mod, ExcludeRefs = [], ExcludeValues = []): 422 | r = mod.GetReference() 423 | v = mod.GetValue() 424 | for pat in ExcludeRefs: 425 | if pat.match(r): 426 | return True 427 | for pat in ExcludeValues: 428 | if pat.match(v): 429 | return True 430 | return False 431 | 432 | removedRefs = {} 433 | def GenBOM(brd = None, layer = pcbnew.F_Cu, type = 1, ExcludeRefs = [], ExcludeValues = [], netList = None): 434 | if not brd: 435 | brd = pcbnew.GetBoard() 436 | bomList = {} 437 | if hasattr(brd, "GetModules"): 438 | mods = brd.GetModules() 439 | else: 440 | mods = brd.GetFootprints() 441 | for mod in mods: 442 | needOutput = False 443 | needRemove = False 444 | if unusedRef: 445 | needRemove = unusedRef.contains(mod.GetReference()) 446 | if needRemove: 447 | global removedRefs 448 | removedRefs[mod.GetReference()] = mod.GetValue() 449 | if (mod.GetLayer() == layer) and (not IsModExclude(mod, ExcludeRefs, ExcludeValues) and (not needRemove)): 450 | needOutput = IsSMD(mod) == (type == 1) 451 | if needOutput: 452 | v = mod.GetValue() 453 | f = footPrintName(mod) 454 | r = mod.GetReference() 455 | vf = v + f 456 | if vf in bomList: 457 | #if bomList.has_key(vf): 458 | bomList[vf].AddRef(r) 459 | else: 460 | bomList[vf] = BOMItem(r,f,v, mod.GetPadCount(), netList) 461 | print('there are ', len(bomList), ' items at layer ', layer) 462 | return sorted(bomList.values(), key = lambda item: ref_comp(item.refs[0])) 463 | 464 | def layerName(layerId): 465 | if layerId == pcbnew.F_Cu: 466 | return 'T' 467 | if layerId == pcbnew.B_Cu: 468 | return 'B' 469 | return 'X' 470 | def toMM(v): 471 | return str(v/1000000.0) + 'mm' 472 | class POSItem: 473 | def __init__(self, mod, offx = 0, offy = 0): 474 | self.MidX = toMM(mod.GetPosition().x-offx) 475 | self.MidY = toMM(offy - mod.GetPosition().y) 476 | self.RefX = toMM(mod.GetPosition().x-offx) 477 | self.RefY = toMM(offy - mod.GetPosition().y) 478 | pad = GetPad1(mod) 479 | if pad: 480 | self.PadX = toMM(pad.GetPosition().x-offx) 481 | self.PadY = toMM(offy - pad.GetPosition().y) 482 | else: 483 | print('Pad1 not found for mod') 484 | self.PadX = self.MidX 485 | self.PadY = self.MidY 486 | self.rot = int(mod.GetOrientation()/10) 487 | self.ref = mod.GetReference() 488 | self.val = mod.GetValue() 489 | self.layer = layerName(mod.GetLayer()) 490 | self.fp = footPrintName(mod) 491 | def Output(self, out = None): 492 | if not out: 493 | out = csv.writer(sys.stdout, lineterminator='\n', delimiter=',', quotechar='\"', quoting=csv.QUOTE_ALL) 494 | out.writerow([self.ref, self.fp, str(self.MidX), str(self.MidY), 495 | str(self.RefX), str(self.RefY), str(self.PadX), str(self.PadY), 496 | self.layer, str(self.rot), self.val]) 497 | 498 | def GenPos(brd = None, layer = pcbnew.F_Cu, type = 1, ExcludeRefs = [], ExcludeValues = []): 499 | if not brd: 500 | brd = pcbnew.GetBoard() 501 | posList = [] 502 | if hasattr(brd, 'GetAuxOrigin'): 503 | pt_org = brd.GetAuxOrigin() 504 | else: 505 | pt_org = brd.GetDesignSettings().GetAuxOrigin() 506 | if hasattr(brd, 'GetModules'): 507 | mods = brd.GetModules() 508 | else: 509 | mods = brd.Footprints() 510 | for mod in mods: 511 | needOutput = False 512 | if (mod.GetLayer() == layer) and (not IsModExclude(mod, ExcludeRefs, ExcludeValues)): 513 | needOutput = IsSMD(mod) == (type == 1) 514 | if needOutput: 515 | posList.append(POSItem(mod, pt_org.x, pt_org.y)) 516 | posList = sorted(posList, key = lambda item: ref_comp(item.ref)) 517 | return posList 518 | def OutputPosHeader(out = None): 519 | if not out: 520 | out = csv.writer(sys.stdout, lineterminator='\n', delimiter=',', quotechar='\"', quoting=csv.QUOTE_ALL) 521 | out.writerow(['Designator','Footprint','Mid X','Mid Y','Ref X','Ref Y','Pad X','Pad Y','Layer','Rotation','Comment']) 522 | def PrintBOM(boms): 523 | OutputBOMHeader() 524 | i = 1 525 | for bom in boms: 526 | print('BOM items for BOM', i) 527 | i = i + 1 528 | for k,v in bom.items(): 529 | v.Output() 530 | def PrintPOS(Poses): 531 | OutputPosHeader() 532 | i = 1 533 | for pos in Poses: 534 | print('Pos items ', i) 535 | i = i+ 1 536 | for v in pos: 537 | v.Output() 538 | def CollectItemByName(filename = None): 539 | try: 540 | brd = pcbnew.LoadBoard(filename) 541 | except IOError: 542 | print('Can not open ', filename) 543 | filename = os.path.split(pcbnew.GetBoard().GetFileName())[0] + '\\' + filename 544 | print('Try to open ', filename) 545 | try: 546 | brd = pcbnew.LoadBoard(filename) 547 | except IOError: 548 | print('Can not open ', filename) 549 | return None 550 | bi = BoardItems() 551 | bi.Collect(brd) 552 | return bi 553 | 554 | def CollectItem(brd = None): 555 | if not brd: 556 | brd = pcbnew.GetBoard() 557 | bi = BoardItems() 558 | bi.Collect(brd) 559 | return bi 560 | 561 | def CopyItemTo(boardItem, x, y): 562 | newBI = boardItem.Clone() 563 | newBI.MoveToMM(x, y) 564 | return newBI 565 | 566 | def MirrorItemTo(boardItem, x, y): 567 | newBI = boardItem.Clone() 568 | newBI.MoveToMM(x, y) 569 | newBI.Mirror() 570 | return newBI 571 | 572 | class UnicodeWriter: 573 | def __init__(self, file, *a, **kw): 574 | self.file = file 575 | def writerow(self, data): 576 | for e in data: 577 | self.file.write(u'"') 578 | #print(isinstance(e, unicode)) 579 | try: 580 | if not isinstance(e, unicode): 581 | self.file.write(unicode(e)) 582 | else: 583 | self.file.write(e) 584 | except NameError: 585 | self.file.write(e) 586 | self.file.write(u'",') 587 | self.file.write(u'\n') 588 | 589 | 590 | def OpenCSV(fileName): 591 | try: 592 | f = io.open(fileName, 'w+', encoding="utf-8") 593 | except IOError: 594 | e = "Can't open output file for writing: " + fileName 595 | print( __file__, ":", e, sys.stderr ) 596 | f = sys.stdout 597 | #out = csv.writer( f, lineterminator='\n', delimiter=',', quotechar='\"', quoting=csv.QUOTE_MINIMAL ) 598 | out = UnicodeWriter(f) 599 | return out 600 | 601 | def PreCompilePattenList(pattenList): 602 | res = [] 603 | for pat in pattenList: 604 | res.append(re.compile(pat)) 605 | return res 606 | 607 | def def_logger(*args): 608 | r = "" 609 | for t in args: 610 | r = r + str(t) + " " 611 | print(r) 612 | 613 | 614 | def GenMFDoc(SplitTopAndBottom = False, ExcludeRef = [], ExcludeValue = [], brd = None, needGenBOM = True, needGenPos = True, logger = def_logger): 615 | if not brd: 616 | brd = pcbnew.GetBoard() 617 | if not needGenBOM and not needGenPos: 618 | return 619 | bound = GetBoardBound(brd) 620 | org_pt = pcbnew.wxPoint( bound.GetLeft(), bound.GetBottom()) 621 | logger("set board aux origin to left bottom point, at", org_pt) 622 | if hasattr(brd, 'SetAuxOrigin'): 623 | brd.SetAuxOrigin(org_pt) 624 | else: 625 | brd.GetDesignSettings().SetAuxOrigin(org_pt) 626 | fName = brd.GetFileName() 627 | path = os.path.split(fName)[0] 628 | fName = os.path.split(fName)[1] 629 | bomName = fName.rsplit('.',1)[0] 630 | netList = loadnet.loadNet(brd) 631 | 632 | excludeRefs = PreCompilePattenList(ExcludeRef) 633 | excludeValues = PreCompilePattenList(ExcludeValue) 634 | 635 | bomSMDTop = GenBOM(brd, pcbnew.F_Cu, 1, excludeRefs, excludeValues, netList) 636 | bomHoleTop = GenBOM(brd, pcbnew.F_Cu, 0, excludeRefs, excludeValues, netList) 637 | 638 | bomSMDBot = GenBOM(brd, pcbnew.B_Cu, 1, excludeRefs, excludeValues, netList) 639 | bomHoleBot = GenBOM(brd, pcbnew.B_Cu, 0, excludeRefs, excludeValues, netList) 640 | 641 | posSMDTop = GenPos(brd, pcbnew.F_Cu, 1, excludeRefs, excludeValues) 642 | posHoleTop = GenPos(brd, pcbnew.F_Cu, 0, excludeRefs, excludeValues) 643 | 644 | posSMDBot = GenPos(brd, pcbnew.B_Cu, 1, excludeRefs, excludeValues) 645 | posHoleBot = GenPos(brd, pcbnew.B_Cu, 0, excludeRefs, excludeValues) 646 | 647 | if SplitTopAndBottom: 648 | fName = bomName 649 | bomName = path + '/' + fName + '_BOM_TOP.csv' 650 | posName = path + '/' + fName + '_POS_TOP.csv' 651 | if needGenBOM: 652 | # Generate BOM for Top layer 653 | logger('Genertate BOM file ', bomName) 654 | csv = OpenCSV(bomName) 655 | OutputBOMHeader(csv) 656 | for v in bomSMDTop: 657 | v.Output(csv) 658 | if len(bomHoleTop)>0: 659 | csv.writerow(['Through Hole Items ']) 660 | for v in bomHoleTop: 661 | v.Output(csv) 662 | if needGenPos: 663 | # Generate POS for Top layer 664 | logger('Genertate POS file ', posName) 665 | csv = OpenCSV(posName) 666 | OutputPosHeader(csv) 667 | for v in posSMDTop: 668 | v.Output(csv) 669 | if len(posHoleTop)>0: 670 | csv.writerow(['Through Hole Items ']) 671 | for v in posHoleTop: 672 | v.Output(csv) 673 | 674 | bomName = path + '/' + fName + '_BOM_BOT.csv' 675 | posName = path + '/' + fName + '_POS_BOT.csv' 676 | if needGenBOM: 677 | # Generate BOM for Bottom layer 678 | logger('Genertate BOM file ', bomName) 679 | csv = OpenCSV(bomName) 680 | OutputBOMHeader(csv) 681 | for v in bomSMDBot: 682 | v.Output(csv) 683 | if len(bomHoleBot)>0: 684 | csv.writerow(['Through Hole Items ']) 685 | for v in bomHoleBot: 686 | v.Output(csv) 687 | if needGenPos: 688 | # Generate POS for Bottom layer 689 | logger('Genertate POS file ', posName) 690 | csv = OpenCSV(posName) 691 | OutputPosHeader(csv) 692 | for v in posSMDBot: 693 | v.Output(csv) 694 | if len(posHoleBot)>0: 695 | csv.writerow(['Through Hole Items ']) 696 | for v in posHoleBot: 697 | v.Output(csv) 698 | 699 | else: 700 | posName = path + '/' + bomName + '_POS.csv' 701 | bomName = path + '/' + bomName + '_BOM.csv' 702 | # Generate BOM for both layer 703 | if needGenBOM: 704 | logger('Genertate BOM file ', bomName) 705 | csv = OpenCSV(bomName) 706 | OutputBOMHeader(csv) 707 | for v in bomSMDTop: 708 | v.Output(csv) 709 | 710 | for v in bomSMDBot: 711 | v.Output(csv) 712 | if len(bomHoleTop)+len(bomHoleBot)>0: 713 | csv.writerow(['Through Hole Items ']) 714 | for v in bomHoleTop: 715 | v.Output(csv) 716 | 717 | for v in bomHoleBot: 718 | v.Output(csv) 719 | 720 | if needGenPos: 721 | # Generate POS for both layer 722 | logger('Genertate POS file ', posName) 723 | 724 | csv = OpenCSV(posName) 725 | OutputPosHeader(csv) 726 | for v in posSMDTop: 727 | v.Output(csv) 728 | 729 | for v in posSMDBot: 730 | v.Output(csv) 731 | if len(posHoleTop)+len(posHoleBot)>0: 732 | csv.writerow(['Through Hole Items ']) 733 | for v in posHoleTop: 734 | v.Output(csv) 735 | 736 | for v in posHoleBot: 737 | v.Output(csv) 738 | return bomName, posName 739 | 740 | def version(): 741 | print("1.2") 742 | 743 | def GenSMTFiles(): 744 | #reload(sys) 745 | #sys.setdefaultencoding("utf8") 746 | GenMFDoc() 747 | gd.GenGerberDrill(board = None, split_G85 = 0.2, plotDir = "gerber/") 748 | 749 | 750 | 751 | def TestDialog(): 752 | tt = MFDialog() 753 | tt.Show() 754 | 755 | class MFDialog(mf_dialog_base.MFDialogBase): 756 | def __init__(self): 757 | try: 758 | mf_dialog_base.MFDialogBase.__init__(self, None) 759 | except TypeError: 760 | self = mf_dialog_base.MFDialogBase() 761 | best_size = self.BestSize 762 | best_size.IncBy(dx=0, dy=30) 763 | self.SetClientSize(best_size) 764 | 765 | self.exclude_ref_text.Clear() 766 | self.exclude_ref_text.AppendText(GetExcludeRefs()) 767 | def OnGenBom(self, event): 768 | try: 769 | if self.chkBOM.GetValue(): 770 | self.area_text.AppendText("Start generate BOM list\n") 771 | if self.chkPos.GetValue(): 772 | self.area_text.AppendText("Start generate position file\n") 773 | global unusedRef 774 | unusedRef = ExcludeRefClass(self.exclude_ref_text.GetValue()) 775 | global removedRefs 776 | removedRefs = {} 777 | GenMFDoc(needGenBOM = self.chkBOM.GetValue(), needGenPos = self.chkPos.GetValue(), logger = lambda *args: self.log(*args) ) 778 | #self.area_text.AppendText("Removed refs in BOM: " + ",".join(ref_sorted(removedRefs.keys())) + "\n") 779 | self.area_text.AppendText("Removed refs in BOM:\n") 780 | for n in ref_sorted(removedRefs.keys()): 781 | self.area_text.AppendText(n+":" + removedRefs[n] + "\n") 782 | if self.chkGerber.GetValue(): 783 | self.area_text.AppendText("Start generate gerber files\n") 784 | split_slot = None 785 | if self.chkSplitSlot.GetValue(): 786 | split_slot = 0.2 787 | gerberPath = gd.GenGerberDrill( 788 | board = None, 789 | split_G85 = split_slot, 790 | plotDir = "gerber/", 791 | plotReference = self.chkPlotRef.GetValue(), 792 | logger = lambda *args: self.log(*args)) 793 | self.area_text.AppendText( 'Gerber file dir is "%s"' % gerberPath) 794 | except Exception as e: 795 | self.area_text.AppendText("Error:\n") 796 | self.area_text.AppendText(traceback.format_exc()) 797 | def ClearLog(self, event): 798 | self.area_text.SetValue("") 799 | def log(self, *args): 800 | for v in args: 801 | try: 802 | self.area_text.AppendText(str(v) + " ") 803 | except Exception as e: 804 | try: 805 | self.area_text.AppendText(v + " ") 806 | except Exception as e1: 807 | self.area_text.AppendText("\nError:\nfail to log content ") 808 | self.area_text.AppendText(traceback.format_exc()) 809 | self.area_text.AppendText("\n") 810 | 811 | class gen_mf_doc( pcbnew.ActionPlugin ): 812 | """ 813 | gen_mf_doc: A plugin used to generate BOM and position file 814 | How to use: 815 | - just call the plugin 816 | - the BOM and Position file will generate under the PCB file folder 817 | BOM file name is _BOM.csv 818 | Position file name is _POS.csv 819 | - the Gerber and drill file will generate under gerber folder 820 | """ 821 | 822 | def defaults( self ): 823 | """ 824 | Method defaults must be redefined 825 | self.name should be the menu label to use 826 | self.category should be the category (not yet used) 827 | self.description should be a comprehensive description 828 | of the plugin 829 | """ 830 | self.name = "Gen Manufacture Docs" 831 | self.category = "Modify PCB" 832 | self.description = "Automatically generate manufacture document, Gerber, Drill, BOM, Position" 833 | self.icon_file_name = os.path.join(os.path.dirname(__file__), "./mf_tool.png") 834 | self.show_toolbar_button = True 835 | 836 | def Run( self ): 837 | tt = MFDialog() 838 | tt.Show() 839 | 840 | gen_mf_doc().register() 841 | 842 | 843 | 844 | 845 | -------------------------------------------------------------------------------- /qrcode_footprint_wizard.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or modify 2 | # it under the terms of the GNU General Public License as published by 3 | # the Free Software Foundation; either version 2 of the License, or 4 | # (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software 13 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 14 | # MA 02110-1301, USA. 15 | 16 | # last change: 2017, Jan 4. 17 | 18 | import pcbnew 19 | import FootprintWizardBase 20 | 21 | # Additional import for QRCode 22 | # see https://github.com/kazuhikoarase/qrcode-generator/blob/master/python/qrcode.py 23 | import qrcode 24 | 25 | class QRCodeWizard(FootprintWizardBase.FootprintWizard): 26 | GetName = lambda self: '2D Barcode QRCode' 27 | GetDescription = lambda self: 'QR Code barcode generator' 28 | GetReferencePrefix = lambda self: 'QR***' 29 | GetValue = lambda self: self.module.Value().GetText() 30 | 31 | def GenerateParameterList(self): 32 | self.AddParam("Barcode", "Pixel Width", self.uMM, 0.5, min_value=0.4) 33 | self.AddParam("Barcode", "Border", self.uInteger, 1) 34 | self.AddParam("Barcode", "Contents", self.uString, 'Example') 35 | self.AddParam("Barcode", "Negative", self.uBool, True) 36 | self.AddParam("Barcode", "Use SilkS layer", self.uBool, True) 37 | self.AddParam("Barcode", "Use Cu layer", self.uBool, False) 38 | self.AddParam("Barcode", "TypeNumber", self.uInteger, 1) 39 | 40 | self.AddParam("Caption", "Enabled", self.uBool, True) 41 | self.AddParam("Caption", "Height", self.uMM, 1.2) 42 | self.AddParam("Caption", "Width", self.uMM, 1.2) 43 | self.AddParam("Caption", "Thickness", self.uMM, 0.12) 44 | 45 | 46 | def CheckParameters(self): 47 | self.Barcode = str(self.parameters['Barcode']['Contents']) 48 | self.X = self.parameters['Barcode']['Pixel Width'] 49 | self.negative = self.parameters['Barcode']['Negative'] 50 | self.UseSilkS = self.parameters['Barcode']['Use SilkS layer'] 51 | self.UseCu = self.parameters['Barcode']['Use Cu layer'] 52 | self.border = int(self.parameters['Barcode']['Border']) 53 | self.textHeight = int(self.parameters['Caption']['Height']) 54 | self.textThickness = int(self.parameters['Caption']['Thickness']) 55 | self.textWidth = int(self.parameters['Caption']['Width']) 56 | self.module.Value().SetText(str(self.Barcode)) 57 | self.typeNumber = int( self.parameters['Barcode']['TypeNumber'] ) 58 | 59 | # Build Qrcode 60 | self.qr = qrcode.QRCode() 61 | self.qr.setTypeNumber(self.typeNumber) 62 | # ErrorCorrectLevel: L = 7%, M = 15% Q = 25% H = 30% 63 | self.qr.setErrorCorrectLevel(qrcode.ErrorCorrectLevel.M) 64 | self.qr.addData(str(self.Barcode)) 65 | self.qr.make() 66 | 67 | def drawSquareArea( self, layer, size, xposition, yposition): 68 | # creates a EDGE_MODULE of polygon type. The polygon is a square 69 | polygon = pcbnew.EDGE_MODULE(self.module) 70 | polygon.SetShape(pcbnew.S_POLYGON) 71 | polygon.SetWidth( 0 ) 72 | polygon.SetLayer(layer) 73 | halfsize = size/2 74 | polygon.GetPolyShape().NewOutline(); 75 | polygon.GetPolyShape().Append( halfsize+xposition, halfsize+yposition ) 76 | polygon.GetPolyShape().Append( halfsize+xposition, -halfsize+yposition ) 77 | polygon.GetPolyShape().Append( -halfsize+xposition, -halfsize+yposition ) 78 | polygon.GetPolyShape().Append( -halfsize+xposition, halfsize+yposition ) 79 | return polygon 80 | 81 | 82 | def _drawPixel(self, xposition, yposition): 83 | # build a rectangular pad as a dot on copper layer, 84 | # and a polygon (a square) on silkscreen 85 | if self.UseCu: 86 | pad = pcbnew.D_PAD(self.module) 87 | pad.SetSize(pcbnew.wxSize(self.X, self.X)) 88 | pad.SetPosition(pcbnew.wxPoint(xposition,yposition)) 89 | pad.SetShape(pcbnew.PAD_SHAPE_RECT) 90 | pad.SetAttribute(pcbnew.PAD_ATTRIB_SMD) 91 | pad.SetName("") 92 | layerset = pcbnew.LSET() 93 | layerset.AddLayer(pcbnew.F_Cu) 94 | layerset.AddLayer(pcbnew.F_Mask) 95 | pad.SetLayerSet( layerset ) 96 | self.module.Add(pad) 97 | if self.UseSilkS: 98 | polygon=self.drawSquareArea(pcbnew.F_SilkS, self.X, xposition, yposition) 99 | self.module.Add(polygon) 100 | 101 | 102 | def BuildThisFootprint(self): 103 | if self.border >= 0: 104 | # Adding border: Create a new array larger than the self.qr.modules 105 | sz = self.qr.modules.__len__() + (self.border * 2) 106 | arrayToDraw = [ [ 0 for a in range(sz) ] for b in range(sz) ] 107 | lineposition = self.border 108 | for i in self.qr.modules: 109 | columnposition = self.border 110 | for j in i: 111 | arrayToDraw[lineposition][columnposition] = j 112 | columnposition += 1 113 | lineposition += 1 114 | else: 115 | # No border: using array as is 116 | arrayToDraw = self.qr.modules 117 | 118 | # used many times... 119 | half_number_of_elements = arrayToDraw.__len__() / 2 120 | 121 | # Center position of QrCode 122 | yposition = - int(half_number_of_elements * self.X) 123 | for line in arrayToDraw: 124 | xposition = - int(half_number_of_elements * self.X) 125 | for pixel in line: 126 | # Trust table for drawing a pixel 127 | # Negative is a boolean; 128 | # each pixel is a boolean (need to draw of not) 129 | # Negative | Pixel | Result 130 | # 0 | 0 | 0 131 | # 0 | 1 | 1 132 | # 1 | 0 | 1 133 | # 1 | 1 | 0 134 | # => Draw as Xor 135 | if self.negative != pixel: # Xor... 136 | self._drawPixel(xposition, yposition) 137 | xposition += self.X 138 | yposition += self.X 139 | #int((5 + half_number_of_elements) * self.X)) 140 | textPosition = int((self.textHeight) + ((1 + half_number_of_elements) * self.X)) 141 | self.module.Value().SetPosition(pcbnew.wxPoint(0, - textPosition)) 142 | self.module.Value().SetTextHeight(self.textHeight) 143 | self.module.Value().SetTextWidth(self.textWidth) 144 | self.module.Value().SetThickness(self.textThickness) 145 | self.module.Reference().SetPosition(pcbnew.wxPoint(0, textPosition)) 146 | self.module.Reference().SetTextHeight(self.textHeight) 147 | self.module.Reference().SetTextWidth(self.textWidth) 148 | self.module.Reference().SetThickness(self.textThickness) 149 | self.module.Value().SetLayer(pcbnew.F_SilkS) 150 | 151 | self.module.Value().SetVisible(False) 152 | self.module.Reference().SetVisible(False) 153 | 154 | QRCodeWizard().register() 155 | -------------------------------------------------------------------------------- /slot_without_ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xtoolbox/kicad_tools/7e8321c02ff002d9ef80bfd45d1e86a2656b0b69/slot_without_ref.png --------------------------------------------------------------------------------