279 lines
11 KiB
Python
279 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
sync_variant.py
|
|
===============
|
|
Sync variant from KiCad schematic - read DNP flags and update variant data.
|
|
|
|
This script reads the current state of DNP flags from the schematic and updates
|
|
the active variant to match.
|
|
"""
|
|
|
|
import sys
|
|
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 sync_variant_from_schematic(schematic_file: str, target_variant: str = None) -> bool:
|
|
"""
|
|
Sync variant from schematic DNP flags and title block.
|
|
|
|
Args:
|
|
schematic_file: Path to .kicad_sch file
|
|
target_variant: Specific variant to sync to (optional). If not provided, uses title block or active variant.
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
manager = VariantManager(schematic_file)
|
|
|
|
# If target_variant specified, use that
|
|
if target_variant:
|
|
if target_variant not in manager.get_variants():
|
|
print(f"Error: Variant '{target_variant}' not found")
|
|
return False
|
|
active_variant = target_variant
|
|
print(f"Syncing to specified variant: {active_variant}")
|
|
else:
|
|
# Read variant name from title block in root schematic
|
|
variant_from_title = None
|
|
sch_path = Path(schematic_file)
|
|
if sch_path.exists():
|
|
try:
|
|
with open(sch_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
lines = content.split('\n')
|
|
in_title_block = False
|
|
for line in lines:
|
|
stripped = line.strip()
|
|
if stripped.startswith('(title_block'):
|
|
in_title_block = True
|
|
elif in_title_block and stripped == ')':
|
|
break
|
|
elif in_title_block and '(comment 1' in stripped:
|
|
# Extract variant name from comment 1
|
|
parts = line.split('"')
|
|
if len(parts) >= 2:
|
|
variant_from_title = parts[1]
|
|
print(f"Found variant in title block: {variant_from_title}")
|
|
break
|
|
except:
|
|
pass
|
|
|
|
# Use variant from title block if found, otherwise use active variant
|
|
if variant_from_title and variant_from_title in manager.get_variants():
|
|
active_variant = variant_from_title
|
|
manager.set_active_variant(variant_from_title)
|
|
print(f"Set active variant to: {active_variant}")
|
|
else:
|
|
active_variant = manager.get_active_variant()
|
|
print(f"Using active variant: {active_variant}")
|
|
|
|
# Get all schematic files (root + hierarchical sheets)
|
|
all_schematics = get_all_schematic_files(schematic_file)
|
|
print(f"Processing {len(all_schematics)} schematic file(s)")
|
|
|
|
all_dnp_uuids = []
|
|
all_uuids = []
|
|
|
|
# Process each schematic file
|
|
for sch_file in all_schematics:
|
|
sch_path = Path(sch_file)
|
|
if not sch_path.exists():
|
|
print(f"Warning: Schematic file not found: {sch_file}")
|
|
continue
|
|
|
|
print(f"\n Processing: {sch_path.name}")
|
|
|
|
try:
|
|
with open(sch_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# Parse schematic to find DNP components
|
|
lines = content.split('\n')
|
|
in_symbol = False
|
|
current_uuid = None
|
|
current_ref = None
|
|
current_lib_id = None
|
|
has_dnp = False
|
|
|
|
# Track line depth to know when we're at symbol level
|
|
for i, line in enumerate(lines):
|
|
stripped = line.strip()
|
|
|
|
# Detect start of symbol
|
|
if stripped.startswith('(symbol'):
|
|
in_symbol = True
|
|
current_uuid = None
|
|
current_ref = None
|
|
current_lib_id = None
|
|
has_dnp = False
|
|
symbol_uuid_found = False # Track if we found the main symbol UUID
|
|
|
|
# Detect end of symbol
|
|
elif in_symbol and stripped == ')':
|
|
# Check if this symbol block is closing (simple heuristic)
|
|
# Skip power symbols
|
|
is_power = current_lib_id and 'power:' in current_lib_id
|
|
is_power = is_power or (current_ref and current_ref.startswith('#'))
|
|
|
|
if current_uuid and has_dnp and not is_power:
|
|
if current_uuid not in all_dnp_uuids:
|
|
all_dnp_uuids.append(current_uuid)
|
|
print(f" Found DNP: {current_ref if current_ref else current_uuid}")
|
|
in_symbol = False
|
|
|
|
# Extract lib_id to check for power symbols
|
|
elif in_symbol and '(lib_id' in stripped:
|
|
lib_parts = line.split('"')
|
|
if len(lib_parts) >= 2:
|
|
current_lib_id = lib_parts[1]
|
|
|
|
# Check for DNP flag - can be (dnp), (dnp yes), or (dnp no)
|
|
# Do this before UUID extraction so we know if we need the UUID
|
|
elif in_symbol and '(dnp' in stripped and not has_dnp:
|
|
# Only set has_dnp if it's (dnp) or (dnp yes), not (dnp no)
|
|
if '(dnp yes)' in stripped or (stripped == '(dnp)'):
|
|
has_dnp = True
|
|
# Now look forward for the UUID (it comes right after DNP)
|
|
for j in range(i + 1, min(len(lines), i + 5)):
|
|
if '(uuid' in lines[j]:
|
|
# Check it's at symbol level
|
|
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]
|
|
symbol_uuid_found = True
|
|
break
|
|
|
|
# Extract reference designator (for logging)
|
|
elif in_symbol and '(property "Reference"' in line and not current_ref:
|
|
# Extract reference from line like: (property "Reference" "R1"
|
|
parts = line.split('"')
|
|
if len(parts) >= 4:
|
|
current_ref = parts[3]
|
|
|
|
# Get all component UUIDs (excluding power symbols)
|
|
# Use same approach - look for UUID after DNP line
|
|
in_symbol = False
|
|
current_uuid = None
|
|
current_lib_id = None
|
|
current_ref = None
|
|
for i, line in enumerate(lines):
|
|
stripped = line.strip()
|
|
if stripped.startswith('(symbol'):
|
|
in_symbol = True
|
|
current_uuid = None
|
|
current_lib_id = None
|
|
current_ref = None
|
|
elif in_symbol and stripped == ')':
|
|
# Skip power symbols
|
|
is_power = current_lib_id and 'power:' in current_lib_id
|
|
is_power = is_power or (current_ref and current_ref.startswith('#'))
|
|
|
|
if current_uuid and not is_power and current_uuid not in all_uuids:
|
|
all_uuids.append(current_uuid)
|
|
in_symbol = False
|
|
elif in_symbol and '(lib_id' in stripped:
|
|
lib_parts = line.split('"')
|
|
if len(lib_parts) >= 2:
|
|
current_lib_id = lib_parts[1]
|
|
elif in_symbol and '(property "Reference"' in stripped and not current_ref:
|
|
ref_parts = line.split('"')
|
|
if len(ref_parts) >= 4:
|
|
current_ref = ref_parts[3]
|
|
elif in_symbol and '(dnp' in stripped and not current_uuid:
|
|
# Found DNP line - look forward for UUID
|
|
for j in range(i + 1, min(len(lines), i + 5)):
|
|
if '(uuid' in lines[j]:
|
|
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
|
|
|
|
except Exception as e:
|
|
print(f" Error processing {sch_path.name}: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
# Update variant with DNP list
|
|
print(f"\nUpdating variant '{active_variant}'...")
|
|
print(f" Found {len(all_uuids)} total UUIDs")
|
|
print(f" Found {len(all_dnp_uuids)} DNP UUIDs")
|
|
|
|
# Build the new DNP list directly instead of calling set_part_dnp multiple times
|
|
# This avoids multiple file saves
|
|
if active_variant in manager.variants["variants"]:
|
|
# Set the DNP list directly
|
|
manager.variants["variants"][active_variant]["dnp_parts"] = sorted(all_dnp_uuids)
|
|
|
|
# Save once at the end
|
|
manager._save_variants()
|
|
|
|
print(f" Updated DNP list with {len(all_dnp_uuids)} parts")
|
|
for uuid in all_dnp_uuids:
|
|
print(f" DNP UUID: {uuid}")
|
|
else:
|
|
print(f" Error: Variant '{active_variant}' not found in variants")
|
|
return False
|
|
|
|
print(f"\nVariant '{active_variant}' updated:")
|
|
print(f" Total components: {len(all_uuids)}")
|
|
print(f" DNP components: {len(all_dnp_uuids)}")
|
|
print(f" Fitted components: {len(all_uuids) - len(all_dnp_uuids)}")
|
|
|
|
return True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python sync_variant.py <schematic.kicad_sch> [variant_name]")
|
|
sys.exit(1)
|
|
|
|
schematic = sys.argv[1]
|
|
variant = sys.argv[2] if len(sys.argv) > 2 else None
|
|
success = sync_variant_from_schematic(schematic, variant)
|
|
sys.exit(0 if success else 1)
|