#!/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 [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)