kicad-sch-api 0.3.0__py3-none-any.whl → 0.5.1__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.
- kicad_sch_api/__init__.py +68 -3
- kicad_sch_api/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/collections/__init__.py +36 -0
- kicad_sch_api/collections/base.py +604 -0
- kicad_sch_api/collections/components.py +1623 -0
- kicad_sch_api/collections/junctions.py +206 -0
- kicad_sch_api/collections/labels.py +508 -0
- kicad_sch_api/collections/wires.py +292 -0
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +34 -7
- kicad_sch_api/core/components.py +213 -52
- kicad_sch_api/core/config.py +110 -15
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +278 -0
- kicad_sch_api/core/formatter.py +60 -9
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +324 -0
- kicad_sch_api/core/managers/__init__.py +30 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +246 -0
- kicad_sch_api/core/managers/format_sync.py +502 -0
- kicad_sch_api/core/managers/graphics.py +580 -0
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +271 -0
- kicad_sch_api/core/managers/sheet.py +492 -0
- kicad_sch_api/core/managers/text_elements.py +537 -0
- kicad_sch_api/core/managers/validation.py +476 -0
- kicad_sch_api/core/managers/wire.py +410 -0
- kicad_sch_api/core/nets.py +305 -0
- kicad_sch_api/core/no_connects.py +252 -0
- kicad_sch_api/core/parser.py +194 -970
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +1328 -1079
- kicad_sch_api/core/texts.py +316 -0
- kicad_sch_api/core/types.py +159 -23
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +38 -0
- kicad_sch_api/geometry/font_metrics.py +22 -0
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/geometry/symbol_bbox.py +608 -0
- kicad_sch_api/interfaces/__init__.py +17 -0
- kicad_sch_api/interfaces/parser.py +76 -0
- kicad_sch_api/interfaces/repository.py +70 -0
- kicad_sch_api/interfaces/resolver.py +117 -0
- kicad_sch_api/parsers/__init__.py +14 -0
- kicad_sch_api/parsers/base.py +145 -0
- kicad_sch_api/parsers/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +216 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/registry.py +155 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +467 -0
- kicad_sch_api/symbols/resolver.py +361 -0
- kicad_sch_api/symbols/validators.py +504 -0
- kicad_sch_api/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- kicad_sch_api/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- kicad_sch_api/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -0
- kicad_sch_api/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
- kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
- kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main Electrical Rules Checker orchestrator.
|
|
3
|
+
|
|
4
|
+
Coordinates all validators and produces comprehensive ERC results.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
9
|
+
|
|
10
|
+
from kicad_sch_api.validation.erc_models import ERCConfig, ERCResult, ERCViolation
|
|
11
|
+
from kicad_sch_api.validation.validators import (
|
|
12
|
+
BaseValidator,
|
|
13
|
+
ComponentValidator,
|
|
14
|
+
ConnectivityValidator,
|
|
15
|
+
PinTypeValidator,
|
|
16
|
+
PowerValidator,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from kicad_sch_api.core.schematic import Schematic
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ElectricalRulesChecker:
|
|
24
|
+
"""Main ERC orchestrator.
|
|
25
|
+
|
|
26
|
+
Coordinates all validation checks and produces comprehensive results.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> import kicad_sch_api as ksa
|
|
30
|
+
>>> from kicad_sch_api.validation import ElectricalRulesChecker
|
|
31
|
+
>>>
|
|
32
|
+
>>> sch = ksa.load_schematic("circuit.kicad_sch")
|
|
33
|
+
>>> erc = ElectricalRulesChecker(sch)
|
|
34
|
+
>>> result = erc.run_all_checks()
|
|
35
|
+
>>>
|
|
36
|
+
>>> if result.has_errors():
|
|
37
|
+
... for error in result.errors:
|
|
38
|
+
... print(f"ERROR: {error.message}")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
schematic: "Schematic",
|
|
44
|
+
config: Optional[ERCConfig] = None
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Initialize ERC checker.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
schematic: Schematic to validate
|
|
50
|
+
config: Optional custom configuration
|
|
51
|
+
"""
|
|
52
|
+
self.schematic = schematic
|
|
53
|
+
self.config = config or ERCConfig()
|
|
54
|
+
self.validators: List[BaseValidator] = []
|
|
55
|
+
|
|
56
|
+
# Register default validators
|
|
57
|
+
self._register_default_validators()
|
|
58
|
+
|
|
59
|
+
def _register_default_validators(self) -> None:
|
|
60
|
+
"""Register default validators."""
|
|
61
|
+
self.validators = [
|
|
62
|
+
PinTypeValidator(self.schematic),
|
|
63
|
+
ConnectivityValidator(self.schematic),
|
|
64
|
+
ComponentValidator(self.schematic),
|
|
65
|
+
PowerValidator(self.schematic),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
def add_validator(self, validator: BaseValidator) -> None:
|
|
69
|
+
"""Add custom validator.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
validator: Custom validator to add
|
|
73
|
+
"""
|
|
74
|
+
self.validators.append(validator)
|
|
75
|
+
|
|
76
|
+
def run_all_checks(self) -> ERCResult:
|
|
77
|
+
"""Run all ERC checks.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Complete ERC result with all violations
|
|
81
|
+
"""
|
|
82
|
+
start_time = time.time()
|
|
83
|
+
|
|
84
|
+
all_violations: List[ERCViolation] = []
|
|
85
|
+
|
|
86
|
+
# Run each validator
|
|
87
|
+
for validator in self.validators:
|
|
88
|
+
violations = validator.validate()
|
|
89
|
+
all_violations.extend(violations)
|
|
90
|
+
|
|
91
|
+
# Apply configuration (severity overrides, suppression)
|
|
92
|
+
all_violations = self._apply_config(all_violations)
|
|
93
|
+
|
|
94
|
+
# Categorize by severity
|
|
95
|
+
errors = [v for v in all_violations if v.severity == "error"]
|
|
96
|
+
warnings = [v for v in all_violations if v.severity == "warning"]
|
|
97
|
+
info = [v for v in all_violations if v.severity == "info"]
|
|
98
|
+
|
|
99
|
+
# Calculate statistics
|
|
100
|
+
total_checks = len(all_violations) + 100 # Placeholder
|
|
101
|
+
passed_checks = total_checks - len(all_violations)
|
|
102
|
+
|
|
103
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
104
|
+
|
|
105
|
+
return ERCResult(
|
|
106
|
+
errors=errors,
|
|
107
|
+
warnings=warnings,
|
|
108
|
+
info=info,
|
|
109
|
+
total_checks=total_checks,
|
|
110
|
+
passed_checks=passed_checks,
|
|
111
|
+
duration_ms=duration_ms,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def run_check(self, check_type: str) -> List[ERCViolation]:
|
|
115
|
+
"""Run specific check type.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
check_type: Type of check ("pin_types", "connectivity", "components", "power")
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
List of violations from that check
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ValueError: If check type is invalid
|
|
125
|
+
"""
|
|
126
|
+
validator_map = {
|
|
127
|
+
"pin_types": PinTypeValidator,
|
|
128
|
+
"connectivity": ConnectivityValidator,
|
|
129
|
+
"components": ComponentValidator,
|
|
130
|
+
"power": PowerValidator,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if check_type not in validator_map:
|
|
134
|
+
raise ValueError(f"Unknown check type: {check_type}")
|
|
135
|
+
|
|
136
|
+
validator = validator_map[check_type](self.schematic)
|
|
137
|
+
violations = validator.validate()
|
|
138
|
+
|
|
139
|
+
return self._apply_config(violations)
|
|
140
|
+
|
|
141
|
+
def _apply_config(self, violations: List[ERCViolation]) -> List[ERCViolation]:
|
|
142
|
+
"""Apply configuration to violations.
|
|
143
|
+
|
|
144
|
+
Applies severity overrides and filters suppressed warnings.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
violations: Raw violations
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Filtered and adjusted violations
|
|
151
|
+
"""
|
|
152
|
+
result: List[ERCViolation] = []
|
|
153
|
+
|
|
154
|
+
for violation in violations:
|
|
155
|
+
# Check if suppressed
|
|
156
|
+
is_suppressed = False
|
|
157
|
+
for component_ref in violation.component_refs:
|
|
158
|
+
if self.config.is_suppressed(violation.error_code, component_ref):
|
|
159
|
+
is_suppressed = True
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
if is_suppressed:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Apply severity override
|
|
166
|
+
if violation.violation_type in self.config.severity_overrides:
|
|
167
|
+
violation.severity = self.config.severity_overrides[violation.violation_type]
|
|
168
|
+
|
|
169
|
+
result.append(violation)
|
|
170
|
+
|
|
171
|
+
return result
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ERC data models: ERCViolation, ERCResult, ERCConfig.
|
|
3
|
+
|
|
4
|
+
These models represent validation results and configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Dict, List, Optional, Set
|
|
10
|
+
|
|
11
|
+
from kicad_sch_api.core.types import Point
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ERCViolation:
|
|
16
|
+
"""Single ERC violation.
|
|
17
|
+
|
|
18
|
+
Represents one electrical rule violation found during validation.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
violation_type: Category of violation (e.g., "pin_conflict", "dangling_wire")
|
|
22
|
+
severity: "error", "warning", or "info"
|
|
23
|
+
message: Human-readable description
|
|
24
|
+
component_refs: List of affected component references
|
|
25
|
+
error_code: Unique error code (e.g., "E001", "W042")
|
|
26
|
+
net_name: Optional net name where violation occurred
|
|
27
|
+
pin_numbers: List of affected pin numbers
|
|
28
|
+
location: Optional schematic coordinates
|
|
29
|
+
suggested_fix: Optional recommended fix
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
violation_type: str
|
|
33
|
+
severity: str
|
|
34
|
+
message: str
|
|
35
|
+
component_refs: List[str]
|
|
36
|
+
error_code: str
|
|
37
|
+
net_name: Optional[str] = None
|
|
38
|
+
pin_numbers: List[str] = field(default_factory=list)
|
|
39
|
+
location: Optional[Point] = None
|
|
40
|
+
suggested_fix: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
43
|
+
"""Convert violation to dictionary for serialization."""
|
|
44
|
+
return {
|
|
45
|
+
"violation_type": self.violation_type,
|
|
46
|
+
"severity": self.severity,
|
|
47
|
+
"message": self.message,
|
|
48
|
+
"component_refs": self.component_refs,
|
|
49
|
+
"error_code": self.error_code,
|
|
50
|
+
"net_name": self.net_name,
|
|
51
|
+
"pin_numbers": self.pin_numbers,
|
|
52
|
+
"location": {
|
|
53
|
+
"x": self.location.x,
|
|
54
|
+
"y": self.location.y
|
|
55
|
+
} if self.location else None,
|
|
56
|
+
"suggested_fix": self.suggested_fix,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ERCResult:
|
|
62
|
+
"""Complete ERC validation results.
|
|
63
|
+
|
|
64
|
+
Aggregates all violations found during validation.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
errors: List of error-level violations
|
|
68
|
+
warnings: List of warning-level violations
|
|
69
|
+
info: List of info-level violations
|
|
70
|
+
total_checks: Total number of checks performed
|
|
71
|
+
passed_checks: Number of checks that passed
|
|
72
|
+
duration_ms: Execution time in milliseconds
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
errors: List[ERCViolation]
|
|
76
|
+
warnings: List[ERCViolation]
|
|
77
|
+
info: List[ERCViolation]
|
|
78
|
+
total_checks: int
|
|
79
|
+
passed_checks: int
|
|
80
|
+
duration_ms: float
|
|
81
|
+
|
|
82
|
+
def has_errors(self) -> bool:
|
|
83
|
+
"""Check if any errors were found."""
|
|
84
|
+
return len(self.errors) > 0
|
|
85
|
+
|
|
86
|
+
def summary(self) -> str:
|
|
87
|
+
"""Generate human-readable summary."""
|
|
88
|
+
error_count = len(self.errors)
|
|
89
|
+
warning_count = len(self.warnings)
|
|
90
|
+
|
|
91
|
+
error_str = f"{error_count} error{'s' if error_count != 1 else ''}"
|
|
92
|
+
warning_str = f"{warning_count} warning{'s' if warning_count != 1 else ''}"
|
|
93
|
+
|
|
94
|
+
return f"{error_str}, {warning_str}"
|
|
95
|
+
|
|
96
|
+
def filter_by_severity(self, severity: str) -> List[ERCViolation]:
|
|
97
|
+
"""Filter violations by severity level.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
severity: "error", "warning", or "info"
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
List of violations matching severity
|
|
104
|
+
"""
|
|
105
|
+
if severity == "error":
|
|
106
|
+
return self.errors
|
|
107
|
+
elif severity == "warning":
|
|
108
|
+
return self.warnings
|
|
109
|
+
elif severity == "info":
|
|
110
|
+
return self.info
|
|
111
|
+
else:
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
def filter_by_component(self, ref: str) -> List[ERCViolation]:
|
|
115
|
+
"""Filter violations affecting a specific component.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
ref: Component reference (e.g., "R1")
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
List of violations involving this component
|
|
122
|
+
"""
|
|
123
|
+
all_violations = self.errors + self.warnings + self.info
|
|
124
|
+
return [v for v in all_violations if ref in v.component_refs]
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
127
|
+
"""Convert result to dictionary for serialization."""
|
|
128
|
+
return {
|
|
129
|
+
"errors": [e.to_dict() for e in self.errors],
|
|
130
|
+
"warnings": [w.to_dict() for w in self.warnings],
|
|
131
|
+
"info": [i.to_dict() for i in self.info],
|
|
132
|
+
"total_checks": self.total_checks,
|
|
133
|
+
"passed_checks": self.passed_checks,
|
|
134
|
+
"duration_ms": self.duration_ms,
|
|
135
|
+
"summary": self.summary(),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def to_json(self) -> str:
|
|
139
|
+
"""Convert result to JSON string."""
|
|
140
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ERCConfig:
|
|
144
|
+
"""ERC configuration.
|
|
145
|
+
|
|
146
|
+
Allows customizing validation behavior, severity levels, and rule suppression.
|
|
147
|
+
|
|
148
|
+
Attributes:
|
|
149
|
+
severity_overrides: Custom severity levels for specific rules
|
|
150
|
+
suppressed_warnings: Set of suppressed warning codes
|
|
151
|
+
custom_rules: List of custom validation rules (not yet implemented)
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self) -> None:
|
|
155
|
+
"""Initialize with default configuration."""
|
|
156
|
+
self.severity_overrides: Dict[str, str] = {}
|
|
157
|
+
self.suppressed_warnings: Set[str] = set()
|
|
158
|
+
self.custom_rules: List[Any] = []
|
|
159
|
+
|
|
160
|
+
def set_severity(self, rule: str, severity: str) -> None:
|
|
161
|
+
"""Override default severity for a rule.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
rule: Rule identifier (e.g., "unconnected_input")
|
|
165
|
+
severity: "error", "warning", or "info"
|
|
166
|
+
"""
|
|
167
|
+
if severity not in ["error", "warning", "info"]:
|
|
168
|
+
raise ValueError(f"Invalid severity: {severity}")
|
|
169
|
+
self.severity_overrides[rule] = severity
|
|
170
|
+
|
|
171
|
+
def suppress_warning(self, code: str, component: Optional[str] = None) -> None:
|
|
172
|
+
"""Suppress a specific warning.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
code: Warning code (e.g., "W001")
|
|
176
|
+
component: Optional component reference to suppress only for that component
|
|
177
|
+
"""
|
|
178
|
+
if component:
|
|
179
|
+
# Store as "code:component" for component-specific suppression
|
|
180
|
+
self.suppressed_warnings.add(f"{code}:{component}")
|
|
181
|
+
else:
|
|
182
|
+
# Store as just "code" for global suppression
|
|
183
|
+
self.suppressed_warnings.add(code)
|
|
184
|
+
|
|
185
|
+
def is_suppressed(self, code: str, component: Optional[str] = None) -> bool:
|
|
186
|
+
"""Check if a warning is suppressed.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
code: Warning code
|
|
190
|
+
component: Optional component reference
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
True if warning should be suppressed
|
|
194
|
+
"""
|
|
195
|
+
# Check global suppression
|
|
196
|
+
if code in self.suppressed_warnings:
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
# Check component-specific suppression
|
|
200
|
+
if component and f"{code}:{component}" in self.suppressed_warnings:
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
return False
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pin Conflict Matrix for ERC validation.
|
|
3
|
+
|
|
4
|
+
Defines electrical compatibility rules between different pin types,
|
|
5
|
+
matching KiCAD's default ERC matrix.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import IntEnum
|
|
9
|
+
from typing import Dict, Tuple
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PinSeverity(IntEnum):
|
|
13
|
+
"""Severity levels for pin connections."""
|
|
14
|
+
OK = 0
|
|
15
|
+
WARNING = 1
|
|
16
|
+
ERROR = 2
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PinConflictMatrix:
|
|
20
|
+
"""Pin type compatibility matrix.
|
|
21
|
+
|
|
22
|
+
Defines which pin type combinations are OK, WARNING, or ERROR.
|
|
23
|
+
Based on KiCAD's default ERC matrix.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Pin type aliases for normalization
|
|
27
|
+
PIN_TYPE_ALIASES = {
|
|
28
|
+
"input": "input",
|
|
29
|
+
"pt_input": "input",
|
|
30
|
+
"i": "input",
|
|
31
|
+
|
|
32
|
+
"output": "output",
|
|
33
|
+
"pt_output": "output",
|
|
34
|
+
"o": "output",
|
|
35
|
+
|
|
36
|
+
"bidirectional": "bidirectional",
|
|
37
|
+
"pt_bidi": "bidirectional",
|
|
38
|
+
"bidi": "bidirectional",
|
|
39
|
+
"b": "bidirectional",
|
|
40
|
+
|
|
41
|
+
"tristate": "tristate",
|
|
42
|
+
"pt_tristate": "tristate",
|
|
43
|
+
"tri": "tristate",
|
|
44
|
+
"t": "tristate",
|
|
45
|
+
|
|
46
|
+
"passive": "passive",
|
|
47
|
+
"pt_passive": "passive",
|
|
48
|
+
"p": "passive",
|
|
49
|
+
|
|
50
|
+
"free": "free",
|
|
51
|
+
"nic": "free",
|
|
52
|
+
"pt_nic": "free",
|
|
53
|
+
"not_connected": "free",
|
|
54
|
+
"f": "free",
|
|
55
|
+
|
|
56
|
+
"unspecified": "unspecified",
|
|
57
|
+
"pt_unspecified": "unspecified",
|
|
58
|
+
"u": "unspecified",
|
|
59
|
+
|
|
60
|
+
"power_input": "power_input",
|
|
61
|
+
"pt_power_in": "power_input",
|
|
62
|
+
"pwr_in": "power_input",
|
|
63
|
+
"w": "power_input",
|
|
64
|
+
|
|
65
|
+
"power_output": "power_output",
|
|
66
|
+
"pt_power_out": "power_output",
|
|
67
|
+
"pwr_out": "power_output",
|
|
68
|
+
|
|
69
|
+
"open_collector": "open_collector",
|
|
70
|
+
"pt_opencollector": "open_collector",
|
|
71
|
+
"oc": "open_collector",
|
|
72
|
+
"c": "open_collector",
|
|
73
|
+
|
|
74
|
+
"open_emitter": "open_emitter",
|
|
75
|
+
"pt_openemitter": "open_emitter",
|
|
76
|
+
"oe": "open_emitter",
|
|
77
|
+
"e": "open_emitter",
|
|
78
|
+
|
|
79
|
+
"nc": "nc",
|
|
80
|
+
"pt_nc": "nc",
|
|
81
|
+
"not_connected": "nc",
|
|
82
|
+
"n": "nc",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def __init__(self) -> None:
|
|
86
|
+
"""Initialize with KiCAD default matrix."""
|
|
87
|
+
self.matrix = self.get_default_matrix()
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def get_default_matrix() -> Dict[Tuple[str, str], int]:
|
|
91
|
+
"""Get KiCAD default pin conflict matrix.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dictionary mapping (pin_type1, pin_type2) to severity
|
|
95
|
+
"""
|
|
96
|
+
# Start with all combinations as OK
|
|
97
|
+
matrix: Dict[Tuple[str, str], int] = {}
|
|
98
|
+
|
|
99
|
+
# Define all pin types
|
|
100
|
+
pin_types = [
|
|
101
|
+
"input", "output", "bidirectional", "tristate", "passive",
|
|
102
|
+
"free", "unspecified", "power_input", "power_output",
|
|
103
|
+
"open_collector", "open_emitter", "nc"
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
# Default: everything is OK
|
|
107
|
+
for pin1 in pin_types:
|
|
108
|
+
for pin2 in pin_types:
|
|
109
|
+
matrix[(pin1, pin2)] = PinSeverity.OK
|
|
110
|
+
matrix[(pin2, pin1)] = PinSeverity.OK # Ensure symmetry
|
|
111
|
+
|
|
112
|
+
# ERROR conditions (serious electrical conflicts)
|
|
113
|
+
error_rules = [
|
|
114
|
+
("output", "output"), # Multiple outputs driving same net
|
|
115
|
+
("power_output", "power_output"), # Multiple power supplies shorted
|
|
116
|
+
("output", "power_output"), # Logic output to power rail
|
|
117
|
+
("nc", "input"), # NC pin should not connect
|
|
118
|
+
("nc", "output"),
|
|
119
|
+
("nc", "bidirectional"),
|
|
120
|
+
("nc", "tristate"),
|
|
121
|
+
("nc", "power_input"),
|
|
122
|
+
("nc", "power_output"),
|
|
123
|
+
("nc", "open_collector"),
|
|
124
|
+
("nc", "open_emitter"),
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
for pin1, pin2 in error_rules:
|
|
128
|
+
matrix[(pin1, pin2)] = PinSeverity.ERROR
|
|
129
|
+
matrix[(pin2, pin1)] = PinSeverity.ERROR
|
|
130
|
+
|
|
131
|
+
# WARNING conditions (potential issues)
|
|
132
|
+
warning_rules = [
|
|
133
|
+
("unspecified", "input"),
|
|
134
|
+
("unspecified", "output"),
|
|
135
|
+
("unspecified", "bidirectional"),
|
|
136
|
+
("unspecified", "tristate"),
|
|
137
|
+
("unspecified", "passive"),
|
|
138
|
+
("unspecified", "power_input"),
|
|
139
|
+
("unspecified", "power_output"),
|
|
140
|
+
("unspecified", "open_collector"),
|
|
141
|
+
("unspecified", "open_emitter"),
|
|
142
|
+
("unspecified", "unspecified"),
|
|
143
|
+
("tristate", "output"), # Tri-state with output can conflict
|
|
144
|
+
("tristate", "tristate"), # Multiple tri-states
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
for pin1, pin2 in warning_rules:
|
|
148
|
+
matrix[(pin1, pin2)] = PinSeverity.WARNING
|
|
149
|
+
matrix[(pin2, pin1)] = PinSeverity.WARNING
|
|
150
|
+
|
|
151
|
+
# Passive is OK with everything (except NC which is already ERROR)
|
|
152
|
+
for pin_type in pin_types:
|
|
153
|
+
if pin_type != "nc":
|
|
154
|
+
matrix[("passive", pin_type)] = PinSeverity.OK
|
|
155
|
+
matrix[(pin_type, "passive")] = PinSeverity.OK
|
|
156
|
+
|
|
157
|
+
# Free/NIC is OK with everything
|
|
158
|
+
for pin_type in pin_types:
|
|
159
|
+
matrix[("free", pin_type)] = PinSeverity.OK
|
|
160
|
+
matrix[(pin_type, "free")] = PinSeverity.OK
|
|
161
|
+
|
|
162
|
+
return matrix
|
|
163
|
+
|
|
164
|
+
def normalize_pin_type(self, pin_type: str) -> str:
|
|
165
|
+
"""Normalize pin type string.
|
|
166
|
+
|
|
167
|
+
Handles case-insensitive matching and aliases.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
pin_type: Pin type string
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Normalized pin type
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
ValueError: If pin type is invalid
|
|
177
|
+
"""
|
|
178
|
+
normalized = pin_type.lower().strip()
|
|
179
|
+
|
|
180
|
+
if normalized in self.PIN_TYPE_ALIASES:
|
|
181
|
+
return self.PIN_TYPE_ALIASES[normalized]
|
|
182
|
+
|
|
183
|
+
raise ValueError(f"Unknown pin type: {pin_type}")
|
|
184
|
+
|
|
185
|
+
def check_connection(self, pin1_type: str, pin2_type: str) -> int:
|
|
186
|
+
"""Check if connection between two pin types is OK, WARNING, or ERROR.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
pin1_type: First pin type
|
|
190
|
+
pin2_type: Second pin type
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
PinSeverity.OK, PinSeverity.WARNING, or PinSeverity.ERROR
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
ValueError: If pin type is invalid
|
|
197
|
+
"""
|
|
198
|
+
# Normalize pin types
|
|
199
|
+
pin1 = self.normalize_pin_type(pin1_type)
|
|
200
|
+
pin2 = self.normalize_pin_type(pin2_type)
|
|
201
|
+
|
|
202
|
+
# Look up in matrix
|
|
203
|
+
key = (pin1, pin2)
|
|
204
|
+
if key in self.matrix:
|
|
205
|
+
return self.matrix[key]
|
|
206
|
+
|
|
207
|
+
# Should not happen if matrix is complete
|
|
208
|
+
raise ValueError(f"No rule for pin combination: {pin1} + {pin2}")
|
|
209
|
+
|
|
210
|
+
def set_rule(self, pin1_type: str, pin2_type: str, severity: int) -> None:
|
|
211
|
+
"""Set custom rule for pin type combination.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
pin1_type: First pin type
|
|
215
|
+
pin2_type: Second pin type
|
|
216
|
+
severity: PinSeverity.OK, WARNING, or ERROR
|
|
217
|
+
"""
|
|
218
|
+
pin1 = self.normalize_pin_type(pin1_type)
|
|
219
|
+
pin2 = self.normalize_pin_type(pin2_type)
|
|
220
|
+
|
|
221
|
+
if severity not in [PinSeverity.OK, PinSeverity.WARNING, PinSeverity.ERROR]:
|
|
222
|
+
raise ValueError(f"Invalid severity: {severity}")
|
|
223
|
+
|
|
224
|
+
# Set both directions for symmetry
|
|
225
|
+
self.matrix[(pin1, pin2)] = severity
|
|
226
|
+
self.matrix[(pin2, pin1)] = severity
|
|
227
|
+
|
|
228
|
+
@classmethod
|
|
229
|
+
def from_dict(cls, custom_matrix: Dict[Tuple[str, str], int]) -> "PinConflictMatrix":
|
|
230
|
+
"""Create matrix from custom dictionary.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
custom_matrix: Dictionary of custom rules
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
PinConflictMatrix with custom rules applied
|
|
237
|
+
"""
|
|
238
|
+
matrix = cls()
|
|
239
|
+
|
|
240
|
+
for (pin1, pin2), severity in custom_matrix.items():
|
|
241
|
+
matrix.set_rule(pin1, pin2, severity)
|
|
242
|
+
|
|
243
|
+
return matrix
|