250 lines
7.2 KiB
Python
250 lines
7.2 KiB
Python
#!/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())
|