initial: add all custom Claude.ai skills
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check BOM output for common issues.
|
||||
|
||||
Usage:
|
||||
python bom_check.py path/to/bom.csv
|
||||
|
||||
Checks:
|
||||
- All components have footprint assigned
|
||||
- All components have value assigned (resistors, capacitors, inductors)
|
||||
- No duplicate reference designators
|
||||
- Passive values are standard E-series (basic check)
|
||||
- Flags DNP components
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Standard E12 values (covers most common passives)
|
||||
E12_VALUES = {1.0, 1.2, 1.5, 1.8, 2.2, 2.7, 3.3, 3.9, 4.7, 5.6, 6.8, 8.2}
|
||||
|
||||
PASSIVE_PREFIXES = {"R", "C", "L"}
|
||||
|
||||
|
||||
def normalize_value(value_str: str) -> float | None:
|
||||
"""Try to extract numeric value from component value string."""
|
||||
match = re.match(r"^(\d+\.?\d*)\s*([kKmMuUnNpP]?)", value_str.strip())
|
||||
if not match:
|
||||
return None
|
||||
|
||||
num = float(match.group(1))
|
||||
suffix = match.group(2).lower()
|
||||
|
||||
multipliers = {"k": 1e3, "m": 1e6, "u": 1e-6, "n": 1e-9, "p": 1e-12}
|
||||
return num * multipliers.get(suffix, 1.0)
|
||||
|
||||
|
||||
def is_e12_compatible(value: float) -> bool:
|
||||
"""Check if a value falls on an E12 series value (within 5% tolerance)."""
|
||||
if value <= 0:
|
||||
return False
|
||||
|
||||
# Normalize to 1.0-9.99 range
|
||||
normalized = value
|
||||
while normalized >= 10:
|
||||
normalized /= 10
|
||||
while normalized < 1:
|
||||
normalized *= 10
|
||||
|
||||
for e12 in E12_VALUES:
|
||||
if abs(normalized - e12) / e12 < 0.05:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_bom(filepath: Path) -> tuple[list[str], list[str]]:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
refs_seen: set[str] = set()
|
||||
|
||||
with filepath.open(newline="", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
|
||||
if reader.fieldnames is None:
|
||||
errors.append("BOM file has no headers")
|
||||
return errors, warnings
|
||||
|
||||
# Try common column name patterns
|
||||
ref_col = next((c for c in reader.fieldnames if c.lower() in ("ref", "reference", "designator")), None)
|
||||
val_col = next((c for c in reader.fieldnames if c.lower() in ("value", "val")), None)
|
||||
fp_col = next((c for c in reader.fieldnames if c.lower() in ("footprint", "package", "fp")), None)
|
||||
|
||||
if ref_col is None:
|
||||
errors.append("Cannot find reference designator column in BOM")
|
||||
return errors, warnings
|
||||
|
||||
for row_num, row in enumerate(reader, start=2):
|
||||
ref = row.get(ref_col, "").strip()
|
||||
if not ref:
|
||||
continue
|
||||
|
||||
# Duplicate check
|
||||
if ref in refs_seen:
|
||||
errors.append(f"Row {row_num}: duplicate reference '{ref}'")
|
||||
refs_seen.add(ref)
|
||||
|
||||
# Footprint check
|
||||
footprint = row.get(fp_col, "").strip() if fp_col else ""
|
||||
if not footprint:
|
||||
errors.append(f"{ref}: missing footprint")
|
||||
|
||||
# Value check for passives
|
||||
value = row.get(val_col, "").strip() if val_col else ""
|
||||
prefix = re.match(r"^[A-Z]+", ref)
|
||||
if prefix and prefix.group() in PASSIVE_PREFIXES:
|
||||
if not value or value.upper() == "DNP":
|
||||
if value.upper() == "DNP":
|
||||
warnings.append(f"{ref}: marked as DNP — verify this is intentional")
|
||||
else:
|
||||
errors.append(f"{ref}: passive component missing value")
|
||||
else:
|
||||
numeric = normalize_value(value)
|
||||
if numeric and not is_e12_compatible(numeric):
|
||||
warnings.append(f"{ref}: value '{value}' may not be standard E12 series")
|
||||
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <path/to/bom.csv>")
|
||||
return 1
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
if not path.exists():
|
||||
print(f"File not found: {path}")
|
||||
return 1
|
||||
|
||||
errors, warnings = check_bom(path)
|
||||
|
||||
for w in warnings:
|
||||
print(f" WARN: {w}")
|
||||
for e in errors:
|
||||
print(f" ERROR: {e}")
|
||||
|
||||
print(f"\n {len(errors)} errors, {len(warnings)} warnings")
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate MCU database structure against the expected schema.
|
||||
|
||||
Usage:
|
||||
python validate_mcu_db.py path/to/mcu_db.py
|
||||
|
||||
Checks:
|
||||
- All required keys present at each level
|
||||
- Pin numbers unique within each package
|
||||
- Every package has at least one power and one ground pin
|
||||
- Capability counts are non-negative integers
|
||||
- Voltage profiles within valid ranges
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REQUIRED_PACKAGE_KEYS = {"description", "pin_count", "pinmap", "capabilities"}
|
||||
REQUIRED_CAPABILITY_KEYS = {"uart_count", "i2c_count", "spi_count"}
|
||||
VALID_ROLES = {"power", "ground", "reset", "boot", "gpio", "analog", "nc"}
|
||||
VOLTAGE_MIN = 0.5
|
||||
VOLTAGE_MAX = 5.5
|
||||
|
||||
|
||||
def load_mcu_db(path: Path) -> dict[str, Any]:
|
||||
"""Dynamically import mcu_db.py and return MCU_DB dict."""
|
||||
spec = importlib.util.spec_from_file_location("mcu_db", path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise FileNotFoundError(f"Cannot load module from {path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module) # type: ignore[union-attr]
|
||||
|
||||
db = getattr(module, "MCU_DB", None)
|
||||
if db is None:
|
||||
raise ValueError(f"MCU_DB not found in {path}")
|
||||
return db
|
||||
|
||||
|
||||
def validate_voltage_profile(profile: dict, path_prefix: str, errors: list[str]) -> None:
|
||||
for field in ("id", "vcore", "vio"):
|
||||
if field not in profile:
|
||||
errors.append(f"{path_prefix}: missing required field '{field}'")
|
||||
|
||||
for vfield in ("vcore", "vio"):
|
||||
v = profile.get(vfield)
|
||||
if isinstance(v, (int, float)) and not (VOLTAGE_MIN <= v <= VOLTAGE_MAX):
|
||||
errors.append(f"{path_prefix}.{vfield}={v} outside valid range [{VOLTAGE_MIN}, {VOLTAGE_MAX}]")
|
||||
|
||||
|
||||
def validate_pinmap(pinmap: dict, path_prefix: str, errors: list[str], warnings: list[str]) -> None:
|
||||
has_power = False
|
||||
has_ground = False
|
||||
seen_pins: set[Any] = set()
|
||||
|
||||
for pin_num, pin_def in pinmap.items():
|
||||
if pin_num in seen_pins:
|
||||
errors.append(f"{path_prefix}: duplicate pin number {pin_num}")
|
||||
seen_pins.add(pin_num)
|
||||
|
||||
if "signal" not in pin_def:
|
||||
errors.append(f"{path_prefix}.pin[{pin_num}]: missing 'signal'")
|
||||
|
||||
roles = pin_def.get("roles", [])
|
||||
if not roles:
|
||||
warnings.append(f"{path_prefix}.pin[{pin_num}] ({pin_def.get('signal', '?')}): no roles defined")
|
||||
|
||||
for role in roles:
|
||||
if role not in VALID_ROLES:
|
||||
warnings.append(f"{path_prefix}.pin[{pin_num}]: unknown role '{role}'")
|
||||
|
||||
if "power" in roles:
|
||||
has_power = True
|
||||
if "ground" in roles:
|
||||
has_ground = True
|
||||
|
||||
if not has_power:
|
||||
errors.append(f"{path_prefix}: no pin with role 'power' found")
|
||||
if not has_ground:
|
||||
errors.append(f"{path_prefix}: no pin with role 'ground' found")
|
||||
|
||||
|
||||
def validate_capabilities(caps: dict, path_prefix: str, errors: list[str]) -> None:
|
||||
for key in REQUIRED_CAPABILITY_KEYS:
|
||||
if key not in caps:
|
||||
errors.append(f"{path_prefix}: missing required capability '{key}'")
|
||||
|
||||
for key, value in caps.items():
|
||||
if key.endswith("_count") or key.endswith("_channels"):
|
||||
if not isinstance(value, int) or value < 0:
|
||||
errors.append(f"{path_prefix}.{key}={value!r}: must be non-negative int")
|
||||
|
||||
|
||||
def validate_db(db: dict[str, Any]) -> tuple[list[str], list[str]]:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
|
||||
if not isinstance(db, dict):
|
||||
errors.append("MCU_DB must be a dict")
|
||||
return errors, warnings
|
||||
|
||||
for vendor, families in db.items():
|
||||
if not isinstance(families, dict):
|
||||
errors.append(f"MCU_DB['{vendor}'] must be a dict of families")
|
||||
continue
|
||||
|
||||
for family_id, family in families.items():
|
||||
fpath = f"{vendor}/{family_id}"
|
||||
|
||||
if "description" not in family:
|
||||
warnings.append(f"{fpath}: missing 'description'")
|
||||
|
||||
for vp in family.get("voltage_profiles", []):
|
||||
validate_voltage_profile(vp, f"{fpath}/voltage_profiles", errors)
|
||||
|
||||
packages = family.get("packages", {})
|
||||
if not packages:
|
||||
errors.append(f"{fpath}: no packages defined")
|
||||
|
||||
for pkg_id, pkg in packages.items():
|
||||
ppath = f"{fpath}/{pkg_id}"
|
||||
|
||||
missing_keys = REQUIRED_PACKAGE_KEYS - set(pkg.keys())
|
||||
if missing_keys:
|
||||
errors.append(f"{ppath}: missing keys {missing_keys}")
|
||||
|
||||
pinmap = pkg.get("pinmap", {})
|
||||
if not pinmap:
|
||||
warnings.append(f"{ppath}: pinmap is empty (needs datasheet data)")
|
||||
else:
|
||||
validate_pinmap(pinmap, ppath, errors, warnings)
|
||||
|
||||
caps = pkg.get("capabilities", {})
|
||||
validate_capabilities(caps, ppath, errors)
|
||||
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <path/to/mcu_db.py>")
|
||||
return 1
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
if not path.exists():
|
||||
print(f"File not found: {path}")
|
||||
return 1
|
||||
|
||||
try:
|
||||
db = load_mcu_db(path)
|
||||
except Exception as exc:
|
||||
print(f"Failed to load MCU_DB: {exc}")
|
||||
return 1
|
||||
|
||||
errors, warnings = validate_db(db)
|
||||
|
||||
for w in warnings:
|
||||
print(f" WARN: {w}")
|
||||
for e in errors:
|
||||
print(f" ERROR: {e}")
|
||||
|
||||
total_vendors = len(db)
|
||||
total_families = sum(len(v) for v in db.values())
|
||||
total_packages = sum(
|
||||
len(f.get("packages", {}))
|
||||
for v in db.values()
|
||||
for f in v.values()
|
||||
)
|
||||
|
||||
print(f"\nSummary: {total_vendors} vendors, {total_families} families, {total_packages} packages")
|
||||
print(f" {len(errors)} errors, {len(warnings)} warnings")
|
||||
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user