Phase 1: Schematic Workflow Fix (Issue #26) - Fixed broken schematic workflow using template-based symbol cloning - Updated create_project to generate both PCB and schematic files - Rewrote add_schematic_component to use kicad-skip clone() API - Added template schematics with cloneable R, C, LED symbols - All schematic tests now passing Phase 2: JLCPCB Integration Complete - Integrated JLCSearch public API (no authentication required) - Access to ~100k JLCPCB parts with real-time stock and pricing - Implemented parametric search for resistors, capacitors, components - Added package-to-footprint mapping for KiCad integration - Cost optimization with Basic vs Extended library classification - Alternative part suggestions with price comparison New Components: - python/commands/jlcsearch.py - JLCSearch API client - python/templates/ - Template schematics for symbol cloning - docs/JLCPCB_INTEGRATION.md - Comprehensive API documentation - docs/SCHEMATIC_WORKFLOW_FIX.md - Phase 1 technical details - CHANGELOG.md - Consolidated unified changelog - PHASE_2_COMPLETE.md - Phase 2 implementation summary MCP Tools Available: - download_jlcpcb_database - Download full parts catalog - search_jlcpcb_parts - Parametric search with filters - get_jlcpcb_part - Part details + footprint suggestions - get_jlcpcb_database_stats - Database statistics - suggest_jlcpcb_alternatives - Find similar/cheaper parts Technical Improvements: - SQLite database with FTS5 full-text search - HMAC-SHA256 authentication support (official JLCPCB API) - Improved .gitignore to exclude credentials and databases - Template-based schematic creation workflow Testing: - All integration tests passing - Database operations validated - Live API connectivity confirmed - Schematic workflow end-to-end verified Credits: - JLCSearch API: @tscircuit - Local JLCPCB search: @l3wi Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
247 lines
7.9 KiB
Python
247 lines
7.9 KiB
Python
"""
|
|
JLCSearch API client (public, no authentication required)
|
|
|
|
Alternative to official JLCPCB API using the community-maintained
|
|
jlcsearch service at https://jlcsearch.tscircuit.com/
|
|
"""
|
|
|
|
import logging
|
|
import requests
|
|
from typing import Optional, Dict, List, Callable
|
|
import time
|
|
|
|
logger = logging.getLogger('kicad_interface')
|
|
|
|
|
|
class JLCSearchClient:
|
|
"""
|
|
Client for JLCSearch public API (tscircuit)
|
|
|
|
Provides access to JLCPCB parts database without authentication
|
|
via the community-maintained jlcsearch service.
|
|
"""
|
|
|
|
BASE_URL = "https://jlcsearch.tscircuit.com"
|
|
|
|
def __init__(self):
|
|
"""Initialize JLCSearch API client"""
|
|
pass
|
|
|
|
def search_components(
|
|
self,
|
|
category: str = "components",
|
|
limit: int = 100,
|
|
offset: int = 0,
|
|
**filters
|
|
) -> List[Dict]:
|
|
"""
|
|
Search components in JLCSearch database
|
|
|
|
Args:
|
|
category: Component category (e.g., "resistors", "capacitors", "components")
|
|
limit: Maximum number of results
|
|
offset: Offset for pagination
|
|
**filters: Additional filters (e.g., package="0603", resistance=1000)
|
|
|
|
Returns:
|
|
List of component dicts
|
|
"""
|
|
url = f"{self.BASE_URL}/{category}/list.json"
|
|
|
|
params = {
|
|
"limit": limit,
|
|
"offset": offset,
|
|
**filters
|
|
}
|
|
|
|
try:
|
|
response = requests.get(url, params=params, timeout=30)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
# The response has the category name as key
|
|
# e.g., {"resistors": [...]} or {"components": [...]}
|
|
for key, value in data.items():
|
|
if isinstance(value, list):
|
|
return value
|
|
|
|
return []
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
logger.error(f"Failed to search JLCSearch: {e}")
|
|
raise Exception(f"JLCSearch API request failed: {e}")
|
|
|
|
def search_resistors(self, resistance: Optional[int] = None, package: Optional[str] = None, limit: int = 100) -> List[Dict]:
|
|
"""
|
|
Search for resistors
|
|
|
|
Args:
|
|
resistance: Resistance value in ohms
|
|
package: Package type (e.g., "0603", "0805")
|
|
limit: Maximum results
|
|
|
|
Returns:
|
|
List of resistor dicts with fields:
|
|
- lcsc: LCSC number (integer)
|
|
- mfr: Manufacturer part number
|
|
- package: Package size
|
|
- is_basic: True if basic library part
|
|
- resistance: Resistance in ohms
|
|
- tolerance_fraction: Tolerance (0.01 = 1%)
|
|
- power_watts: Power rating in mW
|
|
- stock: Available stock
|
|
- price1: Price per unit
|
|
"""
|
|
filters = {}
|
|
if resistance is not None:
|
|
filters["resistance"] = resistance
|
|
if package:
|
|
filters["package"] = package
|
|
|
|
return self.search_components("resistors", limit=limit, **filters)
|
|
|
|
def search_capacitors(self, capacitance: Optional[float] = None, package: Optional[str] = None, limit: int = 100) -> List[Dict]:
|
|
"""
|
|
Search for capacitors
|
|
|
|
Args:
|
|
capacitance: Capacitance value in farads
|
|
package: Package type
|
|
limit: Maximum results
|
|
|
|
Returns:
|
|
List of capacitor dicts
|
|
"""
|
|
filters = {}
|
|
if capacitance is not None:
|
|
filters["capacitance"] = capacitance
|
|
if package:
|
|
filters["package"] = package
|
|
|
|
return self.search_components("capacitors", limit=limit, **filters)
|
|
|
|
def get_part_by_lcsc(self, lcsc_number: int) -> Optional[Dict]:
|
|
"""
|
|
Get part details by LCSC number
|
|
|
|
Args:
|
|
lcsc_number: LCSC number (integer, without 'C' prefix)
|
|
|
|
Returns:
|
|
Part dict or None if not found
|
|
"""
|
|
# Search across all components filtering by LCSC
|
|
# Note: jlcsearch doesn't have a dedicated single-part endpoint
|
|
# so we search and filter
|
|
try:
|
|
results = self.search_components("components", limit=1, lcsc=lcsc_number)
|
|
return results[0] if results else None
|
|
except Exception as e:
|
|
logger.error(f"Failed to get part C{lcsc_number}: {e}")
|
|
return None
|
|
|
|
def download_all_components(
|
|
self,
|
|
callback: Optional[Callable[[int, str], None]] = None,
|
|
batch_size: int = 1000
|
|
) -> List[Dict]:
|
|
"""
|
|
Download all components from jlcsearch database
|
|
|
|
Args:
|
|
callback: Optional progress callback function(parts_count, status_msg)
|
|
batch_size: Number of parts per batch
|
|
|
|
Returns:
|
|
List of all parts
|
|
"""
|
|
all_parts = []
|
|
offset = 0
|
|
|
|
logger.info("Starting full jlcsearch parts database download...")
|
|
|
|
while True:
|
|
try:
|
|
batch = self.search_components(
|
|
"components",
|
|
limit=batch_size,
|
|
offset=offset
|
|
)
|
|
|
|
if not batch:
|
|
break
|
|
|
|
all_parts.extend(batch)
|
|
offset += len(batch)
|
|
|
|
if callback:
|
|
callback(len(all_parts), f"Downloaded {len(all_parts)} parts...")
|
|
else:
|
|
logger.info(f"Downloaded {len(all_parts)} parts so far...")
|
|
|
|
# If we got fewer results than requested, we've reached the end
|
|
if len(batch) < batch_size:
|
|
break
|
|
|
|
# Rate limiting - be nice to the API
|
|
time.sleep(0.1)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error downloading parts at offset {offset}: {e}")
|
|
if len(all_parts) > 0:
|
|
logger.warning(f"Partial download available: {len(all_parts)} parts")
|
|
return all_parts
|
|
else:
|
|
raise
|
|
|
|
logger.info(f"Download complete: {len(all_parts)} parts retrieved")
|
|
return all_parts
|
|
|
|
|
|
def test_jlcsearch_connection() -> bool:
|
|
"""
|
|
Test JLCSearch API connection
|
|
|
|
Returns:
|
|
True if connection successful, False otherwise
|
|
"""
|
|
try:
|
|
client = JLCSearchClient()
|
|
# Test by searching for 1k resistors
|
|
results = client.search_resistors(resistance=1000, limit=5)
|
|
logger.info(f"JLCSearch API connection test successful - found {len(results)} resistors")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"JLCSearch API connection test failed: {e}")
|
|
return False
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# Test the JLCSearch client
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
print("Testing JLCSearch API connection...")
|
|
if test_jlcsearch_connection():
|
|
print("✓ Connection successful!")
|
|
|
|
client = JLCSearchClient()
|
|
|
|
print("\nSearching for 1k 0603 resistors...")
|
|
resistors = client.search_resistors(resistance=1000, package="0603", limit=5)
|
|
print(f"✓ Found {len(resistors)} resistors")
|
|
|
|
if resistors:
|
|
print(f"\nExample resistor:")
|
|
r = resistors[0]
|
|
print(f" LCSC: C{r.get('lcsc')}")
|
|
print(f" MFR: {r.get('mfr')}")
|
|
print(f" Package: {r.get('package')}")
|
|
print(f" Resistance: {r.get('resistance')}Ω")
|
|
print(f" Tolerance: {r.get('tolerance_fraction', 0) * 100}%")
|
|
print(f" Power: {r.get('power_watts')}mW")
|
|
print(f" Stock: {r.get('stock')}")
|
|
print(f" Price: ${r.get('price1')}")
|
|
print(f" Basic Library: {'Yes' if r.get('is_basic') else 'No'}")
|
|
else:
|
|
print("✗ Connection failed")
|