-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
jharvey
committed
Jul 15, 2020
1 parent
0a860b6
commit 4950cef
Showing
7 changed files
with
1,149 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
#!/usr/bin/env python3 | ||
# Copyright (C) 2019 Matthew Lai | ||
# | ||
# This file is part of JLC Kicad Tools. | ||
# | ||
# JLC Kicad Tools is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# JLC Kicad Tools is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with JLC Kicad Tools. If not, see <https://www.gnu.org/licenses/>. | ||
|
||
import os | ||
import re | ||
import sys | ||
import argparse | ||
import logging | ||
import errno | ||
|
||
import csv | ||
import re | ||
import sys | ||
import logging | ||
|
||
# JLC requires columns to be named a certain way. | ||
HEADER_REPLACEMENT_TABLE={ | ||
"Ref": "Designator", | ||
"Val": "Val", | ||
"Package": "PackageReference", | ||
"PosX": "Mid X", | ||
"PosY": "Mid Y", | ||
"Rot": "Rotation", | ||
"Side": "Layer", | ||
} | ||
|
||
ROW_REPLACEMENT_TABLE={ | ||
"TopLayer": "Top", | ||
"BottomLayer": "Bottom", | ||
} | ||
|
||
def ReadDB(filename): | ||
db = {} | ||
with open(filename) as csvfile: | ||
reader = csv.reader(csvfile, delimiter=',') | ||
for row in reader: | ||
if row[0] == "Footprint pattern": | ||
continue | ||
else: | ||
db[re.compile(row[0])] = int(row[1]) | ||
logging.info("Read {} rules from {}".format(len(db), filename)) | ||
return db | ||
|
||
def FixRotations(input_filename, output_filename, db): | ||
with open(input_filename) as csvfile: | ||
reader = csv.reader(csvfile, delimiter=',') | ||
writer = csv.writer(open(output_filename, 'w', newline=''), delimiter=',') | ||
package_index = None | ||
rotation_index = None | ||
for row in reader: | ||
if not package_index: | ||
# This is the first row. Find "Package" and "Rot" column indices. | ||
for i in range(len(row)): | ||
if row[i] == "Package": | ||
package_index = i | ||
elif row[i] == "Rot": | ||
rotation_index = i | ||
if package_index is None: | ||
logging.warning("Failed to find 'Package' column in the csv file") | ||
return False | ||
if rotation_index is None: | ||
logging.warning("Failed to find 'Rot' column in the csv file") | ||
return False | ||
# Replace column names with labels JLC wants. | ||
for i in range(len(row)): | ||
if row[i] in HEADER_REPLACEMENT_TABLE: | ||
row[i] = HEADER_REPLACEMENT_TABLE[row[i]] | ||
else: | ||
for pattern, correction in db.items(): | ||
if pattern.match(row[package_index]): | ||
logging.info("Footprint {} matched {}. Applying {} deg correction" | ||
.format(row[package_index], pattern.pattern, correction)) | ||
row[rotation_index] = "{0:.0f}".format((float(row[rotation_index]) + correction) % 360) | ||
break | ||
for i in range(len(row)): | ||
if row[i] in ROW_REPLACEMENT_TABLE: | ||
row[i] = ROW_REPLACEMENT_TABLE[row[i]] | ||
del row[package_index] | ||
writer.writerow(row) | ||
return True | ||
|
||
DEFAULT_DB_PATH="cpl_KC-to-JLC-rotations_db.csv" | ||
|
||
def main(): | ||
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description='Generates BOM and CPL in CSV fashion to be used in JLCPCB Assembly Service', prog='generate_jlc_files') | ||
parser.add_argument('project_dir', metavar='INPUT_DIRECTORY', type=os.path.abspath, help='Directory of KiCad project. Name should match KiCad project name.') | ||
parser.add_argument('-d', '--database', metavar='DATABASE', type=str, help='Filename of database', default=os.path.join(os.path.dirname(__file__), DEFAULT_DB_PATH)) | ||
verbosity = parser.add_argument_group('verbosity arguments') | ||
verbosity.add_argument('-v', '--verbose', help='Increases log verbosity for each occurrence', dest='verbose_count', action="count", default=0) | ||
verbosity.add_argument('--warn-no-lcsc-partnumber', help='Enable warning output if lcsc part number is not found', dest='warn_no_partnumber', action='store_true') | ||
parser.add_argument('--assume-same-lcsc-partnumber', help='Assume same lcsc partnumber for all components of a group', action='store_true', dest='assume_same_lcsc_partnumber') | ||
parser.add_argument('-o', '--output', metavar='OUTPUT_DIRECTORY', dest='output_dir', type=os.path.abspath, help='Output directory. Default: INPUT_DIRECTORY') | ||
|
||
if (len(sys.argv) == 1): | ||
parser.print_help() | ||
sys.exit() | ||
|
||
# Parse arguments | ||
opts = parser.parse_args(sys.argv[1:]) | ||
|
||
# Default log level is WARNING | ||
logging.basicConfig(format="%(message)s", level=max(logging.WARNING - opts.verbose_count * 10, logging.NOTSET)) | ||
|
||
if not os.path.isdir(opts.project_dir): | ||
logging.error("Failed to open project directory: {}".format(opts.project_dir)) | ||
return errno.ENOENT | ||
|
||
# Set default output directory | ||
if opts.output_dir == None: | ||
opts.output_dir = opts.project_dir | ||
|
||
if not os.path.isdir(opts.output_dir): | ||
logging.info("Creating output directory {}".format(opts.output_dir)) | ||
os.mkdir(opts.output_dir) | ||
|
||
project_name = os.path.basename(opts.project_dir) | ||
logging.debug("Project name is '%s'.", project_name) | ||
cpl_filename = project_name + "-top-pos.csv" | ||
cpl_path = None | ||
|
||
for dir_name, subdir_list, file_list in os.walk(opts.project_dir): | ||
for file_name in file_list: | ||
if file_name == cpl_filename: | ||
cpl_path = os.path.join(dir_name, file_name) | ||
|
||
if cpl_path is None: | ||
logging.error(( | ||
"Failed to find CPL file: {} in {} (and sub-directories). " | ||
"Run 'File -> Fabrication Outputs -> Footprint Position (.pos) File' in Pcbnew. " | ||
"Settings: 'CSV', 'mm', 'single file for board'.").format(cpl_filename, opts.project_dir)) | ||
return errno.ENOENT | ||
|
||
logging.info("CPL file found at: {}".format(cpl_path)) | ||
|
||
cpl_output_path = os.path.join(opts.output_dir, project_name + "_JLC_CPL.csv") | ||
|
||
db = ReadDB(opts.database) | ||
if FixRotations(cpl_path, cpl_output_path, db): | ||
logging.info("JLC CPL file written to: {}".format(cpl_output_path)) | ||
else: | ||
return errno.EINVAL | ||
|
||
return 0 | ||
|
||
if __name__ == '__main__': | ||
sys.exit(main()) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
__version__ = '1.0.0' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
"Footprint pattern","Correction" | ||
"CP_Elec_8x10.5",180 | ||
"SOT-23",180 | ||
"D_SMC",180 | ||
"R_Array_Convex_4x0603",270 | ||
"SOT-223-3_TabPin2",180 | ||
"SO-8_5.3x6.2mm_P1.27mm-150+208",90 | ||
"TSSOP-14_4.4x5mm_P0.65mm",180 | ||
"SOIC-8_3.9x4.9mm_P1.27mm",90 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
# Copyright (C) 2019 Matthew Lai | ||
# | ||
# This file is part of JLC Kicad Tools. | ||
# | ||
# JLC Kicad Tools is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# JLC Kicad Tools is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with JLC Kicad Tools. If not, see <https://www.gnu.org/licenses/>. | ||
|
||
import csv | ||
import re | ||
import sys | ||
import logging | ||
|
||
# JLC requires columns to be named a certain way. | ||
HEADER_REPLACEMENT_TABLE={ | ||
"Ref": "Designator", | ||
"PosX": "Mid X", | ||
"PosY": "Mid Y", | ||
"Rot": "Rotation", | ||
"Side": "Layer" | ||
} | ||
|
||
def ReadDB(filename): | ||
db = {} | ||
with open(filename) as csvfile: | ||
reader = csv.reader(csvfile, delimiter=',') | ||
for row in reader: | ||
if row[0] == "Footprint pattern": | ||
continue | ||
else: | ||
db[re.compile(row[0])] = int(row[1]) | ||
logging.info("Read {} rules from {}".format(len(db), filename)) | ||
return db | ||
|
||
def FixRotations(input_filename, output_filename, db): | ||
with open(input_filename) as csvfile: | ||
reader = csv.reader(csvfile, delimiter=',') | ||
writer = csv.writer(open(output_filename, 'w', newline=''), delimiter=',') | ||
package_index = None | ||
rotation_index = None | ||
for row in reader: | ||
if not package_index: | ||
# This is the first row. Find "Package" and "Rot" column indices. | ||
for i in range(len(row)): | ||
if row[i] == "Package": | ||
package_index = i | ||
elif row[i] == "Rot": | ||
rotation_index = i | ||
if package_index is None: | ||
logging.warning("Failed to find 'Package' column in the csv file") | ||
return False | ||
if rotation_index is None: | ||
logging.warning("Failed to find 'Rot' column in the csv file") | ||
return False | ||
# Replace column names with labels JLC wants. | ||
for i in range(len(row)): | ||
if row[i] in HEADER_REPLACEMENT_TABLE: | ||
row[i] = HEADER_REPLACEMENT_TABLE[row[i]] | ||
else: | ||
for pattern, correction in db.items(): | ||
if pattern.match(row[package_index]): | ||
logging.info("Footprint {} matched {}. Applying {} deg correction" | ||
.format(row[package_index], pattern.pattern, correction)) | ||
row[rotation_index] = "{0:.6f}".format((float(row[rotation_index]) + correction) % 360) | ||
break | ||
writer.writerow(row) | ||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
# Copyright (C) 2019 Matthew Lai | ||
# Copyright (C) 1992-2019 Kicad Developers Team | ||
# | ||
# This file is part of JLC Kicad Tools. | ||
# | ||
# JLC Kicad Tools is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# JLC Kicad Tools is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with JLC Kicad Tools. If not, see <https://www.gnu.org/licenses/>. | ||
|
||
from jlc_kicad_tools.jlc_lib import kicad_netlist_reader | ||
import csv | ||
import re | ||
import logging | ||
|
||
LCSC_PART_NUMBER_MATCHER=re.compile('^C[0-9]+$') | ||
|
||
def GenerateBOM(input_filename, output_filename, opts): | ||
net = kicad_netlist_reader.netlist(input_filename) | ||
|
||
try: | ||
f = open(output_filename, mode='w', encoding='utf-8') | ||
except IOError: | ||
logging.error("Failed to open file for writing: {}".format(output_filename)) | ||
return False | ||
|
||
out = csv.writer(f, lineterminator='\n', delimiter=',', quotechar='\"', | ||
quoting=csv.QUOTE_ALL) | ||
|
||
out.writerow(['Comment', 'Designator', 'Footprint', 'LCSC Part #']) | ||
|
||
grouped = net.groupComponents() | ||
|
||
num_groups_found = 0 | ||
for group in grouped: | ||
refs = [] | ||
lcsc_part_numbers = set() | ||
lcsc_part_numbers_none_found = False | ||
footprints = set() | ||
|
||
for component in group: | ||
refs.append(component.getRef()) | ||
c = component | ||
lcsc_part_number = None | ||
|
||
# Get the field name for the LCSC part number. | ||
for field_name in c.getFieldNames(): | ||
field_value = c.getField(field_name) | ||
|
||
if LCSC_PART_NUMBER_MATCHER.match(field_value): | ||
lcsc_part_number = field_value | ||
|
||
if lcsc_part_number: | ||
lcsc_part_numbers.add(lcsc_part_number) | ||
else: | ||
lcsc_part_numbers_none_found = True | ||
|
||
if c.getFootprint() != '': | ||
footprints.add(c.getFootprint()) | ||
|
||
# Check part numbers for uniqueness | ||
if len(lcsc_part_numbers) == 0: | ||
if opts.warn_no_partnumber: | ||
logging.warning("No LCSC part number found for components {}".format(",".join(refs))) | ||
continue | ||
elif len(lcsc_part_numbers) != 1: | ||
logging.error("Components {components} from same group have different LCSC part numbers: {partnumbers}".format( | ||
components = ", ".join(refs), | ||
partnumbers = ", ".join(lcsc_part_numbers))) | ||
return False | ||
lcsc_part_number = list(lcsc_part_numbers)[0] | ||
|
||
if (not opts.assume_same_lcsc_partnumber) and (lcsc_part_numbers_none_found): | ||
logging.error("Components {components} from same group do not all have LCSC part number {partnumber} set. Use --assume-same-lcsc-partnumber to ignore.".format( | ||
components = ", ".join(refs), | ||
partnumber = lcsc_part_number)) | ||
return False | ||
|
||
# Check footprints for uniqueness | ||
if (len(footprints) == 0): | ||
logging.error("No footprint found for components {}".format(",".join(refs))) | ||
return False | ||
if len(footprints) != 1: | ||
logging.error("Components {components} from same group have different foot prints: {footprints}".format( | ||
components = ", ".join(refs), | ||
footprints = ", ".join(footprints))) | ||
return False | ||
footprint = list(footprints)[0] | ||
|
||
# They don't seem to like ':' in footprint names. | ||
footprint = footprint[(footprint.find(':') + 1):] | ||
|
||
# Fill in the component groups common data | ||
out.writerow([c.getValue(), ",".join(refs), footprint, lcsc_part_number]) | ||
num_groups_found += 1 | ||
|
||
logging.info("{} component groups found from BOM file.".format(num_groups_found)) | ||
|
||
return True |
Oops, something went wrong.