initial commit

This commit is contained in:
brentperteet
2026-02-22 08:16:48 -06:00
commit 3f0aff923d
15 changed files with 4364 additions and 0 deletions

269
apply_variant.py Normal file
View File

@@ -0,0 +1,269 @@
#!/usr/bin/env python3
"""
apply_variant.py
================
Apply a variant to a KiCad schematic by setting DNP (Do Not Place) flags on components.
Uses KiCad CLI to modify the schematic.
"""
import sys
import json
import subprocess
from pathlib import Path
from variant_manager import VariantManager
def get_all_schematic_files(root_schematic: str) -> list:
"""
Get all schematic files in a hierarchical design.
Args:
root_schematic: Path to root .kicad_sch file
Returns:
List of all schematic file paths (including root)
"""
root_path = Path(root_schematic)
if not root_path.exists():
return [root_schematic]
schematic_files = [str(root_path)]
schematic_dir = root_path.parent
# Read root schematic to find sheet files
try:
with open(root_path, 'r', encoding='utf-8') as f:
content = f.read()
# Find all sheet file references
for line in content.split('\n'):
if '(property "Sheetfile"' in line:
parts = line.split('"')
if len(parts) >= 4:
sheet_file = parts[3]
sheet_path = schematic_dir / sheet_file
if sheet_path.exists():
# Recursively get sheets from this sheet
sub_sheets = get_all_schematic_files(str(sheet_path))
for sub in sub_sheets:
if sub not in schematic_files:
schematic_files.append(sub)
except Exception as e:
print(f"Warning: Error reading sheet files: {e}")
return schematic_files
def apply_variant_to_schematic(schematic_file: str, variant_name: str, kicad_cli: str = "kicad-cli") -> bool:
"""
Apply a variant to a schematic by setting DNP flags.
Handles hierarchical schematics by processing all sheets.
Args:
schematic_file: Path to root .kicad_sch file
variant_name: Name of variant to apply
kicad_cli: Path to kicad-cli executable
Returns:
True if successful, False otherwise
"""
manager = VariantManager(schematic_file)
if variant_name not in manager.get_variants():
print(f"Error: Variant '{variant_name}' not found")
return False
dnp_parts = manager.get_dnp_parts(variant_name)
print(f"Applying variant '{variant_name}' to {Path(schematic_file).name}")
print(f"DNP parts ({len(dnp_parts)}): {dnp_parts}")
# Get all schematic files (root + hierarchical sheets)
all_schematics = get_all_schematic_files(schematic_file)
print(f"Processing {len(all_schematics)} schematic file(s)")
overall_success = True
# Process each schematic file
for idx, sch_file in enumerate(all_schematics):
is_root = (idx == 0) # First file is the root schematic
if not process_single_schematic(sch_file, dnp_parts, variant_name, is_root):
overall_success = False
if overall_success:
print(f"\nVariant '{variant_name}' applied successfully")
print(f"Please reload the schematic in KiCad to see changes")
return overall_success
def process_single_schematic(schematic_file: str, dnp_uuids: list, variant_name: str = None, is_root: bool = False) -> bool:
"""
Process a single schematic file to apply DNP flags.
Args:
schematic_file: Path to .kicad_sch file
dnp_uuids: List of UUIDs that should be DNP
variant_name: Name of variant being applied (for title block)
is_root: True if this is the root schematic (not a sub-sheet)
Returns:
True if successful, False otherwise
"""
sch_path = Path(schematic_file)
if not sch_path.exists():
print(f"Error: Schematic file not found: {schematic_file}")
return False
print(f"\n Processing: {sch_path.name}")
try:
with open(sch_path, 'r', encoding='utf-8') as f:
content = f.read()
# Parse schematic and set DNP flags
# KiCad 9 schematic format uses S-expressions
# Component structure:
# (symbol
# (lib_id ...)
# (at ...)
# (uuid "...") <- UUID appears here
# (dnp no) <- DNP flag appears here
# (property "Reference" "U1" ...)
# ...
# )
lines = content.split('\n')
modified = False
# Update title block comment if this is the root schematic
if is_root and variant_name:
in_title_block = False
for i, line in enumerate(lines):
stripped = line.strip()
if stripped.startswith('(title_block'):
in_title_block = True
elif in_title_block and stripped == ')':
in_title_block = False
elif in_title_block and '(comment 1' in stripped:
# Update comment 1 with variant name
indent = line[:len(line) - len(line.lstrip())]
new_line = indent + f'(comment 1 "{variant_name}")'
if lines[i] != new_line:
lines[i] = new_line
modified = True
print(f" Updated title block: Variant = {variant_name}")
break
for i, line in enumerate(lines):
stripped = line.strip()
# Look for DNP lines
if '(dnp' in stripped and (stripped.startswith('(dnp') or '\t(dnp' in line or ' (dnp' in line):
# Find the UUID for this symbol by looking forward (UUID comes after DNP)
current_uuid = None
current_ref = None
is_power_symbol = False
# Look backward to check for power symbols
for j in range(i - 1, max(0, i - 10), -1):
if '(lib_id' in lines[j] and 'power:' in lines[j]:
is_power_symbol = True
break
if lines[j].strip().startswith('(symbol'):
break
# Skip power symbols
if is_power_symbol:
continue
# Look forward for UUID (it comes right after DNP in the structure)
for j in range(i + 1, min(len(lines), i + 10)):
if '(uuid' in lines[j]:
# Extract UUID from line like: (uuid "681abb84-6eb2-4c95-9a2f-a9fc19a34beb")
# Make sure it's at symbol level (minimal indentation)
if '\t(uuid' in lines[j] or ' (uuid' in lines[j]:
uuid_parts = lines[j].split('"')
if len(uuid_parts) >= 2:
current_uuid = uuid_parts[1]
break
# Stop if we hit properties or other structures
if '(property "Reference"' in lines[j]:
break
# Look forward for reference (for logging purposes)
for j in range(i + 1, min(len(lines), i + 20)):
if '(property "Reference"' in lines[j]:
ref_parts = lines[j].split('"')
if len(ref_parts) >= 4:
current_ref = ref_parts[3]
# Also skip if reference starts with #
if current_ref.startswith('#'):
is_power_symbol = True
break
if lines[j].strip().startswith('(symbol') or (lines[j].strip() == ')' and len(lines[j]) - len(lines[j].lstrip()) <= len(line) - len(line.lstrip())):
break
if current_uuid and not is_power_symbol:
# Get indentation
indent = line[:len(line) - len(line.lstrip())]
# Check if this part should be DNP
should_be_dnp = current_uuid in dnp_uuids
# Determine what the DNP line should say
if should_be_dnp:
target_dnp = '(dnp yes)'
else:
target_dnp = '(dnp no)'
# Update DNP flag if it's different from target
if stripped != target_dnp.strip():
lines[i] = indent + target_dnp
modified = True
if should_be_dnp:
print(f" Set DNP: {current_ref if current_ref else current_uuid}")
else:
print(f" Cleared DNP: {current_ref if current_ref else current_uuid}")
if modified:
# Backup original file
backup_path = sch_path.with_suffix('.kicad_sch.bak')
# Remove old backup if it exists
if backup_path.exists():
backup_path.unlink()
# Create new backup
import shutil
shutil.copy2(sch_path, backup_path)
print(f" Backup created: {backup_path.name}")
# Write modified schematic
with open(sch_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
print(f" Updated successfully")
else:
print(f" No changes needed")
return True
except Exception as e:
print(f" Error: {e}")
return False
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python apply_variant.py <schematic.kicad_sch> <variant_name> [kicad-cli]")
sys.exit(1)
schematic = sys.argv[1]
variant = sys.argv[2]
kicad_cli = sys.argv[3] if len(sys.argv) > 3 else "kicad-cli"
success = apply_variant_to_schematic(schematic, variant, kicad_cli)
sys.exit(0 if success else 1)