Initial commit

This commit is contained in:
2026-03-03 00:59:20 -08:00
commit b4e3a6602d
12 changed files with 340 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
temp/

221
.hooks/kicad_cli_tools.py Normal file
View File

@@ -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)

23
README.md Normal file
View File

@@ -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`

1
docs/test_bom.csv Normal file
View File

@@ -0,0 +1 @@
"Refs","Value","Footprint","Qty","DNP"
1 Refs Value Footprint Qty DNP

13
docs/test_drc.rpt Normal file
View File

@@ -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 **

5
docs/test_erc.rpt Normal file
View File

@@ -0,0 +1,5 @@
ERC report (2026-02-18T17:38:29+1300, Encoding UTF8)
***** Sheet /
** ERC messages: 0 Errors 0 Warnings 0

BIN
docs/test_schematic.pdf Normal file

Binary file not shown.

View File

@@ -0,0 +1,2 @@
(kicad_pcb (version 20241229) (generator "pcbnew") (generator_version "9.0")
)

View File

@@ -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": {}
}

View File

@@ -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)
)

BIN
res/test_render.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

28
setup.py Normal file
View File

@@ -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)