#!/usr/bin/env python3 """Parse and summarize STM32CubeMX .ioc files.""" from __future__ import annotations import argparse import json from collections import defaultdict from pathlib import Path from typing import TypedDict StrMap = dict[str, str] class ClassifiedIOC(TypedDict): domains: dict[str, StrMap] peripherals: dict[str, StrMap] other: StrMap def parse_ioc(ioc_path: Path) -> tuple[StrMap, list[str]]: entries: StrMap = {} anomalies: list[str] = [] lines = ioc_path.read_text(encoding="utf-8", errors="replace").splitlines() for index, raw in enumerate(lines, start=1): line = raw.strip() if not line or line.startswith("#"): continue if "=" not in line: anomalies.append(f"line {index}: no '=' separator") continue key, value = line.split("=", 1) key = key.strip() value = value.strip() if not key: anomalies.append(f"line {index}: empty key") continue if key in entries: anomalies.append(f"line {index}: duplicate key '{key}' (last value kept)") entries[key] = value return entries, anomalies def classify(entries: StrMap) -> ClassifiedIOC: domains: dict[str, StrMap] = { "mcu": {}, "rcc": {}, "project_manager": {}, "pins": {}, "middleware": {}, } peripherals: dict[str, StrMap] = defaultdict(dict) other: StrMap = {} middleware_roots = { "freertos", "fatfs", "lwip", "usb", "touchgfx", "azure_rtos", "threadx", "netxduo", "filex", "ux_device", "ux_host", } for key, value in entries.items(): if key.startswith("Mcu."): domains["mcu"][key] = value continue if key.startswith("RCC."): domains["rcc"][key] = value continue if key.startswith("ProjectManager."): domains["project_manager"][key] = value continue # Pin records usually look like PA0.Mode, PA0.Signal, PB6.GPIO_Label... if len(key) >= 3 and key[0] == "P" and key[1].isalpha(): pin = key.split(".", 1)[0] if len(pin) >= 3 and pin[2].isdigit(): domains["pins"][key] = value continue root = key.split(".", 1)[0] root_l = root.lower() if root_l in middleware_roots or root_l.startswith("cmsis"): domains["middleware"][key] = value continue # Heuristic: uppercase root token often indicates a peripheral (USART1, I2C1, TIM3...) if root and root.upper() == root and any(ch.isdigit() for ch in root): peripherals[root][key] = value continue other[key] = value return { "domains": domains, "peripherals": dict(sorted(peripherals.items())), "other": other, } def summarize(classified: ClassifiedIOC, ioc_path: Path, anomalies: list[str]) -> str: domains = classified["domains"] peripherals = classified["peripherals"] other = classified["other"] mcu = domains["mcu"] project_manager = domains["project_manager"] rcc = domains["rcc"] pins = domains["pins"] middleware = domains["middleware"] lines: list[str] = [] lines.append(f"IOC file: {ioc_path}") lines.append("") lines.append("== Project Overview ==") lines.append(f"- MCU keys: {len(mcu)}") lines.append(f"- ProjectManager keys: {len(project_manager)}") lines.append(f"- RCC keys: {len(rcc)}") top_overview = [ "Mcu.Name", "Mcu.Package", "Mcu.Family", "Mcu.UserName", "ProjectManager.ProjectName", "ProjectManager.ToolChain", "ProjectManager.TargetToolchain", ] for key in top_overview: if key in mcu: lines.append(f"- {key}: {mcu[key]}") elif key in project_manager: lines.append(f"- {key}: {project_manager[key]}") lines.append("") lines.append("== Peripherals ==") lines.append(f"- Count: {len(peripherals)}") if peripherals: lines.append("- Enabled/peripheral roots: " + ", ".join(peripherals.keys())) lines.append("") lines.append("== Pins ==") lines.append(f"- Pin-related keys: {len(pins)}") # Extract a compact map of pin -> signal pin_signals: dict[str, str] = {} for key, value in pins.items(): if key.endswith(".Signal"): pin = key.split(".", 1)[0] pin_signals[pin] = value if pin_signals: preview = sorted(pin_signals.items())[:20] for pin, signal in preview: lines.append(f"- {pin}: {signal}") if len(pin_signals) > len(preview): lines.append(f"- ... ({len(pin_signals) - len(preview)} more)") lines.append("") lines.append("== Middleware ==") lines.append(f"- Middleware keys: {len(middleware)}") lines.append("") lines.append("== Diagnostics ==") lines.append(f"- Other keys: {len(other)}") lines.append(f"- Anomalies: {len(anomalies)}") if anomalies: for item in anomalies[:20]: lines.append(f"- {item}") if len(anomalies) > 20: lines.append(f"- ... ({len(anomalies) - 20} more)") return "\n".join(lines) def build_json( ioc_path: Path, entries: StrMap, classified: ClassifiedIOC, anomalies: list[str], ) -> dict[str, object]: return { "ioc_path": str(ioc_path), "counts": { "entries": len(entries), "mcu": len(classified["domains"]["mcu"]), "rcc": len(classified["domains"]["rcc"]), "project_manager": len(classified["domains"]["project_manager"]), "pins": len(classified["domains"]["pins"]), "middleware": len(classified["domains"]["middleware"]), "peripherals": len(classified["peripherals"]), "other": len(classified["other"]), "anomalies": len(anomalies), }, "domains": classified["domains"], "peripherals": classified["peripherals"], "other": classified["other"], "anomalies": anomalies, } def main() -> int: parser = argparse.ArgumentParser( description="Read and summarize STM32CubeMX .ioc files" ) parser.add_argument("--ioc", required=True, help="Path to .ioc file") parser.add_argument("--json", action="store_true", help="Print JSON output") parser.add_argument( "--out", help="Optional output file path (for --json or text summary)" ) args = parser.parse_args() ioc_path = Path(args.ioc) if not ioc_path.exists() or not ioc_path.is_file(): raise SystemExit(f"error: ioc file not found: {ioc_path}") entries, anomalies = parse_ioc(ioc_path) classified = classify(entries) if args.json: payload = build_json(ioc_path, entries, classified, anomalies) rendered = json.dumps(payload, ensure_ascii=True, indent=2) else: rendered = summarize(classified, ioc_path, anomalies) if args.out: Path(args.out).write_text(rendered + "\n", encoding="utf-8") else: print(rendered) return 0 if __name__ == "__main__": raise SystemExit(main())