├── plugins ├── icon.png ├── __init__.py ├── result_event.py ├── plugin.py ├── config.py ├── thread.py ├── utils.py └── process.py ├── resources └── icon.png ├── LICENSE ├── metadata.json └── README.md /plugins/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcbway/PCBWay-Plug-in-for-Kicad/HEAD/plugins/icon.png -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcbway/PCBWay-Plug-in-for-Kicad/HEAD/resources/icon.png -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from .plugin import PCBWayPlugin 3 | plugin = PCBWayPlugin() 4 | plugin.register() 5 | except Exception as e: 6 | import logging 7 | root = logging.getLogger() 8 | root.debug(repr(e)) 9 | -------------------------------------------------------------------------------- /plugins/result_event.py: -------------------------------------------------------------------------------- 1 | import wx 2 | 3 | EVT_RESULT_ID = wx.NewId() 4 | 5 | 6 | def EVT_RESULT(win, func): 7 | win.Connect(-1, -1, EVT_RESULT_ID, func) 8 | 9 | 10 | class ResultEvent(wx.PyEvent): 11 | def __init__(self, data): 12 | wx.PyEvent.__init__(self) 13 | self.SetEventType(EVT_RESULT_ID) 14 | self.data = data 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 PCBWay,Aisler and contributors 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 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://go.kicad.org/pcm/schemas/v1", 3 | "name": "PCBWay Plug-in for KiCad", 4 | "description": "Start prototype and assembly by sending files to PCBWay with just one click.", 5 | "description_full": "Click this plugin for high quality prototyping and assembly services, PCBWay will commit to meeting your needs to the greatest extent.\n\nWhen you click PCBWay Plug-in button, we will export these files in your project:\n\n1. Gerber files in correct format for production\n2. IPC-Netlist file\n3. Bom-file that includes all information of components (please note designator, quantity, MPN/part number, package/footprint are necessary)\n4. Pick and Place-file used in assembly\n\nYou can click \"Save to Cart\" to place an order immediately after uploading the files ( usually only takes a few seconds), our engineers will double check the files before the production.\n\nAbout PCBWay:\n\nPCBWay is a Chinese company specializing in PCB prototyping and assembly, devotes to offer one-stop service. The build-time for standard PCBs is 24hrs. Our upgraded material with TG150-160 costs as low as $5/10pcs while our remarkable after-sale services are standing by for any questions you may encounter. As a sponsor of KiCad, we will always support its development.", 6 | "identifier": "com.github.pcbway.PCBWay-Plug-in-for-Kicad", 7 | "type": "plugin", 8 | "author": { 9 | "name": "PCBWay.com", 10 | "contact": { 11 | "web": "https://www.pcbway.com" 12 | } 13 | }, 14 | "maintainer": { 15 | "name": "PCBWay.com", 16 | "contact": { 17 | "web": "https://www.pcbway.com" 18 | } 19 | }, 20 | "license": "MIT", 21 | "resources": { 22 | "homepage": "https://github.com/pcbway/PCBWay-Plug-in-for-Kicad" 23 | }, 24 | "versions": [ 25 | { 26 | "version": "1.0.2", 27 | "status": "stable", 28 | "kicad_version": "6.00" 29 | 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /plugins/plugin.py: -------------------------------------------------------------------------------- 1 | import wx 2 | import pcbnew 3 | 4 | from .thread import * 5 | from .result_event import * 6 | from .process import * 7 | 8 | class KiCadToPCBWayForm(wx.Frame): 9 | def __init__(self): 10 | wx.Dialog.__init__( 11 | self, 12 | None, 13 | id=wx.ID_ANY, 14 | title=u"PCBWay is processing...", 15 | pos=wx.DefaultPosition, 16 | size=wx.DefaultSize, 17 | style=wx.DEFAULT_DIALOG_STYLE) 18 | 19 | self.SetSizeHints(wx.DefaultSize, wx.DefaultSize) 20 | 21 | bSizer1 = wx.BoxSizer(wx.VERTICAL) 22 | 23 | self.m_gaugeStatus = wx.Gauge( 24 | self, wx.ID_ANY, 100, wx.DefaultPosition, wx.Size( 25 | 300, 20), wx.GA_HORIZONTAL) 26 | self.m_gaugeStatus.SetValue(0) 27 | bSizer1.Add(self.m_gaugeStatus, 0, wx.ALL, 5) 28 | 29 | self.SetSizer(bSizer1) 30 | self.Layout() 31 | bSizer1.Fit(self) 32 | 33 | self.Centre(wx.BOTH) 34 | 35 | EVT_RESULT(self, self.updateDisplay) 36 | PCBWayThread(self) 37 | 38 | def updateDisplay(self, status): 39 | if status.data == -1: 40 | pcbnew.Refresh() 41 | self.Destroy() 42 | else: 43 | self.m_gaugeStatus.SetValue(int(status.data)) 44 | 45 | 46 | class PCBWayPlugin(pcbnew.ActionPlugin): 47 | def __init__(self): 48 | self.name = "PCBWay Plug-in for KiCad" # 插件名称 49 | self.category = "Manufacturing" # 描述性类别名称 50 | self.description = "Start prototype and assembly by sending files to PCBWay with just one click." # 对插件及其功能的描述 51 | self.pcbnew_icon_support = hasattr(self, "show_toolbar_button") 52 | self.show_toolbar_button = True # 可选,默认为 False 53 | self.icon_file_name = os.path.join( 54 | os.path.dirname(__file__), 'icon.png') # 可选,默认为 "" 55 | self.dark_icon_file_name = os.path.join( 56 | os.path.dirname(__file__), 'icon.png') 57 | 58 | def Run(self): 59 | # 在用户操作时执行的插件的入口函数 60 | KiCadToPCBWayForm().Show() 61 | -------------------------------------------------------------------------------- /plugins/config.py: -------------------------------------------------------------------------------- 1 | import pcbnew 2 | 3 | baseUrl = 'https://www.pcbway.com' 4 | #baseUrl = 'http://en.pcbway.com' 5 | netlistFilename = 'PCBWay_netlist.ipc' 6 | bomFilename = 'PCBWay_bom.csv' 7 | positionsFilename = 'PCBWay_positions.csv' 8 | plotPlan = [ 9 | ("F.Cu", pcbnew.F_Cu, "Top Layer"), 10 | ("B.Cu", pcbnew.B_Cu, "Bottom Layer"), 11 | ("In1.Cu", pcbnew.In1_Cu, "Internal plane 1"), 12 | ("In2.Cu", pcbnew.In2_Cu, "Internal plane 2"), 13 | ("In3.Cu", pcbnew.In3_Cu, "Internal plane 3"), 14 | ("In4.Cu", pcbnew.In4_Cu, "Internal plane 4"), 15 | ("In5.Cu", pcbnew.In5_Cu, "Internal plane 5"), 16 | ("In6.Cu", pcbnew.In6_Cu, "Internal plane 6"), 17 | ("In7.Cu", pcbnew.In7_Cu, "Internal plane 7"), 18 | ("In8.Cu", pcbnew.In8_Cu, "Internal plane 8"), 19 | ("In9.Cu", pcbnew.In9_Cu, "Internal plane 9"), 20 | ("In10.Cu", pcbnew.In10_Cu, "Internal plane 10"), 21 | ("In11.Cu", pcbnew.In11_Cu, "Internal plane 11"), 22 | ("In12.Cu", pcbnew.In12_Cu, "Internal plane 12"), 23 | ("In13.Cu", pcbnew.In13_Cu, "Internal plane 13"), 24 | ("In14.Cu", pcbnew.In14_Cu, "Internal plane 14"), 25 | ("In15.Cu", pcbnew.In15_Cu, "Internal plane 15"), 26 | ("In16.Cu", pcbnew.In16_Cu, "Internal plane 16"), 27 | ("In17.Cu", pcbnew.In17_Cu, "Internal plane 17"), 28 | ("In18.Cu", pcbnew.In18_Cu, "Internal plane 18"), 29 | ("In19.Cu", pcbnew.In19_Cu, "Internal plane 19"), 30 | ("In20.Cu", pcbnew.In20_Cu, "Internal plane 20"), 31 | ("In21.Cu", pcbnew.In21_Cu, "Internal plane 21"), 32 | ("In22.Cu", pcbnew.In22_Cu, "Internal plane 22"), 33 | ("In23.Cu", pcbnew.In23_Cu, "Internal plane 23"), 34 | ("In24.Cu", pcbnew.In24_Cu, "Internal plane 24"), 35 | ("In25.Cu", pcbnew.In25_Cu, "Internal plane 25"), 36 | ("In26.Cu", pcbnew.In26_Cu, "Internal plane 26"), 37 | ("In27.Cu", pcbnew.In27_Cu, "Internal plane 27"), 38 | ("In28.Cu", pcbnew.In28_Cu, "Internal plane 28"), 39 | ("In29.Cu", pcbnew.In29_Cu, "Internal plane 29"), 40 | ("In30.Cu", pcbnew.In30_Cu, "Internal plane 30"), 41 | ("F.SilkS", pcbnew.F_SilkS, "Top Silkscreen"), 42 | ("B.SilkS", pcbnew.B_SilkS, "Bottom Silkscreen"), 43 | ("F.Mask", pcbnew.F_Mask, "Top Soldermask"), 44 | ("B.Mask", pcbnew.B_Mask, "Bottom Soldermask"), 45 | ("F.Paste", pcbnew.F_Paste, "Top Paste (Stencil)"), 46 | ("B.Paste", pcbnew.B_Paste, "Bottom Paste (Stencil)"), 47 | ("Edge.Cuts", pcbnew.Edge_Cuts, "Board Outline"), 48 | ("User.Comments", pcbnew.Cmts_User, "User Comments") 49 | ] 50 | -------------------------------------------------------------------------------- /plugins/thread.py: -------------------------------------------------------------------------------- 1 | #https://opensource.org/licenses/MIT 2 | 3 | import os 4 | import webbrowser 5 | import shutil 6 | import requests 7 | import wx 8 | import tempfile 9 | from threading import Thread 10 | from .result_event import * 11 | from .config import * 12 | from .process import * 13 | 14 | 15 | class PCBWayThread(Thread): 16 | def __init__(self, wxObject): 17 | Thread.__init__(self) 18 | self.process = PCBWayProcess() 19 | self.wxObject = wxObject 20 | self.start() 21 | 22 | def run(self): 23 | 24 | temp_dir = tempfile.mkdtemp() 25 | _, temp_file = tempfile.mkstemp() 26 | 27 | try: 28 | self.report(5) 29 | 30 | self.process.get_gerber_file(temp_dir) 31 | 32 | self.report(15) 33 | 34 | self.process.get_netlist_file(temp_dir) 35 | 36 | self.report(25) 37 | 38 | self.process.get_components_file(temp_dir) 39 | 40 | self.report(35) 41 | 42 | gerberData = self.process.get_gerber_parameter() 43 | 44 | self.report(45) 45 | 46 | p_name = self.process.get_name() 47 | 48 | temp_file = shutil.make_archive(p_name, 'zip', temp_dir) 49 | files = {'upload[file]': open(temp_file, 'rb')} 50 | 51 | self.report(55) 52 | 53 | upload_url = baseUrl + '/Common/KiCadUpFile/' 54 | 55 | self.report(65) 56 | 57 | rsp = requests.post( 58 | upload_url, files=files, data={'boardWidth':gerberData['boardWidth'],'boardHeight':gerberData['boardHeight'],'boardLayer':gerberData['boardLayer']}) 59 | 60 | self.report(75) 61 | 62 | urls = json.loads(rsp.content) 63 | 64 | readsofar = 0 65 | totalsize = os.path.getsize(temp_file) 66 | with open(temp_file, 'rb') as file: 67 | while True: 68 | data = file.read(10) 69 | if not data: 70 | break 71 | readsofar += len(data) 72 | percent = readsofar * 1e2 / totalsize 73 | self.report(75 + percent / 9) 74 | 75 | except Exception as e: 76 | wx.MessageBox(str(e), "Error", wx.OK | wx.ICON_ERROR) 77 | self.report(-1) 78 | return 79 | 80 | 81 | webbrowser.open(urls['redirect']) 82 | self.report(-1) 83 | 84 | def report(self, status): 85 | wx.PostEvent(self.wxObject, ResultEvent(status)) 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PCBWay Plug-in for Kicad 2 | 3 | ### Send your layout to PCBWay for instant production with just one click. 4 | 5 | Click this plugin for high quality prototyping and assembly services, PCBWay will commit to meeting your needs to the greatest extent. 6 | 7 | When you click PCBWay Plug-in button, we will export these files in your project: 8 | 1. Gerber files in correct format for production 9 | 2. IPC-Netlist file 10 | 3. Bom-file that includes all information of components 11 | 4. Pick and Place-file used in assembly 12 | 13 | You can click "Save to Cart" to place an order immediately after uploading the files( usually only takes a few seconds), our engineers will double check the files before the production. 14 | 15 | ![kicad-to-pcbway-2](https://user-images.githubusercontent.com/20063837/160805517-c1e80546-4672-46cb-9d0a-65d71400459d.gif) 16 | 17 | **Note: We have updated plugin for Kicad7,you could also place assembly order directly now.But we need to get the component "Manufacture Part Number". Please confirm you run the "Update Board from Schematic".** 18 | 19 | ### Installation from the official KiCad repositories 20 | Just open the "Plugin and Content Manager" from the KiCad main menu an install the "PCBWay Plug-in for KiCad" plugin from the selection list. 21 | ![plugin for kicad](https://user-images.githubusercontent.com/20063837/168219163-f6ccf822-848e-424a-8691-02d67da7b4cb.png) 22 | 23 | 24 | ### Manual installation 25 | You can also download the latest ZIP file from https://github.com/pcbway/PCBWay-Plug-in-for-Kicad/releases/download/v1.0.2/KiCadToPCBWay_v1.0.2.zip, then open the "Plugin and Content Manager" from the main window of KiCad and install the ZIP file via "Install from File". 26 | ![install pcbway plugin](https://user-images.githubusercontent.com/20063837/160970891-4971cb1a-a36a-45bc-b219-93924f0ff070.png) 27 | 28 | 29 | 30 | ### About Bom 31 | 32 | We can get all information of components used in your design. In order to speed up the quotation of components, we need this information: 33 | 1. Designator (necessary) 34 | 2. Quantity (necessary) 35 | 3. MPN/Part Number (necessary) 36 | 4. Package/Footprint (necessary) 37 | 5. Manufacturer (optional) 38 | 6. Description/value (optional) 39 | 40 | You just need to add the properties in your schematic like the picture shows: 41 | ![bom](https://user-images.githubusercontent.com/20063837/160999527-e0a50238-3468-44be-b691-667ab8e8fef1.png) 42 | 43 | 44 | 45 | 46 | 47 | 48 | ### About PCBWay 49 | PCBWay is a Chinese company specializing in PCB prototyping and assembly, devotes to offer one-stop service. The build-time for standard PCBs is 24hrs. Our upgraded material with TG150-160 costs as low as $5/10pcs while our remarkable after-sale services are standing by for any questions you may encounter. Give us a chance and we will give you back a satisfactory result. 50 | 51 | As a sponsor of KiCad, we will always support its development. 52 | 53 | ![pcbway and kicad](https://user-images.githubusercontent.com/20063837/161211870-b4a46c17-2e1c-45b4-bfe7-2ab97ade157d.png) 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /plugins/utils.py: -------------------------------------------------------------------------------- 1 | import pcbnew # type: ignore 2 | import json 3 | import re 4 | import wx # type: ignore 5 | 6 | def get_version(): 7 | bs = pcbnew.GetBuildVersion() 8 | bs = bs.strip() 9 | # (6.0.0) -> 6.0.0 10 | if bs.startswith('(') and bs.endswith(')'): 11 | bs = re.sub(r'^\(|\)$', '', bs) 12 | 13 | return float('.'.join(bs.split(".")[0:2])) # e.g. '8.0.0-r3' -> '8.0' 14 | 15 | def is_v6(): 16 | version = get_version() 17 | return version >= 5.99 and version < 6.99 18 | 19 | def is_v7(): 20 | version = get_version() 21 | return version >= 6.99 and version < 7.99 22 | 23 | def is_v8(): 24 | version = get_version() 25 | return version >= 7.99 and version < 8.99 26 | 27 | def is_v9(): 28 | version = get_version() 29 | return version >= 8.99 and version < 9.99 30 | 31 | def is_greater_v8(): 32 | return get_version() >= 7.99 33 | 34 | def footprint_has_field(footprint, field_name): 35 | if is_greater_v8(): 36 | return footprint.HasFieldByName(field_name) 37 | else: 38 | return footprint.HasProperty(field_name) 39 | 40 | def footprint_get_field(footprint, field_name): 41 | if is_greater_v8(): 42 | return footprint.GetFieldByName(field_name).GetText() 43 | else: 44 | return footprint.GetProperty(field_name) 45 | 46 | def get_mpn_keys(): 47 | keys = [ 48 | 'mpn', 49 | 'MPN', 50 | 'Mpn', 51 | 'PCBWay_MPN', 52 | 'part number', 53 | 'Part Number', 54 | 'Part No.', 55 | 'Mfr. Part No.', 56 | 'Mfg Part', 57 | 'Manufacturer_Part_Number', 58 | ] 59 | return keys 60 | 61 | def get_pack_keys(): 62 | keys = [ 63 | 'pack', 64 | 'PACK', 65 | 'Pack', 66 | 'package', 67 | 'PACKAGE', 68 | 'Package', 69 | 'case', 70 | 'CASE', 71 | 'Case', 72 | ] 73 | return keys 74 | 75 | def get_dnp_keys(): 76 | keys = [ 77 | 'dnp', 78 | 'Dnp', 79 | 'DNp', 80 | 'DNP', 81 | 'dNp', 82 | 'dNP', 83 | 'dnP', 84 | 'DnP', 85 | ] 86 | return keys 87 | 88 | def get_value_from_footprint_by_keys(fp, keys): 89 | if not fp or not keys: 90 | return None 91 | 92 | for key in keys: 93 | if footprint_has_field(fp, key): 94 | return footprint_get_field(fp, key) 95 | 96 | def get_mpn_from_footprint(f): 97 | return get_value_from_footprint_by_keys(f, get_mpn_keys()) 98 | 99 | def get_pack_from_footprint(f): 100 | return get_value_from_footprint_by_keys(f, get_pack_keys()) 101 | 102 | def get_is_dnp_from_footprint(f): 103 | for k in get_dnp_keys(): 104 | if footprint_has_field(f, k): 105 | return True 106 | return ((f.GetValue().upper() == 'DNP') 107 | or getattr(f, 'IsDNP', bool)()) 108 | 109 | def debug_show_object(obj): 110 | wx.MessageBox(json.dumps(obj)) 111 | -------------------------------------------------------------------------------- /plugins/process.py: -------------------------------------------------------------------------------- 1 | #https://opensource.org/licenses/MIT 2 | 3 | import pcbnew # type: ignore 4 | import os 5 | import csv 6 | 7 | from itertools import groupby 8 | 9 | from .config import * 10 | from .utils import * 11 | 12 | class PCBWayProcess: 13 | def __init__(self): 14 | self.board = pcbnew.GetBoard() 15 | self.pctl = pcbnew.PLOT_CONTROLLER(self.board) 16 | self.bom = [] 17 | self.components = [] 18 | 19 | def get_name(self): 20 | return self.board.GetFileName() 21 | 22 | def get_basedir(self): 23 | return os.path.dirname(self.board.GetFileName()) 24 | 25 | def get_basename(self): 26 | return os.path.basename(self.board.GetFileName()) 27 | 28 | def get_gerber_file(self, temp_dir): 29 | settings = self.board.GetDesignSettings() 30 | settings.m_SolderMaskMargin = 0 31 | settings.m_SolderMaskMinWidth = 0 32 | 33 | #pctl = pcbnew.PLOT_CONTROLLER(self.board) 34 | 35 | popt = self.pctl.GetPlotOptions() 36 | popt.SetOutputDirectory(temp_dir) 37 | popt.SetPlotFrameRef(False) 38 | popt.SetSketchPadLineWidth(pcbnew.FromMM(0.1)) 39 | popt.SetAutoScale(False) 40 | popt.SetScale(1) 41 | popt.SetMirror(False) 42 | popt.SetUseGerberAttributes(True) 43 | if hasattr(popt, "SetExcludeEdgeLayer"): 44 | popt.SetExcludeEdgeLayer(True) 45 | popt.SetUseGerberProtelExtensions(False) 46 | popt.SetUseAuxOrigin(True) 47 | popt.SetSubtractMaskFromSilk(True) 48 | popt.SetDrillMarksType(0) # NO_DRILL_SHAPE 49 | 50 | for layer_info in plotPlan: 51 | if self.board.IsLayerEnabled(layer_info[1]): 52 | self.pctl.SetLayer(layer_info[1]) 53 | self.pctl.OpenPlotfile( 54 | layer_info[0], 55 | pcbnew.PLOT_FORMAT_GERBER, 56 | layer_info[2]) 57 | self.pctl.PlotLayer() 58 | 59 | self.pctl.ClosePlot() 60 | 61 | def get_netlist_file(self, temp_dir): 62 | drlwriter = pcbnew.EXCELLON_WRITER(self.board) 63 | 64 | drlwriter.SetOptions( 65 | False, 66 | True, 67 | self.board.GetDesignSettings().GetAuxOrigin(), 68 | False) 69 | drlwriter.SetFormat(False) 70 | drlwriter.CreateDrillandMapFilesSet(self.pctl.GetPlotDirName(), True, False) 71 | 72 | netlist_writer = pcbnew.IPC356D_WRITER(self.board) 73 | netlist_writer.Write(os.path.join(temp_dir, netlistFilename)) 74 | 75 | def get_components_file(self, temp_dir): 76 | if hasattr(self.board, 'GetModules'): 77 | footprints = list(self.board.GetModules()) 78 | else: 79 | footprints = list(self.board.GetFootprints()) 80 | 81 | footprints.sort(key=lambda x: x.GetReference()) 82 | 83 | mpn_keys = get_mpn_keys() 84 | pack_keys = get_pack_keys() 85 | no_show_keys = [ 86 | 'ki_fp_filters', 87 | 'DNP', 88 | 'Reference', 89 | 'Value', 90 | 'Datasheet', 91 | 'Footprint', 92 | ] 93 | ignore_ext_keys = mpn_keys + pack_keys + no_show_keys 94 | 95 | greater_v8 = is_greater_v8() 96 | fp_datas = [] 97 | for i, f in enumerate(footprints): 98 | try: 99 | footprint_name = str(f.GetFPID().GetFootprintName()) 100 | except AttributeError: 101 | footprint_name = str(f.GetFPID().GetLibItemName()) 102 | 103 | layer = { 104 | pcbnew.F_Cu: 'top', 105 | pcbnew.B_Cu: 'bottom', 106 | }.get(f.GetLayer()) 107 | 108 | f_attrs = f.GetAttributes() 109 | parsed_attrs = self.parse_attrs(f_attrs) 110 | 111 | mount_type = 'smt' if parsed_attrs['smd'] else 'tht' 112 | not_in_bom = parsed_attrs['not_in_bom'] 113 | not_in_pos = parsed_attrs['not_in_pos'] 114 | 115 | if not_in_bom and not_in_pos: 116 | continue 117 | 118 | rotation = f.GetOrientation().AsDegrees() if hasattr(f.GetOrientation(), 'AsDegrees') else f.GetOrientation() / 10.0 119 | 120 | pos_x = (f.GetPosition()[0] - self.board.GetDesignSettings().GetAuxOrigin()[0]) / 1000000.0 121 | pos_y = (f.GetPosition()[1] - self.board.GetDesignSettings().GetAuxOrigin()[1]) * -1.0 / 1000000.0 122 | 123 | designator = f.GetReference() 124 | value = f.GetValue() 125 | mpn = get_mpn_from_footprint(f) 126 | pack = get_pack_from_footprint(f) 127 | is_dnp = get_is_dnp_from_footprint(f) if greater_v8 else False 128 | 129 | if not footprint_name: 130 | footprint_name = '' 131 | 132 | if not pack: 133 | pack = '' 134 | 135 | if not mpn: 136 | mpn = '' 137 | 138 | if not_in_pos == False: 139 | self.components.append({ 140 | 'pos_x': pos_x, 141 | 'pos_y': pos_y, 142 | 'rotation': rotation, 143 | 'side': layer, 144 | 'designator': designator, 145 | 'mpn': mpn, 146 | 'pack': pack, 147 | 'footprint': footprint_name, 148 | 'value': value, 149 | 'mount_type': mount_type, 150 | }) 151 | 152 | if not_in_bom: 153 | continue 154 | 155 | fp_item_fields = { 156 | 'designator': designator, 157 | 'value': value, 158 | 'footprint': footprint_name, 159 | 'pack': pack, 160 | 'mpn': mpn, 161 | 'DNP': 'Yes' if is_dnp else '', 162 | 'Mount_Type': mount_type, 163 | } 164 | 165 | if greater_v8: 166 | footprint_fields = f.GetFieldsText() 167 | if footprint_fields: 168 | for k, v in footprint_fields.items(): 169 | if k.upper() == 'DNP' or k.upper() == 'MOUNT_TYPE': 170 | k = 'Custom_' + k 171 | if not v or k in ignore_ext_keys: 172 | continue 173 | fp_item_fields[k] = v 174 | 175 | fp_datas.append(fp_item_fields) 176 | 177 | fp_data_group = {} 178 | for item in fp_datas: 179 | designator = item['designator'] 180 | value = item['value'] 181 | footprint = item['footprint'] 182 | pack = item['pack'] 183 | mpn = item['mpn'] 184 | is_dnp = item['DNP'] 185 | 186 | index = value + '_' + footprint + '_' + pack + '_' + mpn 187 | if is_dnp: 188 | index = designator + '_' + index 189 | 190 | if index in fp_data_group: 191 | fp_data_group[index].append(item) 192 | else: 193 | fp_data_group[index] = [ item ] 194 | 195 | fixed_columns = [ 196 | 'designator', 197 | 'quantity', 198 | 'value', 199 | 'footprint', 200 | 'pack', 201 | 'mpn', 202 | ] 203 | 204 | all_columns = [ 205 | 'Designator', 206 | 'Quantity', 207 | 'Value', 208 | 'Footprint', 209 | 'Package', 210 | 'MPN', 211 | ] 212 | rows = [] 213 | for _key, items in fp_data_group.items(): 214 | first_item = items[0] 215 | 216 | row_datas = {} 217 | row_columns = [] 218 | designators = [] 219 | for item in items: 220 | designator = item['designator'] 221 | designators.append(designator) 222 | 223 | for item_key, item_value in item.items(): 224 | if item_key in fixed_columns: 225 | continue 226 | if item_key not in all_columns: 227 | all_columns.append(item_key) 228 | if item_key not in row_columns: 229 | row_columns.append(item_key) 230 | if item_key not in row_datas: 231 | row_datas[item_key] = [] 232 | row_datas[item_key].append({ 233 | 'key': designator, 234 | 'value': item_value, 235 | }) 236 | 237 | for k in row_columns: 238 | row_data = row_datas[k] 239 | row_data_groupby = {val: list(group) for val, group in groupby(row_data, key=lambda x: x['value'])} 240 | item_text = '' 241 | if len(row_data_groupby) > 1: 242 | item_text = '; '.join(['[' + ','.join([g['key'] for g in group]) + ']'+ group_key for group_key, group in row_data_groupby.items()]) 243 | else: 244 | item_text = row_data[0]['value'] 245 | first_item[k] = item_text 246 | 247 | designator_count = len(designators) 248 | designator = ', '.join(designators) 249 | 250 | 251 | row = { 252 | 'Designator': designator, 253 | 'Quantity': designator_count, 254 | 'Value': first_item['value'], 255 | 'Footprint': first_item['footprint'], 256 | 'Package': first_item['pack'], 257 | 'MPN': first_item['mpn'], 258 | } 259 | 260 | for k in row_columns: 261 | if k in first_item: 262 | row[k] = first_item[k] 263 | 264 | rows.append(row) 265 | 266 | for row in rows: 267 | newRow = {} 268 | for k in all_columns: 269 | if k in row: 270 | newRow[k] = row[k] 271 | else: 272 | newRow[k] = '' 273 | if not greater_v8: 274 | del newRow['DNP'] 275 | self.bom.append(newRow) 276 | 277 | if len(self.components) > 0: 278 | with open((os.path.join(temp_dir, positionsFilename)), 'w', newline='', encoding='utf-8-sig') as outfile: 279 | csvobj = csv.writer(outfile) 280 | csvobj.writerow(self.components[0].keys()) 281 | 282 | for component in self.components: 283 | if ('**' not in component['designator']): 284 | csvobj.writerow(component.values()) 285 | 286 | if len(self.bom) > 0: 287 | with open((os.path.join(temp_dir, bomFilename)), 'w', newline='', encoding='utf-8-sig') as outfile: 288 | csvobj = csv.writer(outfile) 289 | csvobj.writerow(self.bom[0].keys()) 290 | 291 | for component in self.bom: 292 | if ('**' not in component['Designator']): 293 | csvobj.writerow(component.values()) 294 | 295 | 296 | def get_gerber_parameter(self): 297 | boardWidth = pcbnew.ToMM(self.board.GetBoardEdgesBoundingBox().GetWidth()) 298 | boardHeight = pcbnew.ToMM(self.board.GetBoardEdgesBoundingBox().GetHeight()) 299 | 300 | if hasattr(self.board, 'GetCopperLayerCount'): 301 | boardLayer = self.board.GetCopperLayerCount() 302 | 303 | return { 304 | 'boardWidth':boardWidth, 305 | 'boardHeight':boardHeight, 306 | 'boardLayer':boardLayer, 307 | } 308 | 309 | def parse_attrs(self, attrs): 310 | return {} if not isinstance(attrs, int) else { 311 | 'tht': self.parse_attr_flag(attrs, pcbnew.FP_THROUGH_HOLE), 312 | 'smd': self.parse_attr_flag(attrs, pcbnew.FP_SMD), 313 | 'not_in_pos': self.parse_attr_flag(attrs, pcbnew.FP_EXCLUDE_FROM_POS_FILES), 314 | 'not_in_bom': self.parse_attr_flag(attrs, pcbnew.FP_EXCLUDE_FROM_BOM), 315 | 'not_in_plan': self.parse_attr_flag(attrs, pcbnew.FP_BOARD_ONLY) 316 | } 317 | 318 | def parse_attr_flag(self, attr, mask): 319 | return mask == (attr & mask) 320 | --------------------------------------------------------------------------------