From 5869d2c6931eb629a8dc12fc8970dad1f23d91fb Mon Sep 17 00:00:00 2001 From: brentperteet Date: Fri, 27 Feb 2026 12:44:10 -0600 Subject: [PATCH] Enhance library browser with symbol/footprint resolution and datasheet links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add library path resolution for symbols and footprints using KiCad library tables - Parse sym-lib-table and fp-lib-table with multi-line format support - Resolve environment variables (KICAD9_SYMBOL_DIR, UM_KICAD, etc.) - Auto-select symbol and footprint when loading part from database - Add Datasheet column to parts search results with clickable links - Make IPN, Description, Class, Manufacturer, and MPN fields read-only - Remove file extensions (.kicad_sym, .pretty) from library dropdown display - Fix column access in search results using index-based retrieval 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app.py | 217 ++++++++++++++++++++++++++++++++++++++++--- templates/index.html | 172 ++++++++++++++++++++++++++++++---- 2 files changed, 357 insertions(+), 32 deletions(-) diff --git a/app.py b/app.py index aef8ed7..2c49557 100644 --- a/app.py +++ b/app.py @@ -105,8 +105,10 @@ def get_symbol_libraries(): libraries = [] for filename in os.listdir(symbols_dir): if filename.endswith('.kicad_sym'): + # Remove .kicad_sym extension for display + display_name = filename[:-10] if filename.endswith('.kicad_sym') else filename libraries.append({ - 'name': filename, + 'name': display_name, 'path': os.path.join(symbols_dir, filename) }) @@ -126,8 +128,10 @@ def get_footprint_libraries(): for item in os.listdir(footprints_dir): item_path = os.path.join(footprints_dir, item) if os.path.isdir(item_path) and item.endswith('.pretty'): + # Remove .pretty extension for display + display_name = item[:-7] if item.endswith('.pretty') else item libraries.append({ - 'name': item, + 'name': display_name, 'path': item_path }) @@ -632,8 +636,8 @@ def handle_disconnect(): # Shutdown if no clients connected after waiting for potential reconnect if not connected_clients: - print("No clients connected. Waiting 10 seconds for reconnect before shutting down...") - shutdown_timer = threading.Timer(10.0, shutdown_server) + print("No clients connected. Waiting 2 seconds for reconnect before shutting down...") + shutdown_timer = threading.Timer(2.0, shutdown_server) shutdown_timer.start() @socketio.on('heartbeat') @@ -1602,25 +1606,27 @@ def handle_search_parts(data): cursor = conn.cursor() - # Build search query - search across ipn, mpn, manufacturer, and description + # Build search query - search across ipn, mpn, manufacturer, description, symbol, and footprint # Column names in SQLite are lowercase if search_query: sql = """ - SELECT ipn, mpn, manufacturer, description + SELECT ipn, mpn, manufacturer, description, datasheet FROM parts WHERE ipn LIKE ? OR mpn LIKE ? OR manufacturer LIKE ? OR description LIKE ? + OR symbol LIKE ? + OR footprint LIKE ? ORDER BY ipn LIMIT 100 """ search_param = f'%{search_query}%' - cursor.execute(sql, (search_param, search_param, search_param, search_param)) + cursor.execute(sql, (search_param, search_param, search_param, search_param, search_param, search_param)) else: # No search query - return first 100 parts sql = """ - SELECT ipn, mpn, manufacturer, description + SELECT ipn, mpn, manufacturer, description, datasheet FROM parts ORDER BY ipn LIMIT 100 @@ -1632,10 +1638,11 @@ def handle_search_parts(data): parts = [] for row in rows: parts.append({ - 'ipn': row.ipn if row.ipn else '', - 'mpn': row.mpn if row.mpn else '', - 'manufacturer': row.manufacturer if row.manufacturer else '', - 'description': row.description if row.description else '' + 'ipn': row[0] if row[0] else '', + 'mpn': row[1] if row[1] else '', + 'manufacturer': row[2] if row[2] else '', + 'description': row[3] if row[3] else '', + 'datasheet': row[4] if row[4] else '' }) cursor.close() @@ -1787,6 +1794,192 @@ def handle_create_part(data): import traceback emit('library_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'}) +def resolve_library_path(lib_ref, lib_type='symbol'): + """Resolve library reference to full path using KiCad library tables. + + Args: + lib_ref: Library reference in format LibraryName:ItemName + lib_type: 'symbol' or 'footprint' + + Returns: + tuple: (library_path, item_name, library_name) or (None, None, None) if not found + """ + try: + if ':' not in lib_ref: + return (None, None, None) + + lib_name, item_name = lib_ref.split(':', 1) + + # Get KiCad config directory + kicad_config_dir = os.path.expanduser(os.path.join('~', 'AppData', 'Roaming', 'kicad', '9.0')) + + if lib_type == 'symbol': + table_file = os.path.join(kicad_config_dir, 'sym-lib-table') + else: + table_file = os.path.join(kicad_config_dir, 'fp-lib-table') + + if not os.path.exists(table_file): + return (None, None, None) + + # Parse the library table file + with open(table_file, 'r', encoding='utf-8') as f: + content = f.read() + + # Simple s-expression parsing to find the library entry + # Handle both single-line and multi-line formats + # Pattern matches (lib ... (name "X") ... (uri "Y") ... ) + import re + # Use DOTALL flag to match across newlines + pattern = r'\(lib\s+.*?\(name\s+"([^"]+)"\).*?\(uri\s+"([^"]+)"\).*?\)' + matches = re.findall(pattern, content, re.DOTALL) + + for match_name, match_uri in matches: + if match_name == lib_name: + # Resolve environment variables in the path + lib_path = match_uri + + # Replace common KiCad environment variables + env_vars = { + '${KICAD9_SYMBOL_DIR}': os.environ.get('KICAD9_SYMBOL_DIR', ''), + '${KICAD9_FOOTPRINT_DIR}': os.environ.get('KICAD9_FOOTPRINT_DIR', ''), + '${UM_KICAD}': os.environ.get('UM_KICAD', '') + } + + for var, val in env_vars.items(): + if var in lib_path and val: + lib_path = lib_path.replace(var, val) + + # Convert to absolute path with proper separators + lib_path = os.path.normpath(lib_path.replace('/', os.sep)) + + if os.path.exists(lib_path): + return (lib_path, item_name, lib_name) + + return (None, None, None) + + except Exception as e: + print(f"Error resolving library path: {e}") + return (None, None, None) + +@socketio.on('get_part_details') +def handle_get_part_details(data): + try: + ipn = data.get('ipn', '').strip() + + if not ipn: + emit('library_error', {'error': 'IPN is required'}) + return + + conn = get_db_connection() + if not conn: + emit('library_error', {'error': 'Could not connect to database'}) + return + + cursor = conn.cursor() + cursor.execute(""" + SELECT ipn, manufacturer, mpn, description, class, datasheet, symbol, footprint + FROM parts WHERE ipn = ? + """, (ipn,)) + + row = cursor.fetchone() + cursor.close() + conn.close() + + if not row: + emit('library_error', {'error': f'Part {ipn} not found'}) + return + + part = { + 'ipn': row[0], + 'manufacturer': row[1] or '', + 'mpn': row[2] or '', + 'description': row[3] or '', + 'class': row[4] or '', + 'datasheet': row[5] or '', + 'symbol': row[6] or '', + 'footprint': row[7] or '' + } + + # Resolve symbol and footprint library paths + symbol_info = None + footprint_info = None + + if part['symbol']: + symbol_path, symbol_name, symbol_lib = resolve_library_path(part['symbol'], 'symbol') + if symbol_path and symbol_name: + symbol_info = { + 'library_path': symbol_path, + 'symbol_name': symbol_name, + 'library_name': symbol_lib + } + + if part['footprint']: + footprint_path, footprint_name, footprint_lib = resolve_library_path(part['footprint'], 'footprint') + if footprint_path and footprint_name: + footprint_info = { + 'library_path': footprint_path, + 'footprint_name': footprint_name, + 'library_name': footprint_lib + } + + emit('part_details_result', { + 'part': part, + 'symbol_info': symbol_info, + 'footprint_info': footprint_info + }) + + except Exception as e: + import traceback + emit('library_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'}) + +@socketio.on('update_part') +def handle_update_part(data): + try: + ipn = data.get('ipn', '').strip() + manufacturer = data.get('manufacturer', '').strip() + mpn = data.get('mpn', '').strip() + description = data.get('description', '').strip() + class_value = data.get('class', '').strip() + datasheet = data.get('datasheet', '').strip() + symbol = data.get('symbol', '').strip() + footprint = data.get('footprint', '').strip() + + if not ipn: + emit('library_error', {'error': 'IPN is required'}) + return + + conn = get_db_connection() + if not conn: + emit('library_error', {'error': 'Could not connect to database'}) + return + + cursor = conn.cursor() + + # Check if part exists + cursor.execute("SELECT ipn FROM parts WHERE ipn = ?", (ipn,)) + if not cursor.fetchone(): + emit('library_error', {'error': f'Part {ipn} not found in database'}) + cursor.close() + conn.close() + return + + # Update part + cursor.execute(""" + UPDATE parts + SET manufacturer = ?, mpn = ?, description = ?, class = ?, datasheet = ?, symbol = ?, footprint = ? + WHERE ipn = ? + """, (manufacturer, mpn, description, class_value, datasheet, symbol, footprint, ipn)) + + conn.commit() + cursor.close() + conn.close() + + emit('part_updated', {'message': f'Successfully updated part {ipn}'}) + + except Exception as e: + import traceback + emit('library_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'}) + @socketio.on('get_symbol_libraries') def handle_get_symbol_libraries(): try: diff --git a/templates/index.html b/templates/index.html index 80007da..a86ecfd 100644 --- a/templates/index.html +++ b/templates/index.html @@ -411,20 +411,24 @@ - +