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.
- armbuilderx-0.1.1/PKG-INFO +97 -0
- armbuilderx-0.1.1/README.md +82 -0
- armbuilderx-0.1.1/pyproject.toml +26 -0
- armbuilderx-0.1.1/setup.cfg +4 -0
- armbuilderx-0.1.1/src/armbuilderx/__init__.py +1 -0
- armbuilderx-0.1.1/src/armbuilderx/cli.py +258 -0
- armbuilderx-0.1.1/src/armbuilderx/cmsis/__init__.py +0 -0
- armbuilderx-0.1.1/src/armbuilderx/cmsis/csolution.py +46 -0
- armbuilderx-0.1.1/src/armbuilderx/cmsis/generator.py +21 -0
- armbuilderx-0.1.1/src/armbuilderx/cmsis/mdk_project.py +130 -0
- armbuilderx-0.1.1/src/armbuilderx/cmsis/toolbox.py +69 -0
- armbuilderx-0.1.1/src/armbuilderx/cmsis/types.py +17 -0
- armbuilderx-0.1.1/src/armbuilderx/flash.py +54 -0
- armbuilderx-0.1.1/src/armbuilderx/paths.py +66 -0
- armbuilderx-0.1.1/src/armbuilderx/sdk/__init__.py +10 -0
- armbuilderx-0.1.1/src/armbuilderx/sdk/base.py +26 -0
- armbuilderx-0.1.1/src/armbuilderx/sdk/gr551x.py +390 -0
- armbuilderx-0.1.1/src/armbuilderx/sdk/ns800rt5xxx.py +171 -0
- armbuilderx-0.1.1/src/armbuilderx/sdk/registry.py +42 -0
- armbuilderx-0.1.1/src/armbuilderx/serial/__init__.py +0 -0
- armbuilderx-0.1.1/src/armbuilderx/serial/monitor.py +84 -0
- armbuilderx-0.1.1/src/armbuilderx/toolchain/__init__.py +0 -0
- armbuilderx-0.1.1/src/armbuilderx/toolchain/detect.py +97 -0
- armbuilderx-0.1.1/src/armbuilderx/toolchain/ensure.py +106 -0
- armbuilderx-0.1.1/src/armbuilderx/toolchain/install.py +199 -0
- armbuilderx-0.1.1/src/armbuilderx.egg-info/PKG-INFO +97 -0
- armbuilderx-0.1.1/src/armbuilderx.egg-info/SOURCES.txt +36 -0
- armbuilderx-0.1.1/src/armbuilderx.egg-info/dependency_links.txt +1 -0
- armbuilderx-0.1.1/src/armbuilderx.egg-info/entry_points.txt +2 -0
- armbuilderx-0.1.1/src/armbuilderx.egg-info/requires.txt +6 -0
- armbuilderx-0.1.1/src/armbuilderx.egg-info/top_level.txt +1 -0
- armbuilderx-0.1.1/tests/test_cli.py +21 -0
- armbuilderx-0.1.1/tests/test_csolution.py +41 -0
- armbuilderx-0.1.1/tests/test_flash.py +35 -0
- armbuilderx-0.1.1/tests/test_generator.py +44 -0
- armbuilderx-0.1.1/tests/test_gr551x.py +85 -0
- armbuilderx-0.1.1/tests/test_mdk_project.py +51 -0
- 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 @@
|
|
|
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()
|