generated from sirlilpanda/kicad-project-template-actionless
Initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
temp/
|
||||||
221
.hooks/kicad_cli_tools.py
Normal file
221
.hooks/kicad_cli_tools.py
Normal 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
23
README.md
Normal 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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
1
docs/test_bom.csv
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"Refs","Value","Footprint","Qty","DNP"
|
||||||
|
13
docs/test_drc.rpt
Normal file
13
docs/test_drc.rpt
Normal 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
5
docs/test_erc.rpt
Normal 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
BIN
docs/test_schematic.pdf
Normal file
Binary file not shown.
2
hardware/test/test.kicad_pcb
Normal file
2
hardware/test/test.kicad_pcb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
(kicad_pcb (version 20241229) (generator "pcbnew") (generator_version "9.0")
|
||||||
|
)
|
||||||
32
hardware/test/test.kicad_pro
Normal file
32
hardware/test/test.kicad_pro
Normal 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": {}
|
||||||
|
}
|
||||||
14
hardware/test/test.kicad_sch
Normal file
14
hardware/test/test.kicad_sch
Normal 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
BIN
res/test_render.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
28
setup.py
Normal file
28
setup.py
Normal 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)
|
||||||
Reference in New Issue
Block a user