This commit is contained in:
34
kibot/scripts/bootstrap.sh
Normal file
34
kibot/scripts/bootstrap.sh
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
# ============================================================================
|
||||
# KiBot Template Bootstrapping Script
|
||||
# ============================================================================
|
||||
|
||||
# Create the Development Branch
|
||||
git checkout -qb dev
|
||||
echo "Created Development Branch"
|
||||
|
||||
# Add the Asymworks KiCad Library
|
||||
git remote add -qf asymworks-kicad-lib https://git.asymworks.com/asymworks/kicad-library
|
||||
git subtree add -q --prefix lib/asymworks asymworks-kicad-lib main --squash -m "[bootstrap] Add subtree 'Asymworks/KiCad-Library' at 'lib/asymworks'"
|
||||
echo "Added Asymworks KiCad Library"
|
||||
|
||||
# Initialize the Project Metadata
|
||||
python kibot/scripts/metadata.py init
|
||||
python kibot/scripts/metadata.py update Asymworks_Template.kicad_pro
|
||||
ASSY_NUMBER=$(python kibot/scripts/metadata.py print AssemblyNumber)
|
||||
|
||||
# Rename the KiCad Project based on the directory name
|
||||
git mv -q Asymworks_Template.kicad_dru "${ASSY_NUMBER}.kicad_dru"
|
||||
git mv -q Asymworks_Template.kicad_pro "${ASSY_NUMBER}.kicad_pro"
|
||||
git mv -q Asymworks_Template.kicad_sch "${ASSY_NUMBER}.kicad_sch"
|
||||
git mv -q Asymworks_Template.kicad_pcb "${ASSY_NUMBER}.kicad_pcb"
|
||||
echo "Renamed project to \"${ASSY_NUMBER}.kicad_pro\""
|
||||
|
||||
# Push the Develpment Branch to Gitea
|
||||
git add -q project.json
|
||||
git commit -aq -m 'Initialize Project'
|
||||
git push -qu origin dev
|
||||
echo "Initialized and Committed Project"
|
||||
|
||||
# Bootstrap Complete
|
||||
echo "Bootstrap complete. Please run 'git pull' after the CI action finishes.'"
|
||||
49
kibot/scripts/get_sheet_title.py
Normal file
49
kibot/scripts/get_sheet_title.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import argparse
|
||||
import xml.etree.ElementTree as ET
|
||||
import sys
|
||||
|
||||
def get_sheet_title(file_path, page_number, dots_number):
|
||||
try:
|
||||
tree = ET.parse(file_path)
|
||||
root = tree.getroot()
|
||||
|
||||
page_number = str(page_number)
|
||||
titles = []
|
||||
|
||||
for sheet in root.findall(".//sheet"):
|
||||
number = sheet.get("number")
|
||||
if number == page_number:
|
||||
# Get the last part of the 'name' attribute after '/'
|
||||
name = sheet.get("name")
|
||||
title_block = sheet.find("title_block")
|
||||
title = title_block.find("title").text if title_block is not None else None
|
||||
if name:
|
||||
titles.append(name.split("/")[-2 if name.endswith("/") else -1])
|
||||
|
||||
if not titles:
|
||||
print('.'*dots_number)
|
||||
|
||||
elif len(set(titles)) > 1:
|
||||
print("Conflicting page numbers")
|
||||
else:
|
||||
print(titles[0])
|
||||
except ET.ParseError:
|
||||
print("Error: Invalid XML format")
|
||||
except FileNotFoundError:
|
||||
print("Error: XML File not found")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get the sheet title based on page number from a KiCad XML file")
|
||||
parser.add_argument("-p", "--page-number", type=int, required=True, help="Page number to search")
|
||||
parser.add_argument("-f", "--file", type=str, required=True, help="Path to the schematic XML file")
|
||||
parser.add_argument("-d", "--dots-number", type=int, required=True, help="Number of dots for empty lines")
|
||||
|
||||
args = parser.parse_args()
|
||||
get_sheet_title(args.file, args.page_number, args.dots_number)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
257
kibot/scripts/metadata.py
Normal file
257
kibot/scripts/metadata.py
Normal file
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/python3
|
||||
#
|
||||
# Script to maintain the project.json metadata file
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
@dataclass
|
||||
class ProjectMetadata:
|
||||
AssemblyName: str = ''
|
||||
AssemblyNumber: str = ''
|
||||
AssemblyTitle: str = ''
|
||||
Company: str = 'Asymworks, LLC'
|
||||
Designer: str = 'JPK'
|
||||
FabNumber: str = ''
|
||||
FabTitle: str = ''
|
||||
ProjectNumber: str = ''
|
||||
SchematicNumber: str = ''
|
||||
SchematicTitle: str = ''
|
||||
|
||||
|
||||
#: Mapping from `ProjectMetadata` key to KiCad Text Variable name
|
||||
TEXT_VARS = {
|
||||
'AssemblyName': 'ASSEMBLY_NAME',
|
||||
'AssemblyNumber': 'ASSEMBLY_NUMBER',
|
||||
'AssemblyTitle': 'DWG_TITLE_ASSY',
|
||||
'Company': 'COMPANY',
|
||||
'Designer': 'DESIGNER',
|
||||
'FabNumber': 'DWG_NUMBER_PCB',
|
||||
'FabTitle': 'DWG_TITLE_PCB',
|
||||
'ProjectNumber': 'PROJECT_CODE',
|
||||
'SchematicNumber': 'DWG_NUMBER_SCH',
|
||||
'SchematicTitle': 'DWG_TITLE_SCH',
|
||||
}
|
||||
|
||||
|
||||
def read_input(
|
||||
prompt: str,
|
||||
default: str | None = None,
|
||||
*,
|
||||
format_help: str | None = None,
|
||||
optional: bool = False,
|
||||
regex: str | re.Pattern | None = None,
|
||||
use_color: bool = True
|
||||
) -> str | None:
|
||||
"""Read an input string from the console, with defaults and retry."""
|
||||
prompt_str = f'{prompt} [{default or None}]'
|
||||
if use_color:
|
||||
prompt_str = f'{prompt} [\033[1;34m{default or None}\033[0m]'
|
||||
|
||||
check_re = None
|
||||
if isinstance(regex, re.Pattern):
|
||||
check_re = regex
|
||||
elif isinstance(regex, str):
|
||||
check_re = re.compile(regex, re.I)
|
||||
|
||||
error = None
|
||||
while True:
|
||||
error_str = f'\033[31m({error})\033[0m ' if error is not None else ''
|
||||
value = input(f'{error_str}{prompt_str}: ')
|
||||
if not value and not default and not optional:
|
||||
error = 'input required'
|
||||
continue
|
||||
|
||||
if not value:
|
||||
value = default
|
||||
|
||||
if check_re and not check_re.match(value):
|
||||
error = 'invalid format' if not format_help else format_help
|
||||
continue
|
||||
|
||||
break
|
||||
|
||||
if not value and optional:
|
||||
return None
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def load_file(filename: str) -> ProjectMetadata:
|
||||
"""Load the Project Metadata from a JSON File."""
|
||||
with open(filename, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
return ProjectMetadata(**data)
|
||||
|
||||
|
||||
def load_interactive(defaults: ProjectMetadata | None = None) -> ProjectMetadata:
|
||||
"""Load the Project Metadata from an interactive console questionnaire."""
|
||||
md = defaults or ProjectMetadata()
|
||||
|
||||
# Get the Company and Designer information first.
|
||||
md.Company = read_input('Company', md.Company)
|
||||
md.Designer = read_input('Designer', md.Designer)
|
||||
|
||||
# Next get the Assembly Number and Name; this will allow us to
|
||||
# generate sensible defaults for the rest.
|
||||
md.AssemblyNumber = read_input('Assembly Number', md.AssemblyNumber)
|
||||
md.AssemblyName = read_input('Assembly Name', md.AssemblyName)
|
||||
|
||||
if not md.AssemblyTitle:
|
||||
md.AssemblyTitle = f'Assembly, {md.AssemblyName}'
|
||||
|
||||
if not md.FabNumber:
|
||||
md.FabNumber = f'P{md.AssemblyNumber[1:]}'
|
||||
if not md.FabTitle:
|
||||
md.FabTitle = f'PCB Fabrication, {md.AssemblyName}'
|
||||
|
||||
if not md.SchematicNumber:
|
||||
md.SchematicNumber = f'S{md.AssemblyNumber[1:]}'
|
||||
if not md.SchematicTitle:
|
||||
md.SchematicTitle = f'Schematic, {md.AssemblyName}'
|
||||
|
||||
md.AssemblyTitle = read_input('Assembly Drawing Title', md.AssemblyTitle)
|
||||
md.FabNumber = read_input('Fabrication Drawing Number', md.FabNumber)
|
||||
md.FabTitle = read_input('Fabrication Drawing Title', md.FabTitle)
|
||||
md.SchematicNumber = read_input('Schematic Drawing Number', md.SchematicNumber)
|
||||
md.SchematicTitle = read_input('Schematic Drawing Title', md.SchematicTitle)
|
||||
|
||||
# Finally load the Project Number
|
||||
if not md.ProjectNumber:
|
||||
md.ProjectNumber = f'P{md.AssemblyNumber[1:3]}'
|
||||
|
||||
md.ProjectNumber = read_input(
|
||||
'Project Number',
|
||||
md.ProjectNumber,
|
||||
format_help='Format as P##',
|
||||
regex=r'^P[0-9]{2}$',
|
||||
)
|
||||
|
||||
return md
|
||||
|
||||
|
||||
def run_init(filename: str) -> int:
|
||||
"""Run the `init` subcommand to initialize the Metadata."""
|
||||
md = None
|
||||
if os.path.isfile(filename):
|
||||
md = load_file(filename)
|
||||
|
||||
md = load_interactive(md)
|
||||
with open(filename, 'w') as f:
|
||||
json.dump(asdict(md), f, indent=2)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def run_print(filename: str, variable: str) -> int:
|
||||
"""Run the `print` subcommand to echo Metadata to `stdout`."""
|
||||
try:
|
||||
md = load_file(filename)
|
||||
except FileNotFoundError:
|
||||
sys.stderr.write(f'{filename} not found\n')
|
||||
return 1
|
||||
except TypeError as e:
|
||||
sys.stderr.write(f'{filename} has unexpected data\n {str(e)}\n')
|
||||
return 1
|
||||
|
||||
var_lcase = variable.lower()
|
||||
for md_key, txt_var in TEXT_VARS.items():
|
||||
if var_lcase != md_key.lower() and var_lcase != txt_var.lower():
|
||||
continue
|
||||
|
||||
var_found = True
|
||||
if not hasattr(md, md_key):
|
||||
sys.stderr.write(f'Internal error: {md_key} not defined in metadata')
|
||||
return 2
|
||||
|
||||
sys.stdout.write(getattr(md, md_key))
|
||||
return 0
|
||||
|
||||
sys.stderr.write(f'{variable} is not a valid Metadata variable\n')
|
||||
return 1
|
||||
|
||||
def run_update(filename: str, kicad_project: str) -> int:
|
||||
"""Run the `update` subcommand to update the KiCad Project."""
|
||||
try:
|
||||
md = load_file(filename)
|
||||
except FileNotFoundError:
|
||||
sys.stderr.write(f'{filename} not found\n')
|
||||
return 1
|
||||
except TypeError as e:
|
||||
sys.stderr.write(f'{filename} has unexpected data\n {str(e)}\n')
|
||||
return 1
|
||||
|
||||
kicad_filepath = os.path.abspath(kicad_project)
|
||||
if not os.path.isfile(kicad_filepath):
|
||||
sys.stderr.write(f'{kicad_filepath} does not exist\n')
|
||||
return 1
|
||||
|
||||
if os.path.splitext(kicad_filepath)[1] != '.kicad_pro':
|
||||
sys.stderr.write(f'{kicad_filepath} is not a KiCad Project file\n')
|
||||
return 1
|
||||
|
||||
kicad_data = None
|
||||
try:
|
||||
with open(kicad_filepath, 'r') as f:
|
||||
kicad_data = json.load(f)
|
||||
|
||||
if 'text_variables' not in kicad_data:
|
||||
sys.stderr.write(f'Warning: "text-variables" is not set in KiCad Project')
|
||||
|
||||
for md_key, txt_var in TEXT_VARS.items():
|
||||
if not hasattr(md, md_key):
|
||||
sys.stderr.write(f'Internal error: {md_key} not defined in metadata')
|
||||
return 2
|
||||
|
||||
if txt_var not in kicad_data['text_variables']:
|
||||
sys.stderr.write(f'Warning: creating KiCad text variable {txt_var}')
|
||||
|
||||
kicad_data['text_variables'][txt_var] = getattr(md, md_key)
|
||||
|
||||
shutil.copy(kicad_filepath, f'{kicad_filepath}-bak')
|
||||
with open(kicad_filepath, 'w') as f:
|
||||
json.dump(kicad_data, f, indent=2)
|
||||
|
||||
except FileNotFoundError:
|
||||
sys.stderr.write(f'{kicad_filepath} does not exist\n')
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Script to maintain the project.json metadata file')
|
||||
parser.add_argument('-f', '--filename', type=str, default='project.json', help='Project Metadata filename (default is "project.json")')
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', metavar='command', help='subcommands')
|
||||
|
||||
init_parser = subparsers.add_parser('init', help='Initialize the Project Metadata')
|
||||
print_parser = subparsers.add_parser('print', help='Print Metadata values')
|
||||
print_parser.add_argument('variable', help='Metadata variable name to print')
|
||||
update_parser = subparsers.add_parser('update', help='Update the KiCad Project with the Metadata')
|
||||
update_parser.add_argument('kicad_project', help='KiCad project to update with the Metadata')
|
||||
|
||||
# parser.add_argument('-i', '--interactive', action='store_true', help='Run in interactive mode to prompt for each data field.')
|
||||
# parser.add_argument('-u', '--update', nargs=1, metavar='KICAD-PROJECT', help='Write the text variables in the KiCad Project file')
|
||||
|
||||
args = parser.parse_args()
|
||||
filepath = os.path.abspath(args.filename)
|
||||
|
||||
if args.command == 'init':
|
||||
sys.exit(run_init(filepath))
|
||||
|
||||
elif args.command == 'print':
|
||||
sys.exit(run_print(filepath, args.variable))
|
||||
|
||||
elif args.command == 'update':
|
||||
sys.exit(run_update(filepath, args.kicad_project))
|
||||
|
||||
sys.stderr.write(f'Unknown command: {args.command}\n')
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user