Compare commits
101 Commits
feature/upload
...
3.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 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']
|
||||
@@ -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,96 @@
|
||||
## 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 feature.
|
||||
|
||||
⊛ 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.
|
||||
### ① 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 an counter-clockwise orientation offset in degrees to correct this.
|
||||
|
||||
<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 seperated x,y position offset to correct it.
|
||||
|
||||
<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._
|
||||
|
||||
## 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 |
+4
-4
@@ -2,7 +2,7 @@
|
||||
"$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": {
|
||||
@@ -29,9 +29,9 @@
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"version": "0.4.2",
|
||||
"status": "testing",
|
||||
"version": "3.0.2",
|
||||
"status": "stable",
|
||||
"kicad_version": "6.00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+29
-2
@@ -6,7 +6,7 @@ netlistFileName = 'netlist.ipc'
|
||||
designatorsFileName = 'designators.csv'
|
||||
placementFileName = 'positions.csv'
|
||||
bomFileName = 'bom.csv'
|
||||
gerberArchiveName = 'gerber.zip'
|
||||
gerberArchiveName = 'gerbers.zip'
|
||||
outputFolder = 'production'
|
||||
|
||||
plotPlan = [
|
||||
@@ -16,11 +16,38 @@ plotPlan = [
|
||||
("In2.Cu", pcbnew.In2_Cu, "Internal plane 2"),
|
||||
("In3.Cu", pcbnew.In3_Cu, "Internal plane 3"),
|
||||
("In4.Cu", pcbnew.In4_Cu, "Internal plane 4"),
|
||||
("In5.Cu", pcbnew.In5_Cu, "Internal plane 5"),
|
||||
("In6.Cu", pcbnew.In6_Cu, "Internal plane 6"),
|
||||
("In7.Cu", pcbnew.In7_Cu, "Internal plane 7"),
|
||||
("In8.Cu", pcbnew.In8_Cu, "Internal plane 8"),
|
||||
("In9.Cu", pcbnew.In9_Cu, "Internal plane 9"),
|
||||
("In10.Cu", pcbnew.In10_Cu, "Internal plane 10"),
|
||||
("In11.Cu", pcbnew.In11_Cu, "Internal plane 11"),
|
||||
("In12.Cu", pcbnew.In12_Cu, "Internal plane 12"),
|
||||
("In13.Cu", pcbnew.In13_Cu, "Internal plane 13"),
|
||||
("In14.Cu", pcbnew.In14_Cu, "Internal plane 14"),
|
||||
("In15.Cu", pcbnew.In15_Cu, "Internal plane 15"),
|
||||
("In16.Cu", pcbnew.In16_Cu, "Internal plane 16"),
|
||||
("In17.Cu", pcbnew.In17_Cu, "Internal plane 17"),
|
||||
("In18.Cu", pcbnew.In18_Cu, "Internal plane 18"),
|
||||
("In19.Cu", pcbnew.In19_Cu, "Internal plane 19"),
|
||||
("In20.Cu", pcbnew.In20_Cu, "Internal plane 20"),
|
||||
("In21.Cu", pcbnew.In21_Cu, "Internal plane 21"),
|
||||
("In22.Cu", pcbnew.In22_Cu, "Internal plane 22"),
|
||||
("In23.Cu", pcbnew.In23_Cu, "Internal plane 23"),
|
||||
("In24.Cu", pcbnew.In24_Cu, "Internal plane 24"),
|
||||
("In25.Cu", pcbnew.In25_Cu, "Internal plane 25"),
|
||||
("In26.Cu", pcbnew.In26_Cu, "Internal plane 26"),
|
||||
("In27.Cu", pcbnew.In27_Cu, "Internal plane 27"),
|
||||
("In28.Cu", pcbnew.In28_Cu, "Internal plane 28"),
|
||||
("In29.Cu", pcbnew.In29_Cu, "Internal plane 29"),
|
||||
("In30.Cu", pcbnew.In30_Cu, "Internal plane 30"),
|
||||
("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")
|
||||
("Edge.Cuts", pcbnew.Edge_Cuts, "Board Outline"),
|
||||
("User.Comments", pcbnew.Cmts_User, "User Comments")
|
||||
]
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ class KiCadToJLCForm(wx.Frame):
|
||||
pcbnew.Refresh()
|
||||
self.Destroy()
|
||||
else:
|
||||
self.m_gaugeStatus.SetValue(status.data)
|
||||
self.m_gaugeStatus.SetValue(int(status.data))
|
||||
|
||||
|
||||
# Plugin definition
|
||||
|
||||
+209
-61
@@ -1,11 +1,18 @@
|
||||
# from urllib import response
|
||||
# import json
|
||||
# import requests
|
||||
# For better annotation.
|
||||
from __future__ import annotations
|
||||
|
||||
# System base libraries
|
||||
import os
|
||||
import csv
|
||||
import math
|
||||
import shutil
|
||||
import pcbnew
|
||||
from collections import defaultdict
|
||||
import re
|
||||
|
||||
# Interaction with KiCad.
|
||||
import pcbnew
|
||||
|
||||
# Application definitions.
|
||||
from .config import *
|
||||
|
||||
|
||||
@@ -14,10 +21,69 @@ class ProcessManager:
|
||||
self.board = pcbnew.GetBoard()
|
||||
self.bom = []
|
||||
self.components = []
|
||||
self.__rotation_db = self.__read_rotation_db()
|
||||
|
||||
@staticmethod
|
||||
def normalize_filename(filename):
|
||||
return re.sub(r'[^\w\s\.\-]', '', filename)
|
||||
|
||||
@staticmethod
|
||||
def __read_rotation_db(filename: str = os.path.join(os.path.dirname(__file__), 'rotations.cf')) -> dict[str, float]:
|
||||
'''Read the rotations.cf config file so we know what rotations
|
||||
to apply later.
|
||||
'''
|
||||
db = {}
|
||||
|
||||
with open(filename, 'r') as fh:
|
||||
for line in fh:
|
||||
line = line.rstrip()
|
||||
line = re.sub('#.*$', '', line) # remove anything after a comment
|
||||
line = re.sub('\s*$', '', line) # remove all trailing space
|
||||
|
||||
if (line == ""):
|
||||
continue
|
||||
|
||||
match = re.match('^([^\s]+)\s+(\d+)$', line)
|
||||
|
||||
if match:
|
||||
db.update({ match.group(1): int(match.group(2)) })
|
||||
|
||||
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.
|
||||
fpshort = footprint.split(':')[-1]
|
||||
|
||||
for expression, delta in self.db.items():
|
||||
fp = fpshort
|
||||
|
||||
if (re.search(':', expression)):
|
||||
fp = footprint
|
||||
|
||||
if(re.search(expression, fp)):
|
||||
return delta
|
||||
|
||||
return 0.0
|
||||
|
||||
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):
|
||||
'''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,12 +96,16 @@ class ProcessManager:
|
||||
plot_options.SetScale(1)
|
||||
plot_options.SetMirror(False)
|
||||
plot_options.SetUseGerberAttributes(True)
|
||||
plot_options.SetExcludeEdgeLayer(True)
|
||||
plot_options.SetUseGerberProtelExtensions(False)
|
||||
plot_options.SetUseAuxOrigin(True)
|
||||
plot_options.SetSubtractMaskFromSilk(False)
|
||||
plot_options.SetSubtractMaskFromSilk(True)
|
||||
plot_options.SetDrillMarksType(0) # NO_DRILL_SHAPE
|
||||
|
||||
if hasattr(plot_options, "SetExcludeEdgeLayer"):
|
||||
plot_options.SetExcludeEdgeLayer(True)
|
||||
|
||||
# This includes User_1 with Edge_Cuts to allow V Cuts to be defined as User_1 layer and added so JLC accept it
|
||||
# It seems making a separate file just confuses them and they want it in the Edge Cuts file, but that would upset KiCad
|
||||
for layer_info in plotPlan:
|
||||
if self.board.IsLayerEnabled(layer_info[1]):
|
||||
plot_controller.SetLayer(layer_info[1])
|
||||
@@ -43,11 +113,16 @@ class ProcessManager:
|
||||
layer_info[0],
|
||||
pcbnew.PLOT_FORMAT_GERBER,
|
||||
layer_info[2])
|
||||
plot_controller.PlotLayer()
|
||||
seq = pcbnew.LSEQ()
|
||||
seq.push_back(layer_info[1])
|
||||
if layer_info[1] == pcbnew.Edge_Cuts :
|
||||
seq.push_back(pcbnew.User_1)
|
||||
plot_controller.PlotLayers(seq)
|
||||
|
||||
plot_controller.ClosePlot()
|
||||
|
||||
def generate_drills(self, temp_dir):
|
||||
'''Generate the drill file.'''
|
||||
drill_writer = pcbnew.EXCELLON_WRITER(self.board)
|
||||
|
||||
drill_writer.SetOptions(
|
||||
@@ -59,15 +134,20 @@ class ProcessManager:
|
||||
drill_writer.CreateDrillandMapFilesSet(temp_dir, True, False)
|
||||
|
||||
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):
|
||||
'''Generate the position files.'''
|
||||
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 +155,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:
|
||||
@@ -103,11 +184,31 @@ class ProcessManager:
|
||||
unique_id = str(footprint_designators[footprint.GetReference()])
|
||||
footprint_designators[footprint.GetReference()] -= 1
|
||||
|
||||
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().AsDegrees() if hasattr(footprint.GetOrientation(), 'AsDegrees') else footprint.GetOrientation() / 10.0
|
||||
# Get the rotation offset to be added to the actual rotation prioritizing the explicated by the
|
||||
# designer at the standards symbol fields. If not specified use the internal database.
|
||||
rotation_offset = self._get_rotation_offset_from_footprint(footprint) #or self._get_rotation_from_db(footprint)
|
||||
rotation = (rotation + rotation_offset) % 360.0
|
||||
|
||||
# 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)
|
||||
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 = (540.0 - rotation) % 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,
|
||||
})
|
||||
|
||||
@@ -118,48 +219,56 @@ class ProcessManager:
|
||||
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)
|
||||
|
||||
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:
|
||||
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
|
||||
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):
|
||||
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 files.'''
|
||||
temp_file = shutil.make_archive(temp_file, 'zip', temp_dir)
|
||||
temp_file = shutil.move(temp_file, temp_dir)
|
||||
|
||||
@@ -170,20 +279,59 @@ class ProcessManager:
|
||||
|
||||
return temp_file
|
||||
|
||||
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 _get_mpn_from_footprint(self, footprint):
|
||||
''''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']
|
||||
|
||||
files = { 'upload[file]': open(temp_file, 'rb') }
|
||||
upload_url = baseUrl + '/Common/KiCadUpFile/'
|
||||
if footprint.HasProperty('dnp'):
|
||||
return 'DNP'
|
||||
|
||||
# response = requests.post(
|
||||
# upload_url, files=files, data={ 'boardWidth': boardWidth, 'boardHeight': boardHeight, 'boardLayer': boardLayer })
|
||||
# urls = json.loads(response.content)
|
||||
|
||||
def _getMpnFromFootprint(self, footprint):
|
||||
keys = ['mpn', 'Mpn', 'MPN', 'JLC_MPN', 'LCSC_MPN', 'LCSC Part #', 'JLC', 'LCSC']
|
||||
for key in keys:
|
||||
for key in keys + fallback_keys:
|
||||
if footprint.HasProperty(key):
|
||||
return footprint.GetProperty(key)
|
||||
|
||||
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 = None
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint.HasProperty(key):
|
||||
offset = footprint.GetProperty(key)
|
||||
break
|
||||
|
||||
if offset is None or offset == "":
|
||||
return 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):
|
||||
keys = ['JLCPCB Position Offset']
|
||||
fallback_keys = ['JlcPosOffset', 'JLCPosOffset']
|
||||
|
||||
offset = None
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint.HasProperty(key):
|
||||
offset = footprint.GetProperty(key)
|
||||
break
|
||||
|
||||
if offset is None or offset == "":
|
||||
return (0, 0)
|
||||
else:
|
||||
try:
|
||||
return ( float(offset.split(",")[0]), float(offset.split(",")[1]) )
|
||||
except ValueError:
|
||||
raise RuntimeError("Position offset of {} is not a valid pair of numbers".format(footprint.GetReference()))
|
||||
|
||||
def _normalize_footprint_name(self, footprint):
|
||||
# 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)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#
|
||||
# Rotations for jlcpcba
|
||||
#
|
||||
# This defines additional rotations we need to apply to ensure the
|
||||
# footprints are correctly orientated to work with the JLCPCB PBCA
|
||||
# service
|
||||
#
|
||||
# These are regular expressions that are matched by default against
|
||||
# the short footprint name (without the library name), however if there
|
||||
# is a colon in the regex then the full name (with the library name) is
|
||||
# used.
|
||||
#
|
||||
# The matches are checked against one by one, and the first match
|
||||
# is used
|
||||
#
|
||||
# The regex and the rotation value is separated by any amount of
|
||||
# whitespace, blank lines and comments are ignored
|
||||
#
|
||||
|
||||
#
|
||||
# Normal short-name matches
|
||||
#
|
||||
^SOT-223 180
|
||||
^SOT-23 180
|
||||
^D_SOT-23 180
|
||||
^TSOT-23 180
|
||||
^SOT-353 180
|
||||
^QFN- 90
|
||||
^qfn- 90
|
||||
^LQFP- 270
|
||||
^TQFP- 270
|
||||
^MSOP- 270
|
||||
^TSSOP- 270
|
||||
^DFN- 270
|
||||
^SOIC-8_ 270
|
||||
^SOIC-16_ 270
|
||||
^VSSOP-10_- 270
|
||||
|
||||
#
|
||||
# Polarised caps are 180 out
|
||||
#
|
||||
^CP_Elec_ 180
|
||||
^C_Elec_ 180
|
||||
^CP_EIA- 180
|
||||
|
||||
#
|
||||
# Long name matches
|
||||
#
|
||||
#^Lees_Footprints: 270
|
||||
^LED_WS2812B_PLCC4 180
|
||||
|
||||
# More rotation from https://github.com/matthewlai/JLCKicadTools/tree/master/jlc_kicad_tools
|
||||
^R_Array_Convex_ 90
|
||||
^R_Array_Concave_ 90
|
||||
^SOP-4_ 0
|
||||
^SOP-(?!18_) 270
|
||||
^SOP-18_ 0
|
||||
^VSSOP-8_ 270
|
||||
^Bosch_LGA- 90
|
||||
^PowerPAK_SO-8_Single 270
|
||||
^HTSSOP- 270
|
||||
+67
-29
@@ -1,8 +1,10 @@
|
||||
import os
|
||||
import wx
|
||||
import pcbnew
|
||||
import shutil
|
||||
import tempfile
|
||||
import webbrowser
|
||||
import datetime
|
||||
from threading import Thread
|
||||
from .events import StatusEvent
|
||||
from .process import ProcessManager
|
||||
@@ -19,40 +21,52 @@ class ProcessThread(Thread):
|
||||
|
||||
def run(self):
|
||||
# initializing
|
||||
self.report(0)
|
||||
self.progress(0)
|
||||
|
||||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')
|
||||
|
||||
temp_dir = tempfile.mkdtemp() + timestamp
|
||||
os.makedirs(temp_dir)
|
||||
os.makedirs(temp_dir + "_g")
|
||||
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
_, 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)
|
||||
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 + "_g")
|
||||
|
||||
# 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 + "_g")
|
||||
|
||||
# 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 pick and place file
|
||||
self.progress(50)
|
||||
self.process_manager.generate_positions(temp_dir)
|
||||
|
||||
# upload files
|
||||
#self.report(87.5)
|
||||
#self.process_manager.upload_archive(temp_file)
|
||||
# generate BOM file
|
||||
self.progress(60)
|
||||
self.process_manager.generate_bom(temp_dir)
|
||||
|
||||
# generate production archive
|
||||
self.progress(75)
|
||||
temp_file = self.process_manager.generate_archive(temp_dir + "_g", temp_file)
|
||||
shutil.move(temp_file, temp_dir)
|
||||
shutil.rmtree(temp_dir + "_g")
|
||||
temp_file = os.path.join(temp_dir, os.path.basename(temp_file))
|
||||
except Exception as e:
|
||||
wx.MessageBox(str(e), "Error", wx.OK | wx.ICON_ERROR)
|
||||
self.progress(-1)
|
||||
return
|
||||
|
||||
# progress bar done animation
|
||||
read_so_far = 0
|
||||
@@ -64,20 +78,44 @@ class ProcessThread(Thread):
|
||||
break
|
||||
read_so_far += len(data)
|
||||
percent = read_so_far * 1e2 / total_size
|
||||
self.report(75 + percent / 8)
|
||||
self.progress(75 + 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]
|
||||
name = ProcessManager.normalize_filename("_".join(("{} {} {}".format(title or filename, revision or '', timestamp).strip()).split()))
|
||||
output_path = os.path.join(project_directory, outputFolder, name)
|
||||
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))
|
||||
|
||||
# copy to & open output dir
|
||||
try:
|
||||
if os.path.exists(output_path):
|
||||
shutil.rmtree(output_path)
|
||||
|
||||
shutil.copytree(temp_dir, output_path)
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user