From 35da9f8c58a65d50f57ce7b0843eb2ccf7d6c967 Mon Sep 17 00:00:00 2001 From: linuswck Date: Thu, 9 Nov 2023 13:19:32 +0800 Subject: [PATCH] scripts: Add scripts to generate production files - The following files can be generated from the script. 1. Zipped(Gerber, Drill, Drill Map) 2. Bom, 3. Component Placement 4. Schematics PDF 5. Step Files --- .gitignore | 5 ++ scripts/generate_bom_from_xml.py | 62 +++++++++++++++ scripts/generate_production_files.py | 109 +++++++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 scripts/generate_bom_from_xml.py create mode 100644 scripts/generate_production_files.py diff --git a/.gitignore b/.gitignore index 99d5004..e91d255 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ _autosave-* *-save.kicad_pcb fp-info-cache *auto_saved_files* +~* +__pycache__ # Netlist files (exported from Eeschema) *.net @@ -24,3 +26,6 @@ fp-info-cache # Autorouter files (exported from Pcbnew) *.dsn *.ses + +# Generated Production Files +production \ No newline at end of file diff --git a/scripts/generate_bom_from_xml.py b/scripts/generate_bom_from_xml.py new file mode 100644 index 0000000..271aea6 --- /dev/null +++ b/scripts/generate_bom_from_xml.py @@ -0,0 +1,62 @@ +# Modified from "bom_csv_grouped_by_value_with_fp.py" Example BOM Generation Script + +""" + @package + Output: CSV (comma-separated) + The BOM does not include components with DNP or excluded from BOM field(s) checked. + Grouped By: Value, Footprint, MFR_PN, MFR_ALT + Sorted By: Ref + Fields: Ref, Value, MFR_PN, MFR_PN_ALT, Qnty, LibPart, Footprint + + Command line: + python "pathToFile/generate_bom_from_xml.py" "%I" "%O.csv" +""" + +import kicad_netlist_reader +import csv +import sys +import os + +try: + if not os.path.isdir(os.path.dirname(sys.argv[2])): + os.makedirs(os.path.dirname(sys.argv[2])) + f = open(sys.argv[2], 'w', encoding='utf-8') +except IOError: + raise IOError("Can't open output file for writing: " + sys.argv[2]) + +# Custom Equal Operator for "groupComponents" method +def __eq__(self, other): + result = False + if self.getValue() == other.getValue(): + if self.getFootprint() == other.getFootprint(): + if self.getField("MFR_PN") == other.getField("MFR_PN"): + if self.getField("MFR_PN_ALT") == other.getField("MFR_PN_ALT"): + result = True + return result +kicad_netlist_reader.comp.__eq__ = __eq__ + +net = kicad_netlist_reader.netlist(sys.argv[1]) + +out = csv.writer(f, lineterminator='\n', delimiter=',', quotechar='\"', quoting=csv.QUOTE_ALL) +out.writerow(['Source:', net.getSource()]) +out.writerow(['Date:', net.getDate()]) +out.writerow(['Tool:', net.getTool()]) +out.writerow(['Ref', 'Value', 'MFR_PN', 'MFR_PN_ALT', 'Qnty', 'LibPart', 'Footprint']) + +grouped = net.groupComponents(components=net.getInterestingComponents(excludeBOM=True, DNP=True)) + +for group in grouped: + refs = "" + for component in group: + if refs != "": + refs += ", " + refs += component.getRef() + c = component + + out.writerow([refs, + c.getValue(), + c.getField("MFR_PN"), + c.getField("MFR_PN_ALT"), + len(group), + c.getLibName() + ":" + c.getPartName(), + c.getFootprint()]) diff --git a/scripts/generate_production_files.py b/scripts/generate_production_files.py new file mode 100644 index 0000000..c8c1518 --- /dev/null +++ b/scripts/generate_production_files.py @@ -0,0 +1,109 @@ +import os +import argparse + +__cmd_get_kicad_bin_path = "which kicad | xargs readlink | sed 's/$/-cli/' | xargs readlink" + +def setup_env(prefix, sch, out_dir, dir_kicad_plugins, dir_3d_models): + # Get the path to "kicad_netlist_reader.py" script in NixOs + if dir_kicad_plugins is None: + dir_kicad_plugins = os.path.join(os.popen(__cmd_get_kicad_bin_path).read().replace("\n", ""), + "../../share/kicad/plugins") + + # Setup the PYTHONPATH for "generate_bom_from_xml.py" to import "kicad_netlist_reader" + try: + pythonpath = os.environ['PYTHONPATH'] + except KeyError: + pythonpath = '' + pathlist = [dir_kicad_plugins] + if pythonpath: + pathlist.extend(pythonpath.split(os.pathsep)) + os.environ['PYTHONPATH'] = os.pathsep.join(pathlist) + + # NIXOS installs KiCAD Built-in 3D models in a separated folder + if dir_3d_models is None: + model_3d_path = os.path.join("/nix/store", os.popen("ls /nix/store | grep kicad-packages3d | head -1").read().replace("\n", ""), "share/kicad/3dmodels") + # Setup the KICAD7_3DMODEL_DIR for step file to be generated with kicad-cli" + os.environ['KICAD7_3DMODEL_DIR'] = model_3d_path + + # Generate the prefix from the title and revision fields in title block of the schematics top + if prefix is None: + with open (sch, "r") as f: + data = f.read().splitlines() + title_line = data[7][1:-1].split() + revision_line = data[9][1:-1].split() + if title_line[0].find("title") and revision_line[0].find("rev"): + ret_prefix = f"{title_line[1][1:-1]}_{revision_line[1][1:-1]}" + else: + raise ValueError("Prefix cannot be generated from schematic file.") + else: + ret_prefix = prefix + + gerber_drill_dir = os.path.join(out_dir, f"{ret_prefix}_gerber_drill") + if not os.path.exists(out_dir): + os.makedirs(out_dir) + if not os.path.exists(gerber_drill_dir): + os.makedirs(gerber_drill_dir) + + return ret_prefix + +def generate_production_files(prefix, sch, pcb, out_dir): + out_path = os.path.join(out_dir, prefix) + + bom_ret_code = os.system(f"kicad-cli sch export python-bom {sch} -o {out_path}_bom.xml") + bom_ret_code |= os.system(f"python -m scripts.generate_bom_from_xml {out_path}_bom.xml {out_path}_bom.csv") + os.system(f"rm {out_path}_bom.xml") + + pdf_ret_code = os.system(f"kicad-cli sch export pdf {sch} -o {out_path}.pdf") + + pos_ret_code = os.system(f"kicad-cli pcb export pos {pcb} --format csv --units mm -o {out_path}_pos.csv") + + step_ret_code = os.system(f"kicad-cli pcb export step {pcb} --subst-models --force -o {out_path}.step") + + out_path = os.path.join(out_dir, f"{prefix}_gerber_drill") + + gerber_ret_code = os.system(f"kicad-cli pcb export gerbers {pcb} -l 'F.Cu,In1.Cu,In2.Cu,B.Cu,F.Paste,B.Paste,F.Silkscreen,B.Silkscreen,F.Mask,B.Mask,Edge.Cuts' --no-x2 --subtract-soldermask -o {out_path}") + + # The additional trailing slash is due to a bug in the kicad-cli tool. https://gitlab.com/kicad/code/kicad/-/issues/14438 + drill_ret_code = os.system(f"kicad-cli pcb export drill {pcb} -u mm --generate-map --map-format gerberx2 -o {out_path}/") + zip_ret_code = os.system(f"zip -r -j {out_path} {out_path}") + os.system(f"rm -r {out_path}") + + print("=== File Generation Status === ") + print("Gerber: {}".format("Success" if gerber_ret_code == 0 else "Failed")) + print("Drill: {}".format("Success" if drill_ret_code == 0 else "Failed")) + print("Zip_Gerber_Drill: {}".format("Success" if zip_ret_code == 0 else "Failed")) + print("Bom: {}".format("Success" if bom_ret_code == 0 else "Failed")) + print("Pdf: {}".format("Success" if pdf_ret_code == 0 else "Failed")) + print("Pos: {}".format("Success" if pos_ret_code == 0 else "Failed")) + print("Step: {}".format("Success" if step_ret_code == 0 else "Failed")) + +def main(): + parser = argparse.ArgumentParser( + description="Python Script to Generate Production Files(Gerber, Drill, Drill Map, Bom, Component Placement, Schematics PDF, Step Files)") + parser.add_argument("-s", "--sch", + default="kirdy.kicad_sch", + help="schematics top file. defaults to 'kirdy.kicad_sch' if omitted") + parser.add_argument("-p", "--pcb", + default="kirdy.kicad_pcb", + help="pcb file. defaults to 'kirdy.kicad_pcb' if omitted") + parser.add_argument("-o", "--output", + default="./production", + help="output folder, defaults to './production' if omitted") + parser.add_argument("-pre", "--prefix", + default=None, + help="output filename prefix, attempts to generated from schematics top file if omitted") + parser.add_argument("-dir_plugins", "--dir_kicad_plugins", + default=None, + help="path to kicad_netlist_reader.py, attempts to find the required path in the system if omitted") + parser.add_argument("-dir_3d", "--dir_3d_models", + default=None, + help="path to kicad 3d models folder, attempts to find the required path in the system if omitted") + + args = parser.parse_args() + + prefix = setup_env(args.prefix, args.sch, args.output, args.dir_kicad_plugins, args.dir_3d_models) + + generate_production_files(prefix, args.sch, args.pcb, args.output) + +if __name__ == '__main__': + main() \ No newline at end of file