kicad-sch-api 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/__init__.py +2 -2
- kicad_sch_api/core/components.py +69 -2
- kicad_sch_api/core/formatter.py +56 -11
- kicad_sch_api/core/ic_manager.py +187 -0
- kicad_sch_api/core/junctions.py +206 -0
- kicad_sch_api/core/parser.py +637 -35
- kicad_sch_api/core/schematic.py +739 -8
- kicad_sch_api/core/types.py +102 -7
- kicad_sch_api/core/wires.py +248 -0
- kicad_sch_api/library/cache.py +321 -10
- kicad_sch_api/utils/validation.py +3 -3
- {kicad_sch_api-0.0.1.dist-info → kicad_sch_api-0.1.0.dist-info}/METADATA +14 -33
- kicad_sch_api-0.1.0.dist-info/RECORD +21 -0
- kicad_sch_api/mcp/__init__.py +0 -5
- kicad_sch_api/mcp/server.py +0 -500
- kicad_sch_api-0.0.1.dist-info/RECORD +0 -20
- {kicad_sch_api-0.0.1.dist-info → kicad_sch_api-0.1.0.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.0.1.dist-info → kicad_sch_api-0.1.0.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.0.1.dist-info → kicad_sch_api-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.0.1.dist-info → kicad_sch_api-0.1.0.dist-info}/top_level.txt +0 -0
kicad_sch_api/library/cache.py
CHANGED
|
@@ -14,6 +14,8 @@ from dataclasses import dataclass, field
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
16
16
|
|
|
17
|
+
import sexpdata
|
|
18
|
+
|
|
17
19
|
from ..core.types import PinShape, PinType, Point, SchematicPin
|
|
18
20
|
from ..utils.validation import ValidationError
|
|
19
21
|
|
|
@@ -37,6 +39,12 @@ class SymbolDefinition:
|
|
|
37
39
|
power_symbol: bool = False
|
|
38
40
|
graphic_elements: List[Dict[str, Any]] = field(default_factory=list)
|
|
39
41
|
|
|
42
|
+
# Raw KiCAD data for exact format preservation
|
|
43
|
+
raw_kicad_data: Any = None
|
|
44
|
+
|
|
45
|
+
# Symbol inheritance
|
|
46
|
+
extends: Optional[str] = None # Parent symbol name if this symbol extends another
|
|
47
|
+
|
|
40
48
|
# Performance metrics
|
|
41
49
|
load_time: float = 0.0
|
|
42
50
|
access_count: int = 0
|
|
@@ -254,15 +262,19 @@ class SymbolLibraryCache:
|
|
|
254
262
|
Returns:
|
|
255
263
|
Symbol definition if found, None otherwise
|
|
256
264
|
"""
|
|
265
|
+
logger.debug(f"🔧 CACHE: Requesting symbol: {lib_id}")
|
|
266
|
+
|
|
257
267
|
# Check cache first
|
|
258
268
|
if lib_id in self._symbols:
|
|
259
269
|
self._cache_hits += 1
|
|
260
270
|
symbol = self._symbols[lib_id]
|
|
261
271
|
symbol.access_count += 1
|
|
262
272
|
symbol.last_accessed = time.time()
|
|
273
|
+
logger.debug(f"🔧 CACHE: Cache hit for {lib_id}")
|
|
263
274
|
return symbol
|
|
264
275
|
|
|
265
276
|
# Cache miss - try to load symbol
|
|
277
|
+
logger.debug(f"🔧 CACHE: Cache miss for {lib_id}, loading...")
|
|
266
278
|
self._cache_misses += 1
|
|
267
279
|
return self._load_symbol(lib_id)
|
|
268
280
|
|
|
@@ -344,17 +356,22 @@ class SymbolLibraryCache:
|
|
|
344
356
|
|
|
345
357
|
def _load_symbol(self, lib_id: str) -> Optional[SymbolDefinition]:
|
|
346
358
|
"""Load a single symbol from its library."""
|
|
359
|
+
logger.debug(f"🔧 LOAD: Loading symbol {lib_id}")
|
|
360
|
+
|
|
347
361
|
if ":" not in lib_id:
|
|
348
|
-
logger.warning(f"Invalid lib_id format: {lib_id}")
|
|
362
|
+
logger.warning(f"🔧 LOAD: Invalid lib_id format: {lib_id}")
|
|
349
363
|
return None
|
|
350
364
|
|
|
351
365
|
library_name, symbol_name = lib_id.split(":", 1)
|
|
366
|
+
logger.debug(f"🔧 LOAD: Library: {library_name}, Symbol: {symbol_name}")
|
|
352
367
|
|
|
353
368
|
if library_name not in self._library_index:
|
|
354
|
-
logger.warning(f"Library not found: {library_name}")
|
|
369
|
+
logger.warning(f"🔧 LOAD: Library not found: {library_name}")
|
|
370
|
+
logger.debug(f"🔧 LOAD: Available libraries: {list(self._library_index.keys())}")
|
|
355
371
|
return None
|
|
356
372
|
|
|
357
373
|
library_path = self._library_index[library_name]
|
|
374
|
+
logger.debug(f"🔧 LOAD: Library path: {library_path}")
|
|
358
375
|
return self._load_symbol_from_library(library_path, lib_id)
|
|
359
376
|
|
|
360
377
|
def _load_symbol_from_library(
|
|
@@ -364,22 +381,32 @@ class SymbolLibraryCache:
|
|
|
364
381
|
start_time = time.time()
|
|
365
382
|
|
|
366
383
|
try:
|
|
367
|
-
# This is a simplified version - in reality, you'd parse the .kicad_sym file
|
|
368
|
-
# For now, create a basic symbol definition
|
|
369
384
|
library_name, symbol_name = lib_id.split(":", 1)
|
|
370
385
|
|
|
371
|
-
#
|
|
372
|
-
|
|
386
|
+
# Parse the .kicad_sym file to find the symbol
|
|
387
|
+
symbol_data = self._parse_kicad_symbol_file(library_path, lib_id)
|
|
388
|
+
if not symbol_data:
|
|
389
|
+
logger.warning(f"Symbol {symbol_name} not found in {library_path}")
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
# Create SymbolDefinition from parsed data
|
|
373
393
|
symbol = SymbolDefinition(
|
|
374
394
|
lib_id=lib_id,
|
|
375
395
|
name=symbol_name,
|
|
376
396
|
library=library_name,
|
|
377
|
-
reference_prefix=
|
|
378
|
-
description=
|
|
379
|
-
|
|
397
|
+
reference_prefix=symbol_data.get("reference_prefix", "U"),
|
|
398
|
+
description=symbol_data.get("description", ""),
|
|
399
|
+
keywords=symbol_data.get("keywords", ""),
|
|
400
|
+
datasheet=symbol_data.get("datasheet", "~"),
|
|
401
|
+
pins=symbol_data.get("pins", []),
|
|
402
|
+
extends=symbol_data.get("extends"), # Store extends information
|
|
380
403
|
load_time=time.time() - start_time,
|
|
381
404
|
)
|
|
382
405
|
|
|
406
|
+
# Store the raw symbol data for later use in schematic generation
|
|
407
|
+
symbol.raw_kicad_data = symbol_data.get("raw_data", {})
|
|
408
|
+
logger.debug(f"🔧 CREATED: SymbolDefinition for {lib_id}, extends: {symbol.extends}")
|
|
409
|
+
|
|
383
410
|
self._symbols[lib_id] = symbol
|
|
384
411
|
self._symbol_index[symbol_name] = lib_id
|
|
385
412
|
self._total_load_time += symbol.load_time
|
|
@@ -391,6 +418,285 @@ class SymbolLibraryCache:
|
|
|
391
418
|
logger.error(f"Error loading symbol {lib_id} from {library_path}: {e}")
|
|
392
419
|
return None
|
|
393
420
|
|
|
421
|
+
def _parse_kicad_symbol_file(self, library_path: Path, lib_id: str) -> Optional[Dict[str, Any]]:
|
|
422
|
+
"""Parse a KiCAD .kicad_sym file to extract a specific symbol."""
|
|
423
|
+
try:
|
|
424
|
+
# Extract symbol name from lib_id
|
|
425
|
+
library_name, symbol_name = lib_id.split(":", 1)
|
|
426
|
+
|
|
427
|
+
with open(library_path, 'r', encoding='utf-8') as f:
|
|
428
|
+
content = f.read()
|
|
429
|
+
|
|
430
|
+
# Parse the S-expression with symbol preservation
|
|
431
|
+
parsed = sexpdata.loads(content, true=None, false=None, nil=None)
|
|
432
|
+
logger.debug(f"🔧 PARSE: Parsed library file with {len(parsed)} top-level items")
|
|
433
|
+
|
|
434
|
+
# Find the symbol we're looking for
|
|
435
|
+
symbol_data = self._find_symbol_in_parsed_data(parsed, symbol_name)
|
|
436
|
+
if not symbol_data:
|
|
437
|
+
logger.debug(f"🔧 PARSE: Symbol {symbol_name} not found in {library_path}")
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
logger.debug(f"🔧 PARSE: Found symbol {symbol_name} in library")
|
|
441
|
+
|
|
442
|
+
# Extract the library name and symbol name for resolution
|
|
443
|
+
library_name, symbol_name = lib_id.split(":", 1)
|
|
444
|
+
|
|
445
|
+
# Check if this symbol extends another symbol
|
|
446
|
+
extends_symbol = self._check_extends_directive(symbol_data)
|
|
447
|
+
logger.debug(f"🔧 CACHE: Symbol {lib_id} extends: {extends_symbol}")
|
|
448
|
+
|
|
449
|
+
# If this symbol extends another, we need to resolve it
|
|
450
|
+
if extends_symbol:
|
|
451
|
+
resolved_symbol_data = self._resolve_extends_relationship(
|
|
452
|
+
symbol_data, extends_symbol, library_path, library_name
|
|
453
|
+
)
|
|
454
|
+
if resolved_symbol_data:
|
|
455
|
+
symbol_data = resolved_symbol_data
|
|
456
|
+
extends_symbol = None # Clear extends after resolution
|
|
457
|
+
logger.debug(f"🔧 CACHE: Resolved extends for {lib_id}")
|
|
458
|
+
|
|
459
|
+
# Extract symbol information
|
|
460
|
+
result = {
|
|
461
|
+
"raw_data": symbol_data, # Store the raw parsed data
|
|
462
|
+
"reference_prefix": "U", # Default
|
|
463
|
+
"description": "",
|
|
464
|
+
"keywords": "",
|
|
465
|
+
"datasheet": "~",
|
|
466
|
+
"pins": [],
|
|
467
|
+
"extends": extends_symbol # Should be None after resolution
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
# Extract properties from the symbol
|
|
471
|
+
for item in symbol_data[1:]:
|
|
472
|
+
if isinstance(item, list) and len(item) > 0:
|
|
473
|
+
if item[0] == sexpdata.Symbol('property'):
|
|
474
|
+
prop_name = item[1]
|
|
475
|
+
prop_value = item[2]
|
|
476
|
+
|
|
477
|
+
logger.debug(f"🔧 Processing property: {prop_name} = {prop_value}")
|
|
478
|
+
if prop_name == sexpdata.Symbol('Reference'):
|
|
479
|
+
result["reference_prefix"] = str(prop_value)
|
|
480
|
+
logger.debug(f"🔧 Set reference_prefix: {str(prop_value)}")
|
|
481
|
+
elif prop_name == sexpdata.Symbol('Description'):
|
|
482
|
+
result["Description"] = str(prop_value) # Keep original case
|
|
483
|
+
logger.debug(f"🔧 Set Description: {str(prop_value)}")
|
|
484
|
+
elif prop_name == sexpdata.Symbol('ki_keywords'):
|
|
485
|
+
result["keywords"] = str(prop_value)
|
|
486
|
+
elif prop_name == sexpdata.Symbol('Datasheet'):
|
|
487
|
+
result["Datasheet"] = str(prop_value) # Keep original case
|
|
488
|
+
logger.debug(f"🔧 Set Datasheet: {str(prop_value)}")
|
|
489
|
+
|
|
490
|
+
# Extract pins (this is simplified - pins are in symbol sub-definitions)
|
|
491
|
+
# For now, we'll extract pins from the actual symbol structure
|
|
492
|
+
result["pins"] = self._extract_pins_from_symbol(symbol_data)
|
|
493
|
+
|
|
494
|
+
return result
|
|
495
|
+
|
|
496
|
+
except Exception as e:
|
|
497
|
+
logger.error(f"Error parsing {library_path}: {e}")
|
|
498
|
+
return None
|
|
499
|
+
|
|
500
|
+
def _find_symbol_in_parsed_data(self, parsed_data: List, symbol_name: str) -> Optional[List]:
|
|
501
|
+
"""Find a specific symbol in parsed KiCAD library data."""
|
|
502
|
+
logger.debug(f"🔧 FIND: Looking for symbol '{symbol_name}' in parsed data")
|
|
503
|
+
|
|
504
|
+
if not isinstance(parsed_data, list):
|
|
505
|
+
logger.debug(f"🔧 FIND: Parsed data is not a list: {type(parsed_data)}")
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
# First, log all available symbols for debugging
|
|
509
|
+
available_symbols = []
|
|
510
|
+
for item in parsed_data:
|
|
511
|
+
if isinstance(item, list) and len(item) >= 2:
|
|
512
|
+
if item[0] == sexpdata.Symbol('symbol'):
|
|
513
|
+
available_symbols.append(str(item[1]).strip('"'))
|
|
514
|
+
|
|
515
|
+
logger.debug(f"🔧 FIND: Available symbols in library: {available_symbols[:10]}...") # Show first 10
|
|
516
|
+
|
|
517
|
+
# Search through the parsed data for the symbol
|
|
518
|
+
for item in parsed_data:
|
|
519
|
+
if isinstance(item, list) and len(item) >= 2:
|
|
520
|
+
if (item[0] == sexpdata.Symbol('symbol') and
|
|
521
|
+
len(item) > 1 and
|
|
522
|
+
str(item[1]).strip('"') == symbol_name):
|
|
523
|
+
logger.debug(f"🔧 FIND: Found symbol '{symbol_name}'")
|
|
524
|
+
return item
|
|
525
|
+
|
|
526
|
+
logger.debug(f"🔧 FIND: Symbol '{symbol_name}' not found in library")
|
|
527
|
+
return None
|
|
528
|
+
|
|
529
|
+
def _check_extends_directive(self, symbol_data: List) -> Optional[str]:
|
|
530
|
+
"""Check if symbol has extends directive and return parent symbol name."""
|
|
531
|
+
if not isinstance(symbol_data, list):
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
for item in symbol_data[1:]:
|
|
535
|
+
if isinstance(item, list) and len(item) >= 2:
|
|
536
|
+
if item[0] == sexpdata.Symbol('extends'):
|
|
537
|
+
parent_name = str(item[1]).strip('"')
|
|
538
|
+
logger.debug(f"Found extends directive: {parent_name}")
|
|
539
|
+
return parent_name
|
|
540
|
+
return None
|
|
541
|
+
|
|
542
|
+
def _resolve_extends_relationship(self, child_symbol_data: List, parent_name: str,
|
|
543
|
+
library_path: Path, library_name: str) -> Optional[List]:
|
|
544
|
+
"""Resolve extends relationship by merging parent symbol into child."""
|
|
545
|
+
logger.debug(f"🔧 RESOLVE: Resolving extends {parent_name} for child symbol")
|
|
546
|
+
|
|
547
|
+
try:
|
|
548
|
+
# Load the parent symbol from the same library
|
|
549
|
+
with open(library_path, 'r', encoding='utf-8') as f:
|
|
550
|
+
content = f.read()
|
|
551
|
+
|
|
552
|
+
parsed = sexpdata.loads(content, true=None, false=None, nil=None)
|
|
553
|
+
parent_symbol_data = self._find_symbol_in_parsed_data(parsed, parent_name)
|
|
554
|
+
|
|
555
|
+
if not parent_symbol_data:
|
|
556
|
+
logger.warning(f"🔧 RESOLVE: Parent symbol {parent_name} not found in library")
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
logger.debug(f"🔧 RESOLVE: Found parent symbol {parent_name}")
|
|
560
|
+
|
|
561
|
+
# Merge parent into child (adapt from circuit-synth logic)
|
|
562
|
+
merged_symbol = self._merge_parent_into_child(child_symbol_data, parent_symbol_data)
|
|
563
|
+
logger.debug(f"🔧 RESOLVE: Merged parent into child symbol")
|
|
564
|
+
|
|
565
|
+
return merged_symbol
|
|
566
|
+
|
|
567
|
+
except Exception as e:
|
|
568
|
+
logger.error(f"🔧 RESOLVE: Error resolving extends: {e}")
|
|
569
|
+
return None
|
|
570
|
+
|
|
571
|
+
def _merge_parent_into_child(self, child_data: List, parent_data: List) -> List:
|
|
572
|
+
"""Merge parent symbol graphics and pins into child symbol."""
|
|
573
|
+
import copy
|
|
574
|
+
|
|
575
|
+
# Get child and parent symbol names for unit renaming
|
|
576
|
+
child_name = str(child_data[1]).strip('"') if len(child_data) > 1 else "Child"
|
|
577
|
+
parent_name = str(parent_data[1]).strip('"') if len(parent_data) > 1 else "Parent"
|
|
578
|
+
|
|
579
|
+
logger.debug(f"🔧 MERGE: Merging {parent_name} into {child_name}")
|
|
580
|
+
|
|
581
|
+
# Start with child symbol structure
|
|
582
|
+
merged = copy.deepcopy(child_data)
|
|
583
|
+
|
|
584
|
+
# Remove the extends directive from child
|
|
585
|
+
merged = [item for item in merged if not (
|
|
586
|
+
isinstance(item, list) and len(item) >= 2 and
|
|
587
|
+
item[0] == sexpdata.Symbol('extends')
|
|
588
|
+
)]
|
|
589
|
+
|
|
590
|
+
# Copy all graphics and unit definitions from parent
|
|
591
|
+
for item in parent_data[1:]:
|
|
592
|
+
if isinstance(item, list) and len(item) > 0:
|
|
593
|
+
# Copy symbol unit definitions (contain graphics and pins)
|
|
594
|
+
if item[0] == sexpdata.Symbol('symbol'):
|
|
595
|
+
# Rename unit from parent name to child name
|
|
596
|
+
unit_item = copy.deepcopy(item)
|
|
597
|
+
if len(unit_item) > 1:
|
|
598
|
+
old_unit_name = str(unit_item[1]).strip('"')
|
|
599
|
+
# Replace parent name with child name in unit name
|
|
600
|
+
new_unit_name = old_unit_name.replace(parent_name, child_name)
|
|
601
|
+
unit_item[1] = new_unit_name
|
|
602
|
+
logger.debug(f"🔧 MERGE: Renamed unit {old_unit_name} -> {new_unit_name}")
|
|
603
|
+
merged.append(unit_item)
|
|
604
|
+
# Copy other non-property elements (child properties override parent)
|
|
605
|
+
elif item[0] not in [sexpdata.Symbol('property')]:
|
|
606
|
+
merged.append(copy.deepcopy(item))
|
|
607
|
+
|
|
608
|
+
logger.debug(f"🔧 MERGE: Merged symbol has {len(merged)} elements")
|
|
609
|
+
return merged
|
|
610
|
+
|
|
611
|
+
def _extract_pins_from_symbol(self, symbol_data: List) -> List[SchematicPin]:
|
|
612
|
+
"""Extract pins from symbol data."""
|
|
613
|
+
pins = []
|
|
614
|
+
|
|
615
|
+
# Look for symbol sub-definitions like "R_1_1" that contain pins
|
|
616
|
+
for item in symbol_data[1:]:
|
|
617
|
+
if isinstance(item, list) and len(item) > 0:
|
|
618
|
+
if item[0] == sexpdata.Symbol('symbol'):
|
|
619
|
+
# This is a symbol unit definition, look for pins
|
|
620
|
+
pins.extend(self._extract_pins_from_unit(item))
|
|
621
|
+
|
|
622
|
+
return pins
|
|
623
|
+
|
|
624
|
+
def _extract_pins_from_unit(self, unit_data: List) -> List[SchematicPin]:
|
|
625
|
+
"""Extract pins from a symbol unit definition."""
|
|
626
|
+
pins = []
|
|
627
|
+
|
|
628
|
+
for item in unit_data[1:]:
|
|
629
|
+
if isinstance(item, list) and len(item) > 0:
|
|
630
|
+
if item[0] == sexpdata.Symbol('pin'):
|
|
631
|
+
pin = self._parse_pin_definition(item)
|
|
632
|
+
if pin:
|
|
633
|
+
pins.append(pin)
|
|
634
|
+
|
|
635
|
+
return pins
|
|
636
|
+
|
|
637
|
+
def _parse_pin_definition(self, pin_data: List) -> Optional[SchematicPin]:
|
|
638
|
+
"""Parse a pin definition from KiCAD format."""
|
|
639
|
+
try:
|
|
640
|
+
# pin_data format: (pin passive line (at 0 3.81 270) (length 1.27) ...)
|
|
641
|
+
pin_type_str = str(pin_data[1]) if len(pin_data) > 1 else "passive"
|
|
642
|
+
pin_shape_str = str(pin_data[2]) if len(pin_data) > 2 else "line"
|
|
643
|
+
|
|
644
|
+
position = Point(0, 0)
|
|
645
|
+
length = 2.54
|
|
646
|
+
rotation = 0
|
|
647
|
+
name = "~"
|
|
648
|
+
number = "1"
|
|
649
|
+
|
|
650
|
+
# Parse pin attributes
|
|
651
|
+
for item in pin_data[3:]:
|
|
652
|
+
if isinstance(item, list) and len(item) > 0:
|
|
653
|
+
if item[0] == sexpdata.Symbol('at'):
|
|
654
|
+
# (at x y rotation)
|
|
655
|
+
if len(item) >= 3:
|
|
656
|
+
position = Point(float(item[1]), float(item[2]))
|
|
657
|
+
if len(item) >= 4:
|
|
658
|
+
rotation = float(item[3])
|
|
659
|
+
elif item[0] == sexpdata.Symbol('length'):
|
|
660
|
+
length = float(item[1])
|
|
661
|
+
elif item[0] == sexpdata.Symbol('name'):
|
|
662
|
+
name = str(item[1]).strip('"')
|
|
663
|
+
elif item[0] == sexpdata.Symbol('number'):
|
|
664
|
+
number = str(item[1]).strip('"')
|
|
665
|
+
|
|
666
|
+
# Map pin type
|
|
667
|
+
pin_type = PinType.PASSIVE
|
|
668
|
+
if pin_type_str == "input":
|
|
669
|
+
pin_type = PinType.INPUT
|
|
670
|
+
elif pin_type_str == "output":
|
|
671
|
+
pin_type = PinType.OUTPUT
|
|
672
|
+
elif pin_type_str == "bidirectional":
|
|
673
|
+
pin_type = PinType.BIDIRECTIONAL
|
|
674
|
+
elif pin_type_str == "power_in":
|
|
675
|
+
pin_type = PinType.POWER_IN
|
|
676
|
+
elif pin_type_str == "power_out":
|
|
677
|
+
pin_type = PinType.POWER_OUT
|
|
678
|
+
|
|
679
|
+
# Map pin shape
|
|
680
|
+
pin_shape = PinShape.LINE
|
|
681
|
+
if pin_shape_str == "inverted":
|
|
682
|
+
pin_shape = PinShape.INVERTED
|
|
683
|
+
elif pin_shape_str == "clock":
|
|
684
|
+
pin_shape = PinShape.CLOCK
|
|
685
|
+
|
|
686
|
+
return SchematicPin(
|
|
687
|
+
number=number,
|
|
688
|
+
name=name,
|
|
689
|
+
position=position,
|
|
690
|
+
pin_type=pin_type,
|
|
691
|
+
pin_shape=pin_shape,
|
|
692
|
+
length=length,
|
|
693
|
+
rotation=rotation,
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
except Exception as e:
|
|
697
|
+
logger.error(f"Error parsing pin definition: {e}")
|
|
698
|
+
return None
|
|
699
|
+
|
|
394
700
|
def _load_library(self, library_path: Path) -> bool:
|
|
395
701
|
"""Load all symbols from a library file."""
|
|
396
702
|
library_name = library_path.stem
|
|
@@ -471,6 +777,8 @@ class SymbolLibraryCache:
|
|
|
471
777
|
Path("/usr/share/kicad/symbols"),
|
|
472
778
|
Path("/usr/local/share/kicad/symbols"),
|
|
473
779
|
Path.home() / ".local/share/kicad/symbols",
|
|
780
|
+
# macOS KiCAD.app bundle path
|
|
781
|
+
Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"),
|
|
474
782
|
]
|
|
475
783
|
)
|
|
476
784
|
|
|
@@ -482,7 +790,10 @@ class SymbolLibraryCache:
|
|
|
482
790
|
]
|
|
483
791
|
)
|
|
484
792
|
|
|
485
|
-
|
|
793
|
+
logger.debug(f"Potential library search paths: {search_paths}")
|
|
794
|
+
existing_paths = [path for path in search_paths if path.exists()]
|
|
795
|
+
logger.debug(f"Existing library search paths: {existing_paths}")
|
|
796
|
+
return existing_paths
|
|
486
797
|
|
|
487
798
|
def _load_persistent_index(self):
|
|
488
799
|
"""Load persistent symbol index from disk."""
|
|
@@ -87,7 +87,7 @@ class SchematicValidator:
|
|
|
87
87
|
"""
|
|
88
88
|
self.strict = strict
|
|
89
89
|
self.issues = []
|
|
90
|
-
self._valid_reference_pattern = re.compile(r"^[A-Z]+[0-9]
|
|
90
|
+
self._valid_reference_pattern = re.compile(r"^(#[A-Z]+[0-9]+|[A-Z]+[0-9]*[A-Z]?)$")
|
|
91
91
|
self._valid_lib_id_pattern = re.compile(r"^[^:]+:[^:]+$")
|
|
92
92
|
|
|
93
93
|
def validate_schematic_data(self, schematic_data: Dict[str, Any]) -> List[ValidationIssue]:
|
|
@@ -133,7 +133,7 @@ class SchematicValidator:
|
|
|
133
133
|
Validate component reference format.
|
|
134
134
|
|
|
135
135
|
Args:
|
|
136
|
-
reference: Reference to validate (e.g., "R1", "U5")
|
|
136
|
+
reference: Reference to validate (e.g., "R1", "U5", "#PWR01")
|
|
137
137
|
|
|
138
138
|
Returns:
|
|
139
139
|
True if reference is valid
|
|
@@ -231,7 +231,7 @@ class SchematicValidator:
|
|
|
231
231
|
message=f"{context}: Invalid reference format: {reference}",
|
|
232
232
|
level=ValidationLevel.ERROR,
|
|
233
233
|
context={"reference": reference},
|
|
234
|
-
suggestion="Reference should match pattern: [A-Z]+[0-9]*",
|
|
234
|
+
suggestion="Reference should match pattern: [A-Z]+[0-9]* or #[A-Z]+[0-9]* (for power symbols)",
|
|
235
235
|
)
|
|
236
236
|
)
|
|
237
237
|
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kicad-sch-api
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Professional KiCAD schematic manipulation library with exact format preservation and AI agent integration
|
|
5
5
|
Author-email: Circuit-Synth <info@circuit-synth.com>
|
|
6
6
|
Maintainer-email: Circuit-Synth <info@circuit-synth.com>
|
|
7
|
-
License: MIT
|
|
7
|
+
License-Expression: MIT
|
|
8
8
|
Project-URL: Homepage, https://github.com/circuit-synth/kicad-sch-api
|
|
9
9
|
Project-URL: Documentation, https://circuit-synth.github.io/kicad-sch-api/
|
|
10
10
|
Project-URL: Repository, https://github.com/circuit-synth/kicad-sch-api.git
|
|
11
11
|
Project-URL: Bug Reports, https://github.com/circuit-synth/kicad-sch-api/issues
|
|
12
12
|
Project-URL: Changelog, https://github.com/circuit-synth/kicad-sch-api/blob/main/CHANGELOG.md
|
|
13
|
-
Keywords: kicad,schematic,eda,electronics,circuit-design,ai,
|
|
13
|
+
Keywords: kicad,schematic,eda,electronics,circuit-design,ai,automation,pcb
|
|
14
14
|
Classifier: Development Status :: 4 - Beta
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: Intended Audience :: Science/Research
|
|
17
17
|
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
|
|
18
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
19
18
|
Classifier: Programming Language :: Python :: 3
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.8
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -28,8 +27,6 @@ Description-Content-Type: text/markdown
|
|
|
28
27
|
License-File: LICENSE
|
|
29
28
|
Requires-Dist: sexpdata>=0.0.3
|
|
30
29
|
Requires-Dist: typing-extensions>=4.0.0; python_version < "3.11"
|
|
31
|
-
Provides-Extra: mcp
|
|
32
|
-
Requires-Dist: mcp>=0.1.0; extra == "mcp"
|
|
33
30
|
Provides-Extra: dev
|
|
34
31
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
35
32
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -49,13 +46,11 @@ Dynamic: license-file
|
|
|
49
46
|
|
|
50
47
|
**Professional KiCAD Schematic Manipulation Library with AI Agent Integration**
|
|
51
48
|
|
|
52
|
-
A modern, high-performance Python library for programmatic manipulation of KiCAD schematic files (.kicad_sch) with exact format preservation, enhanced component management, and native AI agent support via Model Context Protocol (MCP).
|
|
53
49
|
|
|
54
50
|
## 🚀 Key Features
|
|
55
51
|
|
|
56
52
|
- **📋 Exact Format Preservation**: Output matches KiCAD's native formatting exactly
|
|
57
53
|
- **⚡ High Performance**: Optimized for large schematics with symbol caching
|
|
58
|
-
- **🤖 AI Agent Integration**: Native MCP server for seamless AI agent interaction
|
|
59
54
|
- **🔧 Enhanced API**: Intuitive object-oriented interface with bulk operations
|
|
60
55
|
- **📚 Advanced Library Management**: Multi-source symbol lookup and caching
|
|
61
56
|
- **✅ Professional Validation**: Comprehensive error collection and reporting
|
|
@@ -63,14 +58,13 @@ A modern, high-performance Python library for programmatic manipulation of KiCAD
|
|
|
63
58
|
|
|
64
59
|
## 🆚 vs. Existing Solutions
|
|
65
60
|
|
|
66
|
-
| Feature | kicad-sch-api |
|
|
67
|
-
|
|
68
|
-
| **Schematic Support** | ✅ Full |
|
|
61
|
+
| Feature | kicad-sch-api | Other Solutions | KiCAD Official API |
|
|
62
|
+
|---------|---------------|-----------------|-------------------|
|
|
63
|
+
| **Schematic Support** | ✅ Full | ⚠️ Varies | ❌ PCB Only |
|
|
69
64
|
| **Format Preservation** | ✅ Exact | ❌ Basic | N/A |
|
|
70
65
|
| **Performance** | ✅ Optimized | ⚠️ Basic | N/A |
|
|
71
|
-
| **
|
|
72
|
-
| **
|
|
73
|
-
| **Runtime Dependencies** | ❌ None | ❌ None | ✅ KiCAD Required |
|
|
66
|
+
| **Library Management** | ✅ Advanced | ⚠️ Limited | N/A |
|
|
67
|
+
| **Runtime Dependencies** | ❌ None | ⚠️ Varies | ✅ KiCAD Required |
|
|
74
68
|
|
|
75
69
|
## 📦 Installation
|
|
76
70
|
|
|
@@ -83,8 +77,6 @@ git clone https://github.com/circuit-synth/kicad-sch-api.git
|
|
|
83
77
|
cd kicad-sch-api/python
|
|
84
78
|
pip install -e .
|
|
85
79
|
|
|
86
|
-
# For AI agent integration (MCP server)
|
|
87
|
-
cd ../mcp-server
|
|
88
80
|
npm install
|
|
89
81
|
npm run build
|
|
90
82
|
```
|
|
@@ -96,22 +88,19 @@ npm run build
|
|
|
96
88
|
```python
|
|
97
89
|
import kicad_sch_api as ksa
|
|
98
90
|
|
|
99
|
-
#
|
|
100
|
-
sch = ksa.
|
|
91
|
+
# Create new schematic
|
|
92
|
+
sch = ksa.create_schematic('My Circuit')
|
|
101
93
|
|
|
102
94
|
# Add components
|
|
103
|
-
resistor = sch.components.add('Device:R',
|
|
104
|
-
capacitor = sch.components.add('Device:C',
|
|
95
|
+
resistor = sch.components.add('Device:R', reference='R1', value='10k', position=(100, 100))
|
|
96
|
+
capacitor = sch.components.add('Device:C', reference='C1', value='0.1uF', position=(150, 100))
|
|
105
97
|
|
|
106
98
|
# Update properties
|
|
107
99
|
resistor.footprint = 'Resistor_SMD:R_0603_1608Metric'
|
|
108
100
|
resistor.set_property('MPN', 'RC0603FR-0710KL')
|
|
109
101
|
|
|
110
|
-
# Connect components
|
|
111
|
-
sch.add_wire(resistor.get_pin_position('2'), capacitor.get_pin_position('1'))
|
|
112
|
-
|
|
113
102
|
# Save with exact format preservation
|
|
114
|
-
sch.save()
|
|
103
|
+
sch.save('my_circuit.kicad_sch')
|
|
115
104
|
```
|
|
116
105
|
|
|
117
106
|
### Advanced Operations
|
|
@@ -141,15 +130,12 @@ stats = sch.get_performance_stats()
|
|
|
141
130
|
print(f"Cache hit rate: {stats['symbol_cache']['hit_rate_percent']}%")
|
|
142
131
|
```
|
|
143
132
|
|
|
144
|
-
### AI Agent Integration (MCP)
|
|
145
133
|
|
|
146
|
-
Configure in Claude Desktop or compatible MCP client:
|
|
147
134
|
|
|
148
135
|
```json
|
|
149
136
|
{
|
|
150
137
|
"kicad-sch": {
|
|
151
138
|
"command": "node",
|
|
152
|
-
"args": ["/path/to/kicad-sch-api/mcp-server/dist/index.js"],
|
|
153
139
|
"env": {
|
|
154
140
|
"PYTHON_PATH": "python3",
|
|
155
141
|
"KICAD_SCH_API_PATH": "/path/to/kicad-sch-api/python"
|
|
@@ -165,7 +151,6 @@ User: "Create a voltage divider circuit with two 10k resistors"
|
|
|
165
151
|
|
|
166
152
|
Claude: I'll create a voltage divider circuit for you.
|
|
167
153
|
|
|
168
|
-
[Agent automatically uses MCP tools to:]
|
|
169
154
|
1. Create new schematic
|
|
170
155
|
2. Add R1 (10k resistor) at (100, 100)
|
|
171
156
|
3. Add R2 (10k resistor) at (100, 150)
|
|
@@ -187,8 +172,6 @@ The library consists of two main components:
|
|
|
187
172
|
- **Symbol Caching**: High-performance library symbol management
|
|
188
173
|
- **Comprehensive Validation**: Error collection and professional reporting
|
|
189
174
|
|
|
190
|
-
### MCP Server (AI Integration)
|
|
191
|
-
- **TypeScript MCP Server**: Implements Anthropic's MCP specification
|
|
192
175
|
- **Python Bridge**: Reliable subprocess communication
|
|
193
176
|
- **Comprehensive Tools**: 15+ tools for complete schematic manipulation
|
|
194
177
|
- **Professional Error Handling**: Detailed error context for AI agents
|
|
@@ -200,8 +183,6 @@ The library consists of two main components:
|
|
|
200
183
|
cd python
|
|
201
184
|
python -m pytest tests/ -v --cov=kicad_sch_api
|
|
202
185
|
|
|
203
|
-
# MCP server tests
|
|
204
|
-
cd mcp-server
|
|
205
186
|
npm test
|
|
206
187
|
|
|
207
188
|
# Format preservation tests
|
|
@@ -219,7 +200,7 @@ MIT License - see [LICENSE](LICENSE) for details.
|
|
|
219
200
|
## 🔗 Related Projects
|
|
220
201
|
|
|
221
202
|
- **[circuit-synth](https://github.com/circuit-synth/circuit-synth)**: Comprehensive circuit design automation
|
|
222
|
-
- **[
|
|
203
|
+
- **[sexpdata](https://github.com/jd-boyd/sexpdata)**: S-expression parsing library
|
|
223
204
|
|
|
224
205
|
---
|
|
225
206
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
kicad_sch_api/__init__.py,sha256=mogTeOic6O-WWOfpoRuBzPicVNha-gbdMrhJX4ir5PY,2821
|
|
2
|
+
kicad_sch_api/py.typed,sha256=e4ldqxwpY7pNDG1olbvj4HSKr8sZ9vxgA_2ek8xXn-Q,70
|
|
3
|
+
kicad_sch_api/core/__init__.py,sha256=ur_KeYBlGKl-e1hLpLdxAhGV2A-PCCGkcqd0r6KSeBA,566
|
|
4
|
+
kicad_sch_api/core/components.py,sha256=TiQrl7jdDTnuCWKdrlQC8-3b9PU58KoxTTnPX6VyZZk,24759
|
|
5
|
+
kicad_sch_api/core/formatter.py,sha256=WhJPII8-hd4ou3_4AYa4UB8704cEE8rLshhBhVHnY7I,14173
|
|
6
|
+
kicad_sch_api/core/ic_manager.py,sha256=5pw0FtUBXeF6afbiYej7kQe6ga32N4bNTScEHWYGq7A,7298
|
|
7
|
+
kicad_sch_api/core/junctions.py,sha256=p99iv8pBb-3oktVsTeHgjl7QkkZa9EeN32nSy4g23o4,6251
|
|
8
|
+
kicad_sch_api/core/parser.py,sha256=xVWzRwGPklROWe9_VoauVeoFsCZjKQGyo6AsSg0bqgU,44174
|
|
9
|
+
kicad_sch_api/core/schematic.py,sha256=JxOTHbASxcOVEFK2Ms1s7q7uERaKVQb0Ojomfyip99I,45176
|
|
10
|
+
kicad_sch_api/core/types.py,sha256=sn6_OnOKnNjc_6oXpASEfWI6f7oHsMyJaHk8kc3QFMk,12823
|
|
11
|
+
kicad_sch_api/core/wires.py,sha256=kvANBg79KxOd5TKZNpHbjMyouZbGwRKLPNz4ePvvHgk,8207
|
|
12
|
+
kicad_sch_api/library/__init__.py,sha256=NG9UTdcpn25Bl9tPsYs9ED7bvpaVPVdtLMbnxkQkOnU,250
|
|
13
|
+
kicad_sch_api/library/cache.py,sha256=NPJT-apWH_JfVG1aXJ0gDnfGu8es_2tYiyZr-chcTxY,33511
|
|
14
|
+
kicad_sch_api/utils/__init__.py,sha256=1V_yGgI7jro6MUc4Pviux_WIeJ1wmiYFID186SZwWLQ,277
|
|
15
|
+
kicad_sch_api/utils/validation.py,sha256=XlWGRZJb3cOPYpU9sLQQgC_NASwbi6W-LCN7PzUmaPY,15626
|
|
16
|
+
kicad_sch_api-0.1.0.dist-info/licenses/LICENSE,sha256=Em65Nvte1G9MHc0rHqtYuGkCPcshD588itTa358J6gs,1070
|
|
17
|
+
kicad_sch_api-0.1.0.dist-info/METADATA,sha256=LIRlM5Iev8fS3Elb0zynJXiZIwmIqmUqcaYmnKN8WdA,6674
|
|
18
|
+
kicad_sch_api-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
+
kicad_sch_api-0.1.0.dist-info/entry_points.txt,sha256=VWKsFi2Jv7G_tmio3cNVhhIBfv_OZFaKa-T_ED84lc8,57
|
|
20
|
+
kicad_sch_api-0.1.0.dist-info/top_level.txt,sha256=n0ex4gOJ1b_fARowcGqRzyOGZcHRhc5LZa6_vVgGxcI,14
|
|
21
|
+
kicad_sch_api-0.1.0.dist-info/RECORD,,
|