270 lines
9.7 KiB
Python
270 lines
9.7 KiB
Python
#!/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)
|