armbuilderx 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. armbuilderx-0.1.1/PKG-INFO +97 -0
  2. armbuilderx-0.1.1/README.md +82 -0
  3. armbuilderx-0.1.1/pyproject.toml +26 -0
  4. armbuilderx-0.1.1/setup.cfg +4 -0
  5. armbuilderx-0.1.1/src/armbuilderx/__init__.py +1 -0
  6. armbuilderx-0.1.1/src/armbuilderx/cli.py +258 -0
  7. armbuilderx-0.1.1/src/armbuilderx/cmsis/__init__.py +0 -0
  8. armbuilderx-0.1.1/src/armbuilderx/cmsis/csolution.py +46 -0
  9. armbuilderx-0.1.1/src/armbuilderx/cmsis/generator.py +21 -0
  10. armbuilderx-0.1.1/src/armbuilderx/cmsis/mdk_project.py +130 -0
  11. armbuilderx-0.1.1/src/armbuilderx/cmsis/toolbox.py +69 -0
  12. armbuilderx-0.1.1/src/armbuilderx/cmsis/types.py +17 -0
  13. armbuilderx-0.1.1/src/armbuilderx/flash.py +54 -0
  14. armbuilderx-0.1.1/src/armbuilderx/paths.py +66 -0
  15. armbuilderx-0.1.1/src/armbuilderx/sdk/__init__.py +10 -0
  16. armbuilderx-0.1.1/src/armbuilderx/sdk/base.py +26 -0
  17. armbuilderx-0.1.1/src/armbuilderx/sdk/gr551x.py +390 -0
  18. armbuilderx-0.1.1/src/armbuilderx/sdk/ns800rt5xxx.py +171 -0
  19. armbuilderx-0.1.1/src/armbuilderx/sdk/registry.py +42 -0
  20. armbuilderx-0.1.1/src/armbuilderx/serial/__init__.py +0 -0
  21. armbuilderx-0.1.1/src/armbuilderx/serial/monitor.py +84 -0
  22. armbuilderx-0.1.1/src/armbuilderx/toolchain/__init__.py +0 -0
  23. armbuilderx-0.1.1/src/armbuilderx/toolchain/detect.py +97 -0
  24. armbuilderx-0.1.1/src/armbuilderx/toolchain/ensure.py +106 -0
  25. armbuilderx-0.1.1/src/armbuilderx/toolchain/install.py +199 -0
  26. armbuilderx-0.1.1/src/armbuilderx.egg-info/PKG-INFO +97 -0
  27. armbuilderx-0.1.1/src/armbuilderx.egg-info/SOURCES.txt +36 -0
  28. armbuilderx-0.1.1/src/armbuilderx.egg-info/dependency_links.txt +1 -0
  29. armbuilderx-0.1.1/src/armbuilderx.egg-info/entry_points.txt +2 -0
  30. armbuilderx-0.1.1/src/armbuilderx.egg-info/requires.txt +6 -0
  31. armbuilderx-0.1.1/src/armbuilderx.egg-info/top_level.txt +1 -0
  32. armbuilderx-0.1.1/tests/test_cli.py +21 -0
  33. armbuilderx-0.1.1/tests/test_csolution.py +41 -0
  34. armbuilderx-0.1.1/tests/test_flash.py +35 -0
  35. armbuilderx-0.1.1/tests/test_generator.py +44 -0
  36. armbuilderx-0.1.1/tests/test_gr551x.py +85 -0
  37. armbuilderx-0.1.1/tests/test_mdk_project.py +51 -0
  38. armbuilderx-0.1.1/tests/test_sdk_registry.py +18 -0
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: armbuilderx
3
+ Version: 0.1.1
4
+ Summary: CMake-based build, flash and serial-monitor tooling for Arm Cortex-M Keil/MDK projects, built on CMSIS-Toolbox and pyOCD
5
+ Author: ArmBuilderX
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: click>=8.1
10
+ Requires-Dist: pyocd>=0.36
11
+ Requires-Dist: pyserial>=3.5
12
+ Requires-Dist: requests>=2.31
13
+ Requires-Dist: tqdm>=4.66
14
+ Requires-Dist: PyYAML>=6.0
15
+
16
+ # ArmBuilderX
17
+
18
+ Cross-platform CLI that turns a Keil/MDK Arm Cortex-M project into a
19
+ CMake project, builds it with the Arm GNU Toolchain, flashes it with
20
+ pyOCD, and streams the DAPLink/CMSIS-DAP virtual COM port log.
21
+
22
+ It is built on top of official tooling rather than a custom project
23
+ parser:
24
+
25
+ - **[CMSIS-Toolbox](https://github.com/Open-CMSIS-Pack/devtools)**
26
+ (`csolution` / `cbuild` / `cbuild2cmake` / `cpackget`) resolves the
27
+ CMSIS-Pack device data (CPU, FPU, memory map, startup/linker files)
28
+ and generates the actual `CMakeLists.txt`.
29
+ - **[pyOCD](https://github.com/pyocd/pyOCD)** flashes the target over
30
+ CMSIS-DAP/DAPLink and can also list/open the debug probe's serial
31
+ port.
32
+ - **CMake + Arm GNU Toolchain (`arm-none-eabi-gcc`)** builds the
33
+ generated CMake project. `ninja` is used as the CMake generator.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install armbuilderx
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ ```bash
44
+ # One-time: point ArmBuilderX at the vendor pack(s) that describe your device.
45
+ armbuilderx init --pack path/to/Vendor.PackName.x.y.z.pack
46
+
47
+ # Generate (or refresh) the GCC/CMake project for one example, from its
48
+ # existing MDK project.
49
+ armbuilderx generate examples/device_support/ns800rt5xxx/examples/gpio/gpio_ex2_output
50
+
51
+ # Build it (adds arm-none-eabi-gcc/cmake/ninja/git to ~/.armbuilderx/tools
52
+ # automatically if they are missing from PATH).
53
+ armbuilderx build examples/device_support/ns800rt5xxx/examples/gpio/gpio_ex2_output
54
+
55
+ # Flash the built ELF over CMSIS-DAP/DAPLink with pyOCD.
56
+ # (the build step also emits matching .hex and .bin images alongside the .elf)
57
+ armbuilderx flash examples/device_support/ns800rt5xxx/examples/gpio/gpio_ex2_output
58
+
59
+ # Open the DAPLink virtual COM port and stream log output.
60
+ armbuilderx monitor
61
+
62
+ # All of the above, in one shot.
63
+ armbuilderx run examples/device_support/ns800rt5xxx/examples/gpio/gpio_ex2_output
64
+ ```
65
+
66
+ ## SDK adapters
67
+
68
+ ArmBuilderX keeps vendor-specific SDK rules behind SDK adapters. The current
69
+ `ns800rt5xxx` adapter contains the NS800 project-layout detection, GCC startup
70
+ file replacement, linker-script selection, and NS800-only compiler defines.
71
+ The command layer remains independent of those details and can select an
72
+ adapter explicitly when needed:
73
+
74
+ ```bash
75
+ armbuilderx generate <example-dir> --sdk ns800rt5xxx
76
+ ```
77
+
78
+ Without `--sdk`, ArmBuilderX detects the adapter from the example layout. To
79
+ add another SDK such as GR551x, implement a new adapter under
80
+ `src/armbuilderx/sdk/` and register it in `sdk/registry.py`; do not add its
81
+ directory rules to the NS800 adapter or the generic generator.
82
+
83
+ ### GR551x
84
+
85
+ The `gr551x` adapter supports Goodix GR551x projects that ship a `GCC/`
86
+ CMSIS-Toolbox project. It verifies the `Goodix::GR5xxx_DFP@1.0.0` pack and
87
+ `Goodix::GR551x` device, preserves the SDK's source groups and BLE library,
88
+ and resolves the selected GCC toolchain's Cortex-M4 softfp runtime libraries.
89
+
90
+ ```bash
91
+ armbuilderx init --pack ../GR551x_SDK_V2.1.1/Goodix.GR5xxx_DFP.1.0.0.pack
92
+ armbuilderx build ../GR551x_SDK_V2.1.1/projects/peripheral/gpio/app_gpio --sdk gr551x --clean
93
+ ```
94
+
95
+ Each project keeps its generated CMake project under a `GCC/` folder
96
+ next to the existing `MDK/`/`IAR/` folders, matching the SDK's own
97
+ convention.
@@ -0,0 +1,82 @@
1
+ # ArmBuilderX
2
+
3
+ Cross-platform CLI that turns a Keil/MDK Arm Cortex-M project into a
4
+ CMake project, builds it with the Arm GNU Toolchain, flashes it with
5
+ pyOCD, and streams the DAPLink/CMSIS-DAP virtual COM port log.
6
+
7
+ It is built on top of official tooling rather than a custom project
8
+ parser:
9
+
10
+ - **[CMSIS-Toolbox](https://github.com/Open-CMSIS-Pack/devtools)**
11
+ (`csolution` / `cbuild` / `cbuild2cmake` / `cpackget`) resolves the
12
+ CMSIS-Pack device data (CPU, FPU, memory map, startup/linker files)
13
+ and generates the actual `CMakeLists.txt`.
14
+ - **[pyOCD](https://github.com/pyocd/pyOCD)** flashes the target over
15
+ CMSIS-DAP/DAPLink and can also list/open the debug probe's serial
16
+ port.
17
+ - **CMake + Arm GNU Toolchain (`arm-none-eabi-gcc`)** builds the
18
+ generated CMake project. `ninja` is used as the CMake generator.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install armbuilderx
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ # One-time: point ArmBuilderX at the vendor pack(s) that describe your device.
30
+ armbuilderx init --pack path/to/Vendor.PackName.x.y.z.pack
31
+
32
+ # Generate (or refresh) the GCC/CMake project for one example, from its
33
+ # existing MDK project.
34
+ armbuilderx generate examples/device_support/ns800rt5xxx/examples/gpio/gpio_ex2_output
35
+
36
+ # Build it (adds arm-none-eabi-gcc/cmake/ninja/git to ~/.armbuilderx/tools
37
+ # automatically if they are missing from PATH).
38
+ armbuilderx build examples/device_support/ns800rt5xxx/examples/gpio/gpio_ex2_output
39
+
40
+ # Flash the built ELF over CMSIS-DAP/DAPLink with pyOCD.
41
+ # (the build step also emits matching .hex and .bin images alongside the .elf)
42
+ armbuilderx flash examples/device_support/ns800rt5xxx/examples/gpio/gpio_ex2_output
43
+
44
+ # Open the DAPLink virtual COM port and stream log output.
45
+ armbuilderx monitor
46
+
47
+ # All of the above, in one shot.
48
+ armbuilderx run examples/device_support/ns800rt5xxx/examples/gpio/gpio_ex2_output
49
+ ```
50
+
51
+ ## SDK adapters
52
+
53
+ ArmBuilderX keeps vendor-specific SDK rules behind SDK adapters. The current
54
+ `ns800rt5xxx` adapter contains the NS800 project-layout detection, GCC startup
55
+ file replacement, linker-script selection, and NS800-only compiler defines.
56
+ The command layer remains independent of those details and can select an
57
+ adapter explicitly when needed:
58
+
59
+ ```bash
60
+ armbuilderx generate <example-dir> --sdk ns800rt5xxx
61
+ ```
62
+
63
+ Without `--sdk`, ArmBuilderX detects the adapter from the example layout. To
64
+ add another SDK such as GR551x, implement a new adapter under
65
+ `src/armbuilderx/sdk/` and register it in `sdk/registry.py`; do not add its
66
+ directory rules to the NS800 adapter or the generic generator.
67
+
68
+ ### GR551x
69
+
70
+ The `gr551x` adapter supports Goodix GR551x projects that ship a `GCC/`
71
+ CMSIS-Toolbox project. It verifies the `Goodix::GR5xxx_DFP@1.0.0` pack and
72
+ `Goodix::GR551x` device, preserves the SDK's source groups and BLE library,
73
+ and resolves the selected GCC toolchain's Cortex-M4 softfp runtime libraries.
74
+
75
+ ```bash
76
+ armbuilderx init --pack ../GR551x_SDK_V2.1.1/Goodix.GR5xxx_DFP.1.0.0.pack
77
+ armbuilderx build ../GR551x_SDK_V2.1.1/projects/peripheral/gpio/app_gpio --sdk gr551x --clean
78
+ ```
79
+
80
+ Each project keeps its generated CMake project under a `GCC/` folder
81
+ next to the existing `MDK/`/`IAR/` folders, matching the SDK's own
82
+ convention.
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "armbuilderx"
7
+ version = "0.1.1"
8
+ description = "CMake-based build, flash and serial-monitor tooling for Arm Cortex-M Keil/MDK projects, built on CMSIS-Toolbox and pyOCD"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "ArmBuilderX" }]
13
+ dependencies = [
14
+ "click>=8.1",
15
+ "pyocd>=0.36",
16
+ "pyserial>=3.5",
17
+ "requests>=2.31",
18
+ "tqdm>=4.66",
19
+ "PyYAML>=6.0",
20
+ ]
21
+
22
+ [project.scripts]
23
+ armbuilderx = "armbuilderx.cli:main"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
@@ -0,0 +1,258 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+ from armbuilderx.cmsis import csolution, generator, mdk_project, toolbox
10
+ from armbuilderx.cmsis.csolution import CSolutionError
11
+ from armbuilderx.cmsis.mdk_project import MdkProjectError
12
+ from armbuilderx.paths import pack_root_dir
13
+ from armbuilderx.sdk import available_sdk_ids, get_sdk_adapter
14
+ from armbuilderx.toolchain.ensure import ensure_toolchain
15
+
16
+
17
+ def _echo(msg: str) -> None:
18
+ click.echo(msg)
19
+
20
+
21
+ @click.group()
22
+ def main() -> None:
23
+ """ArmBuilderX: build, flash and monitor Keil/MDK Arm projects with
24
+ CMake, CMSIS-Toolbox and pyOCD."""
25
+
26
+
27
+ @main.command()
28
+ @click.option("--pack", "pack_files", multiple=True, type=click.Path(exists=True, path_type=Path),
29
+ help="Vendor .pack file to install into ArmBuilderX's managed pack root. "
30
+ "Repeat for multiple packs.")
31
+ def init(pack_files: tuple[Path, ...]) -> None:
32
+ """Install CMSIS-Pack(s) needed to resolve device data."""
33
+ if not pack_files:
34
+ _echo(f"pack root: {pack_root_dir()}")
35
+ _echo("Nothing to do. Pass --pack <file> to install a vendor pack.")
36
+ return
37
+ for pack_file in pack_files:
38
+ _echo(f"installing {pack_file} ...")
39
+ toolbox.add_pack(pack_file)
40
+ _echo("done.")
41
+
42
+
43
+ @main.command()
44
+ @click.argument("project_dir", type=click.Path(exists=True, file_okay=False, path_type=Path))
45
+ @click.option("--sdk", type=click.Choice(available_sdk_ids()),
46
+ help="SDK adapter to use. Defaults to automatic project-layout detection.")
47
+ def generate(project_dir: Path, sdk: str | None) -> None:
48
+ """Generate/refresh GCC/*.csolution.yml + *.cproject.yml for PROJECT_DIR
49
+ from its existing MDK project."""
50
+ try:
51
+ result = generator.generate(project_dir, sdk=sdk)
52
+ except (MdkProjectError, generator.GenerateError) as exc:
53
+ raise click.ClickException(str(exc))
54
+ _echo(f"generated {result.csolution_yml}")
55
+ _echo(f"generated {result.cproject_yml}")
56
+
57
+
58
+ def _resolve_pyocd_exe() -> str:
59
+ """pyocd is installed as a console script in the same environment as
60
+ armbuilderx (it's a pip dependency), so look next to the running
61
+ interpreter first; fall back to PATH for the rare case of a
62
+ non-standard install layout.
63
+ """
64
+ exe_dir = Path(sys.executable).parent
65
+ for name in ("pyocd", "pyocd.exe"):
66
+ candidate = exe_dir / name
67
+ if candidate.is_file():
68
+ return str(candidate)
69
+ found = shutil.which("pyocd")
70
+ if found:
71
+ return found
72
+ raise click.ClickException(
73
+ "pyocd not found. It is a pip dependency of armbuilderx; reinstall with "
74
+ "'pip install armbuilderx'."
75
+ )
76
+
77
+
78
+ def _ensure_generated(project_dir: Path, sdk: str | None = None) -> Path:
79
+ gcc_dir = project_dir / "GCC"
80
+ selected_sdk = sdk
81
+ if selected_sdk is None:
82
+ try:
83
+ # SDK adapters own generated configuration such as startup files,
84
+ # ABI flags, and vendor linker inputs. Refresh it on every build;
85
+ # merely finding an existing csolution file must not retain stale
86
+ # adapter output from an earlier ArmBuilderX version.
87
+ selected_sdk = get_sdk_adapter(project_dir).sdk_id
88
+ except generator.GenerateError:
89
+ pass
90
+
91
+ if selected_sdk is not None:
92
+ _echo(f"refreshing project files with SDK adapter '{selected_sdk}'...")
93
+ generator.generate(project_dir, sdk=selected_sdk)
94
+ elif not list(gcc_dir.glob("*.csolution.yml")):
95
+ _echo("no GCC/*.csolution.yml found yet, generating it first...")
96
+ generator.generate(project_dir, sdk=sdk)
97
+ return gcc_dir
98
+
99
+
100
+ @main.command()
101
+ @click.argument("project_dir", type=click.Path(exists=True, file_okay=False, path_type=Path))
102
+ @click.option("--build-type", default="Debug", show_default=True, type=click.Choice(["Debug", "Release"]))
103
+ @click.option("--clean", is_flag=True, help="Remove intermediate/output directories before building.")
104
+ @click.option("--sdk", type=click.Choice(available_sdk_ids()),
105
+ help="SDK adapter to use if generation is needed.")
106
+ def build(project_dir: Path, build_type: str, clean: bool, sdk: str | None) -> None:
107
+ """Ensure the toolchain is available, then build PROJECT_DIR/GCC."""
108
+ project_dir = project_dir.resolve()
109
+ try:
110
+ gcc_dir = _ensure_generated(project_dir, sdk)
111
+ except (MdkProjectError, generator.GenerateError) as exc:
112
+ raise click.ClickException(str(exc))
113
+
114
+ csolution_files = list(gcc_dir.glob("*.csolution.yml"))
115
+ if not csolution_files:
116
+ raise click.ClickException(f"no *.csolution.yml found under {gcc_dir}")
117
+ csolution_yml = csolution_files[0]
118
+
119
+ tc_env = ensure_toolchain(notify=_echo)
120
+ env = tc_env.as_environ()
121
+ env["CMSIS_PACK_ROOT"] = str(pack_root_dir())
122
+
123
+ args = [str(csolution_yml), "--toolchain", "GCC", "--packs"]
124
+ # *.cproject.yml -> project name (strip the ".cproject.yml" suffix, not just ".yml")
125
+ contexts = [c.name[: -len(".cproject.yml")] for c in gcc_dir.glob("*.cproject.yml")]
126
+ for context in contexts:
127
+ args += ["-c", f"{context}.{build_type}"]
128
+ if clean:
129
+ args.append("--rebuild")
130
+
131
+ _echo(f"building {csolution_yml.name} ({build_type}) ...")
132
+ result = toolbox.run(toolbox.cbuild_exe(), args, cwd=gcc_dir, env=env)
133
+ click.echo(result.stdout)
134
+ if result.returncode != 0:
135
+ click.echo(result.stderr, err=True)
136
+ raise click.ClickException("build failed")
137
+ _echo("build succeeded.")
138
+
139
+
140
+ @main.command()
141
+ @click.argument("project_dir", type=click.Path(exists=True, file_okay=False, path_type=Path))
142
+ @click.option("--build-type", default="Debug", show_default=True, type=click.Choice(["Debug", "Release"]))
143
+ @click.option("--pack", "pack_file", type=click.Path(exists=True, path_type=Path),
144
+ help="Vendor .pack file pyOCD should use to resolve the target device.")
145
+ @click.option(
146
+ "--target",
147
+ help="pyOCD target type name (defaults to the device declared by the MDK or CMSIS solution project).",
148
+ )
149
+ @click.option("--unique-id", help="pyOCD probe unique ID, when multiple probes are attached.")
150
+ @click.option("--frequency", help="SWD/JTAG clock frequency, for example 100k or 1m.")
151
+ @click.option("--connect", "connect_mode", type=click.Choice(["halt", "pre-reset", "under-reset", "attach"]),
152
+ help="pyOCD target connection mode.")
153
+ def flash(project_dir: Path, build_type: str, pack_file, target, unique_id, frequency, connect_mode) -> None:
154
+ """Flash the ELF built for PROJECT_DIR onto the target with pyOCD."""
155
+ from armbuilderx import flash as flash_mod
156
+
157
+ project_dir = project_dir.resolve()
158
+ gcc_dir = project_dir / "GCC"
159
+ elf_path = flash_mod.find_elf(gcc_dir, build_type)
160
+
161
+ if pack_file is None or target is None:
162
+ try:
163
+ uvprojx = mdk_project.find_uvprojx(project_dir)
164
+ mdk = mdk_project.parse_uvprojx(uvprojx)
165
+ if target is None:
166
+ target = mdk.device.lower()
167
+ except MdkProjectError:
168
+ if target is None:
169
+ try:
170
+ target = csolution.pyocd_target_from_csolution(project_dir)
171
+ except CSolutionError:
172
+ pass
173
+
174
+ # Goodix links GR551x applications at 0x01002000, while its CMSIS-Pack
175
+ # .FLM file exposes a 0x00200000 flash range to pyOCD. The Goodix
176
+ # downloader performs target-specific initialisation/address mapping that
177
+ # pyOCD cannot infer from this Pack, so do not risk a misleading partial
178
+ # programming attempt through the generic pyOCD backend.
179
+ if target == "gr551x":
180
+ raise click.ClickException(
181
+ "GR551x flashing is not supported by the pyOCD backend: the Goodix Pack flash "
182
+ "address does not match the SDK ELF address. Use Goodix GProgrammer with a J-Link, "
183
+ "or add a verified GR551x J-Link/GProgrammer backend."
184
+ )
185
+
186
+ pyocd_exe = _resolve_pyocd_exe()
187
+
188
+ _echo(f"flashing {elf_path} ...")
189
+ try:
190
+ flash_mod.flash(
191
+ elf_path,
192
+ pack_file=pack_file,
193
+ target=target,
194
+ unique_id=unique_id,
195
+ frequency=frequency,
196
+ connect_mode=connect_mode,
197
+ pyocd_exe=pyocd_exe,
198
+ )
199
+ except flash_mod.FlashError as exc:
200
+ raise click.ClickException(str(exc))
201
+ _echo("flash succeeded.")
202
+
203
+
204
+ @main.command()
205
+ @click.option("--port", help="Serial device to open (skips auto-detect).")
206
+ @click.option("--unique-id", help="pyOCD probe unique ID to match against a serial port's serial number.")
207
+ @click.option("--baud", default=115200, show_default=True)
208
+ def monitor(port, unique_id, baud) -> None:
209
+ """Stream the DAPLink/CMSIS-DAP virtual COM port log."""
210
+ from armbuilderx.serial import monitor as monitor_mod
211
+
212
+ if port is None:
213
+ try:
214
+ if unique_id:
215
+ info = monitor_mod.find_port_for_probe(unique_id)
216
+ else:
217
+ info = monitor_mod.find_single_daplink_port()
218
+ except monitor_mod.SerialPortNotFoundError as exc:
219
+ raise click.ClickException(str(exc))
220
+ port = info.device
221
+ _echo(f"using serial port {port} ({info.description})")
222
+
223
+ _echo(f"streaming {port} @ {baud} baud, Ctrl+C to stop")
224
+ try:
225
+ for chunk in monitor_mod.stream(port, baudrate=baud):
226
+ sys.stdout.buffer.write(chunk)
227
+ sys.stdout.buffer.flush()
228
+ except KeyboardInterrupt:
229
+ _echo("\nstopped.")
230
+
231
+
232
+ @main.command()
233
+ @click.argument("project_dir", type=click.Path(exists=True, file_okay=False, path_type=Path))
234
+ @click.option("--build-type", default="Debug", show_default=True, type=click.Choice(["Debug", "Release"]))
235
+ @click.option("--skip-monitor", is_flag=True, help="Build and flash only; do not open the serial monitor.")
236
+ @click.option("--sdk", type=click.Choice(available_sdk_ids()),
237
+ help="SDK adapter to use if generation is needed.")
238
+ @click.pass_context
239
+ def run(ctx: click.Context, project_dir: Path, build_type: str, skip_monitor: bool,
240
+ sdk: str | None) -> None:
241
+ """Generate, build, flash, then stream the serial log — all in one shot."""
242
+ ctx.invoke(build, project_dir=project_dir, build_type=build_type, clean=False, sdk=sdk)
243
+ ctx.invoke(
244
+ flash,
245
+ project_dir=project_dir,
246
+ build_type=build_type,
247
+ pack_file=None,
248
+ target=None,
249
+ unique_id=None,
250
+ frequency=None,
251
+ connect_mode=None,
252
+ )
253
+ if not skip_monitor:
254
+ ctx.invoke(monitor, port=None, unique_id=None, baud=115200)
255
+
256
+
257
+ if __name__ == "__main__":
258
+ main()
File without changes
@@ -0,0 +1,46 @@
1
+ """Read the target identity declared by a CMSIS Solution file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+
10
+ class CSolutionError(RuntimeError):
11
+ """A CMSIS Solution file cannot provide an unambiguous target."""
12
+
13
+
14
+ def find_csolution(project_dir: Path) -> Path:
15
+ """Return the sole GCC solution file for *project_dir*."""
16
+ candidates = sorted((project_dir / "GCC").glob("*.csolution.yml"))
17
+ if len(candidates) != 1:
18
+ raise CSolutionError(
19
+ f"expected exactly one GCC/*.csolution.yml under {project_dir}, "
20
+ f"found {len(candidates)}"
21
+ )
22
+ return candidates[0]
23
+
24
+
25
+ def pyocd_target_from_csolution(project_dir: Path) -> str:
26
+ """Return pyOCD's lowercase target name from ``target-types[0].device``.
27
+
28
+ CMSIS uses a qualified device name such as ``Goodix::GR551x`` while pyOCD
29
+ registers the same Pack target as ``gr551x``. The device declaration is
30
+ intentionally read from the project rather than maintained in a vendor
31
+ specific flash table.
32
+ """
33
+ path = find_csolution(project_dir)
34
+ try:
35
+ with path.open() as source:
36
+ data = yaml.safe_load(source)
37
+ except (OSError, yaml.YAMLError) as exc:
38
+ raise CSolutionError(f"could not read {path}: {exc}") from exc
39
+
40
+ try:
41
+ device = data["solution"]["target-types"][0]["device"]
42
+ except (KeyError, IndexError, TypeError) as exc:
43
+ raise CSolutionError(f"missing solution.target-types[0].device in {path}") from exc
44
+ if not isinstance(device, str) or not device.strip():
45
+ raise CSolutionError(f"invalid solution.target-types[0].device in {path}")
46
+ return device.rsplit("::", 1)[-1].lower()
@@ -0,0 +1,21 @@
1
+ """SDK-agnostic façade for CMSIS-Toolbox project generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Mapping
7
+
8
+ from armbuilderx.cmsis.types import GenerateError, GeneratedProject
9
+ from armbuilderx.sdk.registry import get_sdk_adapter
10
+
11
+ __all__ = ["GenerateError", "GeneratedProject", "generate"]
12
+
13
+
14
+ def generate(
15
+ project_dir: Path,
16
+ pack_env: Mapping[str, str] | None = None,
17
+ sdk: str | None = None,
18
+ ) -> GeneratedProject:
19
+ """Generate a project through the explicitly selected or detected SDK adapter."""
20
+ project_dir = project_dir.resolve()
21
+ return get_sdk_adapter(project_dir, sdk).generate(project_dir, pack_env)
@@ -0,0 +1,130 @@
1
+ """Parse a Keil MDK (.uvprojx) project just enough to drive CMSIS-Toolbox:
2
+ device/vendor/pack identity, preprocessor defines, and the list of
3
+ source file groups. CMSIS-Toolbox (csolution/cbuild/cbuild2cmake) does
4
+ all of the real work of turning that into a CMake project; this module
5
+ only extracts what already exists in the Keil project file.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import xml.etree.ElementTree as ET
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+
16
+ @dataclass
17
+ class SourceFile:
18
+ path: Path # absolute path
19
+ is_source: bool # True for .c/.cpp/.s/.S that must be compiled; False for headers
20
+
21
+
22
+ @dataclass
23
+ class SourceGroup:
24
+ name: str
25
+ files: list[SourceFile] = field(default_factory=list)
26
+
27
+
28
+ @dataclass
29
+ class MdkTarget:
30
+ target_name: str
31
+ device: str
32
+ vendor: str
33
+ pack_id: str # e.g. "NOVOSENSE.NS800RT5XXX.1.3.0"
34
+ output_name: str
35
+ defines: list[str]
36
+ include_paths: list[Path]
37
+ groups: list[SourceGroup]
38
+
39
+
40
+ class MdkProjectError(RuntimeError):
41
+ pass
42
+
43
+
44
+ # File types compiled from source (Keil's <FileType>): 1=C, 2=Asm, 8=C++.
45
+ _COMPILABLE_FILE_TYPES = {"1", "2", "8"}
46
+
47
+
48
+ def find_uvprojx(project_dir: Path) -> Path:
49
+ mdk_dir = project_dir / "MDK"
50
+ if not mdk_dir.is_dir():
51
+ raise MdkProjectError(f"no MDK/ folder found under {project_dir}")
52
+ candidates = sorted(mdk_dir.glob("*.uvprojx"))
53
+ if not candidates:
54
+ raise MdkProjectError(f"no .uvprojx file found under {mdk_dir}")
55
+ return candidates[0]
56
+
57
+
58
+ def parse_pack_id(pack_id: str) -> tuple[str, str, str]:
59
+ """'NOVOSENSE.NS800RT5XXX.1.3.0' -> ('NOVOSENSE', 'NS800RT5XXX', '1.3.0')."""
60
+ parts = pack_id.split(".")
61
+ if len(parts) < 4:
62
+ raise MdkProjectError(f"unexpected PackID format: {pack_id!r}")
63
+ vendor, name = parts[0], parts[1]
64
+ version = ".".join(parts[2:])
65
+ return vendor, name, version
66
+
67
+
68
+ def parse_uvprojx(uvprojx_path: Path) -> MdkTarget:
69
+ tree = ET.parse(uvprojx_path)
70
+ root = tree.getroot()
71
+
72
+ target_el = root.find("./Targets/Target")
73
+ if target_el is None:
74
+ raise MdkProjectError(f"no <Target> found in {uvprojx_path}")
75
+
76
+ target_name = _text(target_el, "TargetName")
77
+ common = target_el.find("./TargetOption/TargetCommonOption")
78
+ if common is None:
79
+ raise MdkProjectError(f"no TargetCommonOption in {uvprojx_path}")
80
+
81
+ device = _text(common, "Device")
82
+ vendor = _text(common, "Vendor")
83
+ pack_id = _text(common, "PackID")
84
+ output_name = _text(common, "OutputName") or target_name
85
+
86
+ cads = target_el.find("./TargetOption/TargetArmAds/Cads/VariousControls")
87
+ defines: list[str] = []
88
+ include_paths: list[Path] = []
89
+ if cads is not None:
90
+ define_text = _text(cads, "Define")
91
+ defines = [d for d in define_text.split() if d]
92
+ include_text = _text(cads, "IncludePath")
93
+ project_dir = uvprojx_path.parent # .../MDK
94
+ for raw in include_text.split(";"):
95
+ raw = raw.strip()
96
+ if not raw:
97
+ continue
98
+ include_paths.append((project_dir / raw.replace("\\", "/")).resolve())
99
+
100
+ groups: list[SourceGroup] = []
101
+ project_dir = uvprojx_path.parent
102
+ for group_el in target_el.findall("./Groups/Group"):
103
+ group_name = _text(group_el, "GroupName") or "group"
104
+ files: list[SourceFile] = []
105
+ for file_el in group_el.findall("./Files/File"):
106
+ file_type = _text(file_el, "FileType")
107
+ file_path = _text(file_el, "FilePath")
108
+ if not file_path:
109
+ continue
110
+ abs_path = (project_dir / file_path.replace("\\", "/")).resolve()
111
+ files.append(SourceFile(path=abs_path, is_source=file_type in _COMPILABLE_FILE_TYPES))
112
+ groups.append(SourceGroup(name=group_name, files=files))
113
+
114
+ return MdkTarget(
115
+ target_name=target_name,
116
+ device=device,
117
+ vendor=vendor,
118
+ pack_id=pack_id,
119
+ output_name=output_name,
120
+ defines=defines,
121
+ include_paths=include_paths,
122
+ groups=groups,
123
+ )
124
+
125
+
126
+ def _text(parent: ET.Element, tag: str) -> str:
127
+ el = parent.find(tag)
128
+ if el is None or el.text is None:
129
+ return ""
130
+ return el.text.strip()