Compare commits

...

31 Commits

Author SHA1 Message Date
Benny Megidish ecd98adeff feat: normalize footprint name as per #27 2023-01-06 16:04:27 +02:00
Benny Megidish d8d81a7fe9 Update README.md 2023-01-06 00:57:12 +02:00
Benny Megidish d8c9aa775e Update README.md 2023-01-06 00:54:44 +02:00
Benny Megidish b783b289a8 chore: updated images 2023-01-06 00:50:16 +02:00
Benny Megidish 927128651c chore: bump version 2023-01-06 00:20:47 +02:00
Benny Megidish 91fdcc9fc2 Create FUNDING.yml 2023-01-06 00:19:37 +02:00
Benny Megidish 1d1d38b83f refactor: rename 2023-01-06 00:16:53 +02:00
Benny Megidish 49e921d2cf chore: updated readme 2023-01-06 00:07:51 +02:00
Benny Megidish 9b1d84265d Merge pull request #25
Add ability to set a position offset
2023-01-05 23:53:32 +02:00
Benny Megidish 5a675648bc Merge pull request #24
Add rotation DB
2023-01-05 23:51:08 +02:00
Benny Megidish adbd0fdde0 Merge pull request #31 closes #29
Substract maks from silk
2023-01-04 18:15:31 +02:00
kiu 1131b2efa9 Substract maks from silk
See https://github.com/bennymeg/JLC-Plugin-for-KiCad/issues/29
2022-12-30 18:00:42 +01:00
harbaum 0fcaa841a8 Take rotation into account for position offset 2022-11-03 13:17:58 +01:00
Till Harbaum 9147cacdac Add ability to set a position offset 2022-11-01 13:23:19 +01:00
Hildo Guillardi Júnior 17b3c10d16 Add rotation DB 2022-10-21 23:17:14 -03:00
Benny Megidish c53997c639 Merge pull request #22
Skip CSVs when there are no components or designators
2022-10-20 11:20:39 +03:00
Benny Megidish 0a8de1183e Merge pull request #21
Rotation offset field
2022-10-20 11:20:17 +03:00
Carlos Garcia Saura d93476b5a9 Skip CSVs when there are no components or designators 2022-10-19 18:51:53 +02:00
Anton Schirg 1e1b347852 feat: show errors in a message box
before, the progress window would stay on screen if the thread aborts
due to an error
2022-10-17 22:20:00 +02:00
Anton Schirg 7a67087431 feat: rotation offset field to align KiCad and JLC footprints 2022-10-17 22:19:56 +02:00
Benny Megidish adb10d314a chore: minor code ordering 2022-09-28 17:26:51 +03:00
Benny Megidish 5b43ce9047 Merge pull request #19
Fixes for KiCad 7 nightly compatibility
2022-09-28 17:24:35 +03:00
miceuz 45cb109668 Return back KiCad 6 code with conditionals for v7 compatibility 2022-09-28 17:20:10 +03:00
Benny Megidish 94bbea2ac4 Merge branch 'master' of https://github.com/bennymeg/JLC-Plugin-for-KiCad 2022-09-28 16:42:20 +03:00
Benny Megidish 1205f17d7e fix: status gauge value most be an integer #18 2022-09-28 16:42:15 +03:00
miceuz 1bfcc23256 Fixes for KiCad 7 nightly compatibility
* edge layers are not excluded in v7
* use orientation as degrees
2022-09-25 20:53:19 +03:00
Benny Megidish 036754a53b chore: updated readme, added usage, closes #12 2022-09-13 10:28:43 +03:00
Benny Megidish c71afbe1b9 Merge branch 'master' of https://github.com/bennymeg/JLC-Plugin-for-KiCad 2022-08-11 11:03:10 +03:00
Benny Megidish 1c9e78e869 feat: reordered the distributer part number as per #9 2022-08-11 11:03:06 +03:00
Benny Megidish b51024b174 Merge pull request #8 from butter-robotics/master
Update README.md
2022-08-04 23:24:52 +03:00
Benny Megidish 3f19dc2121 Update README.md
chore: fix readme typo
2022-08-04 23:22:59 +03:00
10 changed files with 319 additions and 60 deletions
+13
View File
@@ -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']
+58 -5
View File
@@ -17,21 +17,32 @@
## 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.
Open the "Plugin and Content Manager" from the KiCad main menu and install the "<ins>Fabrication Toolkit</ins>" plugin from the selection list.
### 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".
## 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).
## Options
### Include Component Part Number in Production Files
Add an 'MPN'* field with the LCSC component part number to the symbol's fields property.
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 #' |
| --- | --- |
_The fields will be query in the order denoted above._
#### Fallback Fields*:
| 'lcsc#' | '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.
@@ -42,6 +53,48 @@ Select 'Exclude from position files' or 'Exclude from BOM' in the footprint's fa
<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 JLCPB 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 'JLC 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>
#### 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 'JLC 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>
#### 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
## Contributors
Hildo Guillardi Júnior
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

+3 -3
View File
@@ -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 properties fabrication attributes in order to 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": "1.1.1",
"version": "2.0.0",
"status": "stable",
"kicad_version": "6.00"
}
]
}
}
+1 -1
View File
@@ -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
+159 -32
View File
@@ -1,8 +1,18 @@
# 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 *
@@ -11,8 +21,49 @@ class ProcessManager:
self.board = pcbnew.GetBoard()
self.bom = []
self.components = []
self.__rotation_db = self.__read_rotation_db()
@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 generate_gerber(self, temp_dir):
'''Generate the Gerber files.'''
settings = self.board.GetDesignSettings()
settings.m_SolderMaskMargin = 0
settings.m_SolderMaskMinWidth = 0
@@ -27,11 +78,13 @@ 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)
for layer_info in plotPlan:
if self.board.IsLayerEnabled(layer_info[1]):
@@ -45,6 +98,7 @@ class ProcessManager:
plot_controller.ClosePlot()
def generate_drills(self, temp_dir):
'''Generate the drill file.'''
drill_writer = pcbnew.EXCELLON_WRITER(self.board)
drill_writer.SetOptions(
@@ -56,10 +110,12 @@ 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:
@@ -75,9 +131,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') 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 +160,27 @@ 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)))
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,
})
@@ -130,37 +203,39 @@ class ProcessManager:
if insert:
self.bom.append({
'Designator': "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id),
'Footprint': footprint_name,
'Footprint': self._normalize_footprint_name(footprint_name),
'Quantity': 1,
'Value': footprint.GetValue(),
# 'Mount': mount_type,
'LCSC Part #': self._getMpnFromFootprint(footprint),
'LCSC Part #': self._get_mpn_from_footprint(footprint),
})
with open((os.path.join(temp_dir, placementFileName)), 'w', newline='', encoding='utf-8') as outfile:
csv_writer = csv.writer(outfile)
# writing headers of CSV file
csv_writer.writerow(self.components[0].keys())
if len(self.components) > 0:
with open((os.path.join(temp_dir, placementFileName)), 'w', newline='', encoding='utf-8') as outfile:
csv_writer = csv.writer(outfile)
# writing headers of CSV file
csv_writer.writerow(self.components[0].keys())
for component in self.components:
# 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') 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:
csv_writer = csv.writer(outfile)
# writing headers of CSV file
csv_writer.writerow(self.bom[0].keys())
# 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())
# 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)
@@ -171,8 +246,60 @@ class ProcessManager:
return temp_file
def _getMpnFromFootprint(self, footprint):
keys = ['mpn', 'Mpn', 'MPN', 'JLC_MPN', 'LCSC_MPN', 'LCSC Part #', 'JLC', 'LCSC']
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', 'JLC', 'MPN', 'Mpn', 'mpn']
for key in keys:
if footprint.HasProperty(key):
return footprint.GetProperty(key)
for key in fallback_keys:
if footprint.HasProperty(key):
return footprint.GetProperty(key)
def _get_rotation_offset_from_footprint(self, footprint: str) -> 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)
+61
View File
@@ -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
+24 -19
View File
@@ -26,29 +26,34 @@ class ProcessThread(Thread):
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:
# configure and generate gerber
self.report(5)
self.process_manager.generate_gerber(temp_dir)
# 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 drill file
self.report(15)
self.process_manager.generate_drills(temp_dir)
# generate pick and place file
self.report(40)
self.process_manager.generate_positions(temp_dir)
# generate netlist
self.report(25)
self.process_manager.generate_netlist(temp_dir)
# generate BOM file
self.report(60)
self.process_manager.generate_bom(temp_dir)
# generate pick and place file
self.report(40)
self.process_manager.generate_positions(temp_dir)
# generate production archive
self.report(75)
temp_file = self.process_manager.generate_archive(temp_dir, temp_file)
# generate BOM file
self.report(60)
self.process_manager.generate_bom(temp_dir)
# generate production archive
self.report(75)
temp_file = self.process_manager.generate_archive(temp_dir, temp_file)
except Exception as e:
wx.MessageBox(str(e), "Error", wx.OK | wx.ICON_ERROR)
self.report(-1)
return
# progress bar done animation
read_so_far = 0