This commit is contained in:
2025-11-12 18:16:44 +00:00
commit 944643f6bd
4 changed files with 240 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.vscode/
test/
**__pycache__/
mx_convert.egg-info/

40
README.md Normal file
View File

@@ -0,0 +1,40 @@
# MX Converter
MX Converter is a tool to convert Cube MX generated projects to the ISTSAT-2 repository structure.
## Notice
This guide and the MX Converter were built with the goal of cleanly separating STM generated code from application code. As such, files produced by Cube MX are treated as disposable, and any code in said files is likely to be lost. This applies to C files, header files, CMakeLists and possibly more. Do not include any code of your own in the MX files.
When adapting existing code for use with MX Converter, make sure to back up the original code and to double check all code is present in the final version of your project.
## Cube MX Configuration
For simplicity of development of this tool and for uniformity across different firmware projects, the same configuration should be used when creating the projects with Cube MX.
All the relevant settings are under the 'Project Manager' tab and should be set as listed below. Any unspecified settings can be set to whatever is most convinient.
Setting | Value
-- | --
Project > Do not generate the main() | SET
Project > Toolchain / IDE | 'CMake'
Code Generator > STM32Cube MCU packages and embedded software packages | 'Copy only necessary library files'
Code Generator > Generate peripheral initialization as a pair of '.c/.h' files per peripheral | UNSET
Code Generator > Backup previously generated files when re-generating | UNSET
Code Generator > Keep User Code when re-generating | UNSET
Code Generator > Delete previously generated files when not re-generated | SET
## Project Creation
The project creation process differs somewhat from creating a standard MX project.
1. Start with the creation of a project folder, from now on called ROOT. This will NOT be the destination of the code generation nor the `.ioc` file.
1. Open the CubeMX program, select the board or MCU, setup the hardware as needed and configure the project as described before. Save the project, navigating to the ROOT folder, and name the project 'CubeMX'. This will create a new directory where generated code and the ioc will reside.
1. Follow the Code generation / IOC Update guide.
## Code generation / IOC Update
After project creation of after modifying the IOC, code generation needs to be invoked. Assuming Cube MX is open with the relevant IOC:
1. Save the project and press 'Generate Code'. Any existing code will be **deleted**, and the new code will be placed in the previously created CubeMX folder.
1. TODO: Define how to use the CMakeLists generator

183
mx_convert/mx_convert.py Normal file
View File

@@ -0,0 +1,183 @@
from pathlib import Path
from shutil import rmtree
from sys import argv
from argparse import ArgumentParser, Namespace
import tomllib as toml
MX_DISCARD_PATHS = [
"cmake",
".mxproject",
"CMakePresets.json",
]
#MX_MOVE_PATHS = [
# ("Core/Inc/main.h", "Core/Inc/mx-main.h"), #TODO: Force rename in includes as well
# ("Core/Src/main.c", "Core/Src/mx-main.c"),
#]
SOURCE_EXTS = [
"*.c",
"*.s"
]
HEADER_EXTS = [
"*.h"
]
CMAKE_EXCLUDE_SOURCES = [
"*_template.c"
]
CMAKE_TARGET = "" # Set from argv
ASSUME_YES = False
def mx_cleanup(path: Path):
deletions: list[Path] = []
for disc in MX_DISCARD_PATHS:
disc_path = path / disc
if disc_path.exists():
deletions.append(disc_path)
if len(deletions) == 0:
return
print("%d files selected for deletion:"%(len(deletions)))
for disc in deletions:
print(disc)
if not ASSUME_YES:
print("Accept? (y/N)")
sel = input()
if sel.lower() != "y":
print("Aborting...")
exit(1)
for disc in deletions:
disc.unlink() if disc.is_file() else rmtree(disc)
def recurse_cmakelists(path: Path):
# Detect relevant files
dirs: list[Path] = []
sources: list[Path] = []
has_headers: bool = False
for child in path.iterdir():
if child.is_dir():
dirs.append(child)
elif any(child.match(ext) for ext in SOURCE_EXTS) and \
not any(child.match(exclude) for exclude in CMAKE_EXCLUDE_SOURCES):
sources.append(child)
elif any(child.match(ext) for ext in HEADER_EXTS):
has_headers = True
# Generate CMakeLists content
lines: list[str] = []
if has_headers:
lines.append("target_include_directories(%s PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})"%(CMAKE_TARGET))
if len(sources) != 0:
lines.append("target_sources(%s PRIVATE"%(CMAKE_TARGET))
lines += ["\t%s"%(src.name) for src in sources]
lines.append(")")
lines += ["add_subdirectory(%s)"%(directory.name) for directory in dirs]
# Write CMakeLists
lists_path = path / "CMakeLists.txt"
with open(lists_path, "w") as of:
of.write("\n".join(lines)+"\n")
# Recurse
for directory in dirs:
recurse_cmakelists(directory)
def load_config(path: Path):
if not path.exists():
return
if not path.is_file():
print("Config '%s' is not a file."%(path))
exit(1)
with open(path, "rb") as cfg:
config = toml.load(cfg)
overlay_config(config)
def overlay_config(config: dict):
global MX_DISCARD_PATHS
global HEADER_EXTS
global SOURCE_EXTS
global CMAKE_TARGET
global CMAKE_EXCLUDE_SOURCES
global ASSUME_YES
if "discard" in config and config["discard"]:
if not isinstance(config["discard"], list):
print("discard must be a list of strings")
exit(1)
MX_DISCARD_PATHS += config["discard"]
if "header" in config and config["header"]:
if not isinstance(config["header"], list):
print("header must be a list of strings")
exit(1)
HEADER_EXTS += config["header"]
if "source" in config and config["source"]:
if not isinstance(config["source"], list):
print("source must be a list of strings")
exit(1)
SOURCE_EXTS += config["source"]
if "target" in config and config["target"]:
if not isinstance(config["target"], str):
print("target must be a string")
exit(1)
CMAKE_TARGET = config["target"]
if "exclude" in config and config["exclude"]:
if not isinstance(config["exclude"], list):
print("exclude must be a list of strings")
exit(1)
CMAKE_EXCLUDE_SOURCES = config["exclude"]
if "assume-yes" in config and config["assume-yes"]:
if not isinstance(config["assume-yes"], bool):
print("assume-yes must be a bool")
exit(1)
ASSUME_YES |= config["assume-yes"]
def print_config():
print("MX_DISCARD_PATHS", MX_DISCARD_PATHS)
print("SOURCE_EXTS", SOURCE_EXTS)
print("HEADER_EXTS", HEADER_EXTS)
print("CMAKE_EXCLUDE_SOURCES", CMAKE_EXCLUDE_SOURCES)
print("CMAKE_TARGET", CMAKE_TARGET)
print("ASSUME_YES", ASSUME_YES)
def main2(ns: Namespace):
if not ns.path.is_dir():
print("'%s' is not a directory."%(ns.path))
exit(1)
load_config(ns.config)
overlay_config(vars(ns))
if ns.show_cfg:
print_config()
exit(0)
mx_cleanup(ns.path)
recurse_cmakelists(ns.path)
def main():
parser = ArgumentParser(prog="MX Convert")
parser.add_argument("-c", "--config", action="store", type=Path, default="mx_convert.toml", help="The config file to read. If not specified, defaults to 'mx_convert.toml'.")
parser.add_argument("-d", "--discard", action="append", help="Add a file to the discard list (delete if found).")
parser.add_argument("-H", "--header", action="append", help="Add a pattern to the headers list, single wildcards allowed. Includes *.h by default.")
parser.add_argument("-s", "--source", action="append", help="Add a pattern to the sources list, single wildcards allowed. Includes *.c and *.s by default.")
parser.add_argument("-t", "--target", action="store", default="${EXECUTABLE}", help="The CMake target to generate references to. Defaults to '${EXECUTABLE}'.")
parser.add_argument("-x", "--exclude", action="append", help="Add a file to the source exclusion list, single wildcards allowed.")
parser.add_argument("-y", "--assume-yes", action="store_true", help="Assume yes on all queries, useful for scripts.")
parser.add_argument("--show-cfg", action="store_true", help="Show configs that would be used to run and exit successfully.")
parser.add_argument("path", action="store", type=Path, help="The path to the directory to process.")
namespace = parser.parse_args(argv[1:])
main2(namespace)
if __name__ == "__main__":
main()

13
pyproject.toml Normal file
View File

@@ -0,0 +1,13 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "mx_convert"
version = "0.1.0"
description = "A Cube MX projct conversion helper"
authors = [{ name = "Didas72" }]
dependencies = []
[project.scripts]
mx-convert = "mx_convert.mx_convert:main"