Files
kicad-manager/sync_variant.py
brentperteet ab8d5c0c14 added windows interaction test
adding more variant stuff
2026-02-22 13:31:14 -06:00

330 lines
14 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 = []
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 <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)