From c4010680b88a5df374b85fe29f057d534e635287 Mon Sep 17 00:00:00 2001 From: sirlilpanda Date: Fri, 23 Jan 2026 14:41:04 +1300 Subject: [PATCH] kicad cli hook --- .hooks/kicad_cli_tools.py | 216 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 .hooks/kicad_cli_tools.py diff --git a/.hooks/kicad_cli_tools.py b/.hooks/kicad_cli_tools.py new file mode 100644 index 0000000..c7ab624 --- /dev/null +++ b/.hooks/kicad_cli_tools.py @@ -0,0 +1,216 @@ +# yes we are using json because +# i dont want to install a yaml +# parser on the users computer +import json +import os +import subprocess +import csv +from enum import Enum +from pathlib import Path +from os import listdir +from os.path import isfile, join + +KICAD_CLI_PATH = "kicad-cli" +TEMP_FILE_PATH = "docs/" + +PCB_IMAGE_OUTPUT_PATH = "res/" + +PCB_PDF_OUTPUT_PATH = "docs/" +PCB_PDF_FILE_SUFFIX = "_pcb" + +SCHEMATIC_OUTPUT_PATH = "docs/" +SCHEMATIC_FILE_SUFFIX = "_schematic" + +BOM_OUTPUT_PATH = "docs/" +BOM_REPORT_NAME = "_bom" + +TEMP_DRC_REPORT_NAME = "_drc" +TEMP_ERC_REPORT_NAME = "_erc" + +# quiet +KICAD_CLI_STDOUT=subprocess.DEVNULL +# verbose +# KICAD_CLI_STDOUT=subprocess.STDOUT + +class OutputReportType(Enum): + JSON = 1 + REPORT = 2 + + def get_file_extension(self) -> str: + if (self == OutputReportType.JSON): + return "json" + if (self == OutputReportType.REPORT): + return "rpt" + return "txt" + +# this is a thin vale on the kicad cli tool +class KicadProject: + + def __init__(self, path : Path) -> None: + self.project_path = path.parent + self.project_name = path.name.removesuffix(".kicad_pro") + self.created_files : list[Path] = [] + print(f"{self.project_path=}") + print(f"{self.project_name=}") + + def erc_check( + self, + report_format : OutputReportType = OutputReportType.JSON, + return_report : bool = False + ) -> None | dict | str: + format_type = report_format.name.lower() + pcb_file_path = self.project_path / f"{self.project_name}.kicad_sch" + erc_report_path = Path(TEMP_FILE_PATH) / f"{self.project_name}{TEMP_ERC_REPORT_NAME}.{report_format.get_file_extension()}" + + retcode = subprocess.call( + f'{KICAD_CLI_PATH} sch erc {pcb_file_path} --output {erc_report_path} --format {format_type}', + shell=True, + stdout=KICAD_CLI_STDOUT + ) + + if (retcode != 0): + print(f"erc check failed return code {retcode}") + exit(1) + + self.created_files.append(erc_report_path) + + if (return_report): + with open(erc_report_path, "r") as txt: + if format_type == OutputReportType.JSON: + return json.loads(txt.read()) + if format_type == OutputReportType.RPT: + return txt.read() + + def drc_check( + self, + report_format : OutputReportType = OutputReportType.JSON, + return_report : bool = False + ) -> None | dict | str: + format_type = report_format.name.lower() + pcb_file_path = self.project_path / f"{self.project_name}.kicad_pcb" + drc_report_path = Path(TEMP_FILE_PATH) / f"{self.project_name}{TEMP_DRC_REPORT_NAME}.{report_format.get_file_extension()}" + print(f"{format_type=}, {drc_report_path=}") + retcode = subprocess.call( + f'{KICAD_CLI_PATH} pcb drc {pcb_file_path} --output {drc_report_path} --format {format_type}', + shell=True, + stdout=KICAD_CLI_STDOUT + ) + print(f"{retcode=}") + if (retcode != 0): + print(f"drc check failed return code {retcode}") + exit(1) + + self.created_files.append(drc_report_path) + + if (return_report): + with open(drc_report_path, "r") as txt: + if format_type == OutputReportType.JSON: + return json.loads(txt.read()) + if format_type == OutputReportType.RPT: + return txt.read() + + def process_bom( + self, + return_csv : bool = False + ) -> None | list[list[str]]: + sch_file_path = self.project_path / f"{self.project_name}.kicad_sch" + bom_output_path = Path(BOM_OUTPUT_PATH) / f"{self.project_name}{BOM_REPORT_NAME}.csv" + retcode = subprocess.call( + f'{KICAD_CLI_PATH} sch export bom {sch_file_path} --output {bom_output_path}', + shell=True, + stdout=KICAD_CLI_STDOUT + ) + + if (retcode != 0): + print(f"process_bom failed return code {retcode}") + exit(1) + + self.created_files.append(bom_output_path) + if (return_csv): + with open(bom_output_path, "r") as csvfile: + bom_csv = csv.reader(csvfile, delimiter=',', quotechar='"') + return [row for row in bom_csv] + + def get_image(self, + image_type : str = "png", + height : int = 900, + width : int = 1600, + side : str = "top", + background : str = "default", + preset : str = "follow_pcb_editor", + zoom : int = 2, + ) -> None: + """ + image_typ = "png" | "jpg" + side = "top" | "bottom" | "left" | "right" | "front" | "back" + background = "default" | "transparent" | "opaque" + """ + pcb_file_path = self.project_path / f"{self.project_name}.kicad_pcb" + render_output_path = Path(PCB_IMAGE_OUTPUT_PATH) / f"{self.project_name}_render.{image_type}" + retcode = subprocess.call( + f'{KICAD_CLI_PATH} pcb render {pcb_file_path} --output {render_output_path} --preset {preset} --zoom {zoom} ', + shell=True, + stdout=KICAD_CLI_STDOUT + ) + + if (retcode != 0): + print(f"get_image failed return code {retcode}") + exit(1) + + self.created_files.append(render_output_path) + + # i am not giving you the pdf to output if you want to do that yourself go ahead + def create_schmatic_pdf(self) -> None: + sch_file_path = self.project_path / f"{self.project_name}.kicad_sch" + sch_report_path = Path(SCHEMATIC_OUTPUT_PATH) / f"{self.project_name}{SCHEMATIC_FILE_SUFFIX}.pdf" + retcode = subprocess.call( + f'{KICAD_CLI_PATH} sch export pdf {sch_file_path} --output {sch_report_path}', + shell=True, + stdout=KICAD_CLI_STDOUT + ) + + if (retcode != 0): + print(f"create_schmatic_pdf failed return code {retcode}") + exit(1) + + self.created_files.append(sch_report_path) + + def create_pcb_pdf(self, layers : list[str] = ["F.Cu", "B.Cu"]) -> None: + pcb_file_path = self.project_path / f"{self.project_name}.kicad_pcb" + pcb_report_path = Path(PCB_PDF_OUTPUT_PATH) / f"{self.project_name}{PCB_PDF_FILE_SUFFIX}.pdf" + retcode = subprocess.call( + f'{KICAD_CLI_PATH} pcb export pdf {pcb_file_path} --output {pcb_report_path} --layers {",".join(layers)}', + shell=True, + stdout=KICAD_CLI_STDOUT + ) + + if (retcode != 0): + print(f"create_pcb_pdf failed return code {retcode}") + exit(1) + + self.created_files.append(pcb_report_path) + +def commit_files(files: list[Path], commit_message : str) -> None: + for file in files: + # add & commit, could use the return code however these should never fail + print(f"adding and commiting {file}") + ret_add = subprocess.call(f"git add {file}", shell=True) + ret_commit = subprocess.call(f"git commit -m \"{commit_message}\"", shell=True) + +def main() -> None: + # find all kicad project files to operate on + for path in Path(".").rglob('*.kicad_pro'): + k = KicadProject(path) + k.drc_check(report_format= OutputReportType.REPORT) + k.erc_check(report_format= OutputReportType.REPORT) + k.process_bom() + k.create_schmatic_pdf() + k.get_image() + commit_files(k.created_files, "auto commited") + +if __name__ == "__main__": + try: + main() + exit(0) + except Exception as e: + exit(1)