commit b4e3a6602d9bac20e01575a0e21943c5444706ce Author: panda Date: Tue Mar 3 00:59:20 2026 -0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd78447 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +temp/ \ No newline at end of file diff --git a/.hooks/kicad_cli_tools.py b/.hooks/kicad_cli_tools.py new file mode 100644 index 0000000..36e9783 --- /dev/null +++ b/.hooks/kicad_cli_tools.py @@ -0,0 +1,221 @@ +# 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: + match self: + case OutputReportType.JSON: return "json" + case OutputReportType.REPORT: return "rpt" + # dont trust it + case _: 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, + additional_args : str = "" + ) -> None | dict | str: + format_type = report_format.name.lower() + sch_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 {sch_file_path} --output {erc_report_path} --format {format_type} {additional_args}', + 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, + additional_args : str = "" + ) -> 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} {additional_args}', + 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, + additional_args : str = "" + ) -> 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} {additional_args}', + 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, + additional_args : str = "" + ) -> 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} {additional_args}', + 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, additional_args="") -> 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} {additional_args}', + 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"], additional_args : str = "") -> 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)} {additional_args}', + 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) diff --git a/README.md b/README.md new file mode 100644 index 0000000..1fed5a8 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# How to use this template + +this template will auto: + - run erc checks + - run drc checks + - create schematic pdfs + - create BOM for each project + - create images of the current pcb for your readme like you can see below + +![rendered pcb](res/test_render.png) + +the code for creating all of this lives in `.hooks/` + +## setup +dependencies: + - python3.9+ (used for crossplatfrom scripting) + - kicad-cli + +to set up the hooks just run +``` +python setup.py +``` +This script will add a line in the `.git/hooks/pre-push` to auto run `.hooks/kicad_cli_tools.py` \ No newline at end of file diff --git a/docs/test_bom.csv b/docs/test_bom.csv new file mode 100644 index 0000000..bc40a48 --- /dev/null +++ b/docs/test_bom.csv @@ -0,0 +1 @@ +"Refs","Value","Footprint","Qty","DNP" diff --git a/docs/test_drc.rpt b/docs/test_drc.rpt new file mode 100644 index 0000000..67c1d1d --- /dev/null +++ b/docs/test_drc.rpt @@ -0,0 +1,13 @@ +** Drc report for test.kicad_pcb ** +** Created on 2026-02-18T17:38:29+1300 ** + +** Found 1 DRC violations ** +[invalid_outline]: Board has malformed outline (no edges found on Edge.Cuts layer) + Local override; error + @(0.0000 mm, 0.0000 mm): PCB + +** Found 0 unconnected pads ** + +** Found 0 Footprint errors ** + +** End of Report ** diff --git a/docs/test_erc.rpt b/docs/test_erc.rpt new file mode 100644 index 0000000..db4e72c --- /dev/null +++ b/docs/test_erc.rpt @@ -0,0 +1,5 @@ +ERC report (2026-02-18T17:38:29+1300, Encoding UTF8) + +***** Sheet / + + ** ERC messages: 0 Errors 0 Warnings 0 diff --git a/docs/test_schematic.pdf b/docs/test_schematic.pdf new file mode 100644 index 0000000..c7eb01d Binary files /dev/null and b/docs/test_schematic.pdf differ diff --git a/hardware/test/test.kicad_pcb b/hardware/test/test.kicad_pcb new file mode 100644 index 0000000..c547853 --- /dev/null +++ b/hardware/test/test.kicad_pcb @@ -0,0 +1,2 @@ +(kicad_pcb (version 20241229) (generator "pcbnew") (generator_version "9.0") +) \ No newline at end of file diff --git a/hardware/test/test.kicad_pro b/hardware/test/test.kicad_pro new file mode 100644 index 0000000..f8c65e3 --- /dev/null +++ b/hardware/test/test.kicad_pro @@ -0,0 +1,32 @@ +{ + "board": { + "design_settings": { + "defaults": {}, + "diff_pair_dimensions": [], + "drc_exclusions": [], + "rules": {}, + "track_widths": [], + "via_dimensions": [] + } + }, + "boards": [], + "libraries": { + "pinned_footprint_libs": [], + "pinned_symbol_libs": [] + }, + "meta": { + "filename": "kicad.kicad_pro", + "version": 1 + }, + "net_settings": { + "classes": [], + "meta": { + "version": 0 + } + }, + "pcbnew": { + "page_layout_descr_file": "" + }, + "sheets": [], + "text_variables": {} +} diff --git a/hardware/test/test.kicad_sch b/hardware/test/test.kicad_sch new file mode 100644 index 0000000..5bc70ed --- /dev/null +++ b/hardware/test/test.kicad_sch @@ -0,0 +1,14 @@ +(kicad_sch + (version 20250114) + (generator "eeschema") + (generator_version "9.0") + (uuid f507f841-113f-488d-81ff-25ffa9ae803f) + (paper "A4") + (lib_symbols) + (sheet_instances + (path "/" + (page "1") + ) + ) + (embedded_fonts no) +) \ No newline at end of file diff --git a/res/test_render.png b/res/test_render.png new file mode 100644 index 0000000..dd162fd Binary files /dev/null and b/res/test_render.png differ diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..db860b1 --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +import sys +import os +import stat +from pathlib import Path + +PYTHON_BIN = sys.executable +HOOK_SCRIPT_PATH = Path(".hooks/kicad_cli_tools.py") +HOOK_TYPE = "pre-push" + +HOOK_PATH = Path(f".git/hooks/{HOOK_TYPE}") + +# used if the hook path already exists we dont want to over write anything you have in there +# we would append however all of the default hooks have and `exit 0` which means it wont run +OLD_HOOK_PATH = Path(f".git/hooks/{HOOK_TYPE}-old") + +if (os.path.exists(HOOK_PATH)): + os.rename(HOOK_PATH, OLD_HOOK_PATH) + +with open(HOOK_PATH, "w") as txt: + txt.writelines([ + "#!/bin/sh\n", #shebang + f"{PYTHON_BIN} {HOOK_SCRIPT_PATH}\n" + "exit 0\n" #make sure she closes + ]) + +# make sure its executable +st = os.stat(HOOK_PATH) +os.chmod(HOOK_PATH, st.st_mode | stat.S_IEXEC) \ No newline at end of file