diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 913a2de..5e50759 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,16 @@ "Bash(for uuid in \"12ef4843-0c6b-44b3-b52b-21b354565dc0\" \"17a476c2-1017-41e7-9d81-f4153fe179f7\" \"25a5bbfc-04ad-4755-9a82-80d42d2cd8ce\")", "Bash(do echo \"=== UUID: $uuid ===\")", "Bash(grep -A 15 \"$uuid\" 25w.kicad_sch frequency.kicad_sch)", - "Bash(done)" + "Bash(done)", + "Bash(cat 25w.variants.json)", + "Bash(python sync_variant.py d:/tx/25w-kicad/25w/25w.kicad_sch default)", + "Bash(python -c \"\nimport json\nwith open(''25w.variants.json'', ''r'') as f:\n data = json.load(f)\n\n# Create V2 variant with different value for R1\ndata[''variants''][''V2''] = {\n ''name'': ''V2'',\n ''description'': ''Test variant with different R1 value'',\n ''dnp_parts'': [''4245045e-d174-4c56-920d-fabc03d1e234''],\n ''part_overrides'': {\n ''a2101067-196b-479f-99a2-dd5b5d884d82'': { # R1\n ''Value'': ''10M'' # Different from base 7.32M\n }\n }\n}\n\nwith open(''25w.variants.json'', ''w'') as f:\n json.dump(data, f, indent=2)\n\nprint(''Created V2 variant with R1 Value override'')\n\")", + "Bash(python apply_variant.py d:/tx/25w-kicad/25w/25w.kicad_sch V2)", + "Bash(python apply_variant.py d:/tx/25w-kicad/25w/25w.kicad_sch default)", + "Bash(python -c \"import pygetwindow; import pyautogui; print(''Libraries installed'')\")", + "Bash(pip install pygetwindow pyautogui)", + "Bash(python -c \"import pygetwindow; import pyautogui; print(''Libraries ready'')\")", + "Bash(python -c \"import win32gui; import win32con; print(''pywin32 installed'')\")" ], "deny": [], "ask": [] diff --git a/app.py b/app.py index 0530c4d..9fbdc1a 100644 --- a/app.py +++ b/app.py @@ -721,6 +721,145 @@ def handle_sync_from_schematic(): except Exception as e: emit('variant_error', {'error': str(e)}) +@socketio.on('test_window_interaction') +def handle_test_window_interaction(): + try: + import pygetwindow as gw + import pyautogui + import time + import win32gui + import win32con + + emit('window_test_status', {'status': 'Looking for KiCad schematic window...'}) + + # List all windows for debugging + all_windows = gw.getAllTitles() + emit('window_test_status', {'status': f'DEBUG: Found {len(all_windows)} windows total'}) + + # Find KiCad schematic editor window + windows = gw.getWindowsWithTitle('Schematic Editor') + emit('window_test_status', {'status': f'DEBUG: Found {len(windows)} windows with "Schematic Editor"'}) + + window_found = False + if not windows: + # Try alternative window title + schematic_windows = [w for w in all_windows if 'kicad' in w.lower() and 'schematic' in w.lower()] + emit('window_test_status', {'status': f'DEBUG: Found {len(schematic_windows)} windows with "kicad" and "schematic"'}) + + if schematic_windows: + emit('window_test_status', {'status': f'DEBUG: Using window: {schematic_windows[0]}'}) + windows = gw.getWindowsWithTitle(schematic_windows[0]) + window_found = len(windows) > 0 + else: + window_found = True + + # If window is found, close it + if window_found: + window = windows[0] + emit('window_test_status', {'status': f'Found window: "{window.title}"'}) + + # Get window position and size + hwnd = window._hWnd + rect = win32gui.GetWindowRect(hwnd) + x, y, x2, y2 = rect + width = x2 - x + height = y2 - y + emit('window_test_status', {'status': f'DEBUG: Window position=({x},{y}), size=({width}x{height})'}) + + # Click on the window's title bar to activate it (more reliable than SetForegroundWindow) + click_x = x + width // 2 + click_y = y + 10 # Title bar is usually at the top + emit('window_test_status', {'status': f'Clicking window at ({click_x}, {click_y}) to activate...'}) + pyautogui.click(click_x, click_y) + time.sleep(0.5) + + emit('window_test_status', {'status': 'Sending Ctrl+S (save)...'}) + pyautogui.hotkey('ctrl', 's') + time.sleep(1.0) + + emit('window_test_status', {'status': 'Attempting to close window...'}) + + # Method 1: Try WM_CLOSE message + emit('window_test_status', {'status': 'DEBUG: Sending WM_CLOSE message...'}) + win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0) + time.sleep(1.0) + + # Check if window still exists + if win32gui.IsWindow(hwnd): + emit('window_test_status', {'status': 'DEBUG: Window still exists after WM_CLOSE, trying to click and send Alt+F4...'}) + + # Click on window again to make sure it has focus + try: + rect = win32gui.GetWindowRect(hwnd) + x, y, x2, y2 = rect + click_x = x + (x2 - x) // 2 + click_y = y + 10 + pyautogui.click(click_x, click_y) + time.sleep(0.3) + except: + emit('window_test_status', {'status': 'DEBUG: Could not click window (may already be closed)'}) + + pyautogui.hotkey('alt', 'F4') + time.sleep(1.0) + + # Final check + if win32gui.IsWindow(hwnd): + emit('window_test_status', {'status': 'DEBUG: Window still exists after Alt+F4 - may need manual intervention'}) + else: + emit('window_test_status', {'status': 'DEBUG: Window closed successfully via Alt+F4'}) + else: + emit('window_test_status', {'status': 'DEBUG: Window closed successfully via WM_CLOSE'}) + else: + emit('window_test_status', {'status': 'No KiCad schematic window found - will open it'}) + + # Wait a couple seconds before reopening + emit('window_test_status', {'status': 'Waiting 2 seconds before reopening...'}) + time.sleep(2.0) + + # Reopen the schematic editor + schematic_file = app_args.get('Schematic File', '') + if not schematic_file: + emit('window_test_error', {'error': 'No schematic file specified in app arguments'}) + return + + emit('window_test_status', {'status': f'Relaunching schematic editor with: {schematic_file}'}) + + # Launch KiCad schematic editor + # The schematic editor executable is typically in the same directory as kicad.exe + import os + kicad_bin_dir = r"C:\Program Files\KiCad\9.0\bin" # Default KiCad 9 installation path + if not os.path.exists(kicad_bin_dir): + kicad_bin_dir = r"C:\Program Files\KiCad\8.0\bin" # Try KiCad 8 + + eeschema_exe = os.path.join(kicad_bin_dir, "eeschema.exe") + + if not os.path.exists(eeschema_exe): + emit('window_test_error', {'error': f'KiCad executable not found at: {eeschema_exe}'}) + return + + emit('window_test_status', {'status': f'DEBUG: Launching {eeschema_exe} {schematic_file}'}) + + # Launch KiCad with the schematic file + result = subprocess.Popen([eeschema_exe, schematic_file], shell=False) + emit('window_test_status', {'status': f'DEBUG: Process started with PID {result.pid}'}) + + time.sleep(2.0) + + # Verify the window opened + all_windows = gw.getAllTitles() + schematic_windows = [w for w in all_windows if 'kicad' in w.lower() and 'schematic' in w.lower()] + if schematic_windows: + emit('window_test_status', {'status': f'Successfully reopened schematic: {schematic_windows[0]}'}) + else: + emit('window_test_status', {'status': 'Schematic editor launched but window not detected yet'}) + + emit('window_test_complete', {'message': 'Window interaction test completed!'}) + except ImportError as e: + emit('window_test_error', {'error': f'Missing required library: {str(e)}. Please install: pip install pygetwindow pyautogui pywin32'}) + except Exception as e: + import traceback + emit('window_test_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'}) + def shutdown_server(): print("Server stopped") os._exit(0) diff --git a/apply_variant.py b/apply_variant.py index 698dbf7..9400267 100644 --- a/apply_variant.py +++ b/apply_variant.py @@ -76,8 +76,22 @@ def apply_variant_to_schematic(schematic_file: str, variant_name: str, kicad_cli dnp_parts = manager.get_dnp_parts(variant_name) + # Build property overrides dict: uuid -> {base + variant_overrides} + property_overrides = {} + base_values = manager.variants.get("base_values", {}) + variant_overrides = manager.variants["variants"][variant_name].get("part_overrides", {}) + + # Merge base + variant overrides for each part + all_uuids = set(list(base_values.keys()) + list(variant_overrides.keys())) + for uuid in all_uuids: + props = base_values.get(uuid, {}).copy() + props.update(variant_overrides.get(uuid, {})) + if props: + property_overrides[uuid] = props + print(f"Applying variant '{variant_name}' to {Path(schematic_file).name}") print(f"DNP parts ({len(dnp_parts)}): {dnp_parts}") + print(f"Property overrides for {len(property_overrides)} parts") # Get all schematic files (root + hierarchical sheets) all_schematics = get_all_schematic_files(schematic_file) @@ -88,7 +102,7 @@ def apply_variant_to_schematic(schematic_file: str, variant_name: str, kicad_cli # Process each schematic file for idx, sch_file in enumerate(all_schematics): is_root = (idx == 0) # First file is the root schematic - if not process_single_schematic(sch_file, dnp_parts, variant_name, is_root): + if not process_single_schematic(sch_file, dnp_parts, property_overrides, variant_name, is_root): overall_success = False if overall_success: @@ -98,19 +112,22 @@ def apply_variant_to_schematic(schematic_file: str, variant_name: str, kicad_cli return overall_success -def process_single_schematic(schematic_file: str, dnp_uuids: list, variant_name: str = None, is_root: bool = False) -> bool: +def process_single_schematic(schematic_file: str, dnp_uuids: list, property_overrides: dict = None, variant_name: str = None, is_root: bool = False) -> bool: """ - Process a single schematic file to apply DNP flags. + Process a single schematic file to apply DNP flags and property overrides. Args: schematic_file: Path to .kicad_sch file dnp_uuids: List of UUIDs that should be DNP + property_overrides: Dict of UUID -> {property_name: value} for property overrides variant_name: Name of variant being applied (for title block) is_root: True if this is the root schematic (not a sub-sheet) Returns: True if successful, False otherwise """ + if property_overrides is None: + property_overrides = {} sch_path = Path(schematic_file) if not sch_path.exists(): print(f"Error: Schematic file not found: {schematic_file}") @@ -228,6 +245,70 @@ def process_single_schematic(schematic_file: str, dnp_uuids: list, variant_name: else: print(f" Cleared DNP: {current_ref if current_ref else current_uuid}") + # Apply property overrides + # Parse through symbols and update properties for parts that have overrides + if property_overrides: + in_symbol = False + current_uuid = None + current_ref = None + 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 + symbol_start_indent = indent_level + + # Detect end of symbol (closing paren at same indent level) + elif in_symbol and stripped == ')' and indent_level == symbol_start_indent: + in_symbol = False + current_uuid = None + current_ref = None + + # Extract UUID when in symbol (at symbol level, not nested) + elif in_symbol and '(dnp' in stripped and not current_uuid: + # Look forward for UUID after DNP line + 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 + + # Extract reference (for logging) + 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] + + # Check if this symbol has property overrides + elif in_symbol and current_uuid and current_uuid in property_overrides: + overrides = property_overrides[current_uuid] + + # Check each tracked property + for prop_name in ['Value', 'MPN', 'Manufacturer', 'IPN']: + if prop_name in overrides and f'(property "{prop_name}"' in stripped: + # Extract current value + parts = line.split('"') + if len(parts) >= 4: + current_value = parts[3] + new_value = overrides[prop_name] + + # Update if different + if current_value != new_value: + # Reconstruct the line with new value + indent = line[:len(line) - len(line.lstrip())] + parts[3] = new_value + lines[i] = indent + '"'.join(parts) + modified = True + print(f" Set {prop_name}: {current_ref if current_ref else current_uuid} = {new_value} (was {current_value})") + if modified: # Backup original file backup_path = sch_path.with_suffix('.kicad_sch.bak') diff --git a/sync_variant.py b/sync_variant.py index 4afe1bd..a7d648d 100644 --- a/sync_variant.py +++ b/sync_variant.py @@ -116,6 +116,7 @@ def sync_variant_from_schematic(schematic_file: str, target_variant: str = None) all_dnp_uuids = [] all_uuids = [] + all_part_properties = {} # uuid -> {property_name: value} # Process each schematic file for sch_file in all_schematics: @@ -130,17 +131,21 @@ def sync_variant_from_schematic(schematic_file: str, target_variant: str = None) with open(sch_path, 'r', encoding='utf-8') as f: content = f.read() - # Parse schematic to find DNP components + # 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'): @@ -148,20 +153,30 @@ def sync_variant_from_schematic(schematic_file: str, target_variant: str = None) 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 - elif in_symbol and stripped == ')': - # Check if this symbol block is closing (simple heuristic) + # 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 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}") + 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 @@ -170,22 +185,23 @@ def sync_variant_from_schematic(schematic_file: str, target_variant: str = None) 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) + # 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 - # 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 + + # 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: @@ -194,6 +210,15 @@ def sync_variant_from_schematic(schematic_file: str, target_variant: str = None) 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 @@ -238,31 +263,57 @@ def sync_variant_from_schematic(schematic_file: str, target_variant: str = None) import traceback traceback.print_exc() - # Update variant with DNP list + # 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") - # 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: + 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 diff --git a/templates/index.html b/templates/index.html index aeaf825..bc25627 100644 --- a/templates/index.html +++ b/templates/index.html @@ -306,6 +306,12 @@
+