Skip to content

Commit

Permalink
Added fabrication script
Browse files Browse the repository at this point in the history
  • Loading branch information
jharvey committed Jul 15, 2020
1 parent 0a860b6 commit 4950cef
Show file tree
Hide file tree
Showing 7 changed files with 1,149 additions and 0 deletions.
162 changes: 162 additions & 0 deletions Fabrication/jlc_kicad_tools/KC-POS_to_JLC.py
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())

1 change: 1 addition & 0 deletions Fabrication/jlc_kicad_tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = '1.0.0'
9 changes: 9 additions & 0 deletions Fabrication/jlc_kicad_tools/cpl_KC-to-JLC-rotations_db.csv
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.
76 changes: 76 additions & 0 deletions Fabrication/jlc_kicad_tools/jlc_lib/cpl_fix_rotations.py
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
107 changes: 107 additions & 0 deletions Fabrication/jlc_kicad_tools/jlc_lib/generate_bom.py
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
Loading

0 comments on commit 4950cef

Please sign in to comment.