Enhance library browser with symbol/footprint resolution and datasheet links
- 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 <noreply@anthropic.com>
This commit is contained in:
217
app.py
217
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:
|
||||
|
||||
Reference in New Issue
Block a user