#!/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 = [] all_part_properties = {} # uuid -> {property_name: value} # 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 and read properties # Track properties: Value, MPN, Manufacturer, IPN lines = content.split('\n') in_symbol = False current_uuid = None current_ref = None current_lib_id = None current_properties = {} # Collect properties for this symbol has_dnp = False # Track line depth to know when we're at symbol level symbol_start_indent = None for i, line in enumerate(lines): stripped = line.strip() indent_level = len(line) - len(line.lstrip()) # Detect start of symbol if stripped.startswith('(symbol'): in_symbol = True current_uuid = None current_ref = None current_lib_id = None current_properties = {} has_dnp = False symbol_uuid_found = False # Track if we found the main symbol UUID symbol_start_indent = indent_level # Detect end of symbol - closing paren at same indent as symbol start elif in_symbol and stripped == ')' and indent_level == symbol_start_indent: # 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: # Store properties for this part if current_properties: if current_uuid not in all_part_properties: all_part_properties[current_uuid] = {} all_part_properties[current_uuid].update(current_properties) all_part_properties[current_uuid]['reference'] = current_ref # For display # Track DNP if has_dnp: 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 and extract UUID # DNP line comes before UUID, so we look forward for the UUID elif in_symbol and '(dnp' in stripped and not symbol_uuid_found: # Check if DNP is set if '(dnp yes)' in stripped or (stripped == '(dnp)'): has_dnp = True # 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] # Extract tracked properties elif in_symbol: for prop_name in ['Value', 'MPN', 'Manufacturer', 'IPN']: if f'(property "{prop_name}"' in line: parts = line.split('"') if len(parts) >= 4: current_properties[prop_name] = parts[3] break # 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 and property changes print(f"\nUpdating variant '{active_variant}'...") print(f" Found {len(all_uuids)} total UUIDs") print(f" Found {len(all_dnp_uuids)} DNP UUIDs") print(f" Found {len(all_part_properties)} parts with tracked properties") if active_variant not in manager.variants["variants"]: print(f" Error: Variant '{active_variant}' not found in variants") return False # Update DNP list manager.variants["variants"][active_variant]["dnp_parts"] = sorted(all_dnp_uuids) # Update property overrides based on schematic values property_changes = 0 for uuid, sch_properties in all_part_properties.items(): # Get expected properties (base + variant overrides) expected_props = manager.get_part_properties(active_variant, uuid) # Check each tracked property for prop_name in ['Value', 'MPN', 'Manufacturer', 'IPN']: sch_value = sch_properties.get(prop_name, '') expected_value = expected_props.get(prop_name, '') # If schematic value differs from expected, update the variant if sch_value and sch_value != expected_value: # Check if we have a base value for this property base_value = manager.variants.get("base_values", {}).get(uuid, {}).get(prop_name, '') if not base_value: # No base value exists, so set it manager.set_base_value(uuid, prop_name, sch_value) print(f" Set base {prop_name} for {sch_properties.get('reference', uuid)}: {sch_value}") elif sch_value != base_value: # Base value exists but differs - store as override if "part_overrides" not in manager.variants["variants"][active_variant]: manager.variants["variants"][active_variant]["part_overrides"] = {} if uuid not in manager.variants["variants"][active_variant]["part_overrides"]: manager.variants["variants"][active_variant]["part_overrides"][uuid] = {} manager.variants["variants"][active_variant]["part_overrides"][uuid][prop_name] = sch_value property_changes += 1 print(f" Override {prop_name} for {sch_properties.get('reference', uuid)}: {sch_value} (base: {base_value})") # Save once at the end manager._save_variants() 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)}") print(f" Property overrides: {property_changes}") 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)