hellen-one/bin/jlc_kicad_tools/jlc_fix.py

168 lines
6.4 KiB
Python

#!/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={
"Center-X(mm)": "Mid X",
"Center-Y(mm)": "Mid Y",
}
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:
# if not the first data row, then skip
if len(row) < 4:
continue
# This is the first row. Find "Package" and "Rot" column indices.
for i in range(len(row)):
if row[i] == "PackageReference":
package_index = i
elif row[i] == "Rotation":
rotation_index = i
if package_index is None:
logging.warning("Failed to find 'PackageReference' column in the csv file")
return False
if rotation_index is None:
logging.warning("Failed to find 'Rotation' 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_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('cpl_in_filename', metavar='INPUT_PROJECT', type=str, help='File of KiCad project. Name should match KiCad project name.')
parser.add_argument('cpl_out_filename', metavar='INPUT_PROJECT', type=str, help='File 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)
if opts.cpl_in_filename == None:
project_name = os.path.basename(opts.project_dir)
logging.debug("Project name is '%s'.", project_name)
cpl_filename = project_name + "-CPL.csv"
cpl_outfilename = project_name + "_cpl_jlc.csv"
else:
cpl_filename = opts.cpl_in_filename
cpl_outfilename = opts.cpl_out_filename
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, cpl_outfilename)
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())