Compare commits
147 Commits
feature/upload
...
4.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
| a3081153e6 | |||
| 28ea32fedf | |||
| 05e10eba00 | |||
| 31690863e5 | |||
| da76ad54bc | |||
| e19c0e411b | |||
| 87df32ac65 | |||
| 702e7e6ee9 | |||
| f78ad8c141 | |||
| 8984740e74 | |||
| a43b555c90 | |||
| ab0b336ed9 | |||
| 88ae87bf48 | |||
| 1192d6cc11 | |||
| 266eb3bd31 | |||
| a3aea835b1 | |||
| c9e3ef16c0 | |||
| 0917e9f199 | |||
| bce98f72c3 | |||
| 84e933b566 | |||
| 3714af0afb | |||
| b80a14dfbb | |||
| bfa87db797 | |||
| c1322b705f | |||
| 371611e6f4 | |||
| 038cd1ddbe | |||
| e2e10578cf | |||
| eeca1704e2 | |||
| 6729b564db | |||
| 9e4e497899 | |||
| f92c64e7dc | |||
| 13fc67bbd7 | |||
| 8dc0e8a440 | |||
| e9f4b5b29b | |||
| 3cdcc31e63 | |||
| 8db1a4d446 | |||
| 2b8c898d22 | |||
| bad1d2d1a3 | |||
| 8a0af1a043 | |||
| d3cb95030a | |||
| 355f3c3ac4 | |||
| ee3e238a3d | |||
| a3fb4259dd | |||
| 872c0d90df | |||
| 891b60ac43 | |||
| 6461a362dd | |||
| 6f1426d972 | |||
| 6567a9da07 | |||
| 57fd582a6d | |||
| 9d18a8bc54 | |||
| df06c3c867 | |||
| 8beb878d32 | |||
| ef138c8feb | |||
| 466c008228 | |||
| c8e3daa5ae | |||
| a9cd57a027 | |||
| 387c5b3931 | |||
| 2d2436993a | |||
| 376d6df61d | |||
| f832e4e007 | |||
| d42611753e | |||
| ac0fec8faf | |||
| 2ac859e7e7 | |||
| 830819b27b | |||
| 9832454472 | |||
| 468640df8a | |||
| db4bf8904a | |||
| 1a9d56d0a9 | |||
| 50ad485758 | |||
| 3954ba9b68 | |||
| 3c1254eb9c | |||
| f10292a797 | |||
| 1d5a1b244e | |||
| c88232dbaf | |||
| 9e78ed2f18 | |||
| 99344155b5 | |||
| 7a7a04a714 | |||
| 4a59fb0672 | |||
| 5afaa579f8 | |||
| a1519203c1 | |||
| 37b039b515 | |||
| db103a5775 | |||
| 8394c85f15 | |||
| b783e7bd3e | |||
| e13276ae62 | |||
| 523a48cdff | |||
| f51c44fb00 | |||
| 817a58e8cc | |||
| 11d48c1370 | |||
| 0db7b02636 | |||
| e90cb33e6a | |||
| 67d725a96f | |||
| c62f1e3c1e | |||
| 68b5eb639a | |||
| 9f0fa6eaee | |||
| 6adb746943 | |||
| 79bd50db9e | |||
| fc12cc94b3 | |||
| 76d8ffbf75 | |||
| 30d1684f76 | |||
| a09b42c20f | |||
| 5417532fed | |||
| dec7a3ca1b | |||
| b8d0073846 | |||
| 599f159806 | |||
| cd38035aff | |||
| ecd98adeff | |||
| d8d81a7fe9 | |||
| d8c9aa775e | |||
| b783b289a8 | |||
| 927128651c | |||
| 91fdcc9fc2 | |||
| 1d1d38b83f | |||
| 49e921d2cf | |||
| 9b1d84265d | |||
| 5a675648bc | |||
| adbd0fdde0 | |||
| 1131b2efa9 | |||
| 0fcaa841a8 | |||
| 9147cacdac | |||
| 17b3c10d16 | |||
| c53997c639 | |||
| 0a8de1183e | |||
| d93476b5a9 | |||
| 1e1b347852 | |||
| 7a67087431 | |||
| adb10d314a | |||
| 5b43ce9047 | |||
| 45cb109668 | |||
| 94bbea2ac4 | |||
| 1205f17d7e | |||
| 1bfcc23256 | |||
| 036754a53b | |||
| c71afbe1b9 | |||
| 1c9e78e869 | |||
| b51024b174 | |||
| 3f19dc2121 | |||
| 6255f69e4a | |||
| a5a31059d0 | |||
| 27ac789281 | |||
| 3a33cfff96 | |||
| 0994f04d2c | |||
| 18b3a8074f | |||
| 05e486d949 | |||
| 34318c7de0 | |||
| 8f062359e4 | |||
| 5b821aebe2 |
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: bennymeg
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
Vendored
+8
@@ -1,13 +1,21 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Colour",
|
||||
"csvfile",
|
||||
"Drilland",
|
||||
"EXCELLON",
|
||||
"FPID",
|
||||
"JLCPCB",
|
||||
"LCSC",
|
||||
"LSEQ",
|
||||
"netlist",
|
||||
"pcbnew",
|
||||
"Plotfile",
|
||||
"Protel",
|
||||
"rcos",
|
||||
"restkey",
|
||||
"restval",
|
||||
"rsin",
|
||||
"Soldermask"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
- Hildo Guillardi Júnior
|
||||
@@ -6,6 +6,8 @@
|
||||
| **JLC PCB Plug-in for KiCad** |
|
||||
|:-----:|
|
||||
|
||||
[](https://github.com/sponsors/bennymeg)
|
||||
|
||||
</div>
|
||||
|
||||
## Features
|
||||
@@ -17,27 +19,130 @@
|
||||
## Installation
|
||||
|
||||
### Official Installation
|
||||
Open the "Plugin and Content Manager" from the KiCad main menu an install the "<ins>Fabrication Toolkit</ins>" plugin from the selection list.
|
||||
Fabrication Toolkit is distributed with the official releases of KiCad 6+. Open the "Plugin and Content Manager" from the KiCad main menu and install the "<ins>Fabrication Toolkit</ins>" plugin from the selection list.
|
||||
|
||||
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/manager.png?raw=true" height=275>
|
||||
|
||||
### Manual installation
|
||||
Download the latest ZIP file from https://github.com/bennymeg/JLC-Plugin-for-KiCad. Open the "Plugin and Content Manager" from the KiCads main window and install the ZIP file via "Install from File".
|
||||
Download the [latest release](https://github.com/bennymeg/JLC-Plugin-for-KiCad/releases) ZIP file. Open the "Plugin and Content Manager" from the KiCads main window and install the ZIP file via "Install from File".
|
||||
|
||||
## Usage
|
||||
Click on the Fabrication Toolkit <img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/resources/icon.png?raw=true" style="magin-bottom: 8px;" alt="Logo" height=24> button on the top tool box inside KiCad pcb editor (pcbnew).
|
||||
|
||||
**⊛** Ensure your board is syncronized before invoking this addon [**F8**].
|
||||
|
||||
**⊛** The `User_1` layer in internally defined as a **V-Cuts** layer, please avoid using it for anything else. <span style="text-color: light-grey !important;">_(since v3.0.0)_.</span>
|
||||
|
||||
## Options
|
||||
|
||||
### Include Component Part Number in Production Files
|
||||
Add an 'MPN'* field with the LCSC component part number to the symbol's fields property.
|
||||
Options can be set in the dialog that appears when the plugin is invoked. They are saved in a file called `fabrication-toolkit-options.json` in the project directory so that they are remembered between invocations of the plugin.
|
||||
|
||||
### ① Include Component Part Number in Production Files
|
||||
Add an 'LCSC Part #'* field with the LCSC component part number to the symbol's fields property.
|
||||
|
||||
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/mpn.png?raw=true" height=420>
|
||||
|
||||
#### Similar Fields*:
|
||||
|'mpn' | 'Mpn' | 'MPN' | 'JLC_MPN' | 'LCSC_MPN' | 'LCSC Part #' | 'JLC' | 'LCSC'|
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
#### Primary Fields*:
|
||||
| 'LCSC Part #' | 'JLCPCB Part #' |
|
||||
| --- | --- |
|
||||
|
||||
### Ignore Footprint in Production Files
|
||||
Select 'Exclude from position files' or 'Exclude from BOM' in the symbol's attributes property in order to ignore the footprint from the relevant file.
|
||||
_The fields will be query in the order denoted above._
|
||||
|
||||
#### Fallback Fields*:
|
||||
| 'JLC Part' | 'LCSC Part' | 'LCSC' | 'JLC' | 'MPN' | 'Mpn' | 'mpn' |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
|
||||
_The fields will be query in the order denoted above._
|
||||
|
||||
---
|
||||
|
||||
### ② Ignore Footprint in Production Files
|
||||
Select 'Exclude from board' or 'Exclude from BOM' in the symbol's attributes property in order to ignore the footprint from the relevant file.
|
||||
|
||||
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/attributes.png?raw=true" height=420>
|
||||
|
||||
Select 'Exclude from position files' or 'Exclude from BOM' in the footprint's fabrication attributes property in order to ignore the footprint from the relevant file.
|
||||
|
||||
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/fabrication.png?raw=true" height=505>
|
||||
|
||||
---
|
||||
|
||||
### ③ Offset Component Rotation
|
||||
The rotation of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLC PCB used different variation of the same standard. Most of the rotations may be corrected by the `rotations.cf` definitions. To the exception cases: add an 'JLCPCB Rotation Offset' field - with positive values indicating counter-clockwise orientation offset in degrees.
|
||||
|
||||
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/rotation-jlc.png?raw=true" height=164>
|
||||
|
||||
If the JLC preview shows a footprint like this, enter a rotation offset of -90 to rotate pin 1 to the lower right corner.
|
||||
|
||||
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/rotation.png?raw=true" height=420>
|
||||
|
||||
As the rotation offset is entered in the Schematic Editor, make sure to update your PCB with the changes made to the schematic before generating a new set of production files, otherwise your changes won't be reflected.
|
||||
|
||||
#### Primary Fields*:
|
||||
| 'JLCPCB Rotation Offset' |
|
||||
| --- |
|
||||
|
||||
_The fields will be queried in the order denoted above._
|
||||
|
||||
#### Fallback Fields*:
|
||||
| 'JlcRotOffset' | 'JLCRotOffset' |
|
||||
| --- | --- |
|
||||
|
||||
_The fields will be queried in the order denoted above._
|
||||
|
||||
---
|
||||
|
||||
### ④ Offset Component Position
|
||||
The position of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLCPB used different variation of the same standard. To the exception cases: add an 'JLCPCB Position Offset' field with an comma separated x,y position offset to correct it.
|
||||
|
||||
Use following table to quickly find out to which coordinate enter the correction based on JLC arrows clicks - depending on footprint rotation in Kicad PCB Editor status bar:
|
||||
|Kicad footprint deg | x | y|
|
||||
|----|----|----|
|
||||
|0deg, Front | right arrow | up arrow |
|
||||
|0deg, Back | left arrow | down arrow |
|
||||
|180deg, Front | left arrow | down arrow |
|
||||
|180deg, Back | right arrow | up arrow |
|
||||
|90deg, Front or Back | up arrow | left arrow |
|
||||
|-90deg, Front or Back | down arrow | right arrow |
|
||||
|
||||
For custom angles it's best to place also a temporary straight symbol to perform alignment.
|
||||
Single arrow press in JLC is about 0.07mm shift (8 clicks is about 0.5mm).
|
||||
|
||||
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/position.png?raw=true" height=420>
|
||||
|
||||
As the position offset is entered in the Schematic Editor, make sure to update your PCB with the changes made to the schematic before generating a new set of production files, otherwise your changes won't be reflected.
|
||||
|
||||
#### Primary Fields*:
|
||||
| 'JLCPCB Position Offset' |
|
||||
| --- |
|
||||
|
||||
_The fields will be queried in the order denoted above._
|
||||
|
||||
#### Fallback Fields*:
|
||||
| 'JlcPosOffset' | 'JLCPosOffset' |
|
||||
| --- | --- |
|
||||
|
||||
_The fields will be queried in the order denoted above._
|
||||
|
||||
### ⑤ Override Component Layer
|
||||
Some footprints may have their components defined on the opposite layer to there actual footprints. In these instances you can override mount side by using this field.
|
||||
|
||||
Values can be `top`, `bottom`, `t` or `b`.
|
||||
|
||||
#### Primary Fields*:
|
||||
| 'JLCPCB Layer Override' |
|
||||
| --- |
|
||||
|
||||
_The fields will be queried in the order denoted above._
|
||||
|
||||
#### Fallback Fields*:
|
||||
| 'JlcLayerOverride' | 'JLCLayerOverride' |
|
||||
| --- | --- |
|
||||
|
||||
_The fields will be queried in the order denoted above._
|
||||
|
||||
|
||||
|
||||
## Author
|
||||
|
||||
Benny Megidish
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 189 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 250 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 248 KiB |
+7
-7
@@ -2,24 +2,24 @@
|
||||
"$schema": "https://go.kicad.org/pcm/schemas/v1",
|
||||
"name": "Fabrication Toolkit",
|
||||
"description": "JLC PCB fabrication toolkit",
|
||||
"description_full": "Toolkit for automating PCB fabrication process with KiCad and JLC PCB. \n\n OPTIONS: \n\u2022 Include Component Part Number in Production Files \n Add an 'MPN' field with the LCSC component part number to the footprint component options. \n\n\u2022 Ignore Footprint in Production Files \n Select 'Exclude from position files' or 'Exclude from BOM' in the footprint component options to in order ignore the footprint from the relevant file.",
|
||||
"description_full": "Toolkit for automating PCB fabrication process with KiCad and JLC PCB. \n\n OPTIONS: \n\u2022 Include Component Part Number in Production Files \n Add an 'MPN' field with the LCSC component part number to the footprint component options. \n\n\u2022 Ignore Footprint in Production Files \n Select 'Exclude from position files' or 'Exclude from BOM' in the footprint properties fabrication attributes in order to ignore the footprint from the relevant file. \n\n\u2022 Rotate components \n Add an 'JLCPCB Rotation Offset' field with the additional counter-clockwise rotation in degrees to correct a mismatch between KiCad and JLC footprint orientation.",
|
||||
"identifier": "com.github.bennymeg.JLC-Plugin-for-KiCad",
|
||||
"type": "plugin",
|
||||
"author": {
|
||||
"name": "Benny Megidish",
|
||||
"contact": {
|
||||
"web": "https://github.com/bennymeg/JLC-Plugin-for-KiCad"
|
||||
"web": "https://github.com/bennymeg/Fabrication-Toolkit"
|
||||
}
|
||||
},
|
||||
"maintainer": {
|
||||
"name": "Benny Megidish",
|
||||
"contact": {
|
||||
"web": "https://github.com/bennymeg/JLC-Plugin-for-KiCad"
|
||||
"web": "https://github.com/bennymeg/Fabrication-Toolkit"
|
||||
}
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"resources": {
|
||||
"homepage": "https://github.com/bennymeg/JLC-Plugin-for-KiCad"
|
||||
"homepage": "https://github.com/bennymeg/Fabrication-Toolkit"
|
||||
},
|
||||
"tags": [
|
||||
"jlc",
|
||||
@@ -29,9 +29,9 @@
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"version": "0.4.2",
|
||||
"status": "testing",
|
||||
"version": "4.4.1",
|
||||
"status": "stable",
|
||||
"kicad_version": "6.00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+4
-17
@@ -1,4 +1,4 @@
|
||||
import pcbnew
|
||||
import pcbnew # type: ignore
|
||||
|
||||
baseUrl = 'https://www.jlcpcb.com'
|
||||
|
||||
@@ -6,21 +6,8 @@ netlistFileName = 'netlist.ipc'
|
||||
designatorsFileName = 'designators.csv'
|
||||
placementFileName = 'positions.csv'
|
||||
bomFileName = 'bom.csv'
|
||||
gerberArchiveName = 'gerber.zip'
|
||||
gerberArchiveName = 'gerbers.zip'
|
||||
outputFolder = 'production'
|
||||
bomRowLimit = 200
|
||||
|
||||
plotPlan = [
|
||||
("F.Cu", pcbnew.F_Cu, "Top Layer"),
|
||||
("B.Cu", pcbnew.B_Cu, "Bottom Layer"),
|
||||
("In1.Cu", pcbnew.In1_Cu, "Internal plane 1"),
|
||||
("In2.Cu", pcbnew.In2_Cu, "Internal plane 2"),
|
||||
("In3.Cu", pcbnew.In3_Cu, "Internal plane 3"),
|
||||
("In4.Cu", pcbnew.In4_Cu, "Internal plane 4"),
|
||||
("F.SilkS", pcbnew.F_SilkS, "Top Silkscreen"),
|
||||
("B.SilkS", pcbnew.B_SilkS, "Bottom Silkscreen"),
|
||||
("F.Mask", pcbnew.F_Mask, "Top Soldermask"),
|
||||
("B.Mask", pcbnew.B_Mask, "Bottom Soldermask"),
|
||||
("F.Paste", pcbnew.F_Paste, "Top Paste (Stencil)"),
|
||||
("B.Paste", pcbnew.B_Paste, "Bottom Paste (Stencil)"),
|
||||
("Edge.Cuts", pcbnew.Edge_Cuts, "Board Outline")
|
||||
]
|
||||
optionsFileName = 'fabrication-toolkit-options.json'
|
||||
@@ -0,0 +1,6 @@
|
||||
AUTO_TRANSLATE_OPT = "AUTO TRANSLATE"
|
||||
AUTO_FILL_OPT = "AUTO FILL"
|
||||
EXCLUDE_DNP_OPT = "EXCLUDE DNP"
|
||||
OUTPUT_NAME_OPT = "OUTPUT NAME"
|
||||
EXTEND_EDGE_CUT_OPT = "EXTEND_EDGE_CUT"
|
||||
EXTRA_LAYERS = "EXTRA_LAYERS"
|
||||
+83
-14
@@ -1,9 +1,11 @@
|
||||
import os
|
||||
import wx
|
||||
import pcbnew
|
||||
import pcbnew # type: ignore
|
||||
|
||||
from .thread import ProcessThread
|
||||
from .events import StatusEvent
|
||||
from .options import AUTO_FILL_OPT, AUTO_TRANSLATE_OPT, EXCLUDE_DNP_OPT, EXTEND_EDGE_CUT_OPT, EXTRA_LAYERS
|
||||
from .utils import load_user_options, save_user_options, get_layer_names
|
||||
|
||||
|
||||
# WX GUI form that show the plugin progress
|
||||
@@ -13,37 +15,104 @@ class KiCadToJLCForm(wx.Frame):
|
||||
self,
|
||||
None,
|
||||
id=wx.ID_ANY,
|
||||
title=u"Processing...",
|
||||
title=u"Fabrication Toolkit",
|
||||
pos=wx.DefaultPosition,
|
||||
size=wx.DefaultSize,
|
||||
style=wx.DEFAULT_DIALOG_STYLE)
|
||||
|
||||
# self.app = wx.PySimpleApp()
|
||||
icon = wx.Icon(os.path.join(os.path.dirname(__file__), 'icon.png'))
|
||||
self.SetIcon(icon)
|
||||
self.SetBackgroundColour(wx.LIGHT_GREY)
|
||||
|
||||
self.SetSizeHints(wx.DefaultSize, wx.DefaultSize)
|
||||
self.SetSizeHints(wx.Size(600, 100), wx.DefaultSize)
|
||||
|
||||
bSizer1 = wx.BoxSizer(wx.VERTICAL)
|
||||
userOptions = load_user_options({
|
||||
EXTRA_LAYERS: "",
|
||||
EXTEND_EDGE_CUT_OPT: False,
|
||||
AUTO_TRANSLATE_OPT: True,
|
||||
AUTO_FILL_OPT: True,
|
||||
EXCLUDE_DNP_OPT: False
|
||||
})
|
||||
|
||||
self.m_gaugeStatus = wx.Gauge(
|
||||
self, wx.ID_ANY, 100, wx.DefaultPosition, wx.Size(
|
||||
300, 20), wx.GA_HORIZONTAL)
|
||||
self.m_gaugeStatus.SetValue(0)
|
||||
bSizer1.Add(self.m_gaugeStatus, 0, wx.ALL, 5)
|
||||
self.mOptionsLabel = wx.StaticText(self, label='Options:')
|
||||
# self.mOptionsSeparator = wx.StaticLine(self)
|
||||
|
||||
self.SetSizer(bSizer1)
|
||||
layers = get_layer_names(pcbnew.GetBoard())
|
||||
self.mAdditionalLayersControl = wx.TextCtrl(self, size=wx.Size(600, 50))
|
||||
self.mAdditionalLayersControl.Hint = "Additional layers (comma-separated)"
|
||||
self.mAdditionalLayersControl.AutoComplete(layers)
|
||||
self.mAdditionalLayersControl.Enable()
|
||||
self.mAdditionalLayersControl.SetValue(userOptions[EXTRA_LAYERS])
|
||||
|
||||
self.mExtendEdgeCutsCheckbox = wx.CheckBox(self, label='Set User.1 as V-Cut layer')
|
||||
self.mExtendEdgeCutsCheckbox.SetValue(userOptions[EXTEND_EDGE_CUT_OPT])
|
||||
self.mAutomaticTranslationCheckbox = wx.CheckBox(self, label='Apply automatic translations')
|
||||
self.mAutomaticTranslationCheckbox.SetValue(userOptions[AUTO_TRANSLATE_OPT])
|
||||
self.mAutomaticFillCheckbox = wx.CheckBox(self, label='Apply automatic fill for all zones')
|
||||
self.mAutomaticFillCheckbox.SetValue(userOptions[AUTO_FILL_OPT])
|
||||
self.mExcludeDnpCheckbox = wx.CheckBox(self, label='Exclude DNP components from BOM')
|
||||
self.mExcludeDnpCheckbox.SetValue(userOptions[EXCLUDE_DNP_OPT])
|
||||
|
||||
self.mGaugeStatus = wx.Gauge(
|
||||
self, wx.ID_ANY, 100, wx.DefaultPosition, wx.Size(600, 20), wx.GA_HORIZONTAL)
|
||||
self.mGaugeStatus.SetValue(0)
|
||||
self.mGaugeStatus.Hide()
|
||||
|
||||
self.mGenerateButton = wx.Button(self, label='Generate', size=wx.Size(600, 60))
|
||||
self.mGenerateButton.Bind(wx.EVT_BUTTON, self.onGenerateButtonClick)
|
||||
|
||||
boxSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
boxSizer.Add(self.mOptionsLabel, 0, wx.ALL, 5)
|
||||
# boxSizer.Add(self.mOptionsSeparator, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mAdditionalLayersControl, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mExtendEdgeCutsCheckbox, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mAutomaticTranslationCheckbox, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mAutomaticFillCheckbox, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mExcludeDnpCheckbox, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mGaugeStatus, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mGenerateButton, 0, wx.ALL, 5)
|
||||
|
||||
self.SetSizer(boxSizer)
|
||||
self.Layout()
|
||||
bSizer1.Fit(self)
|
||||
boxSizer.Fit(self)
|
||||
|
||||
self.Centre(wx.BOTH)
|
||||
|
||||
StatusEvent.invoke(self, self.updateDisplay)
|
||||
ProcessThread(self)
|
||||
|
||||
def onGenerateButtonClick(self, event):
|
||||
options = dict()
|
||||
options[EXTRA_LAYERS] = self.mAdditionalLayersControl.GetValue()
|
||||
options[EXTEND_EDGE_CUT_OPT] = self.mExtendEdgeCutsCheckbox.GetValue()
|
||||
options[AUTO_TRANSLATE_OPT] = self.mAutomaticTranslationCheckbox.GetValue()
|
||||
options[AUTO_FILL_OPT] = self.mAutomaticFillCheckbox.GetValue()
|
||||
options[EXCLUDE_DNP_OPT] = self.mExcludeDnpCheckbox.GetValue()
|
||||
|
||||
save_user_options(options)
|
||||
|
||||
self.mOptionsLabel.Hide()
|
||||
self.mAdditionalLayersControl.Hide()
|
||||
self.mExtendEdgeCutsCheckbox.Hide()
|
||||
self.mAutomaticTranslationCheckbox.Hide()
|
||||
self.mAutomaticFillCheckbox.Hide()
|
||||
self.mExcludeDnpCheckbox.Hide()
|
||||
self.mGenerateButton.Hide()
|
||||
self.mGaugeStatus.Show()
|
||||
|
||||
self.Fit()
|
||||
self.SetTitle('Fabrication Toolkit (Processing...)')
|
||||
|
||||
StatusEvent.invoke(self, self.updateDisplay)
|
||||
ProcessThread(self, options)
|
||||
|
||||
def updateDisplay(self, status):
|
||||
if status.data == -1:
|
||||
self.SetTitle('Fabrication Toolkit (Done!)')
|
||||
pcbnew.Refresh()
|
||||
self.Destroy()
|
||||
else:
|
||||
self.m_gaugeStatus.SetValue(status.data)
|
||||
self.mGaugeStatus.SetValue(int(status.data))
|
||||
|
||||
|
||||
# Plugin definition
|
||||
|
||||
+366
-81
@@ -1,11 +1,20 @@
|
||||
# from urllib import response
|
||||
# import json
|
||||
# import requests
|
||||
# For better annotation.
|
||||
from __future__ import annotations
|
||||
|
||||
# System base libraries
|
||||
import os
|
||||
import re
|
||||
import csv
|
||||
import math
|
||||
import shutil
|
||||
import pcbnew
|
||||
from collections import defaultdict
|
||||
from typing import Tuple
|
||||
|
||||
# Interaction with KiCad.
|
||||
import pcbnew # type: ignore
|
||||
from .utils import footprint_has_field, footprint_get_field, get_plot_plan
|
||||
|
||||
# Application definitions.
|
||||
from .config import *
|
||||
|
||||
|
||||
@@ -14,10 +23,30 @@ class ProcessManager:
|
||||
self.board = pcbnew.GetBoard()
|
||||
self.bom = []
|
||||
self.components = []
|
||||
self.__rotation_db = self.__read_rotation_db()
|
||||
|
||||
def generate_gerber(self, temp_dir):
|
||||
@staticmethod
|
||||
def normalize_filename(filename):
|
||||
return re.sub(r'[^\w\s\.\-]', '', filename)
|
||||
|
||||
def update_zone_fills(self):
|
||||
'''Verify all zones have up-to-date fills.'''
|
||||
filler = pcbnew.ZONE_FILLER(self.board)
|
||||
zones = self.board.Zones()
|
||||
|
||||
# Fill returns true/false if a refill was made
|
||||
# We cant use aCheck = True as that would require a rollback on the commit object if
|
||||
# user decided to not perform the zone fill and the commit object is not exposed to python API
|
||||
filler.Fill(zones, False)
|
||||
|
||||
# Finally rebuild the connectivity db
|
||||
self.board.BuildConnectivity()
|
||||
|
||||
def generate_gerber(self, temp_dir, extra_layers, extend_edge_cuts):
|
||||
'''Generate the Gerber files.'''
|
||||
settings = self.board.GetDesignSettings()
|
||||
settings.m_SolderMaskMargin = 0
|
||||
settings.m_SolderMaskMargin = 50000
|
||||
settings.m_SolderMaskToCopperClearance = 5000
|
||||
settings.m_SolderMaskMinWidth = 0
|
||||
|
||||
plot_controller = pcbnew.PLOT_CONTROLLER(self.board)
|
||||
@@ -30,44 +59,86 @@ class ProcessManager:
|
||||
plot_options.SetScale(1)
|
||||
plot_options.SetMirror(False)
|
||||
plot_options.SetUseGerberAttributes(True)
|
||||
plot_options.SetExcludeEdgeLayer(True)
|
||||
plot_options.SetUseGerberProtelExtensions(False)
|
||||
plot_options.SetUseGerberProtelExtensions(True)
|
||||
plot_options.SetUseAuxOrigin(True)
|
||||
plot_options.SetSubtractMaskFromSilk(False)
|
||||
plot_options.SetSubtractMaskFromSilk(True)
|
||||
plot_options.SetUseGerberX2format(False)
|
||||
plot_options.SetDrillMarksType(0) # NO_DRILL_SHAPE
|
||||
|
||||
if hasattr(plot_options, "SetExcludeEdgeLayer"):
|
||||
plot_options.SetExcludeEdgeLayer(True)
|
||||
|
||||
for layer_info in plotPlan:
|
||||
if self.board.IsLayerEnabled(layer_info[1]):
|
||||
if extra_layers is not None:
|
||||
extra_layers = [element.strip() for element in extra_layers.strip().split(',') if element.strip()]
|
||||
else:
|
||||
extra_layers = []
|
||||
|
||||
for layer_info in get_plot_plan(self.board):
|
||||
if self.board.IsLayerEnabled(layer_info[1]) or layer_info[0] in extra_layers:
|
||||
plot_controller.SetLayer(layer_info[1])
|
||||
plot_controller.OpenPlotfile(
|
||||
layer_info[0],
|
||||
pcbnew.PLOT_FORMAT_GERBER,
|
||||
layer_info[2])
|
||||
plot_controller.PlotLayer()
|
||||
plot_controller.OpenPlotfile(layer_info[0], pcbnew.PLOT_FORMAT_GERBER, layer_info[2])
|
||||
|
||||
if layer_info[1] == pcbnew.Edge_Cuts and hasattr(plot_controller, 'PlotLayers') and extend_edge_cuts:
|
||||
# includes User_1 layer with Edge_Cuts layer to allow V Cuts to be defined as User_1 layer
|
||||
# available for KiCad 7.0.1+
|
||||
seq = pcbnew.LSEQ()
|
||||
seq.push_back(layer_info[1])
|
||||
seq.push_back(pcbnew.User_1)
|
||||
plot_controller.PlotLayers(seq)
|
||||
else:
|
||||
plot_controller.PlotLayer()
|
||||
|
||||
plot_controller.ClosePlot()
|
||||
|
||||
def generate_drills(self, temp_dir):
|
||||
'''Generate the drill file.'''
|
||||
drill_writer = pcbnew.EXCELLON_WRITER(self.board)
|
||||
|
||||
drill_writer.SetOptions(
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
self.board.GetDesignSettings().GetAuxOrigin(),
|
||||
False)
|
||||
drill_writer.SetFormat(False)
|
||||
drill_writer.CreateDrillandMapFilesSet(temp_dir, True, False)
|
||||
drill_writer.SetFormat(True)
|
||||
drill_writer.SetMapFileFormat(pcbnew.PLOT_FORMAT_GERBER)
|
||||
drill_writer.CreateDrillandMapFilesSet(temp_dir, True, True)
|
||||
|
||||
def generate_netlist(self, temp_dir):
|
||||
'''Generate the connection netlist.'''
|
||||
netlist_writer = pcbnew.IPC356D_WRITER(self.board)
|
||||
netlist_writer.Write(os.path.join(temp_dir, netlistFileName))
|
||||
|
||||
def generate_positions(self, temp_dir):
|
||||
def _get_footprint_position(self, footprint):
|
||||
"""Calculate position based on center of bounding box."""
|
||||
position = footprint.GetPosition()
|
||||
attributes = footprint.GetAttributes()
|
||||
|
||||
if attributes & pcbnew.FP_SMD:
|
||||
position = footprint.GetPosition()
|
||||
elif attributes & pcbnew.FP_THROUGH_HOLE:
|
||||
position = footprint.GetBoundingBox(False, False).GetCenter()
|
||||
else: # handle Unspecified footprint type
|
||||
pads = footprint.Pads()
|
||||
if len(pads) > 0:
|
||||
# get bounding box based on pads only to ignore non-copper layers, e.g. silkscreen
|
||||
bbox = pads[0].GetBoundingBox() # start with small bounding box
|
||||
for pad in pads:
|
||||
bbox.Merge(pad.GetBoundingBox()) # expand bounding box
|
||||
|
||||
position = bbox.GetCenter()
|
||||
|
||||
return position
|
||||
|
||||
def generate_tables(self, temp_dir, auto_translate, exclude_dnp):
|
||||
'''Generate the data tables.'''
|
||||
if hasattr(self.board, 'GetModules'):
|
||||
footprints = list(self.board.GetModules())
|
||||
else:
|
||||
footprints = list(self.board.GetFootprints())
|
||||
|
||||
# sort footprint after designator
|
||||
footprints.sort(key=lambda x: x.GetReference())
|
||||
|
||||
# unique designator dictionary
|
||||
footprint_designators = defaultdict(int)
|
||||
for i, footprint in enumerate(footprints):
|
||||
@@ -75,9 +146,10 @@ class ProcessManager:
|
||||
footprint_designators[footprint.GetReference()] += 1
|
||||
bom_designators = footprint_designators.copy()
|
||||
|
||||
with open((os.path.join(temp_dir, designatorsFileName)), 'w', encoding='utf-8') as f:
|
||||
for key, value in footprint_designators.items():
|
||||
f.write('%s:%s\n' % (key, value))
|
||||
if len(footprint_designators.items()) > 0:
|
||||
with open((os.path.join(temp_dir, designatorsFileName)), 'w', encoding='utf-8-sig') as f:
|
||||
for key, value in footprint_designators.items():
|
||||
f.write('%s:%s\n' % (key, value))
|
||||
|
||||
for i, footprint in enumerate(footprints):
|
||||
try:
|
||||
@@ -85,81 +157,124 @@ class ProcessManager:
|
||||
except AttributeError:
|
||||
footprint_name = str(footprint.GetFPID().GetLibItemName())
|
||||
|
||||
layer = {
|
||||
pcbnew.F_Cu: 'top',
|
||||
pcbnew.B_Cu: 'bottom',
|
||||
}.get(footprint.GetLayer())
|
||||
layer = self._get_top_or_bottom_side_override_from_footprint(footprint)
|
||||
|
||||
# mount_type = {
|
||||
# 0: 'smt',
|
||||
# 1: 'tht',
|
||||
# 2: 'smt'
|
||||
# 2: 'unspecified'
|
||||
# }.get(footprint.GetAttributes())
|
||||
|
||||
if not footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_POS_FILES:
|
||||
is_dnp = (footprint_has_field(footprint, 'dnp')
|
||||
or (footprint.GetValue().upper() == 'DNP')
|
||||
or getattr(footprint, 'IsDNP', bool)())
|
||||
skip_dnp = exclude_dnp and is_dnp
|
||||
skip_footprint = footprint.GetPadCount() == 0
|
||||
|
||||
if not (footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_POS_FILES) and not skip_footprint and not is_dnp:
|
||||
# append unique ID if duplicate footprint designator
|
||||
unique_id = ""
|
||||
if footprint_designators[footprint.GetReference()] > 1:
|
||||
unique_id = str(footprint_designators[footprint.GetReference()])
|
||||
footprint_designators[footprint.GetReference()] -= 1
|
||||
|
||||
designator = "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id)
|
||||
position = self._get_footprint_position(footprint)
|
||||
mid_x = (position[0] - self.board.GetDesignSettings().GetAuxOrigin()[0]) / 1000000.0
|
||||
mid_y = (position[1] - self.board.GetDesignSettings().GetAuxOrigin()[1]) * -1.0 / 1000000.0
|
||||
rotation = footprint.GetOrientation().AsDegrees() if hasattr(footprint.GetOrientation(), 'AsDegrees') else footprint.GetOrientation() / 10.0
|
||||
rotation_offset_db = self._get_rotation_from_db(footprint_name) # internal database offset
|
||||
rotation_offset_manual = self._get_rotation_offset_from_footprint(footprint) # explicated offset by the designer
|
||||
|
||||
# position offset needs to take rotation into account
|
||||
pos_offset = self._get_position_offset_from_footprint(footprint)
|
||||
rsin = math.sin(rotation / 180 * math.pi)
|
||||
rcos = math.cos(rotation / 180 * math.pi)
|
||||
|
||||
if layer == 'bottom':
|
||||
pos_offset = ( pos_offset[0] * rcos + pos_offset[1] * rsin, pos_offset[0] * rsin - pos_offset[1] * rcos )
|
||||
else:
|
||||
pos_offset = ( pos_offset[0] * rcos - pos_offset[1] * rsin, pos_offset[0] * rsin + pos_offset[1] * rcos )
|
||||
mid_x, mid_y = tuple(map(sum,zip((mid_x, mid_y), pos_offset)))
|
||||
|
||||
# JLC expect 'Rotation' to be 'as viewed from above component', so bottom needs inverting, and ends up 180 degrees out as well
|
||||
if layer == 'bottom':
|
||||
rotation = (180.0 - rotation)
|
||||
|
||||
if auto_translate:
|
||||
rotation += rotation_offset_db
|
||||
|
||||
rotation = (rotation + rotation_offset_manual) % 360.0
|
||||
|
||||
self.components.append({
|
||||
'Designator': "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id),
|
||||
'Mid X': (footprint.GetPosition()[0] - self.board.GetDesignSettings().GetAuxOrigin()[0]) / 1000000.0,
|
||||
'Mid Y': (footprint.GetPosition()[1] - self.board.GetDesignSettings().GetAuxOrigin()[1]) * -1.0 / 1000000.0,
|
||||
'Rotation': footprint.GetOrientation() / 10.0,
|
||||
'Designator': designator,
|
||||
'Mid X': mid_x,
|
||||
'Mid Y': mid_y,
|
||||
'Rotation': rotation,
|
||||
'Layer': layer,
|
||||
})
|
||||
|
||||
if not footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM:
|
||||
if not (footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM) and not skip_footprint and not skip_dnp:
|
||||
# append unique ID if we are dealing with duplicate bom designator
|
||||
unique_id = ""
|
||||
if bom_designators[footprint.GetReference()] > 1:
|
||||
unique_id = str(bom_designators[footprint.GetReference()])
|
||||
bom_designators[footprint.GetReference()] -= 1
|
||||
|
||||
# todo: merge similar parts into single entry
|
||||
# merge similar parts into single entry
|
||||
insert = True
|
||||
for component in self.bom:
|
||||
same_footprint = component['Footprint'] == self._normalize_footprint_name(footprint_name)
|
||||
same_value = component['Value'].upper() == footprint.GetValue().upper()
|
||||
same_lcsc = component['LCSC Part #'] == self._get_mpn_from_footprint(footprint)
|
||||
under_limit = component['Quantity'] < bomRowLimit
|
||||
|
||||
self.bom.append({
|
||||
'Designator': "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id),
|
||||
'Footprint': footprint_name,
|
||||
'Quantity': 1,
|
||||
'Value': footprint.GetValue(),
|
||||
# 'Mount': mount_type,
|
||||
'LCSC Part #': self._getMpnFromFootprint(footprint),
|
||||
})
|
||||
if same_footprint and same_value and same_lcsc and under_limit:
|
||||
component['Designator'] += ", " + "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id)
|
||||
component['Quantity'] += 1
|
||||
insert = False
|
||||
break
|
||||
|
||||
with open((os.path.join(temp_dir, placementFileName)), 'w', newline='', encoding='utf-8') as outfile:
|
||||
header = True
|
||||
csv_writer = csv.writer(outfile)
|
||||
# add component to BOM
|
||||
if insert:
|
||||
self.bom.append({
|
||||
'Designator': "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id),
|
||||
'Footprint': self._normalize_footprint_name(footprint_name),
|
||||
'Quantity': 1,
|
||||
'Value': footprint.GetValue(),
|
||||
# 'Mount': mount_type,
|
||||
'LCSC Part #': self._get_mpn_from_footprint(footprint),
|
||||
})
|
||||
|
||||
for component in self.components:
|
||||
if header:
|
||||
# writing headers of CSV file
|
||||
csv_writer.writerow(component.keys())
|
||||
header = False
|
||||
def generate_positions(self, temp_dir):
|
||||
'''Generate the position file.'''
|
||||
if len(self.components) > 0:
|
||||
with open((os.path.join(temp_dir, placementFileName)), 'w', newline='', encoding='utf-8-sig') as outfile:
|
||||
csv_writer = csv.writer(outfile)
|
||||
# writing headers of CSV file
|
||||
csv_writer.writerow(self.components[0].keys())
|
||||
|
||||
# writing data of CSV file
|
||||
if ('**' not in component['Designator']):
|
||||
csv_writer.writerow(component.values())
|
||||
for component in self.components:
|
||||
# writing data of CSV file
|
||||
if ('**' not in component['Designator']):
|
||||
csv_writer.writerow(component.values())
|
||||
|
||||
def generate_bom(self, temp_dir):
|
||||
'''Generate the bom file.'''
|
||||
if len(self.bom) > 0:
|
||||
with open((os.path.join(temp_dir, bomFileName)), 'w', newline='', encoding='utf-8-sig') as outfile:
|
||||
csv_writer = csv.writer(outfile)
|
||||
# writing headers of CSV file
|
||||
csv_writer.writerow(self.bom[0].keys())
|
||||
|
||||
with open((os.path.join(temp_dir, bomFileName)), 'w', newline='', encoding='utf-8') as outfile:
|
||||
header = True
|
||||
csv_writer = csv.writer(outfile)
|
||||
|
||||
for component in self.bom:
|
||||
if header:
|
||||
# writing headers of CSV file
|
||||
csv_writer.writerow(component.keys())
|
||||
header = False
|
||||
|
||||
# writing data of CSV file
|
||||
if ('**' not in component['Designator']):
|
||||
csv_writer.writerow(component.values())
|
||||
# Output all of the component information
|
||||
for component in self.bom:
|
||||
# writing data of CSV file
|
||||
if ('**' not in component['Designator']):
|
||||
csv_writer.writerow(component.values())
|
||||
|
||||
def generate_archive(self, temp_dir, temp_file):
|
||||
'''Generate the archive file.'''
|
||||
temp_file = shutil.make_archive(temp_file, 'zip', temp_dir)
|
||||
temp_file = shutil.move(temp_file, temp_dir)
|
||||
|
||||
@@ -169,21 +284,191 @@ class ProcessManager:
|
||||
os.remove(os.path.join(temp_dir, item))
|
||||
|
||||
return temp_file
|
||||
|
||||
""" Private """
|
||||
|
||||
def upload_archive(self, temp_file):
|
||||
boardWidth = pcbnew.Iu2Millimeter(self.board.GetBoardEdgesBoundingBox().GetWidth())
|
||||
boardHeight = pcbnew.Iu2Millimeter(self.board.GetBoardEdgesBoundingBox().GetHeight())
|
||||
boardLayer = self.board.GetCopperLayerCount()
|
||||
def __read_rotation_db(self, filename: str = os.path.join(os.path.dirname(__file__), 'transformations.csv')) -> dict[str, float]:
|
||||
'''Read the rotations.cf config file so we know what rotations
|
||||
to apply later.
|
||||
'''
|
||||
db = {}
|
||||
|
||||
files = { 'upload[file]': open(temp_file, 'rb') }
|
||||
upload_url = baseUrl + '/Common/KiCadUpFile/'
|
||||
with open(filename, newline='') as csvfile:
|
||||
csvDialect = csv.Sniffer().sniff(csvfile.read(1024))
|
||||
csvfile.seek(0)
|
||||
csvData = csv.DictReader(csvfile, fieldnames=["footprint", "rotation", "x", "y"],
|
||||
restkey="extra", restval="0", dialect=csvDialect)
|
||||
|
||||
# response = requests.post(
|
||||
# upload_url, files=files, data={ 'boardWidth': boardWidth, 'boardHeight': boardHeight, 'boardLayer': boardLayer })
|
||||
# urls = json.loads(response.content)
|
||||
rowNum = 0
|
||||
for row in csvData:
|
||||
rowNum = rowNum + 1
|
||||
# First row is header row, skip.
|
||||
if rowNum == 1:
|
||||
skipFirst = False
|
||||
continue
|
||||
|
||||
def _getMpnFromFootprint(self, footprint):
|
||||
keys = ['mpn', 'Mpn', 'MPN', 'JLC_MPN', 'LCSC_MPN', 'LCSC Part #', 'JLC', 'LCSC']
|
||||
for key in keys:
|
||||
if footprint.HasProperty(key):
|
||||
return footprint.GetProperty(key)
|
||||
# If there was too many fields, throw an exception.
|
||||
if len(row) > 4:
|
||||
raise RuntimeError("{}: Too many fields found in row {}: {}".format(filename, rowNum, row))
|
||||
|
||||
# See if the values we expect to be floating point numbers
|
||||
# can be converted to floating point, if not throw an exception.
|
||||
if row['rotation'] == "":
|
||||
rotation = 0.0
|
||||
else:
|
||||
try:
|
||||
rotation = float(row['rotation'])
|
||||
except ValueError:
|
||||
raise RuntimeError("{}: Non-numeric rotation value found in row {}".format(filename, rowNum))
|
||||
|
||||
if row['x'] == "":
|
||||
delta_x = 0.0
|
||||
else:
|
||||
try:
|
||||
delta_x = float(row['x'])
|
||||
except ValueError:
|
||||
raise RuntimeError("{}: Non-numeric translation value found in row {}".format(filename, rowNum))
|
||||
|
||||
if row['y'] == "":
|
||||
delta_y = 0.0
|
||||
else:
|
||||
try:
|
||||
delta_y = float(row['y'])
|
||||
except ValueError:
|
||||
raise RuntimeError("{}: Non-numeric translation value found in row {}".format(filename, rowNum))
|
||||
|
||||
# Add the entry to the database in the format we expect.
|
||||
db[rowNum] = {}
|
||||
db[rowNum]['name'] = row['footprint']
|
||||
db[rowNum]['rotation'] = rotation
|
||||
db[rowNum]['x'] = delta_x
|
||||
db[rowNum]['y'] = delta_y
|
||||
|
||||
return db
|
||||
|
||||
def _get_rotation_from_db(self, footprint: str) -> float:
|
||||
'''Get the rotation to be added from the database file.'''
|
||||
# Look for regular expression math of the footprint name and not its root library.
|
||||
|
||||
for entry in self.__rotation_db.items():
|
||||
# If the expression in the DB contains a :, search for it literally.
|
||||
if (re.search(':', entry[1]['name'])):
|
||||
if (re.search(entry[1]['name'], footprint)):
|
||||
return float(entry[1]['rotation'])
|
||||
# There is no : in the expression, so only search the right side of the :
|
||||
else:
|
||||
footprint_segments = footprint.split(':')
|
||||
# Only one means there was no :, just check the short.
|
||||
if (len(footprint_segments) == 1):
|
||||
check = footprint_segments[0]
|
||||
# More means there was a :, check the right side.
|
||||
else:
|
||||
check = footprint_segments[1]
|
||||
if (re.search(entry[1]['name'], check)):
|
||||
return float(entry[1]['rotation'])
|
||||
|
||||
# Not found, no rotation.
|
||||
return 0.0
|
||||
|
||||
def _get_position_offset_from_db(self, footprint: str) -> Tuple[float, float]:
|
||||
'''Get the rotation to be added from the database file.'''
|
||||
# Look for regular expression math of the footprint name and not its root library.
|
||||
|
||||
for entry in self.__rotation_db.items():
|
||||
# If the expression in the DB contains a :, search for it literally.
|
||||
if (re.search(':', entry[1]['name'])):
|
||||
if (re.search(entry[1]['name'], footprint)):
|
||||
return ( float(entry[1]['x']), float(entry[1]['y']) )
|
||||
# There is no : in the expression, so only search the right side of the :
|
||||
else:
|
||||
footprint_segments = footprint.split(':')
|
||||
# Only one means there was no :, just check the short.
|
||||
if (len(footprint_segments) == 1):
|
||||
check = footprint_segments[0]
|
||||
# More means there was a :, check the right side.
|
||||
else:
|
||||
check = footprint_segments[1]
|
||||
if (re.search(entry[1]['name'], check)):
|
||||
return ( float(entry[1]['x']), float(entry[1]['y']) )
|
||||
|
||||
# Not found, no delta.
|
||||
return (0.0, 0.0)
|
||||
|
||||
def _get_mpn_from_footprint(self, footprint) -> str:
|
||||
''''Get the MPN/LCSC stock code from standard symbol fields.'''
|
||||
keys = ['LCSC Part #', 'JLCPCB Part #']
|
||||
fallback_keys = ['LCSC Part', 'JLC Part', 'LCSC', 'JLC', 'MPN', 'Mpn', 'mpn']
|
||||
|
||||
if footprint_has_field(footprint, 'dnp'):
|
||||
return 'DNP'
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint_has_field(footprint, key):
|
||||
return footprint_get_field(footprint, key)
|
||||
|
||||
def _get_top_or_bottom_side_override_from_footprint(self, footprint) -> str:
|
||||
keys = ['JLCPCB Layer Override']
|
||||
fallback_keys = ['JlcLayerOverride', 'JLCLayerOverride']
|
||||
|
||||
layer = {
|
||||
pcbnew.F_Cu: 'top',
|
||||
pcbnew.B_Cu: 'bottom',
|
||||
}.get(footprint.GetLayer())
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint_has_field(footprint, key):
|
||||
temp_layer = footprint_get_field(footprint, key)
|
||||
if (temp_layer[0] == 'b' or temp_layer[0] == 'B'):
|
||||
layer = "bottom"
|
||||
break
|
||||
elif (temp_layer[0] == 't' or temp_layer[0] == 'T'):
|
||||
layer = "top"
|
||||
break
|
||||
|
||||
return layer
|
||||
|
||||
def _get_rotation_offset_from_footprint(self, footprint) -> float:
|
||||
'''Get the rotation from standard symbol fields.'''
|
||||
keys = ['JLCPCB Rotation Offset']
|
||||
fallback_keys = ['JlcRotOffset', 'JLCRotOffset']
|
||||
|
||||
offset = ""
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint_has_field(footprint, key):
|
||||
offset = footprint_get_field(footprint, key)
|
||||
break
|
||||
|
||||
if offset is None or offset == "":
|
||||
return 0.0
|
||||
else:
|
||||
try:
|
||||
return float(offset)
|
||||
except ValueError:
|
||||
raise RuntimeError("Rotation offset of {} is not a valid number".format(footprint.GetReference()))
|
||||
|
||||
def _get_position_offset_from_footprint(self, footprint) -> Tuple[float, float]:
|
||||
keys = ['JLCPCB Position Offset']
|
||||
fallback_keys = ['JlcPosOffset', 'JLCPosOffset']
|
||||
|
||||
offset = ""
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint_has_field(footprint, key):
|
||||
offset = footprint_get_field(footprint, key)
|
||||
break
|
||||
|
||||
if offset == "":
|
||||
return (0.0, 0.0)
|
||||
else:
|
||||
try:
|
||||
offset = offset.split(",")
|
||||
return (float(offset[0]), float(offset[1]))
|
||||
except Exception as e:
|
||||
raise RuntimeError("Position offset of {} is not a valid pair of numbers".format(footprint.GetReference()))
|
||||
|
||||
def _normalize_footprint_name(self, footprint) -> str:
|
||||
# replace footprint names of resistors, capacitors, inductors, diodes, LEDs, fuses etc, with the footprint size only
|
||||
pattern = re.compile(r'^(\w*_SMD:)?\w{1,4}_(\d+)_\d+Metric.*$')
|
||||
|
||||
return pattern.sub(r'\2', footprint)
|
||||
|
||||
+78
-33
@@ -1,58 +1,77 @@
|
||||
import os
|
||||
import wx
|
||||
import pcbnew # type: ignore
|
||||
import shutil
|
||||
import tempfile
|
||||
import webbrowser
|
||||
import datetime
|
||||
from threading import Thread
|
||||
from .events import StatusEvent
|
||||
from .process import ProcessManager
|
||||
from .config import *
|
||||
from .options import *
|
||||
|
||||
|
||||
class ProcessThread(Thread):
|
||||
def __init__(self, wx):
|
||||
def __init__(self, wx, options):
|
||||
Thread.__init__(self)
|
||||
|
||||
self.process_manager = ProcessManager()
|
||||
self.wx = wx
|
||||
self.options = options
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
# initializing
|
||||
self.report(0)
|
||||
self.progress(0)
|
||||
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_dir_gerber = temp_dir + "_g"
|
||||
os.makedirs(temp_dir_gerber)
|
||||
|
||||
_, temp_file = tempfile.mkstemp()
|
||||
project_directory = os.path.dirname(self.process_manager.board.GetFileName())
|
||||
output_path = os.path.join(project_directory, outputFolder)
|
||||
|
||||
# configure and generate gerber
|
||||
self.report(5)
|
||||
self.process_manager.generate_gerber(temp_dir)
|
||||
try:
|
||||
# Verify all zones are up-to-date
|
||||
self.progress(10)
|
||||
if (self.options[AUTO_FILL_OPT]):
|
||||
self.process_manager.update_zone_fills()
|
||||
|
||||
# generate drill file
|
||||
self.report(15)
|
||||
self.process_manager.generate_drills(temp_dir)
|
||||
|
||||
# generate netlist
|
||||
self.report(25)
|
||||
self.process_manager.generate_netlist(temp_dir)
|
||||
# generate gerber
|
||||
self.progress(20)
|
||||
self.process_manager.generate_gerber(temp_dir_gerber, self.options[EXTRA_LAYERS], self.options[EXTEND_EDGE_CUT_OPT])
|
||||
|
||||
# generate pick and place file
|
||||
self.report(40)
|
||||
self.process_manager.generate_positions(temp_dir)
|
||||
# generate drill file
|
||||
self.progress(30)
|
||||
self.process_manager.generate_drills(temp_dir_gerber)
|
||||
|
||||
# generate BOM file
|
||||
self.report(60)
|
||||
self.process_manager.generate_bom(temp_dir)
|
||||
# generate netlist
|
||||
self.progress(40)
|
||||
self.process_manager.generate_netlist(temp_dir)
|
||||
|
||||
# generate production archive
|
||||
self.report(75)
|
||||
temp_file = self.process_manager.generate_archive(temp_dir, temp_file)
|
||||
# generate data tables
|
||||
self.progress(50)
|
||||
self.process_manager.generate_tables(temp_dir, self.options[AUTO_TRANSLATE_OPT], self.options[EXCLUDE_DNP_OPT])
|
||||
|
||||
# upload files
|
||||
#self.report(87.5)
|
||||
#self.process_manager.upload_archive(temp_file)
|
||||
# generate pick and place file
|
||||
self.progress(60)
|
||||
self.process_manager.generate_positions(temp_dir)
|
||||
|
||||
# generate BOM file
|
||||
self.progress(70)
|
||||
self.process_manager.generate_bom(temp_dir)
|
||||
|
||||
# generate production archive
|
||||
self.progress(85)
|
||||
temp_file = self.process_manager.generate_archive(temp_dir_gerber, temp_file)
|
||||
shutil.move(temp_file, temp_dir)
|
||||
shutil.rmtree(temp_dir_gerber)
|
||||
temp_file = os.path.join(temp_dir, os.path.basename(temp_file))
|
||||
except Exception as e:
|
||||
wx.MessageBox(str(e), "Fabrication Toolkit - Error", wx.OK | wx.ICON_ERROR)
|
||||
self.progress(-1)
|
||||
return
|
||||
|
||||
# progress bar done animation
|
||||
read_so_far = 0
|
||||
@@ -64,20 +83,46 @@ class ProcessThread(Thread):
|
||||
break
|
||||
read_so_far += len(data)
|
||||
percent = read_so_far * 1e2 / total_size
|
||||
self.report(75 + percent / 8)
|
||||
self.progress(85 + percent / 8)
|
||||
|
||||
# generate gerber name
|
||||
title_block = self.process_manager.board.GetTitleBlock()
|
||||
title = title_block.GetTitle()
|
||||
revision = title_block.GetRevision()
|
||||
company = title_block.GetCompany()
|
||||
file_date = title_block.GetDate()
|
||||
|
||||
if (hasattr(self.process_manager.board, "GetProject") and hasattr(pcbnew, "ExpandTextVars")):
|
||||
project = self.process_manager.board.GetProject()
|
||||
title = pcbnew.ExpandTextVars(title, project)
|
||||
revision = pcbnew.ExpandTextVars(revision, project)
|
||||
company = pcbnew.ExpandTextVars(company, project)
|
||||
file_date = pcbnew.ExpandTextVars(file_date, project)
|
||||
|
||||
# make output dir
|
||||
filename = os.path.splitext(os.path.basename(self.process_manager.board.GetFileName()))[0]
|
||||
output_path = os.path.join(project_directory, outputFolder)
|
||||
if not os.path.exists(output_path):
|
||||
os.makedirs(output_path)
|
||||
|
||||
# rename gerber archive
|
||||
gerberArchiveName = ProcessManager.normalize_filename("_".join(("{} {}".format(title or filename, revision or '').strip() + '.zip').split()))
|
||||
os.rename(temp_file, os.path.join(temp_dir, gerberArchiveName))
|
||||
|
||||
try:
|
||||
if os.path.exists(output_path):
|
||||
shutil.rmtree(output_path)
|
||||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')
|
||||
backup_name = ProcessManager.normalize_filename("_".join(("{} {} {}".format(title or filename, revision or '', timestamp).strip()).split()))
|
||||
shutil.make_archive(os.path.join(output_path, 'backups', backup_name), 'zip', temp_dir)
|
||||
|
||||
shutil.copytree(temp_dir, output_path)
|
||||
|
||||
# copy to & open output dir
|
||||
try:
|
||||
shutil.copytree(temp_dir, output_path, dirs_exist_ok=True)
|
||||
webbrowser.open("file://%s" % (output_path))
|
||||
shutil.rmtree(temp_dir)
|
||||
except Exception as e:
|
||||
webbrowser.open("file://%s" % (temp_dir))
|
||||
|
||||
self.report(-1)
|
||||
self.progress(-1)
|
||||
|
||||
def report(self, status):
|
||||
wx.PostEvent(self.wx, StatusEvent(status))
|
||||
def progress(self, percent):
|
||||
wx.PostEvent(self.wx, StatusEvent(percent))
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"Regex To Match","Rotation","Delta X","Delta Y"
|
||||
"^Bosch_LGA-",90,0,0
|
||||
"^CP_EIA-",180,0,0
|
||||
"^CP_Elec_",180,0,0
|
||||
"^C_Elec_",180,0,0
|
||||
"^DFN-",270,0,0
|
||||
"^DFN-",270,0,0
|
||||
"^D_SOT-23",180,0,0
|
||||
"^HTSSOP-",270,0,0
|
||||
"^HTSSOP-",270,0,0
|
||||
"^HTSSOP-",270,0,0
|
||||
"^JST_GH_SM",180,0,0
|
||||
"^JST_PH_S",180,0,0
|
||||
"^LQFP-",270,0,0
|
||||
"^MSOP-",270,0,0
|
||||
"^PowerPAK_SO-8_Single",270,0,0
|
||||
"^QFN-",90,0,0
|
||||
"^R_Array_Concave_",90,0,0
|
||||
"^R_Array_Convex_",90,0,0
|
||||
"^SC-74-6",180,0,0
|
||||
"^SOIC-",270,0,0
|
||||
"^SOIC-16_",270,0,0
|
||||
"^SOIC-8_",270,0,0
|
||||
"^SOIC127P798X216-8N",-90,0,0
|
||||
"^SOP-(?!18_)",270,0,0
|
||||
"^SOP-(?!18_)",270,0,0
|
||||
"^SOP-18_",0,0,0
|
||||
"^SOP-18_",0,0,0
|
||||
"^SOP-4_",0,0,0
|
||||
"^SOP-4_",0,0,0
|
||||
"^SOT-143",180,0,0
|
||||
"^SOT-223",180,0,0
|
||||
"^SOT-23",180,0,0
|
||||
"^SOT-353",180,0,0
|
||||
"^SOT-363",180,0,0
|
||||
"^SOT-89",180,0,0
|
||||
"^SSOP-",270,0,0
|
||||
"^SW_SPST_B3",90,0,0
|
||||
"^TDSON-8-1",270,0,0
|
||||
"^TO-277",90,0,0
|
||||
"^TQFP-",270,0,0
|
||||
"^TSOT-23",180,0,0
|
||||
"^TSSOP-",270,0,0
|
||||
"^UDFN-10",270,0,0
|
||||
"^USON-10",270,0,0
|
||||
"^VSON-8_",270,0,0
|
||||
"^VSSOP-10_-",270,0,0
|
||||
"^VSSOP-10_-",270,0,0
|
||||
"^VSSOP-8_",180,0,0
|
||||
"^VSSOP-8_",270,0,0
|
||||
"^VSSOP-8_3.0x3.0mm_P0.65mm",270,0,0
|
||||
"^qfn-",90,0,0
|
||||
|
@@ -0,0 +1,78 @@
|
||||
import pcbnew # type: ignore
|
||||
import os
|
||||
import json
|
||||
from .config import optionsFileName
|
||||
import wx
|
||||
|
||||
def get_version():
|
||||
return float('.'.join(pcbnew.GetBuildVersion().split(".")[0:2])) # e.g GetBuildVersion(): e.g. '7.99.0-3969-gc5ac2337e4'
|
||||
|
||||
def is_v9(version = get_version()):
|
||||
return version >= 8.99 and version < 9.99
|
||||
|
||||
def is_v8(version = get_version()):
|
||||
return version >= 7.99 and version < 8.99
|
||||
|
||||
def is_v7(version = get_version()):
|
||||
return version >= 6.99 and version < 7.99
|
||||
|
||||
def is_v6(version = get_version()):
|
||||
return version >= 5.99 and version < 6.99
|
||||
|
||||
def footprint_has_field(footprint, field_name):
|
||||
version = get_version()
|
||||
|
||||
if is_v8(version) or is_v9(version):
|
||||
return footprint.HasFieldByName(field_name)
|
||||
else:
|
||||
return footprint.HasProperty(field_name)
|
||||
|
||||
def footprint_get_field(footprint, field_name):
|
||||
version = get_version()
|
||||
|
||||
if is_v8(version) or is_v9(version):
|
||||
return footprint.GetFieldByName(field_name).GetText()
|
||||
else:
|
||||
return footprint.GetProperty(field_name)
|
||||
|
||||
def get_user_options_file_path():
|
||||
boardFilePath = pcbnew.GetBoard().GetFileName()
|
||||
return os.path.join(os.path.dirname(boardFilePath), optionsFileName)
|
||||
|
||||
def load_user_options(default_options):
|
||||
try:
|
||||
with open(get_user_options_file_path(), 'r') as f:
|
||||
user_options = json.load(f)
|
||||
except:
|
||||
user_options = default_options
|
||||
|
||||
# merge the user options with the default options
|
||||
options = default_options.copy()
|
||||
options.update(user_options)
|
||||
return options
|
||||
|
||||
def save_user_options(options):
|
||||
try:
|
||||
with open(get_user_options_file_path(), 'w') as f:
|
||||
json.dump(options, f)
|
||||
except:
|
||||
wx.MessageBox("Error saving user options", "Error", wx.OK | wx.ICON_ERROR)
|
||||
|
||||
def get_plot_plan(board, active_only=True):
|
||||
"""Returns `(KiCad standard name, layer id, custom user name)` of all (active) layers of the given board."""
|
||||
layers = []
|
||||
i = pcbnew.PCBNEW_LAYER_ID_START - 1
|
||||
while i < pcbnew.PCBNEW_LAYER_ID_START + pcbnew.PCB_LAYER_ID_COUNT - 1:
|
||||
i += 1
|
||||
if active_only and not board.IsLayerEnabled(i):
|
||||
continue
|
||||
|
||||
layer_std_name = pcbnew.BOARD.GetStandardLayerName(i)
|
||||
layer_name = pcbnew.BOARD.GetLayerName(board, i)
|
||||
|
||||
layers.append((layer_std_name, i, layer_name))
|
||||
return layers
|
||||
def get_layer_names(board, active_only=True):
|
||||
"""Returns a list of (active) layer names of the current board"""
|
||||
plotPlan = get_plot_plan(board, active_only)
|
||||
return [layer_info[0] for layer_info in plotPlan]
|
||||
Reference in New Issue
Block a user