├── .gitignore ├── jlcpcb_parts_pipeline ├── jlc_parts_enriched.schema.json ├── enrich_parts.py └── parts_requirements.yaml ├── KiCAD-Generator-tools ├── scripts │ ├── validate_step1.py │ ├── validate_step2.py │ ├── validate_step4.py │ ├── generate_pin_model.py │ ├── validate_step3.py │ ├── validate_pin_model.py │ ├── generate_skidl.py │ ├── run_pipeline.py │ ├── validate_step5.py │ ├── generate_skidl_schematic.py │ ├── summarize_progress.py │ ├── verify_netlist.py │ ├── generate_kicad_project.py │ ├── generate_skidl_v2.py │ ├── skidl_to_kicad_sch.py │ └── ensure_symbols.py └── prompts │ ├── step2.md │ ├── step5.md │ ├── step4.md │ ├── step3.md │ ├── step6.md │ ├── step7.md │ ├── step0.md │ └── step1.md ├── RadioReceiverV2 └── fsd_review.md └── CLAUDE.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.pyc 4 | *.pyo 5 | 6 | # KiCAD backups and temp files 7 | *-backups/ 8 | *.kicad_prl 9 | fp-info-cache 10 | \#auto_saved_files# 11 | _autosave-* 12 | ~*.lck 13 | *.kicad_sch-bak 14 | 15 | # IDE 16 | .vscode/ 17 | .idea/ 18 | 19 | # AI Agents 20 | .claude/ 21 | .cursor/ 22 | .aider/ 23 | .continue/ 24 | .codeium/ 25 | .tabnine/ 26 | .copilot/ 27 | 28 | # Generated/intermediate files (pipeline output) 29 | **/work/*.json 30 | **/work/*.yaml 31 | **/work/*.csv 32 | **/output/*.kicad_sch 33 | **/output/*.kicad_pro 34 | **/output/*.kicad_pcb 35 | **/output/debug.csv 36 | **/output/erc_report.txt 37 | **/output/netlist.net 38 | **/output/libs/ 39 | **/output/sym-lib-table 40 | **/output/fp-lib-table 41 | **/output/README.md 42 | 43 | # Legacy intermediate files 44 | symbol_pins.json 45 | parts_with_netlabels.json 46 | connections.json 47 | *.net 48 | test_netlabels.kicad_sch 49 | 50 | # OS 51 | .DS_Store 52 | Thumbs.db 53 | 54 | # Enrich parts logs 55 | enrich_parts_*.log 56 | 57 | # Separate repositories 58 | Circuit-Designs/ 59 | RadioReceiver/llm-research-v2/ 60 | -------------------------------------------------------------------------------- /jlcpcb_parts_pipeline/jlc_parts_enriched.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "title": "JLCPCB Parts Enriched", 4 | "type": "object", 5 | "properties": { 6 | "meta": { 7 | "type": "object" 8 | }, 9 | "parts": { 10 | "type": "array", 11 | "items": { 12 | "type": "object", 13 | "required": [ 14 | "key", 15 | "query", 16 | "candidates" 17 | ], 18 | "properties": { 19 | "key": { 20 | "type": "string" 21 | }, 22 | "query": { 23 | "type": "string" 24 | }, 25 | "selection": { 26 | "type": "object", 27 | "properties": { 28 | "lcsc": { 29 | "type": "string" 30 | }, 31 | "jlc_basic_extended": { 32 | "type": "string" 33 | }, 34 | "manufacturer": { 35 | "type": "string" 36 | }, 37 | "mpn": { 38 | "type": "string" 39 | }, 40 | "description": { 41 | "type": "string" 42 | }, 43 | "package": { 44 | "type": "string" 45 | }, 46 | "datasheet_url": { 47 | "type": "string" 48 | }, 49 | "jlc_url": { 50 | "type": "string" 51 | }, 52 | "stock": { 53 | "type": "number" 54 | }, 55 | "prices": { 56 | "type": "array", 57 | "items": { 58 | "type": "object", 59 | "properties": { 60 | "qty": { 61 | "type": "number" 62 | }, 63 | "unit_price": { 64 | "type": "number" 65 | }, 66 | "currency": { 67 | "type": "string" 68 | } 69 | } 70 | } 71 | } 72 | } 73 | }, 74 | "candidates": { 75 | "type": "array" 76 | } 77 | } 78 | } 79 | } 80 | }, 81 | "required": [ 82 | "meta", 83 | "parts" 84 | ] 85 | } -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/validate_step1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Validate step1_primary_parts.yaml 4 | 5 | Checks: 6 | - YAML parses correctly 7 | - primary_parts[*].id is unique 8 | - Each primary part has suggested_part (not empty) 9 | - Required fields present 10 | """ 11 | 12 | import sys 13 | import yaml 14 | from pathlib import Path 15 | 16 | REQUIRED_FIELDS = ['id', 'name', 'suggested_part', 'category', 'quantity'] 17 | VALID_CATEGORIES = ['microcontroller', 'radio', 'power', 'connector', 'ui', 'sensor', 'passive', 'other'] 18 | 19 | 20 | def validate(filepath: Path) -> list: 21 | """Validate step1 file and return list of errors.""" 22 | errors = [] 23 | 24 | # Check file exists 25 | if not filepath.exists(): 26 | return [f"File not found: {filepath}"] 27 | 28 | # Parse YAML 29 | try: 30 | with open(filepath, 'r', encoding='utf-8') as f: 31 | data = yaml.safe_load(f) 32 | except yaml.YAMLError as e: 33 | return [f"YAML parse error: {e}"] 34 | 35 | if not data: 36 | return ["File is empty"] 37 | 38 | # Check primary_parts exists 39 | if 'primary_parts' not in data: 40 | return ["Missing 'primary_parts' key"] 41 | 42 | parts = data['primary_parts'] 43 | if not isinstance(parts, list): 44 | return ["'primary_parts' must be a list"] 45 | 46 | if len(parts) == 0: 47 | errors.append("WARNING: No primary parts defined") 48 | 49 | # Track IDs for uniqueness 50 | seen_ids = set() 51 | 52 | for i, part in enumerate(parts): 53 | prefix = f"primary_parts[{i}]" 54 | 55 | if not isinstance(part, dict): 56 | errors.append(f"{prefix}: Must be a dictionary") 57 | continue 58 | 59 | # Check required fields 60 | for field in REQUIRED_FIELDS: 61 | if field not in part: 62 | errors.append(f"{prefix}: Missing required field '{field}'") 63 | 64 | # Check id uniqueness 65 | part_id = part.get('id') 66 | if part_id: 67 | if part_id in seen_ids: 68 | errors.append(f"{prefix}: Duplicate id '{part_id}'") 69 | seen_ids.add(part_id) 70 | 71 | # Check suggested_part not empty 72 | suggested = part.get('suggested_part', '') 73 | if not suggested or suggested.strip() == '': 74 | errors.append(f"{prefix} ({part_id}): suggested_part is empty") 75 | 76 | # Check category is valid 77 | category = part.get('category') 78 | if category and category not in VALID_CATEGORIES: 79 | errors.append(f"{prefix} ({part_id}): Invalid category '{category}'. Must be one of {VALID_CATEGORIES}") 80 | 81 | # Check quantity is positive integer 82 | qty = part.get('quantity') 83 | if qty is not None: 84 | if not isinstance(qty, int) or qty < 1: 85 | errors.append(f"{prefix} ({part_id}): quantity must be positive integer, got '{qty}'") 86 | 87 | return errors 88 | 89 | 90 | def main(): 91 | script_dir = Path(__file__).parent.parent 92 | filepath = script_dir / "work" / "step1_primary_parts.yaml" 93 | 94 | print(f"Validating: {filepath}") 95 | print("=" * 60) 96 | 97 | errors = validate(filepath) 98 | 99 | if errors: 100 | print("VALIDATION FAILED\n") 101 | for error in errors: 102 | print(f" ❌ {error}") 103 | print(f"\nTotal errors: {len(errors)}") 104 | sys.exit(1) 105 | else: 106 | print("✅ VALIDATION PASSED") 107 | # Print summary 108 | with open(filepath, 'r', encoding='utf-8') as f: 109 | data = yaml.safe_load(f) 110 | parts = data.get('primary_parts', []) 111 | print(f"\nSummary: {len(parts)} primary parts defined") 112 | sys.exit(0) 113 | 114 | 115 | if __name__ == "__main__": 116 | main() 117 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/validate_step2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Validate step2_parts_extended.yaml 4 | 5 | Checks: 6 | - YAML parses correctly 7 | - All belongs_to references exist 8 | - No supporting parts with belongs_to: null (except primary parts) 9 | - Optional parts have optional: true 10 | - Required fields present 11 | """ 12 | 13 | import sys 14 | import yaml 15 | from pathlib import Path 16 | 17 | REQUIRED_FIELDS = ['id', 'name', 'part', 'category', 'quantity', 'belongs_to'] 18 | 19 | 20 | def validate(filepath: Path) -> list: 21 | """Validate step2 file and return list of errors.""" 22 | errors = [] 23 | 24 | # Check file exists 25 | if not filepath.exists(): 26 | return [f"File not found: {filepath}"] 27 | 28 | # Parse YAML 29 | try: 30 | with open(filepath, 'r', encoding='utf-8') as f: 31 | data = yaml.safe_load(f) 32 | except yaml.YAMLError as e: 33 | return [f"YAML parse error: {e}"] 34 | 35 | if not data: 36 | return ["File is empty"] 37 | 38 | # Check parts exists 39 | if 'parts' not in data: 40 | return ["Missing 'parts' key"] 41 | 42 | parts = data['parts'] 43 | if not isinstance(parts, list): 44 | return ["'parts' must be a list"] 45 | 46 | if len(parts) == 0: 47 | errors.append("WARNING: No parts defined") 48 | 49 | # Collect all IDs and primary part IDs 50 | all_ids = set() 51 | primary_ids = set() 52 | 53 | for part in parts: 54 | if isinstance(part, dict): 55 | part_id = part.get('id') 56 | if part_id: 57 | all_ids.add(part_id) 58 | if part.get('belongs_to') is None: 59 | primary_ids.add(part_id) 60 | 61 | # Track IDs for uniqueness 62 | seen_ids = set() 63 | 64 | for i, part in enumerate(parts): 65 | prefix = f"parts[{i}]" 66 | 67 | if not isinstance(part, dict): 68 | errors.append(f"{prefix}: Must be a dictionary") 69 | continue 70 | 71 | part_id = part.get('id', f'') 72 | 73 | # Check required fields 74 | for field in REQUIRED_FIELDS: 75 | if field not in part: 76 | errors.append(f"{prefix} ({part_id}): Missing required field '{field}'") 77 | 78 | # Check id uniqueness 79 | if part_id in seen_ids: 80 | errors.append(f"{prefix}: Duplicate id '{part_id}'") 81 | seen_ids.add(part_id) 82 | 83 | # Check belongs_to reference 84 | belongs_to = part.get('belongs_to') 85 | if belongs_to is not None: 86 | if belongs_to not in primary_ids: 87 | errors.append(f"{prefix} ({part_id}): belongs_to '{belongs_to}' not found in primary parts") 88 | 89 | # Supporting parts should have source/purpose 90 | if 'purpose' not in part and 'source' not in part: 91 | errors.append(f"{prefix} ({part_id}): Supporting part should have 'purpose' or 'source' field") 92 | 93 | # Check optional field if present 94 | optional = part.get('optional') 95 | if optional is not None and not isinstance(optional, bool): 96 | errors.append(f"{prefix} ({part_id}): 'optional' must be true or false") 97 | 98 | # Check quantity is positive 99 | qty = part.get('quantity') 100 | if qty is not None: 101 | if not isinstance(qty, int) or qty < 1: 102 | errors.append(f"{prefix} ({part_id}): quantity must be positive integer") 103 | 104 | # Summary checks 105 | primary_count = len(primary_ids) 106 | supporting_count = len(all_ids) - primary_count 107 | 108 | if primary_count == 0: 109 | errors.append("WARNING: No primary parts (belongs_to: null) found") 110 | 111 | return errors 112 | 113 | 114 | def main(): 115 | script_dir = Path(__file__).parent.parent 116 | filepath = script_dir / "work" / "step2_parts_extended.yaml" 117 | 118 | print(f"Validating: {filepath}") 119 | print("=" * 60) 120 | 121 | errors = validate(filepath) 122 | 123 | if errors: 124 | print("VALIDATION FAILED\n") 125 | for error in errors: 126 | print(f" ❌ {error}") 127 | print(f"\nTotal errors: {len(errors)}") 128 | sys.exit(1) 129 | else: 130 | print("✅ VALIDATION PASSED") 131 | # Print summary 132 | with open(filepath, 'r', encoding='utf-8') as f: 133 | data = yaml.safe_load(f) 134 | parts = data.get('parts', []) 135 | primary = sum(1 for p in parts if p.get('belongs_to') is None) 136 | supporting = len(parts) - primary 137 | print(f"\nSummary: {len(parts)} total parts") 138 | print(f" - Primary parts: {primary}") 139 | print(f" - Supporting parts: {supporting}") 140 | sys.exit(0) 141 | 142 | 143 | if __name__ == "__main__": 144 | main() 145 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/validate_step4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Validate step4_final_parts.yaml 4 | 5 | Checks: 6 | - YAML parses correctly 7 | - No unresolved TBD values 8 | - IDs unique 9 | - All required fields present 10 | - Valid prefixes 11 | """ 12 | 13 | import sys 14 | import yaml 15 | from pathlib import Path 16 | 17 | REQUIRED_FIELDS = ['id', 'name', 'part', 'package', 'prefix', 'category', 'quantity', 'belongs_to'] 18 | VALID_PREFIXES = ['R', 'C', 'U', 'D', 'J', 'SW', 'Y', 'L', 'F', 'ENC', 'ANT', 'TP', 'FB'] 19 | 20 | 21 | def validate(filepath: Path) -> list: 22 | """Validate step4 file and return list of errors.""" 23 | errors = [] 24 | 25 | # Check file exists 26 | if not filepath.exists(): 27 | return [f"File not found: {filepath}"] 28 | 29 | # Parse YAML 30 | try: 31 | with open(filepath, 'r', encoding='utf-8') as f: 32 | data = yaml.safe_load(f) 33 | except yaml.YAMLError as e: 34 | return [f"YAML parse error: {e}"] 35 | 36 | if not data: 37 | return ["File is empty"] 38 | 39 | # Check parts exists 40 | if 'parts' not in data: 41 | return ["Missing 'parts' key"] 42 | 43 | parts = data['parts'] 44 | if not isinstance(parts, list): 45 | return ["'parts' must be a list"] 46 | 47 | if len(parts) == 0: 48 | return ["No parts defined - cannot have empty BOM"] 49 | 50 | # Track IDs for uniqueness 51 | seen_ids = set() 52 | 53 | for i, part in enumerate(parts): 54 | prefix = f"parts[{i}]" 55 | 56 | if not isinstance(part, dict): 57 | errors.append(f"{prefix}: Must be a dictionary") 58 | continue 59 | 60 | part_id = part.get('id', f'') 61 | 62 | # Check required fields 63 | for field in REQUIRED_FIELDS: 64 | if field not in part: 65 | errors.append(f"{prefix} ({part_id}): Missing required field '{field}'") 66 | 67 | # Check id uniqueness 68 | if part_id in seen_ids: 69 | errors.append(f"{prefix}: Duplicate id '{part_id}'") 70 | seen_ids.add(part_id) 71 | 72 | # Check for TBD values 73 | part_value = part.get('part', '') 74 | if 'TBD' in str(part_value).upper(): 75 | errors.append(f"{prefix} ({part_id}): Unresolved TBD in part value '{part_value}'") 76 | 77 | package = part.get('package', '') 78 | if 'TBD' in str(package).upper(): 79 | errors.append(f"{prefix} ({part_id}): Unresolved TBD in package '{package}'") 80 | 81 | # Check prefix is valid 82 | part_prefix = part.get('prefix', '') 83 | if part_prefix and part_prefix not in VALID_PREFIXES: 84 | errors.append(f"{prefix} ({part_id}): Invalid prefix '{part_prefix}'. Must be one of {VALID_PREFIXES}") 85 | 86 | # Check quantity is positive 87 | qty = part.get('quantity') 88 | if qty is not None: 89 | if not isinstance(qty, int) or qty < 1: 90 | errors.append(f"{prefix} ({part_id}): quantity must be positive integer") 91 | 92 | # Check lcsc_hint present (warning only) 93 | if 'lcsc_hint' not in part: 94 | errors.append(f"WARNING: {prefix} ({part_id}): Missing lcsc_hint for JLCPCB lookup") 95 | 96 | return errors 97 | 98 | 99 | def main(): 100 | script_dir = Path(__file__).parent.parent 101 | filepath = script_dir / "work" / "step4_final_parts.yaml" 102 | 103 | print(f"Validating: {filepath}") 104 | print("=" * 60) 105 | 106 | errors = validate(filepath) 107 | 108 | # Separate warnings from errors 109 | warnings = [e for e in errors if e.startswith("WARNING")] 110 | real_errors = [e for e in errors if not e.startswith("WARNING")] 111 | 112 | if real_errors: 113 | print("VALIDATION FAILED\n") 114 | for error in real_errors: 115 | print(f" ❌ {error}") 116 | if warnings: 117 | print("\nWarnings:") 118 | for warning in warnings: 119 | print(f" ⚠️ {warning}") 120 | print(f"\nTotal errors: {len(real_errors)}") 121 | sys.exit(1) 122 | else: 123 | print("✅ VALIDATION PASSED") 124 | if warnings: 125 | print("\nWarnings:") 126 | for warning in warnings: 127 | print(f" ⚠️ {warning}") 128 | # Print summary 129 | with open(filepath, 'r', encoding='utf-8') as f: 130 | data = yaml.safe_load(f) 131 | parts = data.get('parts', []) 132 | total_qty = sum(p.get('quantity', 1) for p in parts) 133 | print(f"\nSummary: {len(parts)} unique parts, {total_qty} total components") 134 | 135 | # Count by prefix 136 | by_prefix = {} 137 | for p in parts: 138 | pfx = p.get('prefix', '?') 139 | by_prefix[pfx] = by_prefix.get(pfx, 0) + p.get('quantity', 1) 140 | print("By type:") 141 | for pfx, count in sorted(by_prefix.items()): 142 | print(f" {pfx}: {count}") 143 | 144 | sys.exit(0) 145 | 146 | 147 | if __name__ == "__main__": 148 | main() 149 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/generate_pin_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Generate Enhanced Pin Model JSON from step4/step5 YAML files. 4 | 5 | Transforms net-centric connections (step5) into part-centric pin mappings 6 | for the SKiDL-compatible pipeline. 7 | 8 | Output format matches the proposed deterministic schema: 9 | - Every pin has exactly one net 10 | - No implicit power pins 11 | - Explicit belongs_to relationships preserved 12 | """ 13 | 14 | import json 15 | import yaml 16 | from pathlib import Path 17 | from collections import defaultdict 18 | 19 | 20 | def load_yaml(filepath: Path) -> dict: 21 | """Load YAML file.""" 22 | with open(filepath, 'r', encoding='utf-8') as f: 23 | return yaml.safe_load(f) 24 | 25 | 26 | def invert_nets_to_pins(nets: dict) -> dict: 27 | """ 28 | Invert net-centric view to part-centric pin mappings. 29 | 30 | Input: {"GND": ["mcu.GND", "ldo.GND"], "+3V3": ["mcu.3V3"]} 31 | Output: {"mcu": {"GND": "GND", "3V3": "+3V3"}, "ldo": {"GND": "GND"}} 32 | """ 33 | part_pins = defaultdict(dict) 34 | 35 | for net_name, connections in nets.items(): 36 | for conn in connections: 37 | if '.' not in conn: 38 | continue 39 | part_id, pin_name = conn.split('.', 1) 40 | part_pins[part_id][pin_name] = net_name 41 | 42 | return dict(part_pins) 43 | 44 | 45 | def generate_ref(prefix: str, index: int) -> str: 46 | """Generate reference designator.""" 47 | return f"{prefix}{index}" 48 | 49 | 50 | def generate_pin_model(parts_file: Path, connections_file: Path) -> dict: 51 | """Generate the enhanced pin model JSON.""" 52 | 53 | # Load source files 54 | parts_data = load_yaml(parts_file) 55 | connections_data = load_yaml(connections_file) 56 | 57 | parts = parts_data.get('parts', []) 58 | nets = connections_data.get('nets', {}) 59 | no_connect = connections_data.get('no_connect', []) 60 | 61 | # Invert nets to per-part pin mappings 62 | part_pins = invert_nets_to_pins(nets) 63 | 64 | # Build NC pins lookup 65 | nc_pins = defaultdict(list) 66 | for nc in no_connect: 67 | comp = nc.get('component', '') 68 | pin = nc.get('pin', '') 69 | reason = nc.get('reason', '') 70 | if comp and pin: 71 | nc_pins[comp].append({'pin': pin, 'reason': reason}) 72 | 73 | # Assign reference designators 74 | prefix_counters = defaultdict(int) 75 | 76 | # Collect all net names 77 | all_nets = sorted(nets.keys()) 78 | 79 | # Build output parts list 80 | output_parts = [] 81 | 82 | for part in parts: 83 | part_id = part.get('id') 84 | prefix = part.get('prefix', 'X') 85 | 86 | # Assign ref designator 87 | prefix_counters[prefix] += 1 88 | ref = generate_ref(prefix, prefix_counters[prefix]) 89 | 90 | # Get pin mappings for this part 91 | pins = part_pins.get(part_id, {}) 92 | 93 | # Build output part entry 94 | output_part = { 95 | "id": part_id, 96 | "ref": ref, 97 | "name": part.get('name', ''), 98 | "symbol": f"JLCPCB:{part.get('lcsc_hint', 'UNKNOWN')}", 99 | "footprint": f"JLCPCB:{part.get('package', 'UNKNOWN')}", 100 | "value": part.get('part', ''), 101 | "lcsc": part.get('lcsc', ''), 102 | "belongs_to": part.get('belongs_to'), 103 | "category": part.get('category', ''), 104 | "pins": pins 105 | } 106 | 107 | # Add no-connect pins if any 108 | if part_id in nc_pins: 109 | output_part["no_connect"] = nc_pins[part_id] 110 | 111 | output_parts.append(output_part) 112 | 113 | # Build complete model 114 | model = { 115 | "_meta": { 116 | "version": "1.0", 117 | "generated_from": [ 118 | str(parts_file.name), 119 | str(connections_file.name) 120 | ], 121 | "date": "2025-12-17", 122 | "description": "Enhanced pin model for SKiDL generation" 123 | }, 124 | "parts": output_parts, 125 | "nets": all_nets, 126 | "statistics": { 127 | "total_parts": len(output_parts), 128 | "total_nets": len(all_nets), 129 | "total_pin_assignments": sum(len(p["pins"]) for p in output_parts) 130 | } 131 | } 132 | 133 | return model 134 | 135 | 136 | def main(): 137 | script_dir = Path(__file__).parent.parent 138 | parts_file = script_dir / "work" / "step4_final_parts.yaml" 139 | connections_file = script_dir / "work" / "step5_connections.yaml" 140 | output_file = script_dir / "work" / "pin_model.json" 141 | 142 | print(f"Reading: {parts_file.name}") 143 | print(f"Reading: {connections_file.name}") 144 | 145 | model = generate_pin_model(parts_file, connections_file) 146 | 147 | # Write JSON output 148 | with open(output_file, 'w', encoding='utf-8') as f: 149 | json.dump(model, f, indent=2) 150 | 151 | print(f"\nGenerated: {output_file}") 152 | print(f" Parts: {model['statistics']['total_parts']}") 153 | print(f" Nets: {model['statistics']['total_nets']}") 154 | print(f" Pin assignments: {model['statistics']['total_pin_assignments']}") 155 | 156 | 157 | if __name__ == "__main__": 158 | main() 159 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/prompts/step2.md: -------------------------------------------------------------------------------- 1 | # Step 2: Enrich with JLCPCB Data 2 | 3 | Fetch JLCPCB data (pricing, stock, variants) for all parts from step1_primary_parts.yaml. 4 | 5 | **Purpose:** Find the best JLCPCB variant for each part (in stock, Basic preferred over Extended). 6 | 7 | --- 8 | 9 | ## Prerequisites Check (MUST PASS BEFORE STARTING) 10 | 11 | **Verify these files exist and are valid before proceeding:** 12 | 13 | ``` 14 | [ ] design/work/step1_primary_parts.yaml exists 15 | [ ] design/input/FSD_*.md exists 16 | ``` 17 | 18 | **Quick validation:** 19 | ```bash 20 | ls -la design/work/step1_primary_parts.yaml 21 | python -c "import yaml; yaml.safe_load(open('design/work/step1_primary_parts.yaml'))" 22 | ``` 23 | 24 | **If prerequisites fail -> Go back to Step 1 and complete it first!** 25 | 26 | --- 27 | 28 | ## Process Overview 29 | 30 | ``` 31 | step1_primary_parts.yaml 32 | | 33 | v 34 | [Automated JLCPCB Enrichment Script] 35 | | 36 | v 37 | step2_parts_extended.yaml (with pricing/availability/variants) 38 | ``` 39 | 40 | --- 41 | 42 | ## Step 2.1: Run JLCPCB Enrichment 43 | 44 | Run the enrichment script to fetch JLCPCB data for ALL parts: 45 | 46 | ```bash 47 | python scripts/enrich_parts.py \ 48 | --input work/step1_primary_parts.yaml \ 49 | --output work/step2_parts_extended.yaml 50 | ``` 51 | 52 | The script will: 53 | 1. Read all parts from step1_primary_parts.yaml 54 | 2. Query JLCPCB API for each part (by LCSC code, part number, and base name) 55 | 3. Find all available variants 56 | 4. Select best variant (in stock, Basic > Preferred > Extended) 57 | 5. Add: price, stock, part_type (Basic/Extended), availability, datasheet URL 58 | 6. Flag parts that are out of stock or unavailable 59 | 7. Output enriched YAML 60 | 61 | **If script is not available**, manually look up each part on jlcpcb.com/parts and add: 62 | - `jlcpcb_price`: Unit price at qty 10 63 | - `jlcpcb_stock`: Current stock quantity 64 | - `jlcpcb_type`: "Basic" or "Extended" 65 | - `jlcpcb_available`: true/false 66 | - `jlcpcb_datasheet`: Datasheet URL 67 | 68 | --- 69 | 70 | ## Step 2.2: Review Enriched Data 71 | 72 | After enrichment, `step2_parts_extended.yaml` will contain: 73 | 74 | ```yaml 75 | meta: 76 | source: step1_primary_parts.yaml 77 | enriched_at: 78 | stats: 79 | total: 80 | found: 81 | not_found: 82 | basic: 83 | extended: 84 | out_of_stock: 85 | 86 | parts: 87 | - id: 88 | name: "" 89 | part_number: "" 90 | package: "" 91 | category: 92 | quantity: 93 | option_group: 94 | # --- Added by enrichment script --- 95 | lcsc: "" 96 | jlcpcb_price: 97 | jlcpcb_stock: 98 | jlcpcb_type: "" 99 | jlcpcb_available: 100 | jlcpcb_package: "" 101 | jlcpcb_datasheet: "" 102 | jlcpcb_lookup: 103 | searched: true 104 | found: 105 | search_queries: [...] 106 | all_candidates: [...] 107 | selected: {...} 108 | ``` 109 | 110 | **Check for issues:** 111 | - Parts with `jlcpcb_available: false` 112 | - Parts with `jlcpcb_stock: 0` 113 | - LCSC codes that couldn't be found 114 | - Parts where original LCSC differs from selected (mismatch) 115 | 116 | --- 117 | 118 | ## Step 2.3: Handle Out-of-Stock Parts 119 | 120 | For parts with no stock: 121 | 1. Check if the enrichment script found alternatives (in `all_candidates`) 122 | 2. If alternatives exist with stock, the script will have selected the best one 123 | 3. If NO alternatives have stock, flag for manual sourcing (AliExpress, Mouser, DigiKey) 124 | 125 | --- 126 | 127 | ## Exit Validation Checklist 128 | 129 | **Before proceeding to Step 3, ALL checks must pass:** 130 | 131 | ### 1. File Exists and Valid 132 | ```bash 133 | ls -la design/work/step2_parts_extended.yaml 134 | python -c "import yaml; yaml.safe_load(open('design/work/step2_parts_extended.yaml'))" 135 | ``` 136 | - [ ] `step2_parts_extended.yaml` exists and is valid YAML 137 | 138 | ### 2. All Parts Enriched 139 | ```bash 140 | # Check stats in meta section 141 | grep -A5 "stats:" design/work/step2_parts_extended.yaml 142 | ``` 143 | - [ ] `found` equals `total` (or document why some parts not found) 144 | - [ ] `not_found` is 0 or explained 145 | 146 | ### 3. Stock Status Reviewed 147 | ```bash 148 | grep "jlcpcb_available: false" design/work/step2_parts_extended.yaml 149 | grep "jlcpcb_stock: 0" design/work/step2_parts_extended.yaml 150 | ``` 151 | - [ ] All out-of-stock parts have been noted 152 | - [ ] Decision made for each (wait, source elsewhere, use alternative) 153 | 154 | ### 4. LCSC Codes Valid 155 | ```bash 156 | grep "lcsc:" design/work/step2_parts_extended.yaml | grep -v "C[0-9]" 157 | ``` 158 | - [ ] All LCSC codes match pattern C followed by digits (or null for manual source) 159 | 160 | --- 161 | 162 | ## If Validation Fails 163 | 164 | **DO NOT proceed to Step 3!** 165 | 166 | 1. Identify which check(s) failed 167 | 2. Re-run enrichment or manually fix issues 168 | 3. Re-run ALL validation checks 169 | 4. Only proceed when ALL checks pass 170 | 171 | ``` 172 | VALIDATION LOOP: Step 2 -> Validate -> Fix if needed -> Validate again -> Step 3 173 | ``` 174 | 175 | --- 176 | 177 | ## What Happens Next 178 | 179 | Step 3 will: 180 | 1. Present option_groups with REAL pricing data from enrichment 181 | 2. Present design_options for user decision 182 | 3. Collect user decisions 183 | 4. Output step3_design_options.yaml 184 | 185 | **STOP in Step 3** to create decisions.yaml before proceeding to Step 4. 186 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/validate_step3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Validate step3_decisions.yaml 4 | 5 | Checks: 6 | - YAML parses correctly 7 | - Each decision has required fields 8 | - Selected options are valid 9 | - All decisions have been made (no pending) 10 | """ 11 | 12 | import sys 13 | import yaml 14 | from pathlib import Path 15 | 16 | REQUIRED_DECISION_FIELDS = ['topic', 'options', 'selected', 'rationale'] 17 | 18 | 19 | def validate(filepath: Path) -> list: 20 | """Validate step3 file and return list of errors.""" 21 | errors = [] 22 | 23 | # Check file exists 24 | if not filepath.exists(): 25 | return [f"File not found: {filepath}"] 26 | 27 | # Parse YAML 28 | try: 29 | with open(filepath, 'r', encoding='utf-8') as f: 30 | data = yaml.safe_load(f) 31 | except yaml.YAMLError as e: 32 | return [f"YAML parse error: {e}"] 33 | 34 | if not data: 35 | return ["File is empty"] 36 | 37 | # Check decisions exists 38 | if 'decisions' not in data: 39 | return ["Missing 'decisions' key"] 40 | 41 | decisions = data['decisions'] 42 | if not isinstance(decisions, list): 43 | return ["'decisions' must be a list"] 44 | 45 | if len(decisions) == 0: 46 | errors.append("WARNING: No decisions defined") 47 | 48 | # Track topics for uniqueness 49 | seen_topics = set() 50 | 51 | for i, decision in enumerate(decisions): 52 | prefix = f"decisions[{i}]" 53 | 54 | if not isinstance(decision, dict): 55 | errors.append(f"{prefix}: Must be a dictionary") 56 | continue 57 | 58 | topic = decision.get('topic', f'') 59 | 60 | # Check required fields 61 | for field in REQUIRED_DECISION_FIELDS: 62 | if field not in decision: 63 | errors.append(f"{prefix} ({topic}): Missing required field '{field}'") 64 | 65 | # Check topic uniqueness 66 | if topic in seen_topics: 67 | errors.append(f"{prefix}: Duplicate topic '{topic}'") 68 | seen_topics.add(topic) 69 | 70 | # Check options is a list 71 | options = decision.get('options', []) 72 | if not isinstance(options, list): 73 | errors.append(f"{prefix} ({topic}): 'options' must be a list") 74 | elif len(options) < 2: 75 | errors.append(f"{prefix} ({topic}): Should have at least 2 options") 76 | else: 77 | # Check each option has required fields 78 | for j, opt in enumerate(options): 79 | if not isinstance(opt, dict): 80 | errors.append(f"{prefix} ({topic}): options[{j}] must be a dictionary") 81 | else: 82 | if 'name' not in opt: 83 | errors.append(f"{prefix} ({topic}): options[{j}] missing 'name'") 84 | 85 | # Check selected is valid 86 | selected = decision.get('selected') 87 | if selected is None: 88 | errors.append(f"PENDING: {prefix} ({topic}): No option selected yet") 89 | elif isinstance(options, list): 90 | option_names = [opt.get('name') for opt in options if isinstance(opt, dict)] 91 | if selected not in option_names: 92 | errors.append(f"{prefix} ({topic}): Selected '{selected}' not in options {option_names}") 93 | 94 | # Check rationale not empty 95 | rationale = decision.get('rationale', '') 96 | if selected and (not rationale or str(rationale).strip() == ''): 97 | errors.append(f"{prefix} ({topic}): Missing rationale for selection") 98 | 99 | return errors 100 | 101 | 102 | def main(): 103 | script_dir = Path(__file__).parent.parent 104 | filepath = script_dir / "work" / "step3_decisions.yaml" 105 | 106 | print(f"Validating: {filepath}") 107 | print("=" * 60) 108 | 109 | errors = validate(filepath) 110 | 111 | # Separate pending, warnings, and errors 112 | pending = [e for e in errors if e.startswith("PENDING")] 113 | warnings = [e for e in errors if e.startswith("WARNING")] 114 | real_errors = [e for e in errors if not e.startswith("WARNING") and not e.startswith("PENDING")] 115 | 116 | if real_errors: 117 | print("VALIDATION FAILED\n") 118 | for error in real_errors: 119 | print(f" {error}") 120 | if pending: 121 | print("\nPending decisions:") 122 | for p in pending: 123 | print(f" {p}") 124 | if warnings: 125 | print("\nWarnings:") 126 | for warning in warnings: 127 | print(f" {warning}") 128 | print(f"\nTotal errors: {len(real_errors)}") 129 | sys.exit(1) 130 | elif pending: 131 | print("VALIDATION INCOMPLETE - Decisions pending\n") 132 | for p in pending: 133 | print(f" {p}") 134 | print(f"\nPending decisions: {len(pending)}") 135 | sys.exit(2) # Special exit code for pending 136 | else: 137 | print("VALIDATION PASSED") 138 | if warnings: 139 | print("\nWarnings:") 140 | for warning in warnings: 141 | print(f" {warning}") 142 | 143 | # Print summary 144 | with open(filepath, 'r', encoding='utf-8') as f: 145 | data = yaml.safe_load(f) 146 | decisions = data.get('decisions', []) 147 | print(f"\nSummary: {len(decisions)} decisions documented") 148 | 149 | # List decisions 150 | for d in decisions: 151 | topic = d.get('topic', 'unknown') 152 | selected = d.get('selected', 'none') 153 | print(f" - {topic}: {selected}") 154 | 155 | sys.exit(0) 156 | 157 | 158 | if __name__ == "__main__": 159 | main() 160 | -------------------------------------------------------------------------------- /RadioReceiverV2/fsd_review.md: -------------------------------------------------------------------------------- 1 | # FSD Review: ESP32-S3 Portable Radio Receiver 2 | 3 | **Date:** 2025-12-18 4 | **Status:** APPROVED (with consignment note) 5 | 6 | --- 7 | 8 | ## Proposed Parts Summary 9 | 10 | | Module | Qty | LCSC | Part Number | Type | Notes | 11 | |--------|-----|------|-------------|------|-------| 12 | | MCU | 1 | C2913206 | ESP32-S3-MINI-1-N8 | Extended | In stock (6,353) | 13 | | RADIO | 1 | CONSIGNMENT | SI4735-D60-GU | Extended | **Out of stock - user consignment** | 14 | | AMP | 1 | C33233 | PAM8908JER | Extended | In stock (291) | 15 | | CHARGER | 1 | C16581 | TP4056-42-ESOP8 | **Preferred** | In stock (54,957) | 16 | | LDO | 1 | C82942 | ME6211C33M5G-N | Extended | In stock (192,509) | 17 | | PFET | 1 | C10487 | SI2301CDS-T1-GE3 | **Basic** | In stock (137,210) | 18 | | ESD | 1 | C2827654 | USBLC6-2SC6 | Extended | In stock (409,188) | 19 | | LED | 3 | C2843785 | XL-5050RGBC-2812B | Extended | In stock (1,717,547) | 20 | | XTAL | 1 | C32346 | Q13FC13500004 | **Basic** | In stock (667,935) | 21 | | ENC | 2 | C470754 | EC11E15244B2 | Extended | In stock (4,304) | 22 | | BATT | 1 | C173752 | S2B-PH-K-S(LF)(SN) | Extended | In stock (88,126) | 23 | | SW | 1 | C2837531 | KH-6X6X5H-STM | Extended | In stock (234,051) | 24 | | USB | 1 | C165948 | TYPE-C-31-M-12 | Extended | In stock (166,073) | 25 | | JACK | 1 | C18185602 | PJ-320A-4P DIP | Extended | In stock (10,115) | 26 | 27 | --- 28 | 29 | ## JLCPCB Assembly Cost Estimate 30 | 31 | | Type | Count | Assembly Fee | 32 | |------|-------|--------------| 33 | | Basic | 2 | $0.00 | 34 | | Preferred | 1 | $0.50 | 35 | | Extended | 11 | $33.00 | 36 | | **Total** | **14** | **$33.50** | 37 | 38 | *Note: SI4735 consignment not included in assembly fee calculation* 39 | 40 | --- 41 | 42 | ## Critical Checks 43 | 44 | ### Power System 45 | 46 | | Check | Status | Notes | 47 | |-------|--------|-------| 48 | | LDO dropout vs battery minimum | MARGINAL | ME6211 100mV dropout at 3.3V cutoff - ESP32-S3 works down to 3.0V | 49 | | Total current budget | OK | ~200mA typical, ME6211 provides 500mA | 50 | | Reverse polarity protection | OK | SI2301 P-FET in high-side | 51 | | USB power limits | OK | 500mA charging via TP4056 | 52 | 53 | ### Component Selection 54 | 55 | | Check | Status | Notes | 56 | |-------|--------|-------| 57 | | SI4735 availability | CONSIGNMENT | All variants out of stock at JLCPCB | 58 | | WS2812B at low battery | MARGINAL | Spec: 3.5-5.5V, Battery min: 3.3V - may glitch at low battery | 59 | | PAM8908 stock | LOW | Only 291 units - order early | 60 | 61 | ### MCU Pin Assignment (ESP32-S3-MINI-1) 62 | 63 | | Check | Status | Notes | 64 | |-------|--------|-------| 65 | | Strapping pins avoided | OK | GPIO0, GPIO3, GPIO45, GPIO46 not used | 66 | | USB D+/D- | OK | GPIO19/GPIO20 for native USB | 67 | | I2C pins | OK | GPIO4 (SDA), GPIO5 (SCL) | 68 | 69 | --- 70 | 71 | ## Power Architecture 72 | 73 | ``` 74 | USB 5V ──────────────────────────────> TP4056 VCC 75 | │ 76 | ┌────┴────┐ 77 | │ Battery │ 78 | │3.3-4.2V │ 79 | └────┬────┘ 80 | │ 81 | ┌────┴────┐ 82 | │ SI2301 │ Reverse polarity 83 | │ P-FET │ protection 84 | └────┬────┘ 85 | │ 86 | VBAT_PROTECTED 87 | │ 88 | ┌──────────────┬─────────────────┼──────────────┐ 89 | │ │ │ │ 90 | ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ 91 | │ TP4056 │ │ WS2812B │ │ ME6211 │ │ ... │ 92 | │ BAT+ │ │ LEDs │ │ LDO │ │ │ 93 | └─────────┘ └─────────┘ └────┬────┘ └─────────┘ 94 | │ 95 | 3.3V Rail 96 | │ 97 | ┌──────────────┬────────────────┼──────────────┐ 98 | │ │ │ │ 99 | ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ 100 | │ESP32-S3 │ │ SI4735 │ │PAM8908 │ │ I2C bus │ 101 | │ MCU │ │ Radio │ │ Amp │ │ pullups │ 102 | └─────────┘ └─────────┘ └─────────┘ └─────────┘ 103 | ``` 104 | 105 | --- 106 | 107 | ## GPIO Allocation Summary 108 | 109 | | GPIO | Function | Direction | 110 | |------|----------|-----------| 111 | | GPIO4 | I2C SDA | Bidirectional | 112 | | GPIO5 | I2C SCL | Output | 113 | | GPIO6 | NeoPixel Data | Output | 114 | | GPIO7 | SI4735 Reset | Output | 115 | | GPIO8 | Encoder1 A | Input | 116 | | GPIO9 | Encoder1 B | Input | 117 | | GPIO10 | Encoder1 SW | Input | 118 | | GPIO12 | BFO Button | Input | 119 | | GPIO13 | PAM8908 SHDN | Output | 120 | | GPIO19 | USB D- | Bidirectional | 121 | | GPIO20 | USB D+ | Bidirectional | 122 | | GPIO21 | Encoder2 A | Input | 123 | | GPIO35 | Encoder2 B | Input | 124 | | GPIO36 | Encoder2 SW | Input | 125 | 126 | --- 127 | 128 | ## User Decisions Required 129 | 130 | 1. **SI4735 Consignment** - User must provide SI4735-D60-GU separately for assembly 131 | 2. **Low Battery Behavior** - Accept potential WS2812B glitches at 3.3V battery 132 | 3. **PAM8908 Stock** - Order promptly due to low stock (291 units) 133 | 134 | --- 135 | 136 | ## Output Files 137 | 138 | - `parts.csv` - Complete parts list with all variants and proposed selections 139 | - Ready for circuit-analysis skill 140 | 141 | --- 142 | 143 | ## Next Steps 144 | 145 | Run the `circuit-analysis` skill to: 146 | 1. Generate `modules.csv` and `module_connections.csv` 147 | 2. Generate `bom.csv` with designators 148 | 3. Generate `connections.csv` with all circuit connections 149 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/validate_pin_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Validate pin_model.json (Phase 2 of SKiDL pipeline) 4 | 5 | Checks: 6 | - No undefined nets (every net used in pins must be in nets list) 7 | - No duplicate drivers (same net driven by multiple outputs) - warning only 8 | - Required power nets exist (GND, at least one supply) 9 | - No floating control pins (pins with no net assignment) 10 | - Every part has at least one pin connection 11 | - Reference designators are unique 12 | """ 13 | 14 | import json 15 | import sys 16 | from pathlib import Path 17 | from collections import defaultdict 18 | 19 | 20 | def validate_pin_model(filepath: Path) -> list: 21 | """Validate pin model and return list of errors.""" 22 | errors = [] 23 | warnings = [] 24 | 25 | # Load JSON 26 | try: 27 | with open(filepath, 'r', encoding='utf-8') as f: 28 | model = json.load(f) 29 | except json.JSONDecodeError as e: 30 | return [f"JSON parse error: {e}"], [] 31 | 32 | parts = model.get('parts', []) 33 | declared_nets = set(model.get('nets', [])) 34 | 35 | # Track all nets used in pin assignments 36 | used_nets = set() 37 | net_drivers = defaultdict(list) # net -> list of (ref, pin) 38 | 39 | # Track refs for uniqueness 40 | seen_refs = set() 41 | seen_ids = set() 42 | 43 | # Required power nets 44 | required_nets = {'GND'} 45 | supply_patterns = ['+3V3', '+5V', 'VCC', 'VBAT', 'VDD', '+3.3V', '+5V', 'VBUS'] 46 | 47 | for part in parts: 48 | part_id = part.get('id', '') 49 | ref = part.get('ref', '') 50 | pins = part.get('pins', {}) 51 | 52 | # Check ref uniqueness 53 | if ref in seen_refs: 54 | errors.append(f"Duplicate ref designator: {ref}") 55 | seen_refs.add(ref) 56 | 57 | # Check id uniqueness 58 | if part_id in seen_ids: 59 | errors.append(f"Duplicate part id: {part_id}") 60 | seen_ids.add(part_id) 61 | 62 | # Check part has at least one pin (unless it's explicitly NC-only) 63 | if not pins and not part.get('no_connect'): 64 | warnings.append(f"{ref} ({part_id}): No pin connections defined") 65 | 66 | # Check each pin assignment 67 | for pin_name, net_name in pins.items(): 68 | used_nets.add(net_name) 69 | net_drivers[net_name].append((ref, pin_name)) 70 | 71 | # Check net is declared 72 | if net_name not in declared_nets: 73 | errors.append(f"{ref}.{pin_name}: Uses undeclared net '{net_name}'") 74 | 75 | # Check for required power nets 76 | if 'GND' not in declared_nets: 77 | errors.append("Missing required 'GND' net") 78 | 79 | has_supply = False 80 | for pattern in supply_patterns: 81 | if pattern in declared_nets: 82 | has_supply = True 83 | break 84 | if not has_supply: 85 | errors.append("No supply rail found (expected +3V3, VCC, VBAT, etc.)") 86 | 87 | # Check for unused declared nets 88 | unused_nets = declared_nets - used_nets 89 | for net in unused_nets: 90 | warnings.append(f"Declared net '{net}' is not used by any pin") 91 | 92 | # Check for potential driver conflicts (multiple outputs on same net) 93 | # This is informational - some nets legitimately have multiple connections 94 | for net, drivers in net_drivers.items(): 95 | if len(drivers) > 10: # Likely a power net, skip 96 | continue 97 | # Could add more sophisticated output detection here 98 | 99 | # Validate belongs_to references 100 | part_ids = {p.get('id') for p in parts} 101 | for part in parts: 102 | belongs_to = part.get('belongs_to') 103 | if belongs_to is not None and belongs_to not in part_ids: 104 | errors.append(f"{part.get('ref')}: belongs_to '{belongs_to}' not found") 105 | 106 | return errors, warnings 107 | 108 | 109 | def main(): 110 | script_dir = Path(__file__).parent.parent 111 | filepath = script_dir / "work" / "pin_model.json" 112 | 113 | print(f"Validating: {filepath}") 114 | print("=" * 60) 115 | 116 | errors, warnings = validate_pin_model(filepath) 117 | 118 | if errors: 119 | print("VALIDATION FAILED\n") 120 | for error in errors: 121 | print(f" ERROR: {error}") 122 | if warnings: 123 | print("\nWarnings:") 124 | for warning in warnings: 125 | print(f" WARN: {warning}") 126 | print(f"\nTotal errors: {len(errors)}") 127 | sys.exit(1) 128 | else: 129 | print("VALIDATION PASSED") 130 | if warnings: 131 | print("\nWarnings:") 132 | for warning in warnings: 133 | print(f" WARN: {warning}") 134 | 135 | # Load and print summary 136 | with open(filepath, 'r', encoding='utf-8') as f: 137 | model = json.load(f) 138 | 139 | stats = model.get('statistics', {}) 140 | print(f"\nSummary:") 141 | print(f" Parts: {stats.get('total_parts', 0)}") 142 | print(f" Nets: {stats.get('total_nets', 0)}") 143 | print(f" Pin assignments: {stats.get('total_pin_assignments', 0)}") 144 | 145 | # Check completeness 146 | parts = model.get('parts', []) 147 | parts_with_pins = sum(1 for p in parts if p.get('pins')) 148 | parts_without_pins = sum(1 for p in parts if not p.get('pins')) 149 | 150 | print(f"\nCompleteness:") 151 | print(f" Parts with pin mappings: {parts_with_pins}") 152 | print(f" Parts without pin mappings: {parts_without_pins}") 153 | 154 | if parts_without_pins > 0: 155 | print("\n Missing pins for:") 156 | for p in parts: 157 | if not p.get('pins'): 158 | print(f" - {p.get('ref')} ({p.get('id')})") 159 | 160 | sys.exit(0) 161 | 162 | 163 | if __name__ == "__main__": 164 | main() 165 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | This is a KiCAD schematic generation pipeline that automates creating PCB designs from a Functional Specification Document (FSD). The project generates a KiCAD 9 schematic for an ESP32-S3 portable radio receiver with SI4735 radio IC. 8 | 9 | The pipeline converts semantic part/net definitions into a complete KiCAD project with symbols, footprints, and net labels, ready for JLCPCB assembly. 10 | 11 | ## Pipeline Commands 12 | 13 | Run from `RadioReceiver/llm-research-v2/design/` directory. 14 | 15 | **Note:** On Windows, use `py` instead of `python`: 16 | 17 | ```bash 18 | # 1. Generate KiCAD schematic with placement and routing 19 | py scripts/kicad9_schematic.py # Full schematic 20 | py scripts/kicad9_schematic.py --debug # Debug schematic + debug.csv 21 | 22 | # 2. Verify connections using KiCad's netlist (requires KiCad 7+) 23 | py scripts/verify_netlist.py output/Debug.kicad_sch 24 | ``` 25 | 26 | After generation, open in KiCAD and run: **Tools > Update Schematic from Symbol Libraries** 27 | 28 | ### Debug Mode 29 | 30 | The `--debug` flag generates: 31 | - `output/Debug.kicad_sch` - Schematic with all parts and wires 32 | - `output/debug.csv` - CSV with pin positions for verification 33 | 34 | ### Verify Netlist 35 | 36 | `verify_netlist.py` uses KiCad CLI to export a netlist and verify all connections: 37 | - Compares actual netlist against expected connections from `pin_model.json` 38 | - Reports missing nets, extra nets, and pin count mismatches 39 | - Requires `kicad-cli` in PATH (included with KiCad 7+) 40 | 41 | **Windows with Git Bash** (if Python/KiCad not in PATH): 42 | ```bash 43 | PATH="$PATH:/c/Program Files/KiCad/9.0/bin" \ 44 | /c/Users/*/AppData/Local/Programs/Python/Python312/python.exe \ 45 | scripts/verify_netlist.py output/Debug.kicad_sch 46 | ``` 47 | 48 | **Windows CMD** (if Python/KiCad not in PATH): 49 | ```cmd 50 | set PATH=%PATH%;C:\Program Files\KiCad\9.0\bin 51 | "C:\Users\%USERNAME%\AppData\Local\Programs\Python\Python312\python.exe" scripts/verify_netlist.py output/Debug.kicad_sch 52 | ``` 53 | 54 | **Typical Windows paths:** 55 | - Python: `C:\Users\\AppData\Local\Programs\Python\Python312\python.exe` 56 | - KiCad CLI: `C:\Program Files\KiCad\9.0\bin\kicad-cli.exe` 57 | 58 | ## Key Input Files (RadioReceiver/) 59 | 60 | - `parts.yaml` - Semantic parts list with quantities/prefixes (e.g., MCU, Battery_Charger) 61 | - `parts_options.csv` - JLCPCB part options; mark `X` in `selected` column 62 | - `connections.yaml` - Semantic nets using `Component.Pin` notation 63 | - `custom_library_overrides.yaml` - Manual symbol/footprint mappings for parts not in JLCPCB library 64 | 65 | ## Key Output Files 66 | 67 | - `work/pin_model.json` - Parts with pin-to-net mappings (input to schematic generator) 68 | - `output/Debug.kicad_sch` - Generated schematic (debug mode) 69 | - `output/debug.csv` - Pin positions CSV for verification 70 | - `output/libs/JLCPCB/symbol/JLCPCB.kicad_sym` - Symbol library with all parts 71 | 72 | ## Architecture 73 | 74 | **Workflow**: FSD.md → (LLM) → parts.yaml + connections.yaml → (Python scripts) → .kicad_sch 75 | 76 | 1. **Semantic layer**: LLM generates `parts.yaml` (components by function) and `connections.yaml` (nets by logical name) 77 | 2. **Enrichment**: `enrich_parts.py` queries JLCPCB for part options 78 | 3. **Human review**: Engineer selects parts in CSV (single touchpoint) 79 | 4. **Resolution**: Scripts assign designators, download symbols, map connections to physical pins 80 | 5. **Generation**: Produces KiCAD schematic with net labels on every pin 81 | 82 | **Key design decisions**: 83 | - Semantic names in LLM output (MCU, not U1) - keeps reasoning at logical level 84 | - Late designator assignment - enables proper sequential numbering 85 | - Unconnected pins get unique labels (U1_45) for future manual connection 86 | - Y-axis inverted between symbol library (Y+ up) and schematic (Y+ down) 87 | 88 | ## Wire Routing System 89 | 90 | The schematic generator (`kicad9_schematic.py`) includes: 91 | 92 | **Placement:** 93 | - Force-directed algorithm groups related parts 94 | - Decoupling capacitors placed in reserved area at bottom 95 | - Parts placed on 2.54mm grid 96 | 97 | **Routing:** 98 | - Obstacle-aware Manhattan routing (avoids crossing parts) 99 | - Stub wires extend from pins, then route between stubs 100 | - Multi-pin nets (>3 connections) use net labels instead of wires 101 | 102 | **KiCad Connection Detection:** 103 | - KiCad uses coordinate matching - wire endpoint must exactly match pin position 104 | - Small circle at pin = unconnected (coordinates don't match) 105 | - Pin positions must NOT be snapped - use exact calculated values 106 | - Python's `round()` uses banker's rounding; use `math.floor(x + 0.5)` instead 107 | 108 | ## Library Locations (Windows) 109 | 110 | - Symbols: `%USERPROFILE%\Documents\KiCad\JLCPCB\symbol\JLCPCB.kicad_sym` 111 | - Footprints: `%USERPROFILE%\Documents\KiCad\JLCPCB\JLCPCB\` 112 | - Index: `%USERPROFILE%\Documents\KiCad\JLCPCB\lcsc_index.json` 113 | 114 | ## Skills and Scripts Guidelines 115 | 116 | **All skills and scripts must be GENERIC:** 117 | - No hardcoded part numbers, LCSC codes, or manufacturer part names 118 | - No project-specific pin names or component references 119 | - Examples in SKILL.md files should use placeholder names (U1, IC-FAMILY, etc.) 120 | - Scripts should work with any project, not just the current one 121 | 122 | **Pin number convention:** 123 | - Use pin NUMBERS (from datasheets) in connections.csv, not pin names 124 | - Pin names vary between datasheets and JLC2KiCadLib symbols 125 | - Pin numbers are consistent and unambiguous 126 | - csv_to_pin_model.py converts pin numbers to actual symbol pin names 127 | 128 | ## Known Issues 129 | 130 | - JLC2KiCadLib generates KiCAD 6 format; scripts patch to KiCAD 9 format 131 | - After schematic generation, must run "Update Schematic from Symbol Libraries" in KiCAD to populate lib_symbols section 132 | - Closely placed parts may cause routing to fall back to crossing paths if no obstacle-free route exists 133 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/prompts/step5.md: -------------------------------------------------------------------------------- 1 | # Step 5: Generate Connections 2 | 3 | Create all electrical connections based on `design/work/step4_final_parts.yaml` and FSD requirements. 4 | 5 | --- 6 | 7 | ## Prerequisites Check (MUST PASS BEFORE STARTING) 8 | 9 | **Verify these files exist and are valid:** 10 | 11 | ``` 12 | □ design/work/step4_final_parts.yaml exists 13 | □ design/input/FSD_*.md exists 14 | □ All symbols exist in JLCPCB library 15 | □ No TBD values in step4_final_parts.yaml 16 | ``` 17 | 18 | **Quick validation:** 19 | ```bash 20 | ls -la design/work/step4_final_parts.yaml 21 | python scripts/ensure_symbols.py --parts work/step4_final_parts.yaml --dry-run 22 | grep -i "TBD" design/work/step4_final_parts.yaml # Should return nothing 23 | ``` 24 | 25 | **If prerequisites fail → Go back to Step 4 and complete it first!** 26 | 27 | --- 28 | 29 | ## Input Files 30 | 31 | - `design/work/step4_final_parts.yaml` - Final parts list 32 | - `design/input/FSD_*.md` - Original requirements 33 | 34 | ## Output Format 35 | 36 | Write to `design/work/step5_connections.yaml`: 37 | 38 | ```yaml 39 | # step5_connections.yaml 40 | # Generated from: step4_final_parts.yaml 41 | # Date: [YYYY-MM-DD] 42 | 43 | nets: 44 | # === Power Rails === 45 | GND: 46 | - .GND 47 | - .GND 48 | - .2 # Capacitor pin 2 = GND side 49 | 50 | : # e.g., VBAT, +3V3, +5V 51 | - . 52 | - .1 # Capacitor pin 1 = power side 53 | 54 | # === Signal Buses === 55 | : # e.g., SDA, SCL, MOSI, CS 56 | - . 57 | - . 58 | - .2 # Resistor pin 2 = signal side 59 | 60 | # Format: component_id.PIN_NAME 61 | # - Use component id from step4_final_parts.yaml 62 | # - Use pin name from datasheet (or number for passives) 63 | # - For passives: use .1 and .2 64 | 65 | # Explicitly unconnected pins 66 | no_connect: 67 | - component: 68 | pin: 69 | reason: "" 70 | 71 | # Test points (optional) 72 | test_points: 73 | - net: 74 | purpose: "" 75 | 76 | # Net notes 77 | notes: 78 | - net: 79 | note: "" 80 | ``` 81 | 82 | ## Connection Rules 83 | 84 | 1. **Every IC power pin must connect to power rail** 85 | 2. **Every IC ground pin must connect to GND** 86 | 3. **Bypass capacitors**: one pin to power, one pin to GND 87 | 4. **Pull-up resistors**: one pin to signal, one pin to power rail 88 | 5. **Pull-down resistors**: one pin to signal, one pin to GND 89 | 6. **No single-pin nets** (unless marked as test point or NC) 90 | 91 | ## Pin Reference Format 92 | 93 | - `component_id.PIN_NAME` - IC pins use datasheet pin names 94 | - `component_id.1` or `component_id.2` - Passive component pins 95 | - Pin names should match datasheet (e.g., `GND`, `VCC`, `GPIO4`, `SDIO`) 96 | 97 | ## Verification Checklist 98 | 99 | For each functional block, verify: 100 | - [ ] All power pins connected 101 | - [ ] All ground pins connected 102 | - [ ] All signal connections per FSD 103 | - [ ] Bypass capacitors properly connected 104 | - [ ] Pull-up/pull-down resistors connected 105 | - [ ] No duplicate pin usage (same pin on multiple nets) 106 | 107 | ## After YAML 108 | 109 | Run validator: 110 | ```bash 111 | python design/scripts/validate_step5.py 112 | ``` 113 | 114 | If validation fails, fix errors and rerun. 115 | 116 | --- 117 | 118 | ## Exit Validation Checklist 119 | 120 | **Before proceeding to Step 6 (Schematic Generation), ALL checks must pass:** 121 | 122 | ### 1. File Exists and Valid 123 | ```bash 124 | ls -la design/work/step5_connections.yaml 125 | python -c "import yaml; yaml.safe_load(open('design/work/step5_connections.yaml'))" 126 | ``` 127 | - [ ] `step5_connections.yaml` exists and is valid YAML 128 | 129 | ### 2. All Power Connections Present 130 | - [ ] Every IC has VCC/VDD connected to appropriate power rail 131 | - [ ] Every IC has GND pin(s) connected to GND net 132 | - [ ] All bypass capacitors connected (one pin to power, one to GND) 133 | 134 | ### 3. Component IDs Match 135 | ```bash 136 | # Extract all component IDs from parts file 137 | grep "^ - id:" design/work/step4_final_parts.yaml | cut -d: -f2 | sort > /tmp/parts_ids.txt 138 | 139 | # Extract all component references from connections 140 | grep -oE "[a-z_]+\." design/work/step5_connections.yaml | sed 's/\.//' | sort -u > /tmp/conn_ids.txt 141 | 142 | # Check for mismatches 143 | diff /tmp/parts_ids.txt /tmp/conn_ids.txt 144 | ``` 145 | - [ ] All component IDs in connections exist in parts list 146 | - [ ] No typos in component IDs 147 | 148 | ### 4. Pin Names Valid 149 | - [ ] IC pin names match datasheet (e.g., GPIO4, SDIO, VCC) 150 | - [ ] Passive component pins use `.1` and `.2` format 151 | - [ ] No invalid pin references 152 | 153 | ### 5. No Single-Pin Nets 154 | - [ ] Every net has at least 2 connections (except test points and NC pins) 155 | - [ ] Single-pin items are explicitly marked as `test_points` or `no_connect` 156 | 157 | ### 6. No Duplicate Pin Usage 158 | ```bash 159 | # Check for pins appearing in multiple nets 160 | grep -E "^\s+-\s+\w+\.\w+" design/work/step5_connections.yaml | sort | uniq -d 161 | ``` 162 | - [ ] No pin appears in multiple nets (grep should return nothing) 163 | 164 | ### 7. FSD Requirements Met 165 | Cross-reference with FSD: 166 | - [ ] All I2C connections per FSD 167 | - [ ] All SPI connections per FSD 168 | - [ ] All GPIO assignments per FSD 169 | - [ ] All power rails per FSD 170 | 171 | --- 172 | 173 | ## If Validation Fails 174 | 175 | **DO NOT proceed to Step 6!** 176 | 177 | 1. Identify which check(s) failed 178 | 2. Fix the issue in step5_connections.yaml 179 | 3. Re-run ALL validation checks 180 | 4. Only proceed when ALL checks pass 181 | 182 | ``` 183 | ⚠️ VALIDATION LOOP: Step 5 → Validate → Fix if needed → Validate again → Step 6 184 | 185 | Common failures: 186 | - Typo in component ID (use exact id from step4_final_parts.yaml) 187 | - Wrong pin name (check datasheet) 188 | - Missing power/ground connections 189 | ``` 190 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/prompts/step4.md: -------------------------------------------------------------------------------- 1 | # Step 4: Apply Decisions and Finalize Parts 2 | 3 | Apply decisions from `design/work/decisions.yaml` to create the final parts list. 4 | 5 | --- 6 | 7 | ## Prerequisites Check (MUST PASS BEFORE STARTING) 8 | 9 | **Verify these files exist and are valid:** 10 | 11 | ``` 12 | □ design/work/step2_parts_extended.yaml exists 13 | □ design/work/step3_design_options.yaml exists 14 | □ design/work/decisions.yaml exists and contains user choices 15 | ``` 16 | 17 | **Quick validation:** 18 | ```bash 19 | ls -la design/work/step2_parts_extended.yaml design/work/step3_design_options.yaml design/work/decisions.yaml 20 | python -c "import yaml; yaml.safe_load(open('design/work/decisions.yaml'))" 21 | ``` 22 | 23 | **If prerequisites fail → Go back to previous step and complete it first!** 24 | 25 | --- 26 | 27 | ## Input Files 28 | 29 | - `design/work/step2_parts_extended.yaml` - Extended parts list 30 | - `design/work/step3_design_options.yaml` - Options presented 31 | - `design/work/decisions.yaml` - User decisions 32 | 33 | ## Output Format 34 | 35 | Write to `design/work/step4_final_parts.yaml`: 36 | 37 | ```yaml 38 | # step4_final_parts.yaml 39 | # Finalized from: step2_parts_extended.yaml + decisions.yaml 40 | # Date: [YYYY-MM-DD] 41 | 42 | parts: 43 | # === Parts without options (direct from step2) === 44 | - id: 45 | name: "" 46 | part_number: "" 47 | lcsc: "" 48 | package: "" 49 | prefix: 50 | category: 51 | quantity: 52 | jlcpcb_type: "" 53 | jlcpcb_price: 54 | 55 | # === Parts selected from option_groups === 56 | - id: 57 | name: "" 58 | part_number: "" 59 | lcsc: "" 60 | package: "" 61 | prefix: 62 | category: 63 | quantity: 64 | jlcpcb_type: "" 65 | jlcpcb_price: 66 | selected_from: 67 | decision_applied: "=" 68 | 69 | # === Conditional parts (added based on design_options) === 70 | - id: 71 | name: "" 72 | part_number: "" 73 | lcsc: "" 74 | package: "" 75 | prefix: 76 | category: 77 | quantity: 78 | jlcpcb_type: "" 79 | jlcpcb_price: 80 | added_by: "=" 81 | ``` 82 | 83 | ## Rules 84 | 85 | 1. **No TBD values** - all parts must have specific values/part numbers 86 | 2. **Apply all decisions** - reference which decision was applied 87 | 3. **Add prefix** - R, C, U, D, J, SW, Y, etc. 88 | 4. **Add lcsc_hint** - search term for JLCPCB lookup 89 | 5. **Consolidate identical parts** - combine into single entry with quantity 90 | 6. **Remove parts eliminated by decisions** - if decision removes an option 91 | 92 | ## Required Fields 93 | 94 | | Field | Required | Description | 95 | |-------|----------|-------------| 96 | | id | Yes | Unique identifier | 97 | | name | Yes | Human-readable name | 98 | | part_number | Yes | Part number or value | 99 | | lcsc | Yes | LCSC code (from step2 enrichment) | 100 | | package | Yes | Footprint package | 101 | | prefix | Yes | Schematic prefix (R/C/U/etc) | 102 | | category | Yes | Component category | 103 | | quantity | Yes | Number needed | 104 | | jlcpcb_type | Yes | Basic or Extended | 105 | | jlcpcb_price | Yes | Unit price | 106 | | selected_from | If from option_group | Which option_group this came from | 107 | | added_by | If conditional | Which design_option added this part | 108 | 109 | ## Prefix Reference 110 | 111 | - `R` - Resistor 112 | - `C` - Capacitor 113 | - `U` - IC (integrated circuit) 114 | - `D` - Diode, LED 115 | - `J` - Connector 116 | - `SW` - Switch 117 | - `Y` - Crystal, oscillator 118 | - `L` - Inductor 119 | - `F` - Fuse 120 | - `ENC` - Encoder 121 | - `ANT` - Antenna 122 | 123 | ## After YAML 124 | 125 | Run validator: 126 | ```bash 127 | python design/scripts/validate_step4.py 128 | ``` 129 | 130 | If validation fails, fix errors and rerun. 131 | 132 | --- 133 | 134 | ## Exit Validation Checklist 135 | 136 | **Before proceeding to Step 5, ALL checks must pass:** 137 | 138 | ### 1. File Exists and Valid 139 | ```bash 140 | ls -la design/work/step4_final_parts.yaml 141 | python -c "import yaml; yaml.safe_load(open('design/work/step4_final_parts.yaml'))" 142 | ``` 143 | - [ ] `step4_final_parts.yaml` exists and is valid YAML 144 | 145 | ### 2. No TBD Values 146 | ```bash 147 | grep -i "TBD\|TODO\|PLACEHOLDER" design/work/step4_final_parts.yaml 148 | ``` 149 | - [ ] No TBD, TODO, or placeholder values remain (grep should return nothing) 150 | 151 | ### 3. All Decisions Applied 152 | - [ ] Every decision from decisions.yaml is reflected in the parts list 153 | - [ ] `decision_applied` field references the decision for affected parts 154 | 155 | ### 4. Required Fields Present 156 | Every part must have: 157 | - [ ] `id` - unique identifier 158 | - [ ] `name` - human-readable name 159 | - [ ] `part` - specific part number (no generic values) 160 | - [ ] `package` - footprint 161 | - [ ] `prefix` - R/C/U/D/J/SW/ENC/Y 162 | - [ ] `category` - component category 163 | - [ ] `quantity` - number needed 164 | - [ ] `belongs_to` - parent id or null 165 | - [ ] `lcsc_hint` - JLCPCB search term 166 | 167 | ### 5. No Duplicate IDs 168 | ```bash 169 | grep "^ - id:" design/work/step4_final_parts.yaml | sort | uniq -d 170 | ``` 171 | - [ ] No duplicate IDs (command should produce no output) 172 | 173 | ### 6. Symbols Exist 174 | ```bash 175 | python scripts/ensure_symbols.py --parts work/step4_final_parts.yaml --dry-run 176 | ``` 177 | - [ ] All symbols present in library 178 | 179 | --- 180 | 181 | ## If Validation Fails 182 | 183 | **DO NOT proceed to Step 5!** 184 | 185 | 1. Identify which check(s) failed 186 | 2. Fix the issue in step4_final_parts.yaml 187 | 3. Re-run ALL validation checks 188 | 4. Only proceed when ALL checks pass 189 | 190 | ``` 191 | ⚠️ VALIDATION LOOP: Step 4 → Validate → Fix if needed → Validate again → Step 5 192 | ``` 193 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/generate_skidl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Generate SKiDL Python file from validated pin_model.json (Phase 3) 4 | 5 | This creates a deterministic SKiDL script that can be executed to produce: 6 | - Netlist (.net file) 7 | - ERC report 8 | 9 | Note: SKiDL does not natively generate schematics (.kicad_sch). 10 | For schematic generation, use a separate tool or the existing generate_schematic.py 11 | 12 | Usage: 13 | python generate_skidl.py # Generate receiver.py 14 | python generate_skidl.py --run # Generate and execute 15 | """ 16 | 17 | import json 18 | import argparse 19 | from pathlib import Path 20 | from datetime import datetime 21 | 22 | 23 | def generate_skidl_code(model: dict) -> str: 24 | """Generate SKiDL Python code from pin model.""" 25 | 26 | parts = model.get('parts', []) 27 | nets = model.get('nets', []) 28 | meta = model.get('_meta', {}) 29 | 30 | # Start building the code 31 | lines = [] 32 | 33 | # Header 34 | lines.append('#!/usr/bin/env python3') 35 | lines.append('"""') 36 | lines.append('ESP32-S3 Radio Receiver - SKiDL Schematic') 37 | lines.append(f'Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}') 38 | lines.append(f'From: {", ".join(meta.get("generated_from", []))}') 39 | lines.append('') 40 | lines.append('This file was auto-generated from pin_model.json') 41 | lines.append('Do not edit manually - regenerate from source YAML files') 42 | lines.append('"""') 43 | lines.append('') 44 | lines.append('from skidl import *') 45 | lines.append('') 46 | lines.append('# Set default tool to KiCad') 47 | lines.append('set_default_tool(KICAD8)') 48 | lines.append('') 49 | 50 | # Reset function 51 | lines.append('def create_schematic():') 52 | lines.append(' """Create the radio receiver schematic."""') 53 | lines.append(' ') 54 | lines.append(' # Reset SKiDL state') 55 | lines.append(' reset()') 56 | lines.append(' ') 57 | 58 | # Create nets 59 | lines.append(' # === Create Nets ===') 60 | for net_name in sorted(nets): 61 | # Sanitize net name for Python variable 62 | var_name = net_name.replace('+', 'P').replace('-', 'N').replace('.', '_') 63 | lines.append(f' net_{var_name} = Net("{net_name}")') 64 | lines.append(' ') 65 | 66 | # Create a lookup for net variables 67 | lines.append(' # Net lookup') 68 | lines.append(' nets = {') 69 | for net_name in sorted(nets): 70 | var_name = net_name.replace('+', 'P').replace('-', 'N').replace('.', '_') 71 | lines.append(f' "{net_name}": net_{var_name},') 72 | lines.append(' }') 73 | lines.append(' ') 74 | 75 | # Create parts 76 | lines.append(' # === Create Parts ===') 77 | lines.append(' parts = {}') 78 | lines.append(' ') 79 | 80 | for part in parts: 81 | ref = part.get('ref', 'X?') 82 | part_id = part.get('id', '') 83 | value = part.get('value', '') 84 | lcsc = part.get('lcsc', '') 85 | footprint = part.get('footprint', '').replace('JLCPCB:', '') 86 | belongs_to = part.get('belongs_to') 87 | 88 | # Comment with part info 89 | belongs_str = f" (belongs_to: {belongs_to})" if belongs_to else "" 90 | lines.append(f' # {ref}: {value}{belongs_str}') 91 | 92 | # For JLCPCB parts, we'd use a custom library 93 | # For now, use generic parts with LCSC as reference 94 | lines.append(f' parts["{part_id}"] = Part(') 95 | lines.append(f' "Device", "R", # Placeholder - replace with actual symbol') 96 | lines.append(f' ref="{ref}",') 97 | lines.append(f' value="{value}",') 98 | if footprint: 99 | lines.append(f' footprint="{footprint}",') 100 | lines.append(f' # LCSC: {lcsc}') 101 | lines.append(f' )') 102 | lines.append(' ') 103 | 104 | # Connect pins 105 | lines.append(' # === Connect Pins ===') 106 | for part in parts: 107 | ref = part.get('ref', 'X?') 108 | part_id = part.get('id', '') 109 | pins = part.get('pins', {}) 110 | 111 | if pins: 112 | lines.append(f' # {ref} connections') 113 | for pin_name, net_name in pins.items(): 114 | lines.append(f' nets["{net_name}"] += parts["{part_id}"]["{pin_name}"]') 115 | lines.append(' ') 116 | 117 | # ERC and output 118 | lines.append(' # === Generate Output ===') 119 | lines.append(' ERC()') 120 | lines.append(' generate_netlist()') 121 | lines.append(' ') 122 | lines.append(' print("Netlist generated successfully")') 123 | lines.append(' ') 124 | lines.append(' return parts, nets') 125 | lines.append('') 126 | lines.append('') 127 | lines.append('if __name__ == "__main__":') 128 | lines.append(' create_schematic()') 129 | lines.append('') 130 | 131 | return '\n'.join(lines) 132 | 133 | 134 | def main(): 135 | parser = argparse.ArgumentParser(description='Generate SKiDL code from pin model') 136 | parser.add_argument('--run', action='store_true', help='Execute generated code') 137 | parser.add_argument('--output', default='receiver.py', help='Output filename') 138 | args = parser.parse_args() 139 | 140 | script_dir = Path(__file__).parent.parent 141 | model_file = script_dir / "work" / "pin_model.json" 142 | output_file = script_dir / "output" / args.output 143 | 144 | # Ensure output directory exists 145 | output_file.parent.mkdir(parents=True, exist_ok=True) 146 | 147 | print(f"Reading: {model_file}") 148 | 149 | with open(model_file, 'r', encoding='utf-8') as f: 150 | model = json.load(f) 151 | 152 | code = generate_skidl_code(model) 153 | 154 | with open(output_file, 'w', encoding='utf-8') as f: 155 | f.write(code) 156 | 157 | print(f"Generated: {output_file}") 158 | print(f" Parts: {len(model.get('parts', []))}") 159 | print(f" Nets: {len(model.get('nets', []))}") 160 | 161 | if args.run: 162 | print("\nExecuting generated code...") 163 | try: 164 | exec(compile(code, output_file, 'exec')) 165 | except ImportError: 166 | print("ERROR: SKiDL not installed. Install with: pip install skidl") 167 | except Exception as e: 168 | print(f"ERROR: {e}") 169 | 170 | 171 | if __name__ == "__main__": 172 | main() 173 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/prompts/step3.md: -------------------------------------------------------------------------------- 1 | # Step 3: Design Options 2 | 3 | Present options with REAL pricing data and collect user decisions. 4 | 5 | **Purpose:** Decide which optional parts to keep and which variant to select for each option_group. 6 | 7 | --- 8 | 9 | ## Prerequisites Check (MUST PASS BEFORE STARTING) 10 | 11 | **Verify these files exist and are valid:** 12 | 13 | ``` 14 | [ ] design/work/step2_parts_extended.yaml exists 15 | [ ] All parts have jlcpcb_price and jlcpcb_stock data 16 | ``` 17 | 18 | **Quick validation:** 19 | ```bash 20 | ls -la design/work/step2_parts_extended.yaml 21 | python -c "import yaml; yaml.safe_load(open('design/work/step2_parts_extended.yaml'))" 22 | ``` 23 | 24 | **If prerequisites fail -> Go back to Step 2 and complete it first!** 25 | 26 | --- 27 | 28 | ## Process Overview 29 | 30 | ``` 31 | step2_parts_extended.yaml (with pricing) 32 | | 33 | v 34 | [Present option_groups with pricing] 35 | | 36 | v 37 | [Present design_options] 38 | | 39 | v 40 | User Decisions 41 | | 42 | v 43 | step3_design_options.yaml + decisions.yaml 44 | ``` 45 | 46 | --- 47 | 48 | ## Step 3.1: Present Option Groups 49 | 50 | For each `option_group` in step2_parts_extended.yaml, present choices with REAL pricing: 51 | 52 | ``` 53 | ## Option Group: 54 | 55 | **Description:** 56 | 57 | | Option | Part Number | LCSC | Type | Price | Stock | Pros | Cons | 58 | |--------|-------------|------|------|-------|-------|------|------| 59 | | A | | | | $ | | | | 60 | | B | | | | $ | | | | 61 | 62 | **Recommendation:** Option - 63 | 64 | **Your choice?** [A/B/...] 65 | ``` 66 | 67 | **Selection criteria:** 68 | 1. In stock (jlcpcb_available: true) 69 | 2. Basic part preferred over Extended ($3 less assembly fee) 70 | 3. Adequate specs for the application 71 | 4. Cost-effective 72 | 73 | --- 74 | 75 | ## Step 3.2: Present Design Options 76 | 77 | For each `design_option` in step1_primary_parts.yaml, present choices: 78 | 79 | ``` 80 | ## Design Option: 81 | 82 | **Question:** 83 | **Context:** 84 | 85 | | Choice | Description | Adds Parts | Est. Cost | 86 | |--------|-------------|------------|-----------| 87 | | | | | | 88 | | | | | | 89 | 90 | **Recommendation:** - 91 | 92 | **Your choice?** [//...] 93 | ``` 94 | 95 | --- 96 | 97 | ## Step 3.3: Collect Decisions 98 | 99 | After user responds, create TWO output files: 100 | 101 | ### File 1: `design/work/decisions.yaml` 102 | 103 | ```yaml 104 | # decisions.yaml 105 | # User decisions from Step 3 106 | # Date: [YYYY-MM-DD] 107 | 108 | component_selections: 109 | # For each option_group, record which candidate was selected 110 | : 111 | 112 | design_options: 113 | # For each design_option, record the user's choice 114 | : "" 115 | 116 | notes: 117 | - "" 118 | - "" 119 | ``` 120 | 121 | ### File 2: `design/work/step3_design_options.yaml` 122 | 123 | ```yaml 124 | # step3_design_options.yaml 125 | # Design options presented and decisions made 126 | # Date: [YYYY-MM-DD] 127 | 128 | option_groups_presented: 129 | : 130 | candidates_shown: [, ] 131 | selected: 132 | reason: "" 133 | 134 | design_options_presented: 135 | : 136 | choices_shown: [, ] 137 | selected: "" 138 | reason: "" 139 | parts_added: [] 140 | 141 | summary: 142 | total_options: 143 | decisions_made: 144 | parts_from_options: 145 | ``` 146 | 147 | --- 148 | 149 | ## Step 3.4: Display Summary 150 | 151 | Show cost summary with selected parts: 152 | 153 | ``` 154 | ## Design Decisions Summary 155 | 156 | ### Option Group Selections 157 | | Option Group | Selected | LCSC | Type | Price | 158 | |--------------|----------|------|------|-------| 159 | | | | | | $ | 160 | 161 | ### Design Option Selections 162 | | Design Option | Selected | Parts Added | 163 | |---------------|----------|-------------| 164 | | | | | 165 | 166 | ### Cost Impact 167 | - Basic parts: 168 | - Extended parts: 169 | - Extended setup fee: $ 170 | ``` 171 | 172 | --- 173 | 174 | ## ⚠️ STOP HERE 175 | 176 | **Create `design/work/decisions.yaml` with ALL user decisions before proceeding!** 177 | 178 | Do NOT proceed to Step 4 until decisions.yaml is complete and validated. 179 | 180 | --- 181 | 182 | ## Exit Validation Checklist 183 | 184 | **Before proceeding to Step 4, ALL checks must pass:** 185 | 186 | ### 1. Files Exist and Valid 187 | ```bash 188 | ls -la design/work/decisions.yaml design/work/step3_design_options.yaml 189 | python -c "import yaml; yaml.safe_load(open('design/work/decisions.yaml'))" 190 | python -c "import yaml; yaml.safe_load(open('design/work/step3_design_options.yaml'))" 191 | ``` 192 | - [ ] Both files exist and are valid YAML 193 | 194 | ### 2. All Option Groups Decided 195 | ```bash 196 | # List option_groups from step1 197 | grep "option_group:" design/work/step1_primary_parts.yaml | grep -v "null" | sort -u 198 | 199 | # Check decisions.yaml has selection for each 200 | cat design/work/decisions.yaml 201 | ``` 202 | - [ ] Every option_group has a selection in decisions.yaml 203 | 204 | ### 3. All Design Options Decided 205 | ```bash 206 | # List design_options from step1 207 | grep -A1 "design_options:" design/work/step1_primary_parts.yaml 208 | 209 | # Check decisions.yaml has selection for each 210 | cat design/work/decisions.yaml 211 | ``` 212 | - [ ] Every design_option has a selection in decisions.yaml 213 | 214 | ### 4. Selected Parts Are Available 215 | - [ ] All selected parts have jlcpcb_available: true 216 | - [ ] Or documented plan for sourcing unavailable parts 217 | 218 | --- 219 | 220 | ## If Validation Fails 221 | 222 | **DO NOT proceed to Step 4!** 223 | 224 | 1. Identify which check(s) failed 225 | 2. Get missing decisions from user 226 | 3. Update decisions.yaml 227 | 4. Re-run ALL validation checks 228 | 5. Only proceed when ALL checks pass 229 | 230 | ``` 231 | VALIDATION LOOP: Step 3 -> Validate -> Get decisions -> Validate again -> Step 4 232 | ``` 233 | 234 | --- 235 | 236 | ## What Happens Next 237 | 238 | Step 4 will: 239 | 1. Read decisions.yaml 240 | 2. Apply selections to create final parts list 241 | 3. Add conditional parts based on design_options 242 | 4. Output step4_final_parts.yaml with ONLY selected parts 243 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/run_pipeline.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | KiCAD Schematic Generation Pipeline Runner 4 | 5 | This script orchestrates the automated steps of the pipeline: 6 | - Step 4: Generate pin_model.json from parts + connections YAML 7 | - Ensure all symbols exist in the library 8 | - Step 5: Generate KiCAD schematic 9 | - Run ERC verification 10 | 11 | Steps 1-3 are LLM-driven and should be completed before running this script. 12 | 13 | Usage: 14 | python run_pipeline.py [--skip-symbols] [--debug] 15 | """ 16 | 17 | import subprocess 18 | import sys 19 | from pathlib import Path 20 | 21 | 22 | def run_command(cmd: list, description: str, cwd: Path = None) -> bool: 23 | """Run a command and return success status.""" 24 | print(f"\n{'='*60}") 25 | print(f" {description}") 26 | print(f"{'='*60}") 27 | print(f" Command: {' '.join(str(c) for c in cmd)}") 28 | print() 29 | 30 | result = subprocess.run(cmd, cwd=cwd) 31 | 32 | if result.returncode != 0: 33 | print(f"\n FAILED with exit code {result.returncode}") 34 | return False 35 | 36 | print(f"\n SUCCESS") 37 | return True 38 | 39 | 40 | def main(): 41 | import argparse 42 | 43 | parser = argparse.ArgumentParser(description='Run KiCAD schematic generation pipeline') 44 | parser.add_argument('--skip-symbols', action='store_true', help='Skip symbol download step') 45 | parser.add_argument('--debug', action='store_true', help='Generate debug schematic') 46 | parser.add_argument('--project', type=Path, help='Project design directory (default: parent of scripts)') 47 | args = parser.parse_args() 48 | 49 | # Determine project directory 50 | script_dir = Path(__file__).parent 51 | if args.project: 52 | project_dir = args.project 53 | else: 54 | # Assume scripts are in design/scripts/ or KiCAD-Generator-tools/scripts/ 55 | project_dir = script_dir.parent 56 | 57 | # Check if this is the tools directory or a project directory 58 | if (project_dir / 'work').exists(): 59 | design_dir = project_dir 60 | elif (project_dir / 'design' / 'work').exists(): 61 | design_dir = project_dir / 'design' 62 | else: 63 | print(f"Error: Cannot find work directory in {project_dir}") 64 | print(" Expected: {project}/work/ or {project}/design/work/") 65 | return 1 66 | 67 | print(f"Project directory: {design_dir}") 68 | 69 | # Define paths 70 | work_dir = design_dir / 'work' 71 | output_dir = design_dir / 'output' 72 | scripts_dir = script_dir 73 | tools_dir = script_dir.parent # KiCAD-Generator-tools directory 74 | 75 | pin_model = work_dir / 'pin_model.json' 76 | parts_yaml = work_dir / 'step2_parts_complete.yaml' 77 | connections_yaml = work_dir / 'step3_connections.yaml' 78 | 79 | # Use central library (shared across all projects) 80 | central_library = tools_dir / 'libs' / 'JLCPCB' / 'symbol' / 'JLCPCB.kicad_sym' 81 | if central_library.exists(): 82 | library = central_library 83 | else: 84 | # Fall back to project-local library 85 | library = output_dir / 'libs' / 'JLCPCB' / 'symbol' / 'JLCPCB.kicad_sym' 86 | 87 | # Check required files exist 88 | print("\nChecking required files...") 89 | missing = [] 90 | for f, name in [(parts_yaml, 'Parts YAML'), (connections_yaml, 'Connections YAML')]: 91 | if not f.exists(): 92 | missing.append(f"{name}: {f}") 93 | else: 94 | print(f" Found: {name}") 95 | 96 | if missing: 97 | print("\nMissing required files:") 98 | for m in missing: 99 | print(f" - {m}") 100 | print("\nComplete Steps 1-3 (LLM steps) before running pipeline.") 101 | return 1 102 | 103 | # Step 4: Generate pin_model.json (if generate_pin_model.py exists) 104 | generate_pin_model = scripts_dir / 'generate_pin_model.py' 105 | if generate_pin_model.exists() and not pin_model.exists(): 106 | if not run_command( 107 | [sys.executable, str(generate_pin_model)], 108 | "Step 4: Generate pin_model.json", 109 | cwd=design_dir 110 | ): 111 | return 1 112 | elif pin_model.exists(): 113 | print(f"\n pin_model.json already exists") 114 | else: 115 | print(f"\n Warning: generate_pin_model.py not found, assuming pin_model.json created by LLM") 116 | 117 | # Check pin_model exists now 118 | if not pin_model.exists(): 119 | print(f"\nError: pin_model.json not found at {pin_model}") 120 | print(" Create it manually or ensure generate_pin_model.py works") 121 | return 1 122 | 123 | # Ensure symbols step 124 | if not args.skip_symbols: 125 | ensure_symbols = scripts_dir / 'ensure_symbols.py' 126 | if ensure_symbols.exists(): 127 | if not run_command( 128 | [sys.executable, str(ensure_symbols), 129 | '--pin-model', str(pin_model), 130 | '--library', str(library)], 131 | "Ensure all symbols exist in library" 132 | ): 133 | print("\nSome symbols could not be downloaded.") 134 | print("You may need to add them manually to the library.") 135 | # Continue anyway - some symbols might work 136 | else: 137 | print("\n Skipping symbol check (--skip-symbols)") 138 | 139 | # Step 5: Generate schematic 140 | schematic_script = scripts_dir / 'kicad9_schematic.py' 141 | if not schematic_script.exists(): 142 | # Try in design/scripts 143 | schematic_script = design_dir / 'scripts' / 'kicad9_schematic.py' 144 | 145 | if not schematic_script.exists(): 146 | print(f"\nError: kicad9_schematic.py not found") 147 | return 1 148 | 149 | cmd = [sys.executable, str(schematic_script)] 150 | if args.debug: 151 | cmd.append('--debug') 152 | 153 | if not run_command(cmd, "Step 5: Generate KiCAD schematic", cwd=design_dir): 154 | return 1 155 | 156 | # Run ERC 157 | schematic = output_dir / ('Debug.kicad_sch' if args.debug else 'RadioReceiver_v3.kicad_sch') 158 | if schematic.exists(): 159 | result = subprocess.run( 160 | ['kicad-cli', 'sch', 'erc', str(schematic), '--exit-code-violations'], 161 | capture_output=True, 162 | text=True 163 | ) 164 | print(f"\n{'='*60}") 165 | print(" ERC Results") 166 | print(f"{'='*60}") 167 | print(result.stdout) 168 | 169 | # Parse error count 170 | if 'Errors 0' in result.stdout: 171 | print(" ERC PASSED - No errors!") 172 | else: 173 | print(" ERC found errors - check the report") 174 | 175 | print(f"\n{'='*60}") 176 | print(" Pipeline Complete") 177 | print(f"{'='*60}") 178 | print(f"\nOutput files:") 179 | print(f" Schematic: {schematic}") 180 | print(f" Debug CSV: {output_dir / 'debug.csv'}") 181 | 182 | return 0 183 | 184 | 185 | if __name__ == "__main__": 186 | exit(main()) 187 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/prompts/step6.md: -------------------------------------------------------------------------------- 1 | # Step 6: Validation 2 | 3 | Verify design completeness and correctness before schematic generation. 4 | 5 | --- 6 | 7 | ## Prerequisites Check (MUST PASS BEFORE STARTING) 8 | 9 | **Verify these files exist and are valid:** 10 | 11 | ``` 12 | □ design/work/step4_final_parts.yaml exists 13 | □ design/work/step5_connections.yaml exists 14 | □ design/input/FSD_*.md exists 15 | □ All symbols exist in JLCPCB library 16 | ``` 17 | 18 | **Quick validation:** 19 | ```bash 20 | ls -la design/work/step4_final_parts.yaml design/work/step5_connections.yaml 21 | python scripts/ensure_symbols.py --parts work/step4_final_parts.yaml --dry-run 22 | ``` 23 | 24 | **If prerequisites fail → Go back to previous step and complete it first!** 25 | 26 | --- 27 | 28 | ## Input Files 29 | 30 | - `design/input/FSD_*.md` - Original requirements 31 | - `design/work/step4_final_parts.yaml` - Final parts 32 | - `design/work/step5_connections.yaml` - All connections 33 | - `design/sources/references.md` - Reference documents 34 | 35 | ## Output Format 36 | 37 | Write to `design/work/step6_validation.yaml`: 38 | 39 | ```yaml 40 | # step6_validation.yaml 41 | # Design validation report 42 | # Date: [YYYY-MM-DD] 43 | 44 | summary: 45 | status: PASS # PASS | PASS_WITH_WARNINGS | FAIL 46 | total_parts: 47 | total_nets: 48 | errors_found: 49 | warnings_found: 50 | 51 | # === FSD Requirements Coverage === 52 | fsd_coverage: 53 | - requirement: "" 54 | section: "" 55 | status: COVERED # COVERED | PARTIAL | MISSING 56 | implemented_by: 57 | 58 | - requirement: "" 59 | section: "" 60 | status: COVERED 61 | implemented_by: [, ] 62 | 63 | - requirement: "" 64 | section: "" 65 | status: PARTIAL 66 | implemented_by: 67 | note: "" 68 | 69 | # === Electrical Checks === 70 | electrical_checks: 71 | power_rails: 72 | - rail: "" # e.g., +3V3, +5V, VBAT 73 | source: 74 | loads: [, , ] 75 | estimated_current_ma: 76 | source_capacity_ma: 77 | status: OK # OK | WARNING | FAIL 78 | 79 | - rail: "" 80 | source: 81 | loads: [, ] 82 | voltage_range: "-V" 83 | status: OK 84 | 85 | bypass_capacitors: 86 | - component: 87 | required: 88 | provided: 89 | status: OK 90 | 91 | pull_resistors: 92 | - bus: # e.g., I2C, SPI 93 | pull_ups_required: 94 | pull_ups_provided: 95 | value: "" 96 | status: OK 97 | 98 | # === Errors Found === 99 | # Only report actual errors that prevent schematic generation 100 | errors: 101 | - id: 1 102 | severity: error # warning | error | critical 103 | category: # electrical | connection | missing_part | missing_symbol 104 | description: "" 105 | affected: [] 106 | 107 | - id: 2 108 | severity: warning 109 | category: 110 | description: "" 111 | affected: [] 112 | 113 | # === Statistics === 114 | statistics: 115 | by_category: 116 | : 117 | : 118 | # ... list all categories with counts 119 | 120 | by_prefix: 121 | U: 122 | R: 123 | C: 124 | D: 125 | J: 126 | # ... list all prefixes with counts 127 | ``` 128 | 129 | ## Validation Checks 130 | 131 | ### 1. Completeness 132 | - [ ] All FSD requirements addressed 133 | - [ ] Every part has at least one connection 134 | - [ ] All IC power pins connected to power rail 135 | - [ ] All IC ground pins connected to GND 136 | 137 | ### 2. Electrical 138 | - [ ] No duplicate pin assignments (same pin on multiple nets) 139 | - [ ] No single-pin nets (except test points) 140 | - [ ] Bypass capacitors have both pins connected 141 | - [ ] Voltage levels compatible between connected parts 142 | 143 | ## After YAML 144 | 145 | Run summary script: 146 | ```bash 147 | python design/scripts/summarize_progress.py 148 | ``` 149 | 150 | This generates a human-readable summary of the design. 151 | 152 | --- 153 | 154 | ## Exit Validation Checklist 155 | 156 | **Before proceeding to Schematic Generation, ALL checks must pass:** 157 | 158 | ### 1. Validation Report Complete 159 | ```bash 160 | ls -la design/work/step6_validation.yaml 161 | python -c "import yaml; yaml.safe_load(open('design/work/step6_validation.yaml'))" 162 | ``` 163 | - [ ] `step6_validation.yaml` exists and is valid YAML 164 | 165 | ### 2. No Critical Errors 166 | ```bash 167 | grep -E "severity: critical|severity: error" design/work/step6_validation.yaml 168 | ``` 169 | - [ ] No critical or error severity items (grep should return nothing) 170 | - [ ] Overall status is PASS or PASS_WITH_WARNINGS 171 | 172 | ### 3. FSD Coverage Complete 173 | - [ ] All FSD requirements have status COVERED or PARTIAL 174 | - [ ] No requirements have status MISSING or NOT_ADDRESSED 175 | - [ ] Any PARTIAL items have explanatory notes 176 | 177 | ### 4. Electrical Checks Pass 178 | - [ ] All power rails have sufficient capacity 179 | - [ ] All bypass capacitors present 180 | - [ ] All pull resistors present and correctly valued 181 | 182 | ### 5. Files Ready for Schematic Generation 183 | ```bash 184 | # Final check of all required files 185 | ls -la design/work/step4_final_parts.yaml \ 186 | design/work/step5_connections.yaml \ 187 | design/work/step6_validation.yaml 188 | 189 | # Symbols check 190 | python scripts/ensure_symbols.py --parts work/step4_final_parts.yaml --dry-run 191 | ``` 192 | - [ ] All YAML files exist and are valid 193 | - [ ] All symbols present in library 194 | 195 | --- 196 | 197 | ## If Validation Fails 198 | 199 | **DO NOT proceed to schematic generation!** 200 | 201 | 1. Review the errors in step6_validation.yaml 202 | 2. Go back to the appropriate step to fix: 203 | - Parts issues → Step 4 204 | - Connection issues → Step 5 205 | - Missing symbols → Run ensure_symbols.py 206 | 3. Re-run Step 6 validation 207 | 4. Only proceed when status is PASS or PASS_WITH_WARNINGS 208 | 209 | ``` 210 | ⚠️ VALIDATION LOOP: Step 6 → Review Issues → Fix in earlier step → Re-validate → Generate Schematic 211 | 212 | The pipeline ensures quality at every step: 213 | Step 1 ──► Step 2 ──► Step 3 ──► Step 4 ──► Step 5 ──► Step 6 ──► Schematic 214 | ▲ ▲ ▲ ▲ ▲ ▲ 215 | └──────────┴──────────┴──────────┴──────────┴──────────┘ 216 | Loop back if validation fails 217 | ``` 218 | 219 | --- 220 | 221 | ## Ready for Schematic Generation 222 | 223 | When all validations pass, proceed to generate the schematic: 224 | 225 | ```bash 226 | python scripts/run_pipeline.py 227 | ``` 228 | 229 | This will: 230 | 1. Generate pin_model.json from parts + connections 231 | 2. Verify all symbols exist in library 232 | 3. Generate KiCAD schematic 233 | 4. Run ERC verification 234 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/validate_step5.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Validate step5_connections.yaml 4 | 5 | Checks: 6 | - YAML parses correctly 7 | - Every connection references an existing component ID (from step4) 8 | - No single-pin nets unless marked as test point or NC 9 | - Required power nets exist (GND, at least one supply rail) 10 | - No duplicate pin usage (same pin on multiple nets) 11 | """ 12 | 13 | import sys 14 | import yaml 15 | from pathlib import Path 16 | 17 | 18 | def validate(filepath: Path, parts_filepath: Path) -> list: 19 | """Validate step5 file and return list of errors.""" 20 | errors = [] 21 | 22 | # Check file exists 23 | if not filepath.exists(): 24 | return [f"File not found: {filepath}"] 25 | 26 | # Parse YAML 27 | try: 28 | with open(filepath, 'r', encoding='utf-8') as f: 29 | data = yaml.safe_load(f) 30 | except yaml.YAMLError as e: 31 | return [f"YAML parse error: {e}"] 32 | 33 | if not data: 34 | return ["File is empty"] 35 | 36 | # Load parts from step4 for reference validation 37 | valid_component_ids = set() 38 | if parts_filepath.exists(): 39 | try: 40 | with open(parts_filepath, 'r', encoding='utf-8') as f: 41 | parts_data = yaml.safe_load(f) 42 | if parts_data and 'parts' in parts_data: 43 | for part in parts_data['parts']: 44 | if isinstance(part, dict) and 'id' in part: 45 | valid_component_ids.add(part['id']) 46 | except yaml.YAMLError: 47 | errors.append("WARNING: Could not parse step4_final_parts.yaml for reference validation") 48 | 49 | # Check nets exists 50 | if 'nets' not in data: 51 | return ["Missing 'nets' key"] 52 | 53 | nets = data['nets'] 54 | if not isinstance(nets, dict): 55 | return ["'nets' must be a dictionary"] 56 | 57 | if len(nets) == 0: 58 | return ["No nets defined - cannot have empty netlist"] 59 | 60 | # Track all pin references for duplicate detection 61 | pin_to_nets = {} # pin_ref -> list of net names 62 | 63 | # Collect test points and no_connect for single-pin exceptions 64 | test_point_nets = set() 65 | nc_pins = set() 66 | 67 | if 'test_points' in data: 68 | for tp in data.get('test_points', []): 69 | if isinstance(tp, dict) and 'net' in tp: 70 | test_point_nets.add(tp['net']) 71 | 72 | if 'no_connect' in data: 73 | for nc in data.get('no_connect', []): 74 | if isinstance(nc, dict): 75 | comp = nc.get('component', '') 76 | pin = nc.get('pin', '') 77 | if comp and pin: 78 | nc_pins.add(f"{comp}.{pin}") 79 | 80 | # Validate each net 81 | for net_name, connections in nets.items(): 82 | if not isinstance(connections, list): 83 | errors.append(f"Net '{net_name}': connections must be a list") 84 | continue 85 | 86 | # Check for single-pin nets 87 | if len(connections) < 2 and net_name not in test_point_nets: 88 | errors.append(f"Net '{net_name}': Only {len(connections)} connection(s) - needs at least 2 (or mark as test_point)") 89 | 90 | # Validate each connection 91 | for conn in connections: 92 | if not isinstance(conn, str): 93 | errors.append(f"Net '{net_name}': Connection must be string, got {type(conn).__name__}") 94 | continue 95 | 96 | # Parse component.pin format 97 | if '.' not in conn: 98 | errors.append(f"Net '{net_name}': Invalid connection format '{conn}' - must be 'component_id.PIN'") 99 | continue 100 | 101 | parts = conn.split('.', 1) 102 | component_id = parts[0] 103 | pin_name = parts[1] if len(parts) > 1 else '' 104 | 105 | # Check component exists (if we have parts data) 106 | if valid_component_ids and component_id not in valid_component_ids: 107 | errors.append(f"Net '{net_name}': Unknown component '{component_id}' in connection '{conn}'") 108 | 109 | # Track for duplicate detection 110 | if conn not in pin_to_nets: 111 | pin_to_nets[conn] = [] 112 | pin_to_nets[conn].append(net_name) 113 | 114 | # Check for duplicate pin usage 115 | for pin_ref, net_names in pin_to_nets.items(): 116 | if len(net_names) > 1: 117 | errors.append(f"Duplicate pin usage: '{pin_ref}' appears in multiple nets: {net_names}") 118 | 119 | # Check required power nets 120 | net_names_upper = {n.upper() for n in nets.keys()} 121 | if 'GND' not in net_names_upper: 122 | errors.append("Missing required 'GND' net") 123 | 124 | # Check for at least one supply rail (common patterns) 125 | supply_patterns = ['+3V3', '+5V', 'VCC', 'VBAT', '+3.3V', '+5.0V', 'VDD', '+12V', '+1V8'] 126 | has_supply = False 127 | for pattern in supply_patterns: 128 | if pattern.upper() in net_names_upper or pattern in nets: 129 | has_supply = True 130 | break 131 | if not has_supply: 132 | errors.append("WARNING: No common supply rail found (expected one of: +3V3, +5V, VCC, VBAT, VDD, etc.)") 133 | 134 | # Validate no_connect entries reference valid components 135 | if valid_component_ids and 'no_connect' in data: 136 | for i, nc in enumerate(data.get('no_connect', [])): 137 | if isinstance(nc, dict): 138 | comp = nc.get('component', '') 139 | if comp and comp not in valid_component_ids: 140 | errors.append(f"no_connect[{i}]: Unknown component '{comp}'") 141 | 142 | return errors 143 | 144 | 145 | def main(): 146 | script_dir = Path(__file__).parent.parent 147 | filepath = script_dir / "work" / "step5_connections.yaml" 148 | parts_filepath = script_dir / "work" / "step4_final_parts.yaml" 149 | 150 | print(f"Validating: {filepath}") 151 | print("=" * 60) 152 | 153 | errors = validate(filepath, parts_filepath) 154 | 155 | # Separate warnings from errors 156 | warnings = [e for e in errors if e.startswith("WARNING")] 157 | real_errors = [e for e in errors if not e.startswith("WARNING")] 158 | 159 | if real_errors: 160 | print("VALIDATION FAILED\n") 161 | for error in real_errors: 162 | print(f" {error}") 163 | if warnings: 164 | print("\nWarnings:") 165 | for warning in warnings: 166 | print(f" {warning}") 167 | print(f"\nTotal errors: {len(real_errors)}") 168 | sys.exit(1) 169 | else: 170 | print("VALIDATION PASSED") 171 | if warnings: 172 | print("\nWarnings:") 173 | for warning in warnings: 174 | print(f" {warning}") 175 | 176 | # Print summary 177 | with open(filepath, 'r', encoding='utf-8') as f: 178 | data = yaml.safe_load(f) 179 | nets = data.get('nets', {}) 180 | total_connections = sum(len(conns) for conns in nets.values() if isinstance(conns, list)) 181 | nc_count = len(data.get('no_connect', [])) 182 | tp_count = len(data.get('test_points', [])) 183 | 184 | print(f"\nSummary: {len(nets)} nets, {total_connections} total connections") 185 | print(f" - No-connect pins: {nc_count}") 186 | print(f" - Test points: {tp_count}") 187 | 188 | # List power nets 189 | power_nets = [n for n in nets.keys() if any(p in n.upper() for p in ['GND', 'VCC', 'VDD', 'BAT', '+3V', '+5V', '+12V', '+1V'])] 190 | if power_nets: 191 | print(f" - Power nets: {', '.join(power_nets)}") 192 | 193 | sys.exit(0) 194 | 195 | 196 | if __name__ == "__main__": 197 | main() 198 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/generate_skidl_schematic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Generate KiCad 5 schematic using SKiDL's built-in schematic generator. 4 | 5 | This script: 6 | 1. Loads pin_model.json (parts with pin-to-net mappings) 7 | 2. Creates SKiDL parts and connects them via nets 8 | 3. Uses SKiDL's gen_schematic() to create a .sch file with proper routing 9 | """ 10 | 11 | import json 12 | import os 13 | from pathlib import Path 14 | 15 | # Set KiCad tool version before importing skidl 16 | os.environ['KICAD_SYMBOL_DIR'] = '' 17 | 18 | from skidl import * 19 | 20 | # Use KiCad 5 for schematic generation (it's the only one with full support) 21 | set_default_tool(KICAD5) 22 | 23 | 24 | def load_pin_model(pin_model_path: Path) -> dict: 25 | """Load the pin model JSON file.""" 26 | with open(pin_model_path, 'r', encoding='utf-8') as f: 27 | return json.load(f) 28 | 29 | 30 | def create_circuit_from_pin_model(model: dict, symbol_lib_path: Path) -> Circuit: 31 | """Create a SKiDL circuit from the pin model.""" 32 | 33 | # Create a new circuit 34 | ckt = Circuit() 35 | 36 | # Add the symbol library 37 | lib_path = str(symbol_lib_path) 38 | lib = SchLib(lib_path, tool=KICAD5) 39 | 40 | parts_data = model.get('parts', []) 41 | 42 | # LCSC to symbol name mapping 43 | lcsc_to_symbol = { 44 | "C2913206": "ESP32-S3-MINI-1-N8", 45 | "C195417": "SI4735-D60-GU", 46 | "C7971": "TDA1306T", 47 | "C16581": "TP4056", 48 | "C6186": "AMS1117-3.3", 49 | "C7519": "USBLC6-2SC6", 50 | "C393939": "TYPE-C-31-M-12", 51 | "C131337": "S2B-PH-K-S", 52 | "C145819": "PJ-327A", 53 | "C124378": "Header-1x04", 54 | "C238128": "TestPoint", 55 | "C470747": "EC11E18244A5", 56 | "C127509": "TS-1102S", 57 | "C2761795": "WS2812B-B", 58 | "C32346": "Crystal-32.768kHz", 59 | "C23186": "R", 60 | "C22975": "R", 61 | "C25804": "R", 62 | "C25900": "R", 63 | "C22775": "R", 64 | "C45783": "C", 65 | "C134760": "C", 66 | "C15850": "C", 67 | "C15849": "C", 68 | "C14663": "C", 69 | "C1653": "C", 70 | } 71 | 72 | # Create parts 73 | skidl_parts = {} 74 | for part_data in parts_data: 75 | ref = part_data.get('ref', 'X?') 76 | value = part_data.get('value', '') 77 | lcsc = part_data.get('lcsc', '') 78 | footprint = part_data.get('footprint', '') 79 | pins = part_data.get('pins', {}) 80 | 81 | # Get symbol name 82 | sym_name = lcsc_to_symbol.get(lcsc, value) 83 | 84 | try: 85 | # Create the part from the library 86 | part = Part(lib, sym_name, ref=ref, value=value, footprint=footprint, dest=TEMPLATE) 87 | part = part() # Instantiate the template 88 | part.ref = ref 89 | skidl_parts[ref] = (part, pins) 90 | print(f"Created part: {ref} ({sym_name})") 91 | except Exception as e: 92 | print(f"Warning: Could not create part {ref} ({sym_name}): {e}") 93 | 94 | # Create nets and connect parts 95 | nets = {} 96 | for ref, (part, pin_mappings) in skidl_parts.items(): 97 | for pin_name, net_name in pin_mappings.items(): 98 | if not net_name: 99 | continue 100 | 101 | # Get or create net 102 | if net_name not in nets: 103 | nets[net_name] = Net(net_name) 104 | 105 | # Connect pin to net 106 | try: 107 | pin = part[pin_name] 108 | nets[net_name] += pin 109 | except Exception as e: 110 | print(f"Warning: Could not connect {ref}.{pin_name} to {net_name}: {e}") 111 | 112 | print(f"\nCreated {len(skidl_parts)} parts and {len(nets)} nets") 113 | return ckt 114 | 115 | 116 | def main(): 117 | base_dir = Path(__file__).parent.parent 118 | 119 | pin_model_path = base_dir / "work" / "pin_model.json" 120 | symbol_lib_path = base_dir / "output" / "libs" / "JLCPCB" / "symbol" / "JLCPCB.kicad_sym" 121 | output_dir = base_dir / "output" 122 | 123 | print(f"Loading pin model from: {pin_model_path}") 124 | model = load_pin_model(pin_model_path) 125 | 126 | print(f"Creating circuit from pin model...") 127 | 128 | # Reset the default circuit 129 | default_circuit.reset() 130 | 131 | # Set library search path 132 | lib_search_paths[KICAD5].append(str(symbol_lib_path.parent)) 133 | 134 | # Load parts and create connections 135 | parts_data = model.get('parts', []) 136 | 137 | # LCSC to symbol name mapping 138 | lcsc_to_symbol = { 139 | "C2913206": "ESP32-S3-MINI-1-N8", 140 | "C195417": "SI4735-D60-GU", 141 | "C7971": "TDA1306T", 142 | "C16581": "TP4056", 143 | "C6186": "AMS1117-3.3", 144 | "C7519": "USBLC6-2SC6", 145 | "C393939": "TYPE-C-31-M-12", 146 | "C131337": "S2B-PH-K-S", 147 | "C145819": "PJ-327A", 148 | "C124378": "Header-1x04", 149 | "C238128": "TestPoint", 150 | "C470747": "EC11E18244A5", 151 | "C127509": "TS-1102S", 152 | "C2761795": "WS2812B-B", 153 | "C32346": "Crystal-32.768kHz", 154 | "C23186": "R", 155 | "C22975": "R", 156 | "C25804": "R", 157 | "C25900": "R", 158 | "C22775": "R", 159 | "C45783": "C", 160 | "C134760": "C", 161 | "C15850": "C", 162 | "C15849": "C", 163 | "C14663": "C", 164 | "C1653": "C", 165 | } 166 | 167 | # Load the library 168 | lib_path = str(symbol_lib_path) 169 | print(f"Loading symbol library: {lib_path}") 170 | 171 | # Create parts 172 | skidl_parts = {} 173 | for part_data in parts_data: 174 | ref = part_data.get('ref', 'X?') 175 | value = part_data.get('value', '') 176 | lcsc = part_data.get('lcsc', '') 177 | footprint = part_data.get('footprint', '') 178 | pins = part_data.get('pins', {}) 179 | 180 | # Get symbol name 181 | sym_name = lcsc_to_symbol.get(lcsc, value) 182 | 183 | try: 184 | # Create the part from the library 185 | part = Part(lib_path, sym_name, ref=ref, value=value, footprint=footprint) 186 | skidl_parts[ref] = (part, pins) 187 | print(f"Created part: {ref} ({sym_name})") 188 | except Exception as e: 189 | print(f"Warning: Could not create part {ref} ({sym_name}): {e}") 190 | 191 | # Create nets and connect parts 192 | nets = {} 193 | for ref, (part, pin_mappings) in skidl_parts.items(): 194 | for pin_name, net_name in pin_mappings.items(): 195 | if not net_name: 196 | continue 197 | 198 | # Get or create net 199 | if net_name not in nets: 200 | nets[net_name] = Net(net_name) 201 | 202 | # Connect pin to net 203 | try: 204 | pin = part[pin_name] 205 | nets[net_name] += pin 206 | except Exception as e: 207 | print(f"Warning: Could not connect {ref}.{pin_name} to {net_name}: {e}") 208 | 209 | print(f"\nCreated {len(skidl_parts)} parts and {len(nets)} nets") 210 | 211 | # Generate schematic 212 | print(f"\nGenerating schematic...") 213 | output_file = output_dir / "Debug" 214 | 215 | try: 216 | generate_schematic( 217 | filepath=str(output_dir), 218 | top_name="Debug", 219 | title="SKiDL Generated - ENC1 Debug", 220 | flatness=1.0, # Flat schematic (no hierarchy) 221 | retries=3 222 | ) 223 | print(f"\nSchematic generated: {output_file}.sch") 224 | except Exception as e: 225 | print(f"Error generating schematic: {e}") 226 | import traceback 227 | traceback.print_exc() 228 | 229 | 230 | if __name__ == "__main__": 231 | main() 232 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/summarize_progress.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Summarize design progress across all pipeline steps. 4 | 5 | Generates a human-readable report showing: 6 | - Which steps have been completed 7 | - Part counts and changes between steps 8 | - Connection statistics 9 | - Outstanding issues 10 | """ 11 | 12 | import sys 13 | import yaml 14 | from pathlib import Path 15 | from datetime import datetime 16 | 17 | 18 | def load_yaml(filepath: Path) -> dict | None: 19 | """Load YAML file, return None if missing or invalid.""" 20 | if not filepath.exists(): 21 | return None 22 | try: 23 | with open(filepath, 'r', encoding='utf-8') as f: 24 | return yaml.safe_load(f) 25 | except yaml.YAMLError: 26 | return None 27 | 28 | 29 | def summarize(): 30 | """Generate progress summary.""" 31 | script_dir = Path(__file__).parent.parent 32 | work_dir = script_dir / "work" 33 | 34 | print("=" * 70) 35 | print("DESIGN PIPELINE PROGRESS REPORT") 36 | print(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") 37 | print("=" * 70) 38 | 39 | # Track overall status 40 | steps_complete = 0 41 | total_steps = 6 42 | 43 | # === Step 1: Primary Parts === 44 | print("\n[Step 1] Primary Parts Extraction") 45 | print("-" * 40) 46 | step1 = load_yaml(work_dir / "step1_primary_parts.yaml") 47 | if step1: 48 | parts = step1.get('primary_parts', []) 49 | print(f" Status: COMPLETE") 50 | print(f" Primary parts identified: {len(parts)}") 51 | if parts: 52 | categories = {} 53 | for p in parts: 54 | cat = p.get('category', 'unknown') 55 | categories[cat] = categories.get(cat, 0) + 1 56 | print(f" By category: {dict(sorted(categories.items()))}") 57 | steps_complete += 1 58 | else: 59 | print(" Status: NOT STARTED") 60 | 61 | # === Step 2: Extended Parts === 62 | print("\n[Step 2] Supporting Parts Research") 63 | print("-" * 40) 64 | step2 = load_yaml(work_dir / "step2_parts_extended.yaml") 65 | if step2: 66 | parts = step2.get('parts', []) 67 | primary = sum(1 for p in parts if p.get('belongs_to') is None) 68 | supporting = len(parts) - primary 69 | print(f" Status: COMPLETE") 70 | print(f" Total parts: {len(parts)}") 71 | print(f" - Primary: {primary}") 72 | print(f" - Supporting: {supporting}") 73 | if step1: 74 | step1_count = len(step1.get('primary_parts', [])) 75 | print(f" Delta from Step 1: +{len(parts) - step1_count} parts") 76 | steps_complete += 1 77 | else: 78 | print(" Status: NOT STARTED") 79 | 80 | # === Step 3: Decisions === 81 | print("\n[Step 3] Design Decisions") 82 | print("-" * 40) 83 | step3 = load_yaml(work_dir / "step3_decisions.yaml") 84 | if step3: 85 | decisions = step3.get('decisions', []) 86 | made = sum(1 for d in decisions if d.get('selected')) 87 | pending = len(decisions) - made 88 | print(f" Status: {'COMPLETE' if pending == 0 else 'IN PROGRESS'}") 89 | print(f" Total decisions: {len(decisions)}") 90 | print(f" - Made: {made}") 91 | print(f" - Pending: {pending}") 92 | if pending > 0: 93 | print(" Pending decisions:") 94 | for d in decisions: 95 | if not d.get('selected'): 96 | print(f" - {d.get('topic', 'unknown')}") 97 | steps_complete += 1 98 | else: 99 | print(" Status: NOT STARTED") 100 | 101 | # === Step 4: Final Parts === 102 | print("\n[Step 4] Final Parts List") 103 | print("-" * 40) 104 | step4 = load_yaml(work_dir / "step4_final_parts.yaml") 105 | if step4: 106 | parts = step4.get('parts', []) 107 | total_qty = sum(p.get('quantity', 1) for p in parts) 108 | print(f" Status: COMPLETE") 109 | print(f" Unique parts: {len(parts)}") 110 | print(f" Total components: {total_qty}") 111 | 112 | # Count by prefix 113 | by_prefix = {} 114 | for p in parts: 115 | pfx = p.get('prefix', '?') 116 | by_prefix[pfx] = by_prefix.get(pfx, 0) + p.get('quantity', 1) 117 | print(f" By type:") 118 | for pfx in sorted(by_prefix.keys()): 119 | print(f" {pfx}: {by_prefix[pfx]}") 120 | 121 | # Check for TBD values 122 | tbd_count = 0 123 | for p in parts: 124 | if 'TBD' in str(p.get('part', '')).upper(): 125 | tbd_count += 1 126 | if 'TBD' in str(p.get('package', '')).upper(): 127 | tbd_count += 1 128 | if tbd_count > 0: 129 | print(f" WARNING: {tbd_count} unresolved TBD values") 130 | 131 | steps_complete += 1 132 | else: 133 | print(" Status: NOT STARTED") 134 | 135 | # === Step 5: Connections === 136 | print("\n[Step 5] Connections/Netlist") 137 | print("-" * 40) 138 | step5 = load_yaml(work_dir / "step5_connections.yaml") 139 | if step5: 140 | nets = step5.get('nets', {}) 141 | total_conns = sum(len(c) for c in nets.values() if isinstance(c, list)) 142 | nc_count = len(step5.get('no_connect', [])) 143 | tp_count = len(step5.get('test_points', [])) 144 | print(f" Status: COMPLETE") 145 | print(f" Total nets: {len(nets)}") 146 | print(f" Total connections: {total_conns}") 147 | print(f" No-connect pins: {nc_count}") 148 | print(f" Test points: {tp_count}") 149 | 150 | # Power nets 151 | power_nets = [n for n in nets.keys() if any(p in n.upper() for p in ['GND', 'VCC', 'VDD', 'BAT', '+3V', '+5V', '+12V'])] 152 | if power_nets: 153 | print(f" Power nets: {', '.join(power_nets)}") 154 | 155 | steps_complete += 1 156 | else: 157 | print(" Status: NOT STARTED") 158 | 159 | # === Step 6: Validation === 160 | print("\n[Step 6] Design Validation") 161 | print("-" * 40) 162 | step6 = load_yaml(work_dir / "step6_validation.yaml") 163 | if step6: 164 | summary = step6.get('summary', {}) 165 | status = summary.get('status', 'UNKNOWN') 166 | issues = summary.get('issues_found', 0) 167 | critical = summary.get('critical_issues', 0) 168 | print(f" Status: {status}") 169 | print(f" Issues found: {issues}") 170 | print(f" Critical issues: {critical}") 171 | 172 | if step6.get('issues'): 173 | print(" Issue summary:") 174 | for issue in step6.get('issues', []): 175 | severity = issue.get('severity', 'unknown') 176 | desc = issue.get('description', '') 177 | print(f" [{severity}] {desc}") 178 | 179 | steps_complete += 1 180 | else: 181 | print(" Status: NOT STARTED") 182 | 183 | # === Overall Summary === 184 | print("\n" + "=" * 70) 185 | print("OVERALL PROGRESS") 186 | print("=" * 70) 187 | print(f"Steps completed: {steps_complete}/{total_steps}") 188 | 189 | progress_bar = "[" + "#" * steps_complete + "." * (total_steps - steps_complete) + "]" 190 | print(f"Progress: {progress_bar} {steps_complete*100//total_steps}%") 191 | 192 | if steps_complete == total_steps: 193 | print("\nDesign pipeline COMPLETE. Ready for schematic generation.") 194 | else: 195 | next_step = steps_complete + 1 196 | print(f"\nNext step: Step {next_step}") 197 | step_names = { 198 | 1: "Extract primary parts from FSD", 199 | 2: "Research supporting parts", 200 | 3: "Make design decisions", 201 | 4: "Finalize parts list", 202 | 5: "Generate connections", 203 | 6: "Validate design" 204 | } 205 | print(f" {step_names.get(next_step, 'Unknown step')}") 206 | 207 | return steps_complete == total_steps 208 | 209 | 210 | def main(): 211 | success = summarize() 212 | sys.exit(0 if success else 1) 213 | 214 | 215 | if __name__ == "__main__": 216 | main() 217 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/prompts/step7.md: -------------------------------------------------------------------------------- 1 | # Step 7: Pin Model and KiCAD Schematic Generation 2 | 3 | Generate the pin model from final parts and connections, then produce a KiCAD 9 schematic. 4 | 5 | ## Prerequisites Check 6 | 7 | Before starting, verify: 8 | - [ ] `design/work/step4_final_parts.yaml` exists and is valid 9 | - [ ] `design/work/step5_connections.yaml` exists and is valid 10 | - [ ] `design/work/step6_validation.yaml` shows status: PASS 11 | - [ ] All LCSC part numbers are specified (no TBD values) 12 | 13 | If any prerequisite fails, go back and fix the previous step. 14 | 15 | ## Input Files 16 | 17 | - `design/work/step4_final_parts.yaml` - Final parts list with LCSC codes 18 | - `design/work/step5_connections.yaml` - All electrical connections 19 | - `design/work/step6_validation.yaml` - Validation report 20 | 21 | ## Output Files 22 | 23 | - `design/work/pin_model.json` - Parts with pin-to-net mappings 24 | - `design/output/Debug.kicad_sch` - Generated KiCAD 9 schematic 25 | - `design/output/Debug.kicad_pro` - KiCAD project file 26 | - `design/output/sym-lib-table` - Symbol library configuration 27 | - `design/output/fp-lib-table` - Footprint library configuration 28 | 29 | --- 30 | 31 | ## Phase 3: Pin Model Generation 32 | 33 | ### 3.1 Ensure Symbols Exist 34 | 35 | Download any missing symbols from JLCPCB/LCSC: 36 | 37 | ```bash 38 | cd design 39 | python ../../../KiCAD-Generator-tools/scripts/ensure_symbols.py \ 40 | --parts work/step4_final_parts.yaml \ 41 | --library ../../../KiCAD-Generator-tools/libs/JLCPCB/symbol/JLCPCB.kicad_sym 42 | ``` 43 | 44 | **Expected output:** 45 | - Reports which symbols already exist 46 | - Downloads missing symbols via JLC2KiCadLib 47 | - Adds new symbols to central JLCPCB.kicad_sym library 48 | 49 | **If symbol download fails:** 50 | 1. Check LCSC part number is correct 51 | 2. Part may not have EasyEDA symbol (add to `manual_symbol_pins.yaml`) 52 | 3. Add custom symbol to `custom_library_overrides.yaml` 53 | 54 | ### 3.2 Generate Pin Model 55 | 56 | Create pin_model.json from parts and connections: 57 | 58 | ```bash 59 | python ../../../KiCAD-Generator-tools/scripts/generate_pin_model.py \ 60 | --parts work/step4_final_parts.yaml \ 61 | --connections work/step5_connections.yaml \ 62 | --output work/pin_model.json 63 | ``` 64 | 65 | ### 3.3 Validate Pin Model 66 | 67 | ```bash 68 | python ../../../KiCAD-Generator-tools/scripts/validate_pin_model.py \ 69 | --input work/pin_model.json 70 | ``` 71 | 72 | **Validation checks:** 73 | - [ ] All parts have symbol references 74 | - [ ] All pins have coordinates (no `?,?,?`) 75 | - [ ] All nets reference valid pins 76 | - [ ] No duplicate pin assignments 77 | - [ ] Power pins connected to power nets 78 | - [ ] Ground pins connected to GND net 79 | 80 | **If validation fails:** 81 | - `?,?,?` coordinates → Symbol not found, run ensure_symbols.py 82 | - Missing pins → Check pin names match symbol definition 83 | - Duplicate pins → Fix connections.yaml 84 | 85 | --- 86 | 87 | ## Phase 4: KiCAD Schematic Generation 88 | 89 | ### 4.1 Generate Schematic 90 | 91 | ```bash 92 | python ../../../KiCAD-Generator-tools/scripts/kicad9_schematic.py \ 93 | --pin-model work/pin_model.json \ 94 | --output output/Debug.kicad_sch \ 95 | --debug 96 | ``` 97 | 98 | **Options:** 99 | - `--debug` - Generate debug.csv with pin positions 100 | - `--no-route` - Skip wire routing (net labels only) 101 | 102 | **Expected output:** 103 | ``` 104 | output/ 105 | ├── Debug.kicad_pro # KiCAD project file 106 | ├── Debug.kicad_sch # Generated schematic 107 | ├── sym-lib-table # Symbol library paths 108 | ├── fp-lib-table # Footprint library paths 109 | ├── debug.csv # Pin positions (debug mode) 110 | └── libs/ 111 | └── JLCPCB/ 112 | ├── symbol/ 113 | │ └── JLCPCB.kicad_sym 114 | └── footprint/ 115 | └── JLCPCB.pretty/ 116 | ``` 117 | 118 | ### 4.2 Run ERC (Electrical Rules Check) 119 | 120 | **Linux:** 121 | ```bash 122 | cd output 123 | kicad-cli sch erc --output erc_report.txt Debug.kicad_sch 124 | cat erc_report.txt 125 | ``` 126 | 127 | **Windows:** 128 | ```cmd 129 | set PATH=%PATH%;C:\Program Files\KiCad\9.0\bin 130 | kicad-cli sch erc --output output\erc_report.txt output\Debug.kicad_sch 131 | type output\erc_report.txt 132 | ``` 133 | 134 | **Expected results:** 135 | - **0 Errors** - Design is electrically correct 136 | - **Warnings** - Library path warnings (OK if symbols load) 137 | 138 | ### 4.3 Common ERC Errors and Fixes 139 | 140 | | Error | Cause | Fix | 141 | |-------|-------|-----| 142 | | `pin_not_connected` | Unconnected pin | Add to `no_connect` in step5 or add wire | 143 | | `pin_to_pin` | Multiple power outputs | Remove duplicate PWR_FLAG | 144 | | `power_pin_not_driven` | No power source | Add PWR_FLAG to power net | 145 | | `different_unit_net` | Net name mismatch | Check net naming consistency | 146 | 147 | --- 148 | 149 | ## Phase 5: Post-Generation Verification 150 | 151 | ### 5.1 Open in KiCAD 152 | 153 | **Important:** Open the PROJECT file, not the schematic: 154 | ``` 155 | output/Debug.kicad_pro 156 | ``` 157 | 158 | This ensures library tables load correctly. 159 | 160 | ### 5.2 Visual Inspection Checklist 161 | 162 | - [ ] All symbols visible (no missing library warnings) 163 | - [ ] Net labels on all pins 164 | - [ ] Power symbols present (+3V3, GND, VBAT, etc.) 165 | - [ ] Components grouped logically 166 | - [ ] No overlapping components 167 | - [ ] Decoupling capacitors near IC power pins 168 | 169 | ### 5.3 Update from Symbol Libraries (Optional) 170 | 171 | In KiCAD: 172 | 1. **Tools > Update Schematic from Symbol Libraries** 173 | 2. This populates the `lib_symbols` section for portability 174 | 175 | ### 5.4 Final ERC in KiCAD GUI 176 | 177 | 1. **Inspect > Electrical Rules Checker** 178 | 2. Run ERC 179 | 3. Review and resolve any remaining issues 180 | 181 | --- 182 | 183 | ## Output Format 184 | 185 | Write to `design/work/step7_generation.yaml`: 186 | 187 | ```yaml 188 | # step7_generation.yaml 189 | # KiCAD Schematic Generation Report 190 | # Date: [YYYY-MM-DD] 191 | 192 | pin_model: 193 | status: GENERATED # GENERATED | FAILED 194 | file: "work/pin_model.json" 195 | total_parts: 38 196 | total_pins: 256 197 | symbols_downloaded: 3 198 | symbols_existing: 35 199 | 200 | schematic: 201 | status: GENERATED # GENERATED | FAILED 202 | file: "output/Debug.kicad_sch" 203 | project: "output/Debug.kicad_pro" 204 | format: "KiCAD 9" 205 | 206 | erc: 207 | status: PASS # PASS | PASS_WITH_WARNINGS | FAIL 208 | errors: 0 209 | warnings: 2 210 | report: "output/erc_report.txt" 211 | 212 | warnings: 213 | - type: "library_path" 214 | message: "Library path is relative" 215 | severity: info 216 | 217 | errors: [] 218 | 219 | files_generated: 220 | - "work/pin_model.json" 221 | - "output/Debug.kicad_pro" 222 | - "output/Debug.kicad_sch" 223 | - "output/sym-lib-table" 224 | - "output/fp-lib-table" 225 | - "output/debug.csv" 226 | ``` 227 | 228 | --- 229 | 230 | ## Troubleshooting 231 | 232 | ### Symbol Not Found (?,?,? coordinates) 233 | 234 | 1. Check LCSC code in step4_final_parts.yaml 235 | 2. Run `ensure_symbols.py` again 236 | 3. If part has no EasyEDA symbol: 237 | - Add to `manual_symbol_pins.yaml` with pin definitions 238 | - Or add custom symbol to `custom_library_overrides.yaml` 239 | 240 | ### Pins Not Connecting in KiCAD 241 | 242 | KiCAD requires exact coordinate match between wire endpoint and pin position: 243 | - Wire endpoint must exactly match pin position 244 | - Small circle at pin = unconnected 245 | - Check debug.csv for actual pin positions 246 | 247 | ### Library Not Loading 248 | 249 | 1. Open `Debug.kicad_pro`, NOT `Debug.kicad_sch` 250 | 2. Check `sym-lib-table` exists in output folder 251 | 3. Verify library path in sym-lib-table is correct 252 | 253 | --- 254 | 255 | ## Exit Validation 256 | 257 | Before marking Step 7 complete: 258 | 259 | - [ ] `pin_model.json` generated successfully 260 | - [ ] `Debug.kicad_sch` generated successfully 261 | - [ ] ERC reports 0 errors 262 | - [ ] Schematic opens correctly in KiCAD 263 | - [ ] All symbols load without errors 264 | - [ ] `step7_generation.yaml` written 265 | 266 | If any check fails, fix the issue and regenerate. 267 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/prompts/step0.md: -------------------------------------------------------------------------------- 1 | # Step 0: FSD Review & Clarification Gate 2 | 3 | Perform a strict technical review of the FSD to ensure it is unambiguous, electrically feasible, safe, and compatible with the KiCad schematic generator. 4 | 5 | --- 6 | 7 | ## Purpose 8 | 9 | Before parts extraction and schematic generation, validate the FSD against all technical requirements and generate questions where information is ambiguous, missing, or contradictory. 10 | present this to the user and discoss until all isues are solved by the enhanced Functional Specification Document. 11 | 12 | **The output must be:** 13 | - A list of questions and required clarifications 14 | - OR a statement that the FSD is ready for Step 1 15 | 16 | **No further processing is allowed until all critical issues are resolved.** 17 | 18 | --- 19 | 20 | ## Required Inputs 21 | 22 | - `design/input/FSD_*.md` - Functional Specification Document 23 | - `KiCAD_Generator.md` - Defines all schematic-generation constraints 24 | 25 | --- 26 | 27 | ## Section A — Component & Electrical Requirements 28 | 29 | ### A1. Component Completeness 30 | 31 | **Ask questions when:** 32 | - Important ICs do not have a specific part number 33 | - Supply voltages for components are missing 34 | - Required peripherals (I²C, SPI, ADC, GPIO) are unspecified or incomplete 35 | 36 | ### A2. Voltage Levels 37 | 38 | **Ask:** 39 | - What are the exact operating voltages for each subsystem? 40 | - Is every digital interface voltage-compatible with the MCU? 41 | 42 | ### A3. Current Requirements 43 | 44 | **Ask:** 45 | - What is the estimated current consumption of each subsystem? 46 | - Is the chosen regulator able to supply the total load? 47 | 48 | **If missing → generate questions.** 49 | 50 | --- 51 | 52 | ## Section B — Pin Assignments & Restrictions 53 | 54 | ### B1. Avoid MCU Strapping Pins 55 | 56 | **The review must detect use of restricted pins such as:** 57 | 58 | | MCU Family | Restricted Pins | Reason | 59 | |------------|-----------------|--------| 60 | | ESP32 | GPIO0, GPIO2, GPIO12, GPIO15 | Boot strapping | 61 | | ESP32-S3 | GPIO0, GPIO3, GPIO45, GPIO46 | Boot strapping | 62 | | ESP32-C3 | GPIO2, GPIO8, GPIO9 | Boot strapping | 63 | 64 | **Ask:** 65 | > The FSD assigns function X to a restricted/strapping pin. Do you want to reassign it? 66 | 67 | **If the pin is avoidable → require reassignment.** 68 | 69 | ### B2. Clarify Reserved Pins 70 | 71 | **Ask:** 72 | - Are any GPIOs reserved for debugging, boot mode, or programming? 73 | - Is JTAG/SWD debugging required? (reserves specific pins) 74 | - Are USB D+/D- pins needed for USB functionality? 75 | 76 | --- 77 | 78 | ## Section C — Communication Buses 79 | 80 | ### C1. Verify I²C Address Space 81 | 82 | For every I²C device, verify its I²C address: 83 | 84 | **Ask:** 85 | > Device X has I²C address 0xNN. Device Y also uses 0xNN. Should we change one device or add an I²C multiplexer? 86 | 87 | **Also ask:** 88 | - Are all I²C devices intended to share the same bus? 89 | - Are there any devices with configurable addresses that should be changed? 90 | 91 | ### C2. Level Shifting 92 | 93 | **Ask:** 94 | > Device X operates at Vx but connects to MCU at Vy. Should we insert a level shifter? 95 | 96 | **Or:** 97 | > Can we safely avoid a level shifter by selecting a 3.3V-tolerant variant? 98 | 99 | **If user wants to avoid level shifters → enforce 3.3V-compatible devices only.** 100 | 101 | ### C3. Bus Pull-ups 102 | 103 | **Ask:** 104 | - What pull-up resistor value should be used for I²C? (typical: 2.2kΩ - 10kΩ) 105 | - Should pull-ups be on the MCU side or peripheral side? 106 | 107 | --- 108 | 109 | ## Section D — Protection, Bypass & Required Supporting Components 110 | 111 | The FSD must include or allow the generation of: 112 | 113 | | Component | Purpose | Required When | 114 | |-----------|---------|---------------| 115 | | Bypass capacitors | IC power filtering | Every IC | 116 | | Bulk capacitor | Rail stability | Main power rails | 117 | | ESD protection | Transient suppression | External connectors | 118 | | USB TVS diodes | USB protection | USB present | 119 | | Ferrite beads | EMI filtering | Per datasheet | 120 | | Matching networks | RF impedance | RF components | 121 | 122 | **Ask:** 123 | > Do you approve the automatic insertion of datasheet-recommended bypass capacitors? 124 | 125 | > Allow the generator to add mandatory protection elements such as TVS diodes, ferrite beads, and input filters? 126 | 127 | **If not approved → require explicit definition in FSD.** 128 | 129 | ### D1. Reference Design Components 130 | 131 | **Ask:** 132 | - Should we include all components from the manufacturer's reference design? 133 | - Are there any reference design components that should be omitted? 134 | 135 | --- 136 | 137 | ## Section E — Package Sizes 138 | 139 | **Ask:** 140 | > What is the preferred passive package size? (0402 / 0603 / 0805) 141 | 142 | > What is the maximum acceptable package size for ICs? 143 | 144 | > Are through-hole components acceptable, or SMD only? 145 | 146 | **If not defined → request clarification.** 147 | 148 | | Package | Use Case | 149 | |---------|----------| 150 | | 0402 | Space-constrained, reflow only | 151 | | 0603 | Standard, hand-solderable | 152 | | 0805 | Easy assembly, higher power | 153 | 154 | --- 155 | 156 | ## Section F — Conflict & Feasibility Checks 157 | 158 | **Generate questions if:** 159 | 160 | ### F1. Power Budget 161 | - Total current draw exceeds regulator capacity 162 | - Total current draw exceeds USB power (500mA / 1.5A) 163 | - LDO power dissipation exceeds thermal limits: `P = (Vin - Vout) × Iload` 164 | 165 | ### F2. Signal Integrity 166 | - High-speed signals routed without consideration for impedance 167 | - Analog signals sharing ground with noisy digital circuits 168 | 169 | ### F3. RF Requirements 170 | - RF components lack required matching networks 171 | - Antenna specifications incomplete 172 | 173 | ### F4. Internal Contradictions 174 | - FSD contradicts itself across sections 175 | - Pin assignments conflict with stated requirements 176 | - Voltage levels inconsistent between sections 177 | 178 | --- 179 | 180 | ## Section G — Output Format 181 | 182 | ### If issues exist: 183 | 184 | ```markdown 185 | ## Step 0 Review Results — Issues Found 186 | 187 | ### Critical (must be resolved before Step 1) 188 | 1. **** 189 | - Section: 190 | - Problem: 191 | - Question: 192 | 193 | ### Required Clarifications 194 | 1. **** 195 | - Question: 196 | - Options: 197 | 198 | ### Optional Improvements 199 | 1. **** 200 | - Recommendation: 201 | - Benefit: 202 | ``` 203 | 204 | ### If ready: 205 | 206 | ```markdown 207 | ## Step 0 Review Results — READY 208 | 209 | The FSD is complete, consistent, and satisfies all generator requirements. 210 | 211 | **Verified:** 212 | - [ ] All components have part numbers or clear specifications 213 | - [ ] Voltage levels are compatible 214 | - [ ] Current budget is within regulator capacity 215 | - [ ] No I²C address conflicts 216 | - [ ] No restricted pins used (or explicitly approved) 217 | - [ ] Protection measures defined or auto-insertion approved 218 | - [ ] Package sizes specified 219 | 220 | **Proceed to Step 1.** 221 | ``` 222 | 223 | --- 224 | 225 | ## Exit Criteria (Blocker Logic) 226 | 227 | **Only proceed to Step 1 when ALL of the following are satisfied:** 228 | 229 | | Check | Requirement | 230 | |-------|-------------| 231 | | Critical issues | None remaining | 232 | | I²C addresses | Verified collision-free | 233 | | Restricted pins | Not used (unless unavoidable and explicitly approved) | 234 | | Voltage levels | Compatible, or level shifters accepted | 235 | | Protection | Bypass capacitors, ESD, filters permitted or specified | 236 | | Package sizes | Preferences defined | 237 | | Electrical specs | All missing information clarified | 238 | | Power budget | Verified feasible | 239 | 240 | --- 241 | 242 | ## If Issues Cannot Be Resolved 243 | 244 | **DO NOT proceed to Step 1!** 245 | 246 | 1. Document all unresolved issues 247 | 2. Present questions to the user 248 | 3. Wait for clarification 249 | 4. Update FSD with answers 250 | 5. Re-run Step 0 review 251 | 252 | ``` 253 | ⚠️ BLOCKER: Step 0 → Review → Get clarifications → Update FSD → Re-validate → Step 1 254 | 255 | This gate ensures no ambiguity propagates into the schematic generation pipeline. 256 | ``` 257 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/verify_netlist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Verify schematic connections using KiCad's netlist export. 4 | 5 | This script: 6 | 1. Uses kicad-cli to export a netlist from the schematic 7 | 2. Parses the netlist to extract actual connections 8 | 3. Compares against expected connections from pin_model.json 9 | 4. Reports any missing or extra connections 10 | 11 | Usage: 12 | python verify_netlist.py [schematic.kicad_sch] 13 | 14 | Requires KiCad 7+ installed with kicad-cli in PATH. 15 | """ 16 | 17 | import json 18 | import subprocess 19 | import sys 20 | import re 21 | from pathlib import Path 22 | from collections import defaultdict 23 | 24 | 25 | def export_netlist(schematic_path: Path, output_path: Path) -> bool: 26 | """Export netlist from schematic using kicad-cli.""" 27 | cmd = [ 28 | "kicad-cli", "sch", "export", "netlist", 29 | "--output", str(output_path), 30 | str(schematic_path) 31 | ] 32 | 33 | print(f"Running: {' '.join(cmd)}") 34 | try: 35 | result = subprocess.run(cmd, capture_output=True, text=True) 36 | if result.returncode != 0: 37 | print(f"Error: {result.stderr}") 38 | return False 39 | return True 40 | except FileNotFoundError: 41 | print("Error: kicad-cli not found. Make sure KiCad 7+ is installed and in PATH.") 42 | return False 43 | 44 | 45 | def parse_kicad_netlist(netlist_path: Path) -> dict: 46 | """ 47 | Parse KiCad netlist file (.net format). 48 | 49 | Returns dict: net_name -> [(ref, pin_number), ...] 50 | """ 51 | content = netlist_path.read_text() 52 | 53 | nets = {} 54 | 55 | # Find all net definitions: (net (code X) (name "NET_NAME") ... (node (ref X) (pin Y)) ...) 56 | net_pattern = re.compile( 57 | r'\(net\s+\(code\s+\d+\)\s+\(name\s+"([^"]+)"\)(.*?)\)\s*(?=\(net|\(libparts|\Z)', 58 | re.DOTALL 59 | ) 60 | 61 | node_pattern = re.compile(r'\(node\s+\(ref\s+"?([^")\s]+)"?\)\s+\(pin\s+"?([^")\s]+)"?\)') 62 | 63 | for net_match in net_pattern.finditer(content): 64 | net_name = net_match.group(1) 65 | net_content = net_match.group(2) 66 | 67 | nodes = [] 68 | for node_match in node_pattern.finditer(net_content): 69 | ref = node_match.group(1) 70 | pin = node_match.group(2) 71 | nodes.append((ref, pin)) 72 | 73 | if nodes: 74 | nets[net_name] = nodes 75 | 76 | return nets 77 | 78 | 79 | def load_expected_connections(pin_model_path: Path) -> dict: 80 | """ 81 | Load expected connections from pin_model.json. 82 | 83 | Returns dict: net_name -> [(ref, pin_name), ...] 84 | """ 85 | with open(pin_model_path) as f: 86 | model = json.load(f) 87 | 88 | nets = defaultdict(list) 89 | 90 | for part in model.get('parts', []): 91 | ref = part['ref'] 92 | for pin_name, net_name in part.get('pins', {}).items(): 93 | if net_name: 94 | nets[net_name].append((ref, pin_name)) 95 | 96 | return dict(nets) 97 | 98 | 99 | def compare_netlists(expected: dict, actual: dict, symbol_lib_path: Path = None) -> dict: 100 | """ 101 | Compare expected vs actual netlists. 102 | 103 | Note: expected uses pin NAMES, actual uses pin NUMBERS. 104 | We need the symbol library to map between them. 105 | 106 | Returns dict with 'missing', 'extra', 'matched' lists. 107 | """ 108 | results = { 109 | 'missing': [], # In expected but not in actual 110 | 'extra': [], # In actual but not in expected 111 | 'matched': [], # Correctly matched 112 | 'net_mismatches': [] # Pin exists but on wrong net 113 | } 114 | 115 | # For now, do a simple comparison by net name 116 | # This won't catch pin name vs number mismatches 117 | 118 | all_nets = set(expected.keys()) | set(actual.keys()) 119 | 120 | for net in sorted(all_nets): 121 | exp_pins = set(expected.get(net, [])) 122 | act_pins = set(actual.get(net, [])) 123 | 124 | # Find matches (note: expected has pin names, actual has pin numbers) 125 | # For a proper comparison, we'd need to map names to numbers 126 | # For now, report the raw comparison 127 | 128 | if net in expected and net not in actual: 129 | results['missing'].append({ 130 | 'net': net, 131 | 'expected_pins': list(exp_pins), 132 | 'issue': 'Net not found in schematic' 133 | }) 134 | elif net not in expected and net in actual: 135 | results['extra'].append({ 136 | 'net': net, 137 | 'actual_pins': list(act_pins), 138 | 'issue': 'Unexpected net in schematic' 139 | }) 140 | else: 141 | # Both exist - compare pin counts at least 142 | if len(exp_pins) != len(act_pins): 143 | results['net_mismatches'].append({ 144 | 'net': net, 145 | 'expected_count': len(exp_pins), 146 | 'actual_count': len(act_pins), 147 | 'expected_pins': list(exp_pins), 148 | 'actual_pins': list(act_pins) 149 | }) 150 | else: 151 | results['matched'].append(net) 152 | 153 | return results 154 | 155 | 156 | def main(): 157 | # Paths 158 | base_dir = Path(__file__).parent.parent 159 | 160 | if len(sys.argv) > 1: 161 | schematic_path = Path(sys.argv[1]) 162 | else: 163 | schematic_path = base_dir / "output" / "Debug.kicad_sch" 164 | 165 | if not schematic_path.exists(): 166 | print(f"Error: Schematic not found: {schematic_path}") 167 | sys.exit(1) 168 | 169 | pin_model_path = base_dir / "work" / "pin_model.json" 170 | netlist_path = base_dir / "output" / "netlist.net" 171 | 172 | print(f"Schematic: {schematic_path}") 173 | print(f"Pin model: {pin_model_path}") 174 | print() 175 | 176 | # Export netlist 177 | print("=== Exporting netlist from KiCad ===") 178 | if not export_netlist(schematic_path, netlist_path): 179 | print("\nFailed to export netlist.") 180 | print("Make sure:") 181 | print(" 1. KiCad 7+ is installed") 182 | print(" 2. kicad-cli is in your PATH") 183 | print(" 3. The schematic has been opened in KiCad and 'Update from Symbol Library' was run") 184 | sys.exit(1) 185 | 186 | print(f"Netlist exported to: {netlist_path}") 187 | print() 188 | 189 | # Parse netlist 190 | print("=== Parsing netlist ===") 191 | actual_nets = parse_kicad_netlist(netlist_path) 192 | print(f"Found {len(actual_nets)} nets in schematic") 193 | print() 194 | 195 | # Load expected 196 | print("=== Loading expected connections ===") 197 | expected_nets = load_expected_connections(pin_model_path) 198 | print(f"Expected {len(expected_nets)} nets from pin_model.json") 199 | print() 200 | 201 | # Compare 202 | print("=== Comparing connections ===") 203 | results = compare_netlists(expected_nets, actual_nets) 204 | 205 | print(f"\nMatched nets: {len(results['matched'])}") 206 | 207 | if results['missing']: 208 | print(f"\n*** MISSING NETS ({len(results['missing'])}) ***") 209 | for item in results['missing']: 210 | print(f" {item['net']}: {item['issue']}") 211 | for pin in item['expected_pins']: 212 | print(f" Expected: {pin}") 213 | 214 | if results['extra']: 215 | print(f"\n*** EXTRA NETS ({len(results['extra'])}) ***") 216 | for item in results['extra']: 217 | print(f" {item['net']}: {item['issue']}") 218 | 219 | if results['net_mismatches']: 220 | print(f"\n*** PIN COUNT MISMATCHES ({len(results['net_mismatches'])}) ***") 221 | for item in results['net_mismatches']: 222 | print(f" {item['net']}: expected {item['expected_count']} pins, got {item['actual_count']}") 223 | print(f" Expected: {item['expected_pins']}") 224 | print(f" Actual: {item['actual_pins']}") 225 | 226 | # Summary 227 | print("\n=== Summary ===") 228 | total_issues = len(results['missing']) + len(results['extra']) + len(results['net_mismatches']) 229 | if total_issues == 0: 230 | print("All connections verified successfully!") 231 | else: 232 | print(f"Found {total_issues} issue(s)") 233 | 234 | # Also print actual netlist for inspection 235 | print("\n=== Actual Netlist Contents ===") 236 | for net, pins in sorted(actual_nets.items()): 237 | print(f" {net}: {pins}") 238 | 239 | 240 | if __name__ == "__main__": 241 | main() 242 | -------------------------------------------------------------------------------- /jlcpcb_parts_pipeline/enrich_parts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Enrich a parts requirement list with JLCPCB/LCSC details and write a machine-readable file. 4 | 5 | Input: 6 | - parts_requirements.yaml 7 | 8 | Output: 9 | - jlc_parts_enriched.json 10 | 11 | Data sources (in order): 12 | 1) Official JLCPCB Components API (requires approval + API key) 13 | 2) No-auth fallback: jlcsearch.tscircuit.com API (public mirror of in-stock JLCPCB parts) 14 | 15 | Notes: 16 | - For production use, prefer the official JLCPCB Components API once approved. 17 | - The fallback mirror is suitable for prototyping and automation pipelines. 18 | 19 | """ 20 | from __future__ import annotations 21 | import os 22 | import json 23 | import time 24 | import urllib.parse 25 | import requests 26 | import yaml 27 | from typing import Any, Dict, List, Optional 28 | 29 | DEFAULT_TIMEOUT = 30 30 | 31 | def load_yaml(path: str) -> dict: 32 | with open(path, "r", encoding="utf-8") as f: 33 | return yaml.safe_load(f) 34 | 35 | def save_json(path: str, obj: Any) -> None: 36 | with open(path, "w", encoding="utf-8") as f: 37 | json.dump(obj, f, indent=2, ensure_ascii=False) 38 | 39 | # ------------------------- 40 | # Source A: Official JLCPCB Components API (requires key) 41 | # ------------------------- 42 | def jlc_components_api_search(query: str) -> Optional[dict]: 43 | api_key = os.getenv("JLCPCB_API_KEY") 44 | base = os.getenv("JLCPCB_API_BASE", "https://api.jlcpcb.com") 45 | if not api_key: 46 | return None 47 | 48 | # This is intentionally a stub: endpoint paths depend on your JLCAPI account docs. 49 | # Implement once you have access to the developer portal. 50 | raise NotImplementedError("Fill in official Components API endpoints from your JLCAPI docs.") 51 | 52 | # ------------------------- 53 | # Source B: No-auth fallback mirror (jlcsearch) 54 | # ------------------------- 55 | def jlcsearch_search(query: str, limit: int = 10) -> dict: 56 | """ 57 | Query jlcsearch.tscircuit.com API. 58 | 59 | API returns: {"components": [...]} 60 | Each component has: lcsc (int), mfr (str), package (str), 61 | is_basic (bool), is_preferred (bool), 62 | description (str), stock (int), price (float) 63 | """ 64 | url = "https://jlcsearch.tscircuit.com/api/search?" + urllib.parse.urlencode({ 65 | "q": query, 66 | "limit": str(limit) 67 | }) 68 | r = requests.get(url, timeout=DEFAULT_TIMEOUT) 69 | r.raise_for_status() 70 | return r.json() 71 | 72 | def pick_best_candidate(components: List[dict], prefer_basic: bool = True) -> Optional[dict]: 73 | """ 74 | Pick the best component from results. 75 | Preference order: basic > preferred > in-stock > highest stock 76 | """ 77 | if not components: 78 | return None 79 | 80 | def score(c): 81 | # Higher score = better choice 82 | s = 0 83 | if c.get("is_basic"): 84 | s += 10000 85 | if c.get("is_preferred"): 86 | s += 5000 87 | stock = c.get("stock", 0) or 0 88 | if stock > 0: 89 | s += 1000 90 | s += min(stock, 999) # Cap stock contribution 91 | return s 92 | 93 | sorted_components = sorted(components, key=score, reverse=True) 94 | return sorted_components[0] 95 | 96 | def format_lcsc(lcsc_num) -> str: 97 | """Format LCSC number with C prefix""" 98 | if lcsc_num is None: 99 | return None 100 | if isinstance(lcsc_num, str) and lcsc_num.startswith("C"): 101 | return lcsc_num 102 | return f"C{lcsc_num}" 103 | 104 | def get_part_type(component: dict) -> str: 105 | """Determine part type from component flags""" 106 | if component.get("is_basic"): 107 | return "basic" 108 | if component.get("is_preferred"): 109 | return "preferred" 110 | return "extended" 111 | 112 | def enrich_parts(req_path: str, out_path: str) -> dict: 113 | req = load_yaml(req_path) 114 | out = {"meta": req.get("meta", {}), "parts": []} 115 | 116 | total = len(req.get("parts", [])) 117 | 118 | for idx, p in enumerate(req.get("parts", []), 1): 119 | designator = p.get("designator") or p.get("key") 120 | query = p.get("mpn") 121 | known_lcsc = p.get("lcsc") 122 | 123 | print(f"[{idx}/{total}] Searching: {designator} - {query}") 124 | 125 | record = { 126 | "designator": designator, 127 | "query": query, 128 | "function": p.get("function"), 129 | "package": p.get("package"), 130 | "value": p.get("value"), 131 | "known_lcsc": known_lcsc, 132 | # KiCAD fields 133 | "symbol": p.get("symbol"), 134 | "footprint": p.get("footprint"), 135 | "datasheet": p.get("datasheet"), 136 | "candidates": [], 137 | "selection": None, 138 | "error": None 139 | } 140 | 141 | # Try official API (if configured) 142 | try: 143 | api_res = jlc_components_api_search(query) 144 | if api_res: 145 | record["candidates"] = api_res.get("components", []) 146 | except NotImplementedError: 147 | pass 148 | except Exception as e: 149 | record["error"] = f"Official API error: {str(e)}" 150 | 151 | # Fall back to jlcsearch 152 | if not record["candidates"]: 153 | try: 154 | res = jlcsearch_search(query, limit=10) 155 | components = res.get("components", []) 156 | record["candidates"] = components 157 | 158 | best = pick_best_candidate(components) 159 | if best: 160 | record["selection"] = { 161 | "lcsc": format_lcsc(best.get("lcsc")), 162 | "mpn": best.get("mfr"), 163 | "package": best.get("package"), 164 | "description": best.get("description"), 165 | "stock": best.get("stock"), 166 | "price": best.get("price"), 167 | "part_type": get_part_type(best), 168 | "is_basic": best.get("is_basic", False), 169 | "is_preferred": best.get("is_preferred", False), 170 | } 171 | print(f" -> Found: {record['selection']['lcsc']} ({record['selection']['part_type']}) stock={record['selection']['stock']}") 172 | else: 173 | # If no results from search, use known LCSC from YAML 174 | if known_lcsc: 175 | record["selection"] = { 176 | "lcsc": known_lcsc if known_lcsc.startswith("C") else f"C{known_lcsc}", 177 | "mpn": query, 178 | "package": p.get("package"), 179 | "description": p.get("function"), 180 | "stock": None, 181 | "price": None, 182 | "part_type": p.get("part_type", "unknown"), 183 | "is_basic": p.get("part_type") == "basic", 184 | "is_preferred": False, 185 | "note": "Using known LCSC from requirements (no API match)" 186 | } 187 | print(f" -> Using known: {known_lcsc}") 188 | else: 189 | print(f" -> NO MATCH FOUND") 190 | 191 | except requests.exceptions.HTTPError as e: 192 | error_msg = f"HTTP {e.response.status_code}: {str(e)}" 193 | record["error"] = error_msg 194 | print(f" -> Error: {error_msg}") 195 | # Fall back to known LCSC 196 | if known_lcsc: 197 | record["selection"] = { 198 | "lcsc": known_lcsc if known_lcsc.startswith("C") else f"C{known_lcsc}", 199 | "mpn": query, 200 | "package": p.get("package"), 201 | "description": p.get("function"), 202 | "stock": None, 203 | "price": None, 204 | "part_type": p.get("part_type", "unknown"), 205 | "is_basic": p.get("part_type") == "basic", 206 | "is_preferred": False, 207 | "note": "Using known LCSC from requirements (API error)" 208 | } 209 | print(f" -> Fallback to known: {known_lcsc}") 210 | except Exception as e: 211 | record["error"] = str(e) 212 | print(f" -> Error: {str(e)}") 213 | 214 | out["parts"].append(record) 215 | time.sleep(0.3) # be polite to the API 216 | 217 | # Summary stats 218 | found = sum(1 for p in out["parts"] if p.get("selection")) 219 | basic = sum(1 for p in out["parts"] if p.get("selection") and p["selection"].get("is_basic")) 220 | preferred = sum(1 for p in out["parts"] if p.get("selection") and p["selection"].get("is_preferred")) 221 | 222 | out["summary"] = { 223 | "total_parts": total, 224 | "found": found, 225 | "not_found": total - found, 226 | "basic_parts": basic, 227 | "preferred_parts": preferred, 228 | "extended_parts": found - basic - preferred 229 | } 230 | 231 | print(f"\nSummary: {found}/{total} found, {basic} basic, {preferred} preferred") 232 | 233 | return out 234 | 235 | def main() -> int: 236 | import argparse 237 | ap = argparse.ArgumentParser() 238 | ap.add_argument("--in", dest="inp", default="parts_requirements.yaml") 239 | ap.add_argument("--out", dest="out", default="jlc_parts_enriched.json") 240 | args = ap.parse_args() 241 | 242 | enriched = enrich_parts(args.inp, args.out) 243 | save_json(args.out, enriched) 244 | print(f"\nWrote {args.out}") 245 | return 0 246 | 247 | if __name__ == "__main__": 248 | raise SystemExit(main()) 249 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/generate_kicad_project.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Generate KiCAD Project from pin_model.json 4 | 5 | Creates a complete KiCAD project with: 6 | - .kicad_pro (project file) 7 | - .kicad_sch (schematic with parts and net labels) 8 | - .kicad_pcb (empty PCB) 9 | - sym-lib-table / fp-lib-table (library references) 10 | 11 | Parts are placed in a grid layout with net labels on each pin. 12 | Symbol references use JLCPCB:LCSC_CODE format for later library linking. 13 | """ 14 | 15 | import json 16 | import uuid 17 | from pathlib import Path 18 | from datetime import datetime 19 | 20 | # LCSC part number to symbol name mapping 21 | # Maps LCSC codes to our custom symbol library names 22 | LCSC_TO_SYMBOL = { 23 | # ICs 24 | "C2913206": "ESP32-S3-MINI-1-N8", 25 | "C195417": "SI4735-D60-GU", 26 | "C7971": "TDA1306T", 27 | "C16581": "TP4056", 28 | "C6186": "AMS1117-3.3", 29 | "C7519": "USBLC6-2SC6", 30 | # Connectors 31 | "C393939": "TYPE-C-31-M-12", 32 | "C131337": "S2B-PH-K-S", 33 | "C145819": "PJ-327A", 34 | "C124378": "Header-1x04", 35 | "C238128": "TestPoint", 36 | # UI components 37 | "C470747": "EC11E18244A5", 38 | "C127509": "TS-1102S", 39 | "C2761795": "WS2812B-B", 40 | # Passive components 41 | "C32346": "Crystal-32.768kHz", 42 | # Resistors - all map to generic "R" symbol 43 | "C23186": "R", # 5.1k 44 | "C22975": "R", # 2k 45 | "C25804": "R", # 10k 46 | "C25900": "R", # 4.7k 47 | "C22775": "R", # 100R 48 | # Capacitors - all map to generic "C" symbol 49 | "C45783": "C", # 22uF 50 | "C134760": "C", # 220uF 51 | "C15850": "C", # 10uF 52 | "C15849": "C", # 1uF 53 | "C14663": "C", # 100nF 54 | "C1653": "C", # 22pF 55 | } 56 | 57 | 58 | def generate_uuid(): 59 | return str(uuid.uuid4()) 60 | 61 | 62 | def generate_project_file(project_name: str) -> str: 63 | """Generate KiCAD project file content.""" 64 | return f'''{{ 65 | "board": {{ 66 | "3dviewports": [], 67 | "design_settings": {{}}, 68 | "ipc2581": {{}}, 69 | "layer_presets": [], 70 | "viewports": [] 71 | }}, 72 | "meta": {{ 73 | "filename": "{project_name}.kicad_pro", 74 | "version": 1 75 | }}, 76 | "sheets": [ 77 | ["", ""] 78 | ] 79 | }}''' 80 | 81 | 82 | def generate_pcb_file(project_name: str) -> str: 83 | """Generate empty KiCAD PCB file.""" 84 | return f'''(kicad_pcb 85 | (version 20240108) 86 | (generator "python_generator") 87 | (generator_version "9.0") 88 | (general (thickness 1.6) (legacy_teardrops no)) 89 | (paper "A4") 90 | (layers 91 | (0 "F.Cu" signal) 92 | (31 "B.Cu" signal) 93 | (32 "B.Adhes" user "B.Adhesive") 94 | (33 "F.Adhes" user "F.Adhesive") 95 | (34 "B.Paste" user) 96 | (35 "F.Paste" user) 97 | (36 "B.SilkS" user "B.Silkscreen") 98 | (37 "F.SilkS" user "F.Silkscreen") 99 | (38 "B.Mask" user) 100 | (39 "F.Mask" user) 101 | (40 "Dwgs.User" user "User.Drawings") 102 | (41 "Cmts.User" user "User.Comments") 103 | (42 "Eco1.User" user "User.Eco1") 104 | (43 "Eco2.User" user "User.Eco2") 105 | (44 "Edge.Cuts" user) 106 | (45 "Margin" user) 107 | (46 "B.CrtYd" user "B.Courtyard") 108 | (47 "F.CrtYd" user "F.Courtyard") 109 | (48 "B.Fab" user) 110 | (49 "F.Fab" user) 111 | ) 112 | (setup (pad_to_mask_clearance 0)) 113 | (net 0 "") 114 | )''' 115 | 116 | 117 | def generate_sym_lib_table() -> str: 118 | """Generate symbol library table with portable paths.""" 119 | return '''(sym_lib_table 120 | (version 7) 121 | (lib (name "JLCPCB")(type "KiCad")(uri "${KIPRJMOD}/libs/JLCPCB/symbol/JLCPCB.kicad_sym")(options "")(descr "JLCPCB parts")) 122 | )''' 123 | 124 | 125 | def generate_fp_lib_table() -> str: 126 | """Generate footprint library table with portable paths.""" 127 | return '''(fp_lib_table 128 | (version 7) 129 | (lib (name "JLCPCB")(type "KiCad")(uri "${KIPRJMOD}/libs/JLCPCB/JLCPCB")(options "")(descr "JLCPCB footprints")) 130 | )''' 131 | 132 | 133 | def get_symbol_category(ref: str) -> str: 134 | """Determine symbol category from reference designator.""" 135 | prefix = ''.join(c for c in ref if c.isalpha()) 136 | categories = { 137 | 'U': 'ic', 138 | 'R': 'resistor', 139 | 'C': 'capacitor', 140 | 'D': 'led', 141 | 'J': 'connector', 142 | 'SW': 'switch', 143 | 'ENC': 'encoder', 144 | 'Y': 'crystal', 145 | 'TP': 'testpoint', 146 | } 147 | return categories.get(prefix, 'generic') 148 | 149 | 150 | def generate_symbol_instance(designator: str, lib_id: str, footprint: str, 151 | value: str, lcsc: str, x: float, y: float, 152 | project_name: str) -> str: 153 | """Generate a KiCAD symbol instance.""" 154 | sym_uuid = generate_uuid() 155 | 156 | return f''' (symbol (lib_id "{lib_id}") (at {x:.2f} {y:.2f} 0) 157 | (uuid "{sym_uuid}") 158 | (property "Reference" "{designator}" (at {x:.2f} {y - 5:.2f} 0) 159 | (effects (font (size 1.27 1.27)))) 160 | (property "Value" "{value}" (at {x:.2f} {y + 5:.2f} 0) 161 | (effects (font (size 1.27 1.27)))) 162 | (property "Footprint" "{footprint}" (at {x:.2f} {y + 7:.2f} 0) 163 | (effects (font (size 1.27 1.27)) hide)) 164 | (property "LCSC" "{lcsc}" (at {x:.2f} {y + 9:.2f} 0) 165 | (effects (font (size 1.27 1.27)) hide)) 166 | (instances (project "{project_name}" (path "/" (reference "{designator}") (unit 1)))) 167 | ) 168 | ''' 169 | 170 | 171 | def generate_net_label(net_name: str, x: float, y: float, rotation: int = 0) -> str: 172 | """Generate a net label.""" 173 | justify = "left" if rotation == 0 else "right" 174 | return f''' (label "{net_name}" (at {x:.2f} {y:.2f} {rotation}) (fields_autoplaced yes) 175 | (effects (font (size 1.27 1.27)) (justify {justify})) 176 | (uuid "{generate_uuid()}")) 177 | ''' 178 | 179 | 180 | def sort_parts_key(part: dict): 181 | """Sort key for parts - ICs first, then connectors, then passives.""" 182 | ref = part.get('ref', 'X?') 183 | prefix = ''.join(c for c in ref if c.isalpha()) 184 | num = ''.join(c for c in ref if c.isdigit()) 185 | order = {'U': 0, 'J': 1, 'SW': 2, 'ENC': 3, 'D': 4, 'Y': 5, 'TP': 6, 'R': 7, 'C': 8} 186 | return (order.get(prefix, 99), int(num) if num else 0) 187 | 188 | 189 | def generate_schematic(model: dict, project_name: str) -> str: 190 | """Generate KiCAD schematic from pin model.""" 191 | 192 | parts = model.get('parts', []) 193 | 194 | # Header 195 | content = f'''(kicad_sch (version 20231120) (generator "python_generator") 196 | (uuid "{generate_uuid()}") 197 | (paper "A2") 198 | (title_block 199 | (title "ESP32-S3 Portable Radio Receiver") 200 | (date "{datetime.now().strftime('%Y-%m-%d')}") 201 | (rev "1.0") 202 | (comment 1 "Generated from pin_model.json via LLM pipeline") 203 | ) 204 | (lib_symbols) 205 | 206 | ''' 207 | 208 | # Layout configuration 209 | start_x = 50 210 | start_y = 50 211 | x_spacing = 80 212 | y_spacing = 50 213 | cols = 5 214 | 215 | col = 0 216 | row = 0 217 | 218 | # Sort parts by category 219 | sorted_parts = sorted(parts, key=sort_parts_key) 220 | 221 | symbols_content = "" 222 | labels_content = "" 223 | 224 | for part in sorted_parts: 225 | ref = part.get('ref', 'X?') 226 | lcsc = part.get('lcsc', '') 227 | value = part.get('value', '') 228 | footprint = part.get('footprint', '') 229 | pins = part.get('pins', {}) 230 | 231 | # Build lib_id using LCSC to symbol mapping 232 | # This ensures lib_ids match our custom symbol library 233 | if lcsc and lcsc in LCSC_TO_SYMBOL: 234 | lib_id = f"JLCPCB:{LCSC_TO_SYMBOL[lcsc]}" 235 | elif lcsc: 236 | lib_id = f"JLCPCB:{lcsc}" 237 | else: 238 | lib_id = f"JLCPCB:{ref}" 239 | 240 | # Calculate position 241 | x = start_x + col * x_spacing 242 | y = start_y + row * y_spacing 243 | 244 | # Generate symbol 245 | symbols_content += generate_symbol_instance( 246 | ref, lib_id, footprint, value, lcsc, x, y, project_name 247 | ) 248 | 249 | # Generate net labels for each pin 250 | pin_offset_y = 0 251 | for pin_name, net_name in pins.items(): 252 | label_x = x + 30 # Labels to the right of symbol 253 | label_y = y + pin_offset_y 254 | labels_content += generate_net_label(net_name, label_x, label_y, 0) 255 | pin_offset_y += 2.54 # Standard KiCAD pin spacing 256 | 257 | # Move to next grid position 258 | col += 1 259 | if col >= cols: 260 | col = 0 261 | row += 1 262 | 263 | content += symbols_content 264 | content += labels_content 265 | 266 | # Footer 267 | content += ''' 268 | (sheet_instances (path "/" (page "1"))) 269 | ) 270 | ''' 271 | 272 | return content 273 | 274 | 275 | def main(): 276 | script_dir = Path(__file__).parent.parent 277 | model_file = script_dir / "work" / "pin_model.json" 278 | output_dir = script_dir / "output" 279 | 280 | project_name = "RadioReceiver" 281 | 282 | print(f"KiCAD Project Generator") 283 | print("=" * 60) 284 | 285 | # Load pin model 286 | print(f"\nLoading: {model_file}") 287 | with open(model_file, 'r', encoding='utf-8') as f: 288 | model = json.load(f) 289 | 290 | stats = model.get('statistics', {}) 291 | print(f" Parts: {stats.get('total_parts', 0)}") 292 | print(f" Nets: {stats.get('total_nets', 0)}") 293 | print(f" Pin assignments: {stats.get('total_pin_assignments', 0)}") 294 | 295 | # Ensure output directory exists 296 | output_dir.mkdir(parents=True, exist_ok=True) 297 | 298 | # Generate project files 299 | print(f"\nGenerating KiCAD project files in: {output_dir}") 300 | 301 | # Project file 302 | pro_file = output_dir / f"{project_name}.kicad_pro" 303 | pro_file.write_text(generate_project_file(project_name), encoding='utf-8') 304 | print(f" {pro_file.name}") 305 | 306 | # Schematic file 307 | sch_file = output_dir / f"{project_name}.kicad_sch" 308 | sch_content = generate_schematic(model, project_name) 309 | sch_file.write_text(sch_content, encoding='utf-8') 310 | print(f" {sch_file.name}") 311 | 312 | # PCB file 313 | pcb_file = output_dir / f"{project_name}.kicad_pcb" 314 | pcb_file.write_text(generate_pcb_file(project_name), encoding='utf-8') 315 | print(f" {pcb_file.name}") 316 | 317 | # Library tables 318 | sym_table = output_dir / "sym-lib-table" 319 | sym_table.write_text(generate_sym_lib_table(), encoding='utf-8') 320 | print(f" {sym_table.name}") 321 | 322 | fp_table = output_dir / "fp-lib-table" 323 | fp_table.write_text(generate_fp_lib_table(), encoding='utf-8') 324 | print(f" {fp_table.name}") 325 | 326 | print(f"\n" + "=" * 60) 327 | print("SUCCESS!") 328 | print(f"\nTo complete setup:") 329 | print(f" 1. Copy the project folder to Windows where KiCAD is installed") 330 | print(f" 2. Run download_jlcpcb_libs.py to fetch symbol/footprint libraries") 331 | print(f" 3. Copy libraries to: {output_dir}/libs/JLCPCB/") 332 | print(f" 4. Open {project_name}.kicad_pro in KiCAD") 333 | print(f" 5. Run: Tools > Update Schematic from Symbol Libraries") 334 | print("=" * 60) 335 | 336 | 337 | if __name__ == "__main__": 338 | main() 339 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/generate_skidl_v2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Generate working SKiDL Python file from validated pin_model.json 4 | 5 | This is the AUTHORITATIVE source of electrical truth. 6 | SKiDL owns: Parts, Nets, Power, ERC 7 | KiCad owns: Symbol placement, Wire layout, Notes, Sheet structure 8 | 9 | Workflow: 10 | pin_model.json → generate_skidl_v2.py → receiver.py → [netlist + schematic] 11 | """ 12 | 13 | import json 14 | from pathlib import Path 15 | from datetime import datetime 16 | 17 | 18 | # LCSC part number to symbol name mapping 19 | # Maps LCSC codes to our custom symbol library names 20 | LCSC_TO_SYMBOL = { 21 | # ICs 22 | "C2913206": "ESP32-S3-MINI-1-N8", 23 | "C195417": "SI4735-D60-GU", 24 | "C7971": "TDA1306T", 25 | "C16581": "TP4056", 26 | "C6186": "AMS1117-3.3", 27 | "C7519": "USBLC6-2SC6", 28 | # Connectors 29 | "C393939": "TYPE-C-31-M-12", 30 | "C131337": "S2B-PH-K-S", 31 | "C145819": "PJ-327A", 32 | "C124378": "Header-1x04", 33 | "C238128": "TestPoint", 34 | # UI components 35 | "C470747": "EC11E18244A5", 36 | "C127509": "TS-1102S", 37 | "C2761795": "WS2812B-B", 38 | # Passive components 39 | "C32346": "Crystal-32.768kHz", 40 | # Resistors - all map to generic "R" symbol 41 | "C23186": "R", # 5.1k 42 | "C22975": "R", # 2k 43 | "C25804": "R", # 10k 44 | "C25900": "R", # 4.7k 45 | "C22775": "R", # 100R 46 | # Capacitors - all map to generic "C" symbol 47 | "C45783": "C", # 22uF 48 | "C134760": "C", # 220uF 49 | "C15850": "C", # 10uF 50 | "C15849": "C", # 1uF 51 | "C14663": "C", # 100nF 52 | "C1653": "C", # 22pF 53 | } 54 | 55 | 56 | def generate_skidl_code(model: dict) -> str: 57 | """Generate authoritative SKiDL Python code from pin model. 58 | 59 | Args: 60 | model: The pin_model.json data 61 | 62 | The generated code uses KiCad 7 format and JLCPCB library symbols. 63 | Requires: JLCPCB symbol library installed in KiCad. 64 | """ 65 | 66 | parts = model.get('parts', []) 67 | nets = model.get('nets', []) 68 | meta = model.get('_meta', {}) 69 | 70 | lines = [] 71 | 72 | # Header 73 | lines.append('#!/usr/bin/env python3') 74 | lines.append('"""') 75 | lines.append('ESP32-S3 Radio Receiver - SKiDL Circuit Definition') 76 | lines.append(f'Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}') 77 | lines.append('') 78 | lines.append('THIS IS THE AUTHORITATIVE SOURCE OF ELECTRICAL TRUTH.') 79 | lines.append('') 80 | lines.append('SKiDL owns: Parts, Nets, Power, ERC') 81 | lines.append('KiCad owns: Symbol placement, Wire layout, Notes') 82 | lines.append('') 83 | lines.append('Do NOT edit electrical connections in KiCad.') 84 | lines.append('All changes must be made here and regenerated.') 85 | lines.append('"""') 86 | lines.append('') 87 | lines.append('import sys') 88 | lines.append('from pathlib import Path') 89 | lines.append('from skidl import *') 90 | lines.append('') 91 | lines.append('# Use KiCad 8 format for netlist and ERC') 92 | lines.append('set_default_tool(KICAD8)') 93 | lines.append('') 94 | lines.append('# Import our custom schematic generator') 95 | lines.append('sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))') 96 | lines.append('from skidl_to_kicad_sch import generate_kicad_schematic') 97 | lines.append('') 98 | lines.append('# Project configuration') 99 | lines.append('PROJECT_NAME = "RadioReceiver"') 100 | lines.append(f'TOTAL_PARTS = {len(parts)}') 101 | lines.append(f'TOTAL_NETS = {len(nets)}') 102 | lines.append('') 103 | 104 | # Add LCSC to symbol mapping 105 | lines.append('# LCSC part number to symbol name mapping') 106 | lines.append('LCSC_TO_SYMBOL = {') 107 | for lcsc, sym in LCSC_TO_SYMBOL.items(): 108 | lines.append(f' "{lcsc}": "{sym}",') 109 | lines.append('}') 110 | lines.append('') 111 | 112 | lines.append('') 113 | lines.append('def create_circuit():') 114 | lines.append(' """') 115 | lines.append(' Create the radio receiver circuit.') 116 | lines.append(' ') 117 | lines.append(' Returns:') 118 | lines.append(' tuple: (parts_dict, nets_dict) for inspection') 119 | lines.append(' """') 120 | lines.append(' ') 121 | lines.append(' reset()') 122 | lines.append(' ') 123 | 124 | # Create nets 125 | lines.append(' # ========== NETS ==========') 126 | lines.append(' # All nets are defined here. This is the single source of truth.') 127 | lines.append(' ') 128 | net_vars = {} 129 | for net_name in sorted(nets): 130 | var_name = 'net_' + net_name.replace('+', 'P').replace('-', 'N').replace('.', '_') 131 | net_vars[net_name] = var_name 132 | lines.append(f' {var_name} = Net("{net_name}")') 133 | lines.append(' ') 134 | 135 | # Create parts 136 | lines.append(' # ========== PARTS ==========') 137 | lines.append(' # All parts are defined here. Do not add parts in KiCad.') 138 | lines.append(' ') 139 | 140 | for part in parts: 141 | ref = part.get('ref', 'X?') 142 | part_id = part.get('id', '') 143 | value = part.get('value', '') 144 | lcsc = part.get('lcsc', '') 145 | symbol = part.get('symbol', '') 146 | footprint = part.get('footprint', '').replace('JLCPCB:', '') 147 | pins = part.get('pins', {}) 148 | belongs_to = part.get('belongs_to') 149 | 150 | pin_names = list(pins.keys()) 151 | 152 | if not pin_names: 153 | lines.append(f' # {ref} ({part_id}): No pins connected - skipping') 154 | continue 155 | 156 | # Comment with part info 157 | belongs_str = f" [belongs_to: {belongs_to}]" if belongs_to else "" 158 | lines.append(f' # {ref}: {value} (LCSC: {lcsc}){belongs_str}') 159 | 160 | # Use JLCPCB library symbol (required for schematic generation) 161 | # Symbol name comes from mapping (JLC2KiCadLib uses part names, not LCSC numbers) 162 | lib_name = "JLCPCB" 163 | sym_name = LCSC_TO_SYMBOL.get(lcsc, value.replace(' ', '_')) 164 | 165 | lines.append(f' {part_id} = Part("{lib_name}", "{sym_name}",') 166 | lines.append(f' ref="{ref}",') 167 | lines.append(f' value="{value}",') 168 | if footprint: 169 | lines.append(f' footprint="{footprint}",') 170 | lines.append(f' )') 171 | lines.append(' ') 172 | 173 | # Connect pins to nets 174 | lines.append(' # ========== CONNECTIONS ==========') 175 | lines.append(' # All electrical connections. Do NOT modify in KiCad.') 176 | lines.append(' ') 177 | 178 | for part in parts: 179 | part_id = part.get('id', '') 180 | ref = part.get('ref', '') 181 | pins = part.get('pins', {}) 182 | 183 | if not pins: 184 | continue 185 | 186 | lines.append(f' # {ref}') 187 | for pin_name, net_name in pins.items(): 188 | net_var = net_vars.get(net_name, 'NC') 189 | lines.append(f' {net_var} += {part_id}["{pin_name}"]') 190 | lines.append(' ') 191 | 192 | # Return parts and nets for inspection 193 | lines.append(' # Return for inspection') 194 | lines.append(' return {') 195 | for part in parts: 196 | part_id = part.get('id', '') 197 | ref = part.get('ref', '') 198 | if part.get('pins'): 199 | lines.append(f' "{ref}": {part_id},') 200 | lines.append(' }, {') 201 | for net_name in sorted(nets): 202 | var_name = net_vars[net_name] 203 | lines.append(f' "{net_name}": {var_name},') 204 | lines.append(' }') 205 | lines.append('') 206 | lines.append('') 207 | 208 | # Generate output function 209 | lines.append('def generate_outputs(output_dir: Path = None):') 210 | lines.append(' """') 211 | lines.append(' Generate all outputs from the circuit.') 212 | lines.append(' ') 213 | lines.append(' This is the ONLY way to create/update the schematic.') 214 | lines.append(' Do not edit the schematic directly in KiCad.') 215 | lines.append(' """') 216 | lines.append(' ') 217 | lines.append(' if output_dir is None:') 218 | lines.append(' output_dir = Path(__file__).parent') 219 | lines.append(' ') 220 | lines.append(' print(f"Creating circuit with {TOTAL_PARTS} parts and {TOTAL_NETS} nets...")') 221 | lines.append(' parts, nets = create_circuit()') 222 | lines.append(' ') 223 | lines.append(' # Run ERC') 224 | lines.append(' print("Running Electrical Rules Check...")') 225 | lines.append(' ERC()') 226 | lines.append(' ') 227 | lines.append(' # Generate netlist (always)') 228 | lines.append(' netlist_file = output_dir / f"{PROJECT_NAME}.net"') 229 | lines.append(' generate_netlist(file_=str(netlist_file))') 230 | lines.append(' print(f"Generated: {netlist_file}")') 231 | lines.append(' ') 232 | lines.append(' # Generate schematic using our custom generator') 233 | lines.append(' # This traverses SKiDL circuit objects and creates KiCad 9 format') 234 | lines.append(' sch_file = output_dir / f"{PROJECT_NAME}.kicad_sch"') 235 | lines.append(' lib_path = output_dir / "libs" / "JLCPCB" / "symbol" / "JLCPCB.kicad_sym"') 236 | lines.append(' generate_kicad_schematic(') 237 | lines.append(' default_circuit,') 238 | lines.append(' symbol_lib_path=lib_path,') 239 | lines.append(' output_path=sch_file,') 240 | lines.append(' title="ESP32-S3 Portable Radio Receiver",') 241 | lines.append(' lcsc_to_symbol=LCSC_TO_SYMBOL') 242 | lines.append(' )') 243 | lines.append(' ') 244 | lines.append(' print()') 245 | lines.append(' print("=" * 60)') 246 | lines.append(' print("IMPORTANT: SKiDL owns electrical truth.")') 247 | lines.append(' print("In KiCad you may ONLY:")') 248 | lines.append(' print(" - Move symbols")') 249 | lines.append(' print(" - Route wires")') 250 | lines.append(' print(" - Add notes and frames")') 251 | lines.append(' print("You may NOT:")') 252 | lines.append(' print(" - Add/delete components")') 253 | lines.append(' print(" - Change pin connections")') 254 | lines.append(' print(" - Rename nets")') 255 | lines.append(' print("=" * 60)') 256 | lines.append(' ') 257 | lines.append(' return True') 258 | lines.append('') 259 | lines.append('') 260 | lines.append('if __name__ == "__main__":') 261 | lines.append(' generate_outputs()') 262 | lines.append('') 263 | 264 | return '\n'.join(lines) 265 | 266 | 267 | def main(): 268 | script_dir = Path(__file__).parent.parent 269 | model_file = script_dir / "work" / "pin_model.json" 270 | output_file = script_dir / "output" / "receiver_v2.py" 271 | 272 | output_file.parent.mkdir(parents=True, exist_ok=True) 273 | 274 | print(f"Reading: {model_file}") 275 | 276 | with open(model_file, 'r', encoding='utf-8') as f: 277 | model = json.load(f) 278 | 279 | code = generate_skidl_code(model) 280 | 281 | with open(output_file, 'w', encoding='utf-8') as f: 282 | f.write(code) 283 | 284 | print(f"Generated: {output_file}") 285 | print(f" Parts: {len(model.get('parts', []))}") 286 | print(f" Nets: {len(model.get('nets', []))}") 287 | 288 | 289 | if __name__ == "__main__": 290 | main() 291 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/skidl_to_kicad_sch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | SKiDL to KiCad Schematic Generator 4 | 5 | Traverses SKiDL's in-memory circuit objects and generates a modern 6 | KiCad schematic (.kicad_sch) with symbols and net labels. 7 | 8 | This bridges SKiDL (electrical truth) → KiCad (visual representation). 9 | 10 | Usage: 11 | from skidl_to_kicad_sch import generate_kicad_schematic 12 | 13 | # After creating SKiDL circuit 14 | generate_kicad_schematic( 15 | default_circuit, 16 | symbol_lib_path="libs/JLCPCB/symbol/JLCPCB.kicad_sym", 17 | output_path="RadioReceiver.kicad_sch" 18 | ) 19 | """ 20 | 21 | import re 22 | import uuid 23 | from pathlib import Path 24 | from dataclasses import dataclass 25 | from typing import Dict, List, Tuple, Optional 26 | from datetime import datetime 27 | 28 | 29 | @dataclass 30 | class SymbolPin: 31 | """Pin information from symbol library""" 32 | name: str 33 | number: str 34 | x: float 35 | y: float 36 | rotation: int # 0=right, 90=up, 180=left, 270=down 37 | length: float 38 | 39 | 40 | @dataclass 41 | class SymbolDef: 42 | """Symbol definition from library""" 43 | name: str 44 | pins: Dict[str, SymbolPin] # keyed by pin name 45 | bbox: Tuple[float, float, float, float] # minx, miny, maxx, maxy 46 | 47 | 48 | def parse_symbol_library(lib_path: Path) -> Dict[str, SymbolDef]: 49 | """Parse KiCad symbol library to extract pin positions.""" 50 | 51 | symbols = {} 52 | content = lib_path.read_text() 53 | 54 | # Find all symbol definitions 55 | # Pattern: (symbol "NAME" ... ENDSYMBOL) 56 | symbol_pattern = re.compile( 57 | r'\(symbol\s+"([^"]+)"\s+\(in_bom[^)]+\)\s+\(on_board[^)]+\)(.*?)\n \)', 58 | re.DOTALL 59 | ) 60 | 61 | for match in symbol_pattern.finditer(content): 62 | sym_name = match.group(1) 63 | sym_content = match.group(2) 64 | 65 | pins = {} 66 | 67 | # Find pins: (pin TYPE STYLE (at X Y ROT) (length L) (name "NAME"...) (number "NUM"...)) 68 | # Use non-greedy match to skip effects sections with nested parens 69 | pin_pattern = re.compile( 70 | r'\(pin\s+\w+\s+\w+\s+\(at\s+([-\d.]+)\s+([-\d.]+)\s+(\d+)\)\s+\(length\s+([-\d.]+)\)\s+\(name\s+"([^"]+)".*?\(number\s+"([^"]+)"', 71 | re.DOTALL 72 | ) 73 | 74 | for pin_match in pin_pattern.finditer(sym_content): 75 | x = float(pin_match.group(1)) 76 | y = float(pin_match.group(2)) 77 | rot = int(pin_match.group(3)) 78 | length = float(pin_match.group(4)) 79 | name = pin_match.group(5) 80 | number = pin_match.group(6) 81 | 82 | pins[name] = SymbolPin( 83 | name=name, 84 | number=number, 85 | x=x, 86 | y=y, 87 | rotation=rot, 88 | length=length 89 | ) 90 | 91 | # Calculate bounding box from rectangle if present 92 | rect_pattern = re.compile(r'\(rectangle\s+\(start\s+([-\d.]+)\s+([-\d.]+)\)\s+\(end\s+([-\d.]+)\s+([-\d.]+)\)') 93 | rect_match = rect_pattern.search(sym_content) 94 | if rect_match: 95 | x1, y1 = float(rect_match.group(1)), float(rect_match.group(2)) 96 | x2, y2 = float(rect_match.group(3)), float(rect_match.group(4)) 97 | bbox = (min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)) 98 | else: 99 | # Estimate from pins 100 | if pins: 101 | xs = [p.x for p in pins.values()] 102 | ys = [p.y for p in pins.values()] 103 | bbox = (min(xs) - 10, min(ys) - 10, max(xs) + 10, max(ys) + 10) 104 | else: 105 | bbox = (-10, -10, 10, 10) 106 | 107 | symbols[sym_name] = SymbolDef(name=sym_name, pins=pins, bbox=bbox) 108 | 109 | return symbols 110 | 111 | 112 | def get_pin_endpoint(pin: SymbolPin, sym_x: float, sym_y: float) -> Tuple[float, float]: 113 | """Calculate the endpoint (connection point) of a pin in schematic coordinates. 114 | 115 | Pin position in symbol is where wire connects. Symbol Y is inverted in schematic. 116 | """ 117 | # In KiCad schematic, Y increases downward, but symbol Y increases upward 118 | return (sym_x + pin.x, sym_y - pin.y) 119 | 120 | 121 | def generate_uuid() -> str: 122 | return str(uuid.uuid4()) 123 | 124 | 125 | def generate_kicad_schematic( 126 | circuit, 127 | symbol_lib_path: Path, 128 | output_path: Path, 129 | title: str = "SKiDL Generated Schematic", 130 | lcsc_to_symbol: Dict[str, str] = None 131 | ): 132 | """ 133 | Generate KiCad schematic from SKiDL circuit. 134 | 135 | Args: 136 | circuit: SKiDL circuit object (default_circuit) 137 | symbol_lib_path: Path to .kicad_sym library 138 | output_path: Output .kicad_sch file path 139 | title: Schematic title 140 | lcsc_to_symbol: Mapping from LCSC codes to symbol names 141 | """ 142 | 143 | # Parse symbol library 144 | symbols = parse_symbol_library(symbol_lib_path) 145 | print(f"Loaded {len(symbols)} symbols from library") 146 | 147 | # Layout configuration 148 | start_x = 50.0 149 | start_y = 50.0 150 | x_spacing = 80.0 151 | y_spacing = 60.0 152 | cols = 5 153 | 154 | # Build schematic content 155 | lines = [] 156 | 157 | # Header 158 | lines.append('(kicad_sch (version 20231120) (generator "skidl_to_kicad_sch")') 159 | lines.append(f' (uuid "{generate_uuid()}")') 160 | lines.append(' (paper "A2")') 161 | lines.append(' (title_block') 162 | lines.append(f' (title "{title}")') 163 | lines.append(f' (date "{datetime.now().strftime("%Y-%m-%d")}")') 164 | lines.append(' (rev "1.0")') 165 | lines.append(' (comment 1 "Generated from SKiDL circuit")') 166 | lines.append(' (comment 2 "SKiDL owns electrical truth")') 167 | lines.append(' )') 168 | lines.append('') 169 | 170 | # Lib symbols section (empty - KiCad will populate from library) 171 | lines.append(' (lib_symbols') 172 | lines.append(' )') 173 | lines.append('') 174 | 175 | # Track net labels to add 176 | net_labels = [] 177 | 178 | # Place symbols 179 | col = 0 180 | row = 0 181 | 182 | for part in circuit.parts: 183 | # Get symbol name 184 | sym_name = part.name 185 | if lcsc_to_symbol and hasattr(part, 'lcsc'): 186 | sym_name = lcsc_to_symbol.get(part.lcsc, sym_name) 187 | 188 | # Get symbol definition 189 | sym_def = symbols.get(sym_name) 190 | if not sym_def: 191 | print(f"Warning: Symbol '{sym_name}' not found in library, using placeholder") 192 | sym_def = SymbolDef(name=sym_name, pins={}, bbox=(-10, -10, 10, 10)) 193 | 194 | # Calculate position 195 | x = start_x + col * x_spacing 196 | y = start_y + row * y_spacing 197 | 198 | # Symbol instance 199 | sym_uuid = generate_uuid() 200 | lib_name = "JLCPCB" 201 | 202 | lines.append(f' (symbol (lib_id "{lib_name}:{sym_name}") (at {x:.2f} {y:.2f} 0)') 203 | lines.append(f' (uuid "{sym_uuid}")') 204 | lines.append(f' (property "Reference" "{part.ref}" (at {x:.2f} {y - 5:.2f} 0)') 205 | lines.append(' (effects (font (size 1.27 1.27))))') 206 | 207 | value = getattr(part, 'value', sym_name) or sym_name 208 | lines.append(f' (property "Value" "{value}" (at {x:.2f} {y + 5:.2f} 0)') 209 | lines.append(' (effects (font (size 1.27 1.27))))') 210 | 211 | footprint = getattr(part, 'footprint', '') or '' 212 | lines.append(f' (property "Footprint" "{footprint}" (at {x:.2f} {y + 7:.2f} 0)') 213 | lines.append(' (effects (font (size 1.27 1.27)) hide))') 214 | 215 | lcsc = getattr(part, 'lcsc', '') or '' 216 | lines.append(f' (property "LCSC" "{lcsc}" (at {x:.2f} {y + 9:.2f} 0)') 217 | lines.append(' (effects (font (size 1.27 1.27)) hide))') 218 | 219 | lines.append(f' (instances (project "RadioReceiver" (path "/" (reference "{part.ref}") (unit 1))))') 220 | lines.append(' )') 221 | 222 | # Add net labels for each connected pin 223 | for pin in part.pins: 224 | if pin.net and pin.net.name: 225 | net_name = pin.net.name 226 | 227 | # Find pin in symbol definition 228 | pin_def = sym_def.pins.get(pin.name) 229 | if pin_def: 230 | # Calculate label position at pin endpoint 231 | label_x, label_y = get_pin_endpoint(pin_def, x, y) 232 | 233 | # Determine label rotation based on pin orientation 234 | # Pin rotation: 0=right, 90=up, 180=left, 270=down 235 | # Label should face outward from symbol 236 | if pin_def.rotation == 0: 237 | label_rot = 0 238 | label_x -= 2 # Offset left of pin 239 | elif pin_def.rotation == 180: 240 | label_rot = 0 241 | label_x += 2 # Offset right of pin 242 | elif pin_def.rotation == 90: 243 | label_rot = 90 244 | label_y += 2 245 | else: # 270 246 | label_rot = 90 247 | label_y -= 2 248 | 249 | net_labels.append((net_name, label_x, label_y, label_rot)) 250 | else: 251 | # Fallback: place label near symbol 252 | net_labels.append((net_name, x + 20, y + row * 2.54, 0)) 253 | 254 | # Move to next position 255 | col += 1 256 | if col >= cols: 257 | col = 0 258 | row += 1 259 | 260 | # Add net labels 261 | lines.append('') 262 | lines.append(' ; Net labels for connectivity') 263 | for net_name, lx, ly, rot in net_labels: 264 | justify = "left" if rot == 0 else "left" 265 | lines.append(f' (label "{net_name}" (at {lx:.2f} {ly:.2f} {rot}) (fields_autoplaced yes)') 266 | lines.append(f' (effects (font (size 1.27 1.27)) (justify {justify}))') 267 | lines.append(f' (uuid "{generate_uuid()}"))') 268 | 269 | # Power symbols for common nets 270 | power_nets = {"+3V3", "GND", "VBAT", "VBUS"} 271 | for net in circuit.nets: 272 | if net.name in power_nets: 273 | # Add power port symbols 274 | pass # Could add power symbols here 275 | 276 | # Footer 277 | lines.append('') 278 | lines.append(' (sheet_instances (path "/" (page "1")))') 279 | lines.append(')') 280 | 281 | # Write file 282 | output_path = Path(output_path) 283 | output_path.write_text('\n'.join(lines)) 284 | print(f"Generated: {output_path}") 285 | print(f" Parts: {len(circuit.parts)}") 286 | print(f" Nets: {len(circuit.nets)}") 287 | print(f" Labels: {len(net_labels)}") 288 | 289 | return output_path 290 | 291 | 292 | if __name__ == "__main__": 293 | # Test with a simple circuit 294 | from skidl import * 295 | 296 | reset() 297 | set_default_tool(KICAD8) 298 | 299 | lib_path = Path(__file__).parent.parent / "output" / "libs" / "JLCPCB" / "symbol" / "JLCPCB.kicad_sym" 300 | 301 | # Create test circuit 302 | gnd = Net("GND") 303 | vcc = Net("+3V3") 304 | 305 | # Would need actual parts here 306 | print(f"Library path: {lib_path}") 307 | print(f"Exists: {lib_path.exists()}") 308 | 309 | if lib_path.exists(): 310 | symbols = parse_symbol_library(lib_path) 311 | print(f"Found {len(symbols)} symbols:") 312 | for name in symbols: 313 | print(f" - {name}: {len(symbols[name].pins)} pins") 314 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/prompts/step1.md: -------------------------------------------------------------------------------- 1 | # Step 1: Extract Primary Parts from FSD 2 | 3 | Extract ALL parts from the FSD, including optional parts where the FSD requests design options. 4 | 5 | **IMPORTANT:** No new parts will be added after this step. This is the complete parts list. 6 | 7 | **This step focuses on:** 8 | 1. WHAT parts are needed (not which specific JLCPCB variants to use) 9 | 2. WHAT design options exist (protection circuits, optional features, etc.) 10 | 3. Capturing ALL alternatives mentioned in the FSD 11 | 12 | --- 13 | 14 | ## Process 15 | 16 | ### 1. Read the FSD Thoroughly 17 | 18 | Identify all components mentioned in: 19 | - Bill of Materials sections 20 | - Block diagrams 21 | - Interface specifications 22 | - Pin allocations 23 | - Any "or equivalent" mentions 24 | 25 | **Also identify design choices NOT explicitly in FSD but important for a robust design:** 26 | - Protection circuits (ESD, reverse polarity, overcurrent) 27 | - Optional features (debug headers, test points) 28 | - Interface options (antenna type, connector variants) 29 | 30 | ### 2. Extract Required Parts 31 | 32 | For each component, capture: 33 | - **Functional role** (what it does in the circuit) 34 | - **Part number** from FSD (if specified) 35 | - **LCSC code** from FSD (if specified) 36 | - **Package** from FSD (if specified) 37 | - **Key specifications** (voltage, current, value, etc.) 38 | 39 | ### 3. Identify Functional Alternatives 40 | 41 | Add alternatives where **functionally reasonable**: 42 | 43 | | When to Add Alternatives | Examples | 44 | |--------------------------|----------| 45 | | FSD says "or equivalent" | "AMS1117 or equivalent LDO" | 46 | | Multiple valid topologies | Linear LDO vs switching regulator | 47 | | Package variants exist | SMD vs through-hole encoder | 48 | | Common substitutes exist | ME6211 vs AMS1117 (both 3.3V LDOs) | 49 | | FSD mentions options | "0603 minimum" implies 0805 is also OK | 50 | 51 | **Do NOT add alternatives for:** 52 | - Specific ICs with unique functionality (SI4735, ESP32-S3) 53 | - Parts where FSD is explicit about requirements 54 | - Standard passives (just use FSD values) 55 | 56 | ### 4. Group Options Together 57 | 58 | Parts that are alternatives to each other share the same `option_group`. 59 | Only ONE part from each option group will be selected in Step 2. 60 | 61 | ### 5. Identify Design Options 62 | 63 | Design options are choices that affect the circuit but aren't explicit part alternatives. 64 | These need user decisions before finalizing the design. 65 | 66 | **Common design options to consider:** 67 | 68 | | Category | Option | Typical Choices | 69 | |----------|--------|-----------------| 70 | | Protection | USB ESD | Yes (add TVS/ESD IC) / No (rely on MCU) | 71 | | Protection | Battery reverse polarity | None / Schottky diode / P-FET | 72 | | Protection | Overcurrent | Fuse / PTC / None | 73 | | Debug | UART header | Yes / No | 74 | | Debug | SWD/JTAG header | Yes / No | 75 | | Interface | Antenna connection | Wire pad / SMA connector / Headphone cable | 76 | | Assembly | Through-hole parts | Allow TH / SMD only | 77 | 78 | **For each design option, capture:** 79 | - What the option is 80 | - Available choices with pros/cons 81 | - Recommended choice with reasoning 82 | - Parts that would be added if "yes" (include in parts list with `conditional: true`) 83 | 84 | --- 85 | 86 | ## Output Format 87 | 88 | Write to `design/work/step1_primary_parts.yaml`: 89 | 90 | ```yaml 91 | # step1_primary_parts.yaml 92 | # Extracted from: FSD_*.md 93 | # Date: [YYYY-MM-DD] 94 | # 95 | # This file contains ALL parts including optional parts and alternatives. 96 | # NO NEW PARTS will be added after this step. 97 | # Step 2 will enrich with JLCPCB data (pricing, stock, variants). 98 | 99 | parts: 100 | # === Microcontroller (no alternatives - FSD specifies exact part) === 101 | - id: mcu 102 | name: "ESP32-S3 MCU Module" 103 | function: "Main microcontroller with WiFi/BLE" 104 | part_number: "ESP32-S3-MINI-1-N8" 105 | lcsc: "C2913206" # From FSD 106 | package: "Module" 107 | category: microcontroller 108 | quantity: 1 109 | option_group: null # No alternatives 110 | specs: 111 | voltage: "3.0-3.6V" 112 | flash: "8MB" 113 | 114 | # === Radio IC (no alternatives - specific functionality) === 115 | - id: radio_ic 116 | name: "AM/FM/SW Radio Receiver" 117 | function: "Multi-band radio reception" 118 | part_number: "SI4735-D60-GU" 119 | lcsc: "C195417" # From FSD (verify in Step 2) 120 | package: "SSOP-24" 121 | category: radio 122 | quantity: 1 123 | option_group: null 124 | specs: 125 | interface: "I2C" 126 | bands: "AM/FM/SW" 127 | 128 | # === LDO Regulator (alternatives exist) === 129 | - id: ldo_ams1117 130 | name: "3.3V LDO Regulator" 131 | function: "Voltage regulation from battery to 3.3V" 132 | part_number: "AMS1117-3.3" 133 | lcsc: "C6186" # From FSD 134 | package: "SOT-223" 135 | category: power 136 | quantity: 1 137 | option_group: ldo_3v3 # Alternatives share this group 138 | specs: 139 | output_voltage: "3.3V" 140 | max_current: "1A" 141 | dropout: "1.1V" 142 | quiescent: "5mA" 143 | pros: "High current capacity, widely available" 144 | cons: "Higher dropout, higher quiescent current" 145 | 146 | - id: ldo_me6211 147 | name: "3.3V LDO Regulator" 148 | function: "Voltage regulation from battery to 3.3V" 149 | part_number: "ME6211C33M5G-N" 150 | lcsc: "C82942" 151 | package: "SOT-23-5" 152 | category: power 153 | quantity: 1 154 | option_group: ldo_3v3 # Same group as above 155 | specs: 156 | output_voltage: "3.3V" 157 | max_current: "500mA" 158 | dropout: "0.1V" 159 | quiescent: "40uA" 160 | pros: "Low dropout, ultra-low quiescent current, smaller" 161 | cons: "Lower max current (500mA vs 1A)" 162 | 163 | # === Rotary Encoder (SMD vs TH options) === 164 | - id: encoder_smd 165 | name: "Rotary Encoder with Switch" 166 | function: "User input for tuning and volume" 167 | part_number: "EC11J1525402" # SMD variant 168 | lcsc: null # To be found in Step 2 169 | package: "SMD" 170 | category: ui 171 | quantity: 2 172 | option_group: encoder 173 | specs: 174 | type: "Incremental quadrature" 175 | switch: "Integrated momentary" 176 | pros: "SMD assembly, no manual soldering" 177 | cons: "May need assembly fixture, typically more expensive" 178 | 179 | - id: encoder_th 180 | name: "Rotary Encoder with Switch" 181 | function: "User input for tuning and volume" 182 | part_number: "EC11E18244A5" # Through-hole variant 183 | lcsc: null # To be found in Step 2 184 | package: "TH" 185 | category: ui 186 | quantity: 2 187 | option_group: encoder 188 | specs: 189 | type: "Incremental quadrature" 190 | switch: "Integrated momentary" 191 | pros: "Easier to source, often cheaper, more mechanical options" 192 | cons: "Requires manual soldering or selective assembly" 193 | 194 | # === Parts without alternatives (standard values from FSD) === 195 | - id: c_bypass_100n 196 | name: "Bypass Capacitor" 197 | function: "IC power supply filtering" 198 | part_number: "100nF" 199 | lcsc: "C14663" # Common 0603 100nF 200 | package: "0603" 201 | category: passive 202 | quantity: 11 # Count from FSD 203 | option_group: null 204 | specs: 205 | capacitance: "100nF" 206 | voltage: "16V+" 207 | dielectric: "X7R" 208 | 209 | # ... continue for all parts 210 | 211 | # === Option Groups Summary === 212 | # List all option groups for Step 2 decision-making 213 | option_groups: 214 | ldo_3v3: 215 | description: "3.3V voltage regulator" 216 | candidates: [ldo_ams1117, ldo_me6211] 217 | decision_factors: 218 | - "Current requirements (check power budget)" 219 | - "Battery life (quiescent current)" 220 | - "Cost and availability" 221 | 222 | encoder: 223 | description: "Rotary encoder type" 224 | candidates: [encoder_smd, encoder_th] 225 | decision_factors: 226 | - "Assembly method (full SMD vs manual)" 227 | - "Cost" 228 | - "Mechanical feel preference" 229 | 230 | # === Design Options === 231 | # Design choices identified that need user decision 232 | # Format: question, context, choices with pros/cons, recommendation 233 | design_options: 234 | : 235 | question: "" 236 | context: "" 237 | choices: 238 | - value: "" 239 | description: "" 240 | pros: ["", ""] 241 | cons: ["", ""] 242 | adds_parts: [, ] # References conditional_parts 243 | recommendation: "" 244 | reason: "" 245 | 246 | # === Conditional Parts === 247 | # Parts only included based on design_options decisions 248 | # These get enriched in Step 2 along with regular parts 249 | conditional_parts: 250 | - id: 251 | name: "" 252 | function: "" 253 | part_number: "" 254 | lcsc: null # Will be found in Step 2 255 | package: "" 256 | category: protection|connector|passive 257 | quantity: 1 258 | condition: " == " 259 | ``` 260 | 261 | --- 262 | 263 | ## Categories Reference 264 | 265 | | Category | Description | Examples | 266 | |----------|-------------|----------| 267 | | microcontroller | Main MCU | ESP32-S3 | 268 | | radio | RF/radio ICs | SI4735 | 269 | | power | Voltage regulators, chargers | LDO, TP4056 | 270 | | audio | Amplifiers, DACs | TDA1308 | 271 | | ui | User interface | Encoders, buttons, LEDs | 272 | | connector | Physical connectors | USB-C, JST, audio jack | 273 | | passive | R, C, L, crystals | Resistors, capacitors | 274 | | protection | ESD, fuses | TVS diodes | 275 | 276 | --- 277 | 278 | ## Checklist: When to Add Alternatives 279 | 280 | - [ ] FSD explicitly mentions alternatives ("or equivalent") 281 | - [ ] Different packages available (SMD vs TH) 282 | - [ ] Different power topologies possible (linear vs switching) 283 | - [ ] Common drop-in replacements exist 284 | - [ ] FSD gives minimum spec (implies larger/better is OK) 285 | 286 | ## Checklist: When NOT to Add Alternatives 287 | 288 | - [ ] FSD specifies exact part number for unique functionality 289 | - [ ] Part has no common substitutes (specialized ICs) 290 | - [ ] Standard passive values (use what FSD says) 291 | - [ ] Adding alternatives would require circuit changes 292 | 293 | --- 294 | 295 | ## Exit Validation Checklist 296 | 297 | **Before proceeding to Step 2, ALL checks must pass:** 298 | 299 | ### 1. File Exists and Valid 300 | ```bash 301 | ls -la design/work/step1_primary_parts.yaml 302 | python -c "import yaml; yaml.safe_load(open('design/work/step1_primary_parts.yaml'))" 303 | ``` 304 | - [ ] `step1_primary_parts.yaml` exists and is valid YAML 305 | 306 | ### 2. All FSD Parts Captured 307 | - [ ] Every component from FSD BOM is represented 308 | - [ ] Every IC from block diagram is included 309 | - [ ] All connectors are listed 310 | - [ ] All passives are counted correctly 311 | 312 | ### 3. Required Fields Present 313 | Every part must have: 314 | - [ ] `id` - unique identifier 315 | - [ ] `name` - human-readable name 316 | - [ ] `function` - what it does in the circuit 317 | - [ ] `part_number` - from FSD or suggested 318 | - [ ] `package` - footprint type 319 | - [ ] `category` - component category 320 | - [ ] `quantity` - number needed 321 | - [ ] `option_group` - null or group name 322 | 323 | ### 4. Option Groups Valid 324 | - [ ] Each option_group has 2+ candidates 325 | - [ ] All candidates in a group serve the same function 326 | - [ ] decision_factors explain trade-offs 327 | - [ ] No part belongs to multiple option_groups 328 | 329 | ### 5. Design Options Complete 330 | - [ ] Common protection options considered (USB ESD, battery reverse polarity) 331 | - [ ] Each design_option has question, context, choices, recommendation 332 | - [ ] Each choice has value, description, pros, cons, adds_parts 333 | - [ ] Conditional parts listed for each "adds_parts" reference 334 | - [ ] Conditional parts have valid `condition` field 335 | 336 | ### 6. No Pricing Data (That's Step 2) 337 | ```bash 338 | grep -E "price|cost|\\\$" design/work/step1_primary_parts.yaml 339 | ``` 340 | - [ ] No pricing information in file (grep should return nothing except comments) 341 | 342 | --- 343 | 344 | ## If Validation Fails 345 | 346 | **DO NOT proceed to Step 2!** 347 | 348 | 1. Identify which check(s) failed 349 | 2. Fix the issue in step1_primary_parts.yaml 350 | 3. Re-run ALL validation checks 351 | 4. Only proceed when ALL checks pass 352 | 353 | ``` 354 | VALIDATION LOOP: Step 1 → Validate → Fix if needed → Validate again → Step 2 355 | ``` 356 | 357 | --- 358 | 359 | ## What Happens Next 360 | 361 | Step 2 will: 362 | 1. Run automated script to fetch JLCPCB data (pricing, stock, variants) for ALL parts 363 | 2. Select best variant for each part (in stock, Basic preferred) 364 | 3. Output enriched parts list with JLCPCB details 365 | 366 | Step 3 will: 367 | 1. Present design options to user 368 | 2. Collect decisions on optional parts 369 | 3. Output final design options for Step 4 370 | -------------------------------------------------------------------------------- /KiCAD-Generator-tools/scripts/ensure_symbols.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Ensure all required symbols exist in the JLCPCB library. 4 | 5 | This script: 6 | 1. Reads pin_model.json OR step2_parts_complete.yaml to get all LCSC codes 7 | 2. Checks which symbols are missing from JLCPCB.kicad_sym 8 | 3. Downloads missing symbols using JLC2KiCadLib 9 | 4. Appends them to the library 10 | 11 | Run this AFTER Step 2 (parts list) and BEFORE schematic generation. 12 | 13 | Usage: 14 | # From pin_model.json (default) 15 | python ensure_symbols.py 16 | 17 | # From step2 YAML file 18 | python ensure_symbols.py --parts work/step2_parts_complete.yaml 19 | 20 | # Specify library path 21 | python ensure_symbols.py --library output/libs/JLCPCB/symbol/JLCPCB.kicad_sym 22 | """ 23 | 24 | import json 25 | import subprocess 26 | import tempfile 27 | import re 28 | import shutil 29 | from pathlib import Path 30 | from typing import Set, Dict, List 31 | 32 | # Try to import yaml, fall back to basic parsing if not available 33 | try: 34 | import yaml 35 | HAS_YAML = True 36 | except ImportError: 37 | HAS_YAML = False 38 | 39 | 40 | # Generic parts that use standard symbols (don't need individual LCSC symbols) 41 | # Set to empty to download ALL symbols including passives 42 | GENERIC_PREFIXES = set() # Previously: {'R', 'C', 'L'} 43 | 44 | 45 | def extract_lcsc_from_yaml(yaml_path: Path) -> Dict[str, str]: 46 | """ 47 | Extract LCSC codes from step2_parts_complete.yaml. 48 | Returns dict of {lcsc_code: part_value} for tracking. 49 | Skips generic passives (R, C, L) that use standard symbols. 50 | """ 51 | lcsc_parts = {} 52 | 53 | if HAS_YAML: 54 | with open(yaml_path, 'r', encoding='utf-8') as f: 55 | data = yaml.safe_load(f) 56 | for part in data.get('parts', []): 57 | lcsc = part.get('lcsc', '') 58 | value = part.get('part', '') or part.get('value', '') 59 | prefix = part.get('prefix', '') 60 | 61 | # Skip generic passives - they use standard R/C/L symbols 62 | if prefix in GENERIC_PREFIXES: 63 | continue 64 | 65 | if lcsc and lcsc.startswith('C'): 66 | lcsc_parts[lcsc] = value 67 | else: 68 | # Basic regex parsing if PyYAML not installed 69 | content = yaml_path.read_text(encoding='utf-8') 70 | # Find lcsc: "Cxxxxx" patterns (can't filter by prefix without full parsing) 71 | for match in re.finditer(r'lcsc:\s*["\']?(C\d+)["\']?', content): 72 | lcsc = match.group(1) 73 | lcsc_parts[lcsc] = lcsc # Use LCSC as value if can't parse 74 | 75 | return lcsc_parts 76 | 77 | 78 | def extract_lcsc_from_json(json_path: Path) -> Dict[str, str]: 79 | """ 80 | Extract LCSC codes from pin_model.json. 81 | Returns dict of {lcsc_code: part_value} for tracking. 82 | Skips generic passives (R, C, L) that use standard symbols. 83 | """ 84 | with open(json_path, 'r', encoding='utf-8') as f: 85 | model = json.load(f) 86 | 87 | lcsc_parts = {} 88 | for part in model.get('parts', []): 89 | lcsc = part.get('lcsc', '') 90 | value = part.get('value', '') 91 | ref = part.get('ref', '') 92 | 93 | # Skip generic passives - check if ref starts with R, C, or L 94 | if ref and ref[0] in GENERIC_PREFIXES: 95 | continue 96 | 97 | if lcsc and lcsc.startswith('C'): 98 | lcsc_parts[lcsc] = value 99 | 100 | return lcsc_parts 101 | 102 | 103 | def extract_lcsc_codes(parts_path: Path) -> Dict[str, str]: 104 | """ 105 | Extract LCSC codes from either JSON or YAML file. 106 | Returns dict of {lcsc_code: part_value} for tracking. 107 | """ 108 | if parts_path.suffix in ['.yaml', '.yml']: 109 | return extract_lcsc_from_yaml(parts_path) 110 | else: 111 | return extract_lcsc_from_json(parts_path) 112 | 113 | 114 | def get_existing_symbols(library_path: Path) -> Set[str]: 115 | """ 116 | Get set of LCSC codes that already have symbols in the library. 117 | Looks for property "LCSC" "Cxxxxx" in the .kicad_sym file. 118 | """ 119 | existing = set() 120 | 121 | if not library_path.exists(): 122 | return existing 123 | 124 | content = library_path.read_text(encoding='utf-8') 125 | 126 | # Find all LCSC properties: (property "LCSC" "C12345" ...) 127 | pattern = r'\(property\s+"LCSC"\s+"(C\d+)"' 128 | matches = re.findall(pattern, content) 129 | existing.update(matches) 130 | 131 | return existing 132 | 133 | 134 | def download_symbol(lcsc_code: str, temp_dir: Path) -> Path | None: 135 | """ 136 | Download symbol using JLC2KiCadLib. 137 | Returns path to downloaded .kicad_sym file or None if failed. 138 | """ 139 | try: 140 | result = subprocess.run( 141 | ['JLC2KiCadLib', lcsc_code, '-dir', str(temp_dir)], 142 | capture_output=True, 143 | text=True, 144 | timeout=60 145 | ) 146 | 147 | if result.returncode != 0: 148 | print(f" Warning: JLC2KiCadLib failed for {lcsc_code}") 149 | print(f" {result.stderr}") 150 | return None 151 | 152 | # Find the generated .kicad_sym file 153 | symbol_dir = temp_dir / 'symbol' 154 | if symbol_dir.exists(): 155 | sym_files = list(symbol_dir.glob('*.kicad_sym')) 156 | if sym_files: 157 | return sym_files[0] 158 | 159 | return None 160 | 161 | except subprocess.TimeoutExpired: 162 | print(f" Warning: Timeout downloading {lcsc_code}") 163 | return None 164 | except FileNotFoundError: 165 | print(" Error: JLC2KiCadLib not found. Install with: pip install JLC2KiCadLib") 166 | return None 167 | 168 | 169 | def extract_symbol_from_file(sym_file: Path, lcsc_code: str) -> str | None: 170 | """ 171 | Extract the symbol definition from a .kicad_sym file. 172 | Returns the symbol S-expression string or None. 173 | """ 174 | content = sym_file.read_text(encoding='utf-8') 175 | 176 | # Find the main symbol definition (not the _0_1 or _1_1 sub-symbols) 177 | # Pattern: (symbol "NAME" (in_bom ...) ... until matching close paren 178 | 179 | # Simple approach: find all top-level symbols 180 | lines = content.split('\n') 181 | in_symbol = False 182 | symbol_lines = [] 183 | paren_depth = 0 184 | 185 | for line in lines: 186 | if '(symbol "' in line and '_0_1' not in line and '_1_1' not in line and not in_symbol: 187 | # Check if this is a top-level symbol (not nested) 188 | if line.strip().startswith('(symbol "'): 189 | in_symbol = True 190 | paren_depth = 0 191 | 192 | if in_symbol: 193 | symbol_lines.append(line) 194 | paren_depth += line.count('(') - line.count(')') 195 | 196 | if paren_depth <= 0 and len(symbol_lines) > 1: 197 | break 198 | 199 | if symbol_lines: 200 | symbol_text = '\n'.join(symbol_lines) 201 | # Ensure LCSC property exists 202 | if f'"LCSC"' not in symbol_text: 203 | # Add LCSC property after Value property 204 | symbol_text = re.sub( 205 | r'(\(property "Value"[^)]+\)\s*\))', 206 | f'\\1\n (property "LCSC" "{lcsc_code}" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))', 207 | symbol_text 208 | ) 209 | return symbol_text 210 | 211 | return None 212 | 213 | 214 | def append_symbol_to_library(library_path: Path, symbol_text: str): 215 | """ 216 | Append a symbol definition to the JLCPCB.kicad_sym library. 217 | """ 218 | content = library_path.read_text(encoding='utf-8') 219 | 220 | # Find the closing paren of the library 221 | # Insert the new symbol before the final ) 222 | if content.rstrip().endswith(')'): 223 | # Remove final ) and add new symbol + ) 224 | content = content.rstrip()[:-1] 225 | content += f"\n\n {symbol_text}\n\n)" 226 | library_path.write_text(content, encoding='utf-8') 227 | return True 228 | 229 | return False 230 | 231 | 232 | def ensure_symbols(pin_model_path: Path, library_path: Path, dry_run: bool = False) -> Dict[str, str]: 233 | """ 234 | Main function to ensure all symbols exist. 235 | 236 | Returns dict of {lcsc_code: status} where status is: 237 | - "exists" - already in library 238 | - "added" - successfully downloaded and added 239 | - "failed" - could not download 240 | """ 241 | print(f"Reading: {pin_model_path}") 242 | lcsc_parts = extract_lcsc_codes(pin_model_path) 243 | print(f" Found {len(lcsc_parts)} unique LCSC codes") 244 | 245 | print(f"\nChecking library: {library_path}") 246 | existing = get_existing_symbols(library_path) 247 | print(f" Library has {len(existing)} symbols with LCSC codes") 248 | 249 | # Find missing symbols 250 | missing = set(lcsc_parts.keys()) - existing 251 | 252 | if not missing: 253 | print("\n All symbols present!") 254 | return {code: "exists" for code in lcsc_parts} 255 | 256 | print(f"\n Missing {len(missing)} symbols:") 257 | for code in sorted(missing): 258 | print(f" - {code}: {lcsc_parts[code]}") 259 | 260 | if dry_run: 261 | print("\n Dry run - not downloading") 262 | return {code: "exists" if code in existing else "missing" for code in lcsc_parts} 263 | 264 | # Download missing symbols 265 | results = {code: "exists" for code in existing} 266 | 267 | with tempfile.TemporaryDirectory() as temp_dir: 268 | temp_path = Path(temp_dir) 269 | 270 | for code in sorted(missing): 271 | print(f"\n Downloading {code} ({lcsc_parts[code]})...") 272 | 273 | # Clear temp dir for each download 274 | for item in temp_path.iterdir(): 275 | if item.is_dir(): 276 | shutil.rmtree(item) 277 | else: 278 | item.unlink() 279 | 280 | sym_file = download_symbol(code, temp_path) 281 | 282 | if sym_file: 283 | symbol_text = extract_symbol_from_file(sym_file, code) 284 | if symbol_text: 285 | if append_symbol_to_library(library_path, symbol_text): 286 | print(f" Added to library") 287 | results[code] = "added" 288 | else: 289 | print(f" Failed to append to library") 290 | results[code] = "failed" 291 | else: 292 | print(f" Could not extract symbol") 293 | results[code] = "failed" 294 | else: 295 | results[code] = "failed" 296 | 297 | # Summary 298 | added = sum(1 for s in results.values() if s == "added") 299 | failed = sum(1 for s in results.values() if s == "failed") 300 | print(f"\n Summary: {added} added, {failed} failed, {len(existing)} already existed") 301 | 302 | return results 303 | 304 | 305 | def main(): 306 | import argparse 307 | 308 | parser = argparse.ArgumentParser(description='Ensure all symbols exist in JLCPCB library') 309 | parser.add_argument('--parts', type=Path, help='Path to parts file (step2_parts_complete.yaml or pin_model.json)') 310 | parser.add_argument('--pin-model', type=Path, help='Alias for --parts (deprecated)') 311 | parser.add_argument('--library', type=Path, help='Path to JLCPCB.kicad_sym') 312 | parser.add_argument('--dry-run', action='store_true', help='Check only, do not download') 313 | parser.add_argument('--create-library', action='store_true', help='Create library file if it does not exist') 314 | args = parser.parse_args() 315 | 316 | # Default paths 317 | script_dir = Path(__file__).parent 318 | tools_dir = script_dir.parent # KiCAD-Generator-tools directory 319 | 320 | # Central library location (shared across all projects) 321 | central_library = tools_dir / "libs" / "JLCPCB" / "symbol" / "JLCPCB.kicad_sym" 322 | 323 | # Find parts file (prefer --parts, fall back to --pin-model, then defaults) 324 | parts_file = args.parts or args.pin_model 325 | if not parts_file: 326 | # Try current directory first, then tools_dir 327 | cwd = Path.cwd() 328 | candidates = [ 329 | cwd / "work" / "step2_parts_complete.yaml", 330 | cwd / "work" / "pin_model.json", 331 | tools_dir / "work" / "step2_parts_complete.yaml", 332 | tools_dir / "work" / "pin_model.json", 333 | ] 334 | for candidate in candidates: 335 | if candidate.exists(): 336 | parts_file = candidate 337 | break 338 | 339 | if not parts_file: 340 | print(f"Error: No parts file found") 341 | print(f" Looked for:") 342 | for c in candidates: 343 | print(f" - {c}") 344 | print(f" Specify with --parts ") 345 | return 1 346 | 347 | # Use central library by default, fall back to project-local library 348 | library = args.library 349 | if not library: 350 | if central_library.exists(): 351 | library = central_library 352 | else: 353 | # Fall back to project-local library 354 | library = Path.cwd() / "output" / "libs" / "JLCPCB" / "symbol" / "JLCPCB.kicad_sym" 355 | 356 | if not parts_file.exists(): 357 | print(f"Error: Parts file not found at {parts_file}") 358 | return 1 359 | 360 | if not library.exists(): 361 | if args.create_library: 362 | # Create empty library 363 | library.parent.mkdir(parents=True, exist_ok=True) 364 | library.write_text('(kicad_symbol_lib (version 20220914) (generator ensure_symbols)\n\n)\n') 365 | print(f"Created empty library: {library}") 366 | else: 367 | print(f"Error: Library not found at {library}") 368 | print(" Create the library first, specify --library path, or use --create-library") 369 | return 1 370 | 371 | results = ensure_symbols(parts_file, library, dry_run=args.dry_run) 372 | 373 | # Exit with error if any failed 374 | if any(s == "failed" for s in results.values()): 375 | return 1 376 | return 0 377 | 378 | 379 | if __name__ == "__main__": 380 | exit(main()) 381 | -------------------------------------------------------------------------------- /jlcpcb_parts_pipeline/parts_requirements.yaml: -------------------------------------------------------------------------------- 1 | meta: 2 | generated_on: '2025-12-16' 3 | source_document: DESIGN_SPEC.md 4 | project: ESP32-S3 Portable Radio Receiver 5 | notes: 6 | - Prefer JLCPCB Basic parts where possible (lower assembly fee) 7 | - Secondary preference for Promotional Extended parts 8 | - LCSC codes provided where known from design spec 9 | - KiCAD symbol/footprint mappings included for import 10 | part_type_preference: 11 | - basic 12 | - promotional_extended 13 | - extended 14 | 15 | parts: 16 | # === MAIN ICs === 17 | - designator: U1 18 | mpn: ESP32-S3-MINI-1-N8 19 | lcsc: C2913206 20 | function: Main MCU module 21 | package: Module 22 | part_type: extended 23 | symbol: RF_Module:ESP32-S3-MINI-1 24 | footprint: RF_Module:ESP32-S3-MINI-1 25 | datasheet: https://www.espressif.com/sites/default/files/documentation/esp32-s3-mini-1_mini-1u_datasheet_en.pdf 26 | 27 | - designator: U2 28 | mpn: TP4056 29 | lcsc: C16581 30 | function: Li-Ion battery charger IC 31 | package: ESOP-8 32 | part_type: extended 33 | symbol: Battery_Management:TP4056 34 | footprint: Package_SO:SOIC-8_3.9x4.9mm_P1.27mm 35 | datasheet: https://dlnmh9ip6v2uc.cloudfront.net/datasheets/Prototyping/TP4056.pdf 36 | 37 | - designator: U3 38 | mpn: AMS1117-3.3 39 | lcsc: C6186 40 | function: 3.3V LDO regulator 41 | package: SOT-223 42 | part_type: basic 43 | symbol: Regulator_Linear:AMS1117-3.3 44 | footprint: Package_TO_SOT_SMD:SOT-223-3_TabPin2 45 | datasheet: http://www.advanced-monolithic.com/pdf/ds1117.pdf 46 | 47 | - designator: U4 48 | mpn: SI4735-D60-GU 49 | lcsc: C195417 50 | function: AM/FM/SW radio receiver IC 51 | package: SSOP-24 52 | part_type: extended 53 | symbol: Interface_Expansion:SI4735-D60-GU 54 | footprint: Package_SO:SSOP-24_5.3x8.2mm_P0.65mm 55 | datasheet: https://www.silabs.com/documents/public/data-sheets/Si4730-31-34-35-D60.pdf 56 | 57 | # === LEDs === 58 | - designator: D1 59 | mpn: WS2812B-B 60 | lcsc: C2761795 61 | function: NeoPixel RGB LED 62 | package: "5050" 63 | part_type: extended 64 | symbol: LED:WS2812B 65 | footprint: LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm 66 | datasheet: https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf 67 | 68 | - designator: D2 69 | mpn: WS2812B-B 70 | lcsc: C2761795 71 | function: NeoPixel RGB LED 72 | package: "5050" 73 | part_type: extended 74 | symbol: LED:WS2812B 75 | footprint: LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm 76 | datasheet: https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf 77 | 78 | - designator: D3 79 | mpn: WS2812B-B 80 | lcsc: C2761795 81 | function: NeoPixel RGB LED 82 | package: "5050" 83 | part_type: extended 84 | symbol: LED:WS2812B 85 | footprint: LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm 86 | datasheet: https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf 87 | 88 | # === CONNECTORS === 89 | - designator: J1 90 | mpn: TYPE-C-31-M-12 91 | lcsc: C393939 92 | function: USB-C 16-pin power + data connector 93 | package: SMD 94 | part_type: basic 95 | symbol: Connector:USB_C_Receptacle_USB2.0 96 | footprint: Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12 97 | datasheet: https://www.lcsc.com/datasheet/lcsc_datasheet_1811131825_Korean-Hroparts-Elec-TYPE-C-31-M-12_C393939.pdf 98 | 99 | - designator: J2 100 | mpn: S2B-PH-K-S 101 | lcsc: C131337 102 | function: JST-PH 2-pin battery connector 103 | package: SMD 104 | part_type: basic 105 | symbol: Connector:Conn_01x02 106 | footprint: Connector_JST:JST_PH_S2B-PH-K_1x02_P2.00mm_Horizontal 107 | datasheet: https://www.jst-mfg.com/product/pdf/eng/ePH.pdf 108 | 109 | - designator: J3 110 | mpn: PJ-320A 111 | lcsc: C145819 112 | function: 3.5mm TRS headphone jack 113 | package: TH 114 | part_type: extended 115 | symbol: Connector:AudioJack3 116 | footprint: Connector_Audio:Jack_3.5mm_PJ320A_Horizontal 117 | datasheet: https://datasheet.lcsc.com/szlcsc/PJ-320A_C145819.pdf 118 | 119 | - designator: J4 120 | mpn: 1x04 Pin Header 2.54mm 121 | lcsc: C2337 122 | function: OLED I2C display header 123 | package: TH 2.54mm 124 | part_type: basic 125 | symbol: Connector:Conn_01x04 126 | footprint: Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical 127 | datasheet: "" 128 | 129 | # === USER INTERFACE === 130 | - designator: SW1 131 | mpn: TS-1187A-B-A-B 132 | lcsc: C127509 133 | function: Reset button (tactile switch) 134 | package: SMD 6x6mm 135 | part_type: basic 136 | symbol: Switch:SW_Push 137 | footprint: Button_Switch_SMD:SW_SPST_TL3342 138 | datasheet: https://datasheet.lcsc.com/szlcsc/TS-1187A-B-A-B_C127509.pdf 139 | 140 | - designator: ENC1 141 | mpn: EC11E18244A5 142 | lcsc: C255515 143 | function: Rotary encoder with push button (tuning) 144 | package: TH 145 | part_type: extended 146 | symbol: Device:Rotary_Encoder_Switch 147 | footprint: Rotary_Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm 148 | datasheet: https://tech.alpsalpine.com/prod/e/html/encoder/incremental/ec11/ec11e18244a5.html 149 | 150 | - designator: ENC2 151 | mpn: EC11E18244A5 152 | lcsc: C255515 153 | function: Rotary encoder with push button (volume) 154 | package: TH 155 | part_type: extended 156 | symbol: Device:Rotary_Encoder_Switch 157 | footprint: Rotary_Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm 158 | datasheet: https://tech.alpsalpine.com/prod/e/html/encoder/incremental/ec11/ec11e18244a5.html 159 | 160 | # === CAPACITORS === 161 | - designator: C1 162 | mpn: CL21A106KOQNNNE 163 | lcsc: C15850 164 | function: USB-C VBUS input filter 165 | package: "0805" 166 | value: 10uF 167 | part_type: basic 168 | symbol: Device:C 169 | footprint: Capacitor_SMD:C_0805_2012Metric 170 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL21A106KOQNNNE_C15850.pdf 171 | 172 | - designator: C2 173 | mpn: CL21A106KOQNNNE 174 | lcsc: C15850 175 | function: TP4056 input capacitor 176 | package: "0805" 177 | value: 10uF 178 | part_type: basic 179 | symbol: Device:C 180 | footprint: Capacitor_SMD:C_0805_2012Metric 181 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL21A106KOQNNNE_C15850.pdf 182 | 183 | - designator: C3 184 | mpn: CL21A106KOQNNNE 185 | lcsc: C15850 186 | function: TP4056 output/battery capacitor 187 | package: "0805" 188 | value: 10uF 189 | part_type: basic 190 | symbol: Device:C 191 | footprint: Capacitor_SMD:C_0805_2012Metric 192 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL21A106KOQNNNE_C15850.pdf 193 | 194 | - designator: C4 195 | mpn: CL21A226MQQNNNE 196 | lcsc: C45783 197 | function: AMS1117 input capacitor 198 | package: "0805" 199 | value: 22uF 200 | part_type: basic 201 | symbol: Device:C 202 | footprint: Capacitor_SMD:C_0805_2012Metric 203 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL21A226MQQNNNE_C45783.pdf 204 | 205 | - designator: C5 206 | mpn: CL21A226MQQNNNE 207 | lcsc: C45783 208 | function: AMS1117 output capacitor 209 | package: "0805" 210 | value: 22uF 211 | part_type: basic 212 | symbol: Device:C 213 | footprint: Capacitor_SMD:C_0805_2012Metric 214 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL21A226MQQNNNE_C45783.pdf 215 | 216 | - designator: C6 217 | mpn: CL10B104KB8NNNC 218 | lcsc: C14663 219 | function: ESP32 decoupling 220 | package: "0603" 221 | value: 100nF 222 | part_type: basic 223 | symbol: Device:C 224 | footprint: Capacitor_SMD:C_0603_1608Metric 225 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL10B104KB8NNNC_C14663.pdf 226 | 227 | - designator: C7 228 | mpn: CL10B104KB8NNNC 229 | lcsc: C14663 230 | function: ESP32 decoupling 231 | package: "0603" 232 | value: 100nF 233 | part_type: basic 234 | symbol: Device:C 235 | footprint: Capacitor_SMD:C_0603_1608Metric 236 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL10B104KB8NNNC_C14663.pdf 237 | 238 | - designator: C8 239 | mpn: CL10B104KB8NNNC 240 | lcsc: C14663 241 | function: ESP32 decoupling 242 | package: "0603" 243 | value: 100nF 244 | part_type: basic 245 | symbol: Device:C 246 | footprint: Capacitor_SMD:C_0603_1608Metric 247 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL10B104KB8NNNC_C14663.pdf 248 | 249 | - designator: C9 250 | mpn: CL10B104KB8NNNC 251 | lcsc: C14663 252 | function: WS2812B D1 decoupling 253 | package: "0603" 254 | value: 100nF 255 | part_type: basic 256 | symbol: Device:C 257 | footprint: Capacitor_SMD:C_0603_1608Metric 258 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL10B104KB8NNNC_C14663.pdf 259 | 260 | - designator: C10 261 | mpn: CL10B104KB8NNNC 262 | lcsc: C14663 263 | function: WS2812B D2 decoupling 264 | package: "0603" 265 | value: 100nF 266 | part_type: basic 267 | symbol: Device:C 268 | footprint: Capacitor_SMD:C_0603_1608Metric 269 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL10B104KB8NNNC_C14663.pdf 270 | 271 | - designator: C11 272 | mpn: CL10B104KB8NNNC 273 | lcsc: C14663 274 | function: WS2812B D3 decoupling 275 | package: "0603" 276 | value: 100nF 277 | part_type: basic 278 | symbol: Device:C 279 | footprint: Capacitor_SMD:C_0603_1608Metric 280 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL10B104KB8NNNC_C14663.pdf 281 | 282 | - designator: C12 283 | mpn: CL10B104KB8NNNC 284 | lcsc: C14663 285 | function: SI4735 VDD bypass 286 | package: "0603" 287 | value: 100nF 288 | part_type: basic 289 | symbol: Device:C 290 | footprint: Capacitor_SMD:C_0603_1608Metric 291 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL10B104KB8NNNC_C14663.pdf 292 | 293 | - designator: C13 294 | mpn: CL10C220JB8NNNC 295 | lcsc: C1653 296 | function: SI4735 crystal load capacitor 297 | package: "0603" 298 | value: 22pF 299 | part_type: basic 300 | symbol: Device:C 301 | footprint: Capacitor_SMD:C_0603_1608Metric 302 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL10C220JB8NNNC_C1653.pdf 303 | 304 | - designator: C14 305 | mpn: CL10C220JB8NNNC 306 | lcsc: C1653 307 | function: SI4735 crystal load capacitor 308 | package: "0603" 309 | value: 22pF 310 | part_type: basic 311 | symbol: Device:C 312 | footprint: Capacitor_SMD:C_0603_1608Metric 313 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL10C220JB8NNNC_C1653.pdf 314 | 315 | - designator: C15 316 | mpn: CL10B104KB8NNNC 317 | lcsc: C14663 318 | function: SI4735 VA bypass 319 | package: "0603" 320 | value: 100nF 321 | part_type: basic 322 | symbol: Device:C 323 | footprint: Capacitor_SMD:C_0603_1608Metric 324 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL10B104KB8NNNC_C14663.pdf 325 | 326 | - designator: C16 327 | mpn: CL10B104KB8NNNC 328 | lcsc: C14663 329 | function: SI4735 VD bypass 330 | package: "0603" 331 | value: 100nF 332 | part_type: basic 333 | symbol: Device:C 334 | footprint: Capacitor_SMD:C_0603_1608Metric 335 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL10B104KB8NNNC_C14663.pdf 336 | 337 | - designator: C17 338 | mpn: CL10B104KB8NNNC 339 | lcsc: C14663 340 | function: ESP32 EN pin noise filter 341 | package: "0603" 342 | value: 100nF 343 | part_type: basic 344 | symbol: Device:C 345 | footprint: Capacitor_SMD:C_0603_1608Metric 346 | datasheet: https://datasheet.lcsc.com/szlcsc/Samsung-Electro-Mechanics-CL10B104KB8NNNC_C14663.pdf 347 | 348 | # === RESISTORS === 349 | - designator: R1 350 | mpn: 0603WAF5101T5E 351 | lcsc: C23186 352 | function: USB-C CC1 pulldown 353 | package: "0603" 354 | value: 5.1k 355 | part_type: basic 356 | symbol: Device:R 357 | footprint: Resistor_SMD:R_0603_1608Metric 358 | datasheet: https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-0603WAF5101T5E_C23186.pdf 359 | 360 | - designator: R2 361 | mpn: 0603WAF5101T5E 362 | lcsc: C23186 363 | function: USB-C CC2 pulldown 364 | package: "0603" 365 | value: 5.1k 366 | part_type: basic 367 | symbol: Device:R 368 | footprint: Resistor_SMD:R_0603_1608Metric 369 | datasheet: https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-0603WAF5101T5E_C23186.pdf 370 | 371 | - designator: R3 372 | mpn: 0603WAF2001T5E 373 | lcsc: C22975 374 | function: TP4056 PROG (sets 500mA charge current) 375 | package: "0603" 376 | value: 2k 377 | part_type: basic 378 | symbol: Device:R 379 | footprint: Resistor_SMD:R_0603_1608Metric 380 | datasheet: https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-0603WAF2001T5E_C22975.pdf 381 | 382 | - designator: R4 383 | mpn: 0603WAF1002T5E 384 | lcsc: C25804 385 | function: ESP32 EN pullup 386 | package: "0603" 387 | value: 10k 388 | part_type: basic 389 | symbol: Device:R 390 | footprint: Resistor_SMD:R_0603_1608Metric 391 | datasheet: https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-0603WAF1002T5E_C25804.pdf 392 | 393 | - designator: R5 394 | mpn: 0603WAF4701T5E 395 | lcsc: C25900 396 | function: I2C SDA pullup 397 | package: "0603" 398 | value: 4.7k 399 | part_type: basic 400 | symbol: Device:R 401 | footprint: Resistor_SMD:R_0603_1608Metric 402 | datasheet: https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-0603WAF4701T5E_C25900.pdf 403 | 404 | - designator: R6 405 | mpn: 0603WAF4701T5E 406 | lcsc: C25900 407 | function: I2C SCL pullup 408 | package: "0603" 409 | value: 4.7k 410 | part_type: basic 411 | symbol: Device:R 412 | footprint: Resistor_SMD:R_0603_1608Metric 413 | datasheet: https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-0603WAF4701T5E_C25900.pdf 414 | 415 | - designator: R7 416 | mpn: 0603WAF1000T5E 417 | lcsc: C22775 418 | function: Audio output series resistor (Left) 419 | package: "0603" 420 | value: 100R 421 | part_type: basic 422 | symbol: Device:R 423 | footprint: Resistor_SMD:R_0603_1608Metric 424 | datasheet: https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-0603WAF1000T5E_C22775.pdf 425 | 426 | - designator: R8 427 | mpn: 0603WAF1000T5E 428 | lcsc: C22775 429 | function: Audio output series resistor (Right) 430 | package: "0603" 431 | value: 100R 432 | part_type: basic 433 | symbol: Device:R 434 | footprint: Resistor_SMD:R_0603_1608Metric 435 | datasheet: https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-0603WAF1000T5E_C22775.pdf 436 | 437 | # === CRYSTAL === 438 | - designator: Y1 439 | mpn: X322532768KSB4SI 440 | lcsc: C32346 441 | function: SI4735 32.768kHz reference crystal 442 | package: "3215" 443 | value: 32.768kHz 444 | part_type: basic 445 | symbol: Device:Crystal 446 | footprint: Crystal:Crystal_SMD_3215-2Pin_3.2x1.5mm 447 | datasheet: https://datasheet.lcsc.com/szlcsc/Yangxing-Tech-X322532768KSB4SI_C32346.pdf 448 | --------------------------------------------------------------------------------