first commit
This commit is contained in:
57
stm32-cubemx-ioc-reader/SKILL.md
Normal file
57
stm32-cubemx-ioc-reader/SKILL.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# STM32CubeMX IOC Reader
|
||||
|
||||
## Use this skill when
|
||||
|
||||
- The user asks to read, analyze, summarize, or validate an STM32CubeMX `.ioc` file.
|
||||
- The user wants pin mapping, clock config, peripheral setup, middleware settings, or project metadata from `.ioc`.
|
||||
- The user wants a machine-readable export (JSON) of `.ioc` content.
|
||||
|
||||
## Do not use this skill when
|
||||
|
||||
- The request is about generated C code (`main.c`, `stm32xx_hal_msp.c`) and not `.ioc` itself.
|
||||
- The file is not an STM32CubeMX `.ioc` file.
|
||||
|
||||
## What this skill does
|
||||
|
||||
- Parses `.ioc` key-value entries.
|
||||
- Groups configuration by domain: MCU, pins, RCC/clock, peripherals, middleware, and project manager.
|
||||
- Produces readable summaries for fast review.
|
||||
- Optionally exports structured JSON for tooling.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Confirm the target `.ioc` path.
|
||||
2. Run parser script:
|
||||
|
||||
```bash
|
||||
python scripts/parse_ioc.py --ioc <path/to/project.ioc>
|
||||
```
|
||||
|
||||
3. For JSON output:
|
||||
|
||||
```bash
|
||||
python scripts/parse_ioc.py --ioc <path/to/project.ioc> --json
|
||||
```
|
||||
|
||||
4. For saved JSON file:
|
||||
|
||||
```bash
|
||||
python scripts/parse_ioc.py --ioc <path/to/project.ioc> --json --out parsed_ioc.json
|
||||
```
|
||||
|
||||
## Response style guidelines
|
||||
|
||||
- Start with a concise project overview (MCU, board, toolchain, project name).
|
||||
- Then list enabled peripherals and notable pin assignments.
|
||||
- Then call out clock source and important RCC settings.
|
||||
- Mention anomalies (empty values, duplicate keys, unknown domains).
|
||||
|
||||
## Notes about `.ioc` format
|
||||
|
||||
- `.ioc` is an INI-like key-value format (not strict XML).
|
||||
- Typical key prefixes:
|
||||
- `Mcu.` for target MCU/package/family
|
||||
- `PAx`/`PBx`... for pin assignment and signal labels
|
||||
- `RCC.` for clocks
|
||||
- `<PERIPH>.` (for example `USART1.`, `I2C1.`, `TIM3.`) for peripheral settings
|
||||
- `ProjectManager.` for generated project metadata
|
||||
249
stm32-cubemx-ioc-reader/scripts/parse_ioc.py
Normal file
249
stm32-cubemx-ioc-reader/scripts/parse_ioc.py
Normal file
@@ -0,0 +1,249 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user