initial commit
This commit is contained in:
278
sync_variant.py
Normal file
278
sync_variant.py
Normal file
@@ -0,0 +1,278 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user