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