Files
claude-skills/pcb-ai-engineer/scripts/validate_mcu_db.py
T
2026-03-21 19:36:11 +03:00

181 lines
5.8 KiB
Python

#!/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())