330 lines
14 KiB
Python
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)
|