alloy-embedded 0.1.0__py3-none-any.whl

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 (87) hide show
  1. alloy_cli/__init__.py +3 -0
  2. alloy_cli/build.py +201 -0
  3. alloy_cli/cli.py +351 -0
  4. alloy_cli/debug.py +61 -0
  5. alloy_cli/emit/__init__.py +89 -0
  6. alloy_cli/emit/board.py +433 -0
  7. alloy_cli/emit/boot.py +31 -0
  8. alloy_cli/emit/common.py +55 -0
  9. alloy_cli/emit/device.py +211 -0
  10. alloy_cli/emit/ip.py +125 -0
  11. alloy_cli/emit/linker.py +190 -0
  12. alloy_cli/emit/vectors.py +133 -0
  13. alloy_cli/flash.py +277 -0
  14. alloy_cli/monitor.py +104 -0
  15. alloy_cli/payload/boards/esp32_devkit/board.json +62 -0
  16. alloy_cli/payload/boards/esp_wrover_kit/board.json +62 -0
  17. alloy_cli/payload/boards/nucleo_f722ze/board.json +38 -0
  18. alloy_cli/payload/boards/nucleo_g071rb/board.json +54 -0
  19. alloy_cli/payload/boards/nucleo_g0b1re/board.json +54 -0
  20. alloy_cli/payload/boards/raspberry_pi_pico/board.json +23 -0
  21. alloy_cli/payload/boards/rp2040_zero/board.json +24 -0
  22. alloy_cli/payload/boards/same70_xplained/board.json +60 -0
  23. alloy_cli/payload/src/alloy/adc.hpp +78 -0
  24. alloy_cli/payload/src/alloy/arch/cortex_m/irq_dispatch.cpp +27 -0
  25. alloy_cli/payload/src/alloy/arch/cortex_m/nvic.hpp +41 -0
  26. alloy_cli/payload/src/alloy/arch/cortex_m/startup.cpp +56 -0
  27. alloy_cli/payload/src/alloy/arch/cortex_m/systick.cpp +77 -0
  28. alloy_cli/payload/src/alloy/arch/cortex_m/systick.hpp +20 -0
  29. alloy_cli/payload/src/alloy/arch/irq.hpp +26 -0
  30. alloy_cli/payload/src/alloy/arch/xtensa/irq_ctrl.cpp +112 -0
  31. alloy_cli/payload/src/alloy/arch/xtensa/startup.S +47 -0
  32. alloy_cli/payload/src/alloy/arch/xtensa/startup.cpp +39 -0
  33. alloy_cli/payload/src/alloy/arch/xtensa/systick.cpp +58 -0
  34. alloy_cli/payload/src/alloy/arch/xtensa/systick.hpp +22 -0
  35. alloy_cli/payload/src/alloy/arch/xtensa/vectors.S +467 -0
  36. alloy_cli/payload/src/alloy/concepts.hpp +73 -0
  37. alloy_cli/payload/src/alloy/core/mmio.hpp +79 -0
  38. alloy_cli/payload/src/alloy/core/routes.hpp +51 -0
  39. alloy_cli/payload/src/alloy/core/types.hpp +82 -0
  40. alloy_cli/payload/src/alloy/dma.hpp +112 -0
  41. alloy_cli/payload/src/alloy/drivers/at24.hpp +132 -0
  42. alloy_cli/payload/src/alloy/drivers/spi_flash.hpp +156 -0
  43. alloy_cli/payload/src/alloy/drivers/ws2812.hpp +89 -0
  44. alloy_cli/payload/src/alloy/gpio.hpp +63 -0
  45. alloy_cli/payload/src/alloy/hal/adc/adc_impl.hpp +12 -0
  46. alloy_cli/payload/src/alloy/hal/adc/microchip_afec_v1.hpp +89 -0
  47. alloy_cli/payload/src/alloy/hal/adc/st_adc_v2.hpp +95 -0
  48. alloy_cli/payload/src/alloy/hal/clock_program.hpp +53 -0
  49. alloy_cli/payload/src/alloy/hal/dma/dma_impl.hpp +12 -0
  50. alloy_cli/payload/src/alloy/hal/dma/microchip_xdmac_v1.hpp +132 -0
  51. alloy_cli/payload/src/alloy/hal/dma/st_dma_v1.hpp +130 -0
  52. alloy_cli/payload/src/alloy/hal/gpio/espressif_gpio_v1.hpp +123 -0
  53. alloy_cli/payload/src/alloy/hal/gpio/microchip_pio_v1.hpp +80 -0
  54. alloy_cli/payload/src/alloy/hal/gpio/pin_impl.hpp +14 -0
  55. alloy_cli/payload/src/alloy/hal/gpio/raspberrypi_io_bank0.hpp +80 -0
  56. alloy_cli/payload/src/alloy/hal/gpio/st_gpio_v2.hpp +78 -0
  57. alloy_cli/payload/src/alloy/hal/i2c/espressif_i2c_v1.hpp +179 -0
  58. alloy_cli/payload/src/alloy/hal/i2c/i2c_impl.hpp +12 -0
  59. alloy_cli/payload/src/alloy/hal/i2c/microchip_twihs_v1.hpp +148 -0
  60. alloy_cli/payload/src/alloy/hal/i2c/st_i2c_v2.hpp +117 -0
  61. alloy_cli/payload/src/alloy/hal/pwm/espressif_ledc_v1.hpp +82 -0
  62. alloy_cli/payload/src/alloy/hal/pwm/pwm_impl.hpp +12 -0
  63. alloy_cli/payload/src/alloy/hal/pwm/st_tim_gp16.hpp +96 -0
  64. alloy_cli/payload/src/alloy/hal/spi/microchip_spi_v1.hpp +69 -0
  65. alloy_cli/payload/src/alloy/hal/spi/spi_impl.hpp +12 -0
  66. alloy_cli/payload/src/alloy/hal/spi/st_spi_v2.hpp +65 -0
  67. alloy_cli/payload/src/alloy/hal/uart/espressif_uart_v1.hpp +99 -0
  68. alloy_cli/payload/src/alloy/hal/uart/microchip_usart_v1.hpp +107 -0
  69. alloy_cli/payload/src/alloy/hal/uart/raspberrypi_uart_pl011.hpp +100 -0
  70. alloy_cli/payload/src/alloy/hal/uart/st_usart_v3.hpp +109 -0
  71. alloy_cli/payload/src/alloy/hal/uart/st_usart_v4.hpp +110 -0
  72. alloy_cli/payload/src/alloy/hal/uart/uart_impl.hpp +13 -0
  73. alloy_cli/payload/src/alloy/i2c.hpp +106 -0
  74. alloy_cli/payload/src/alloy/irq.hpp +66 -0
  75. alloy_cli/payload/src/alloy/pwm.hpp +100 -0
  76. alloy_cli/payload/src/alloy/spi.hpp +97 -0
  77. alloy_cli/payload/src/alloy/time.hpp +24 -0
  78. alloy_cli/payload/src/alloy/uart.hpp +162 -0
  79. alloy_cli/ports.py +52 -0
  80. alloy_cli/project.py +137 -0
  81. alloy_cli/toolchains.json +139 -0
  82. alloy_cli/toolchains.py +195 -0
  83. alloy_embedded-0.1.0.dist-info/METADATA +44 -0
  84. alloy_embedded-0.1.0.dist-info/RECORD +87 -0
  85. alloy_embedded-0.1.0.dist-info/WHEEL +4 -0
  86. alloy_embedded-0.1.0.dist-info/entry_points.txt +2 -0
  87. alloy_embedded-0.1.0.dist-info/licenses/LICENSE +21 -0
alloy_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """alloy CLI: codegen + build orchestration for the Alloy framework."""
2
+
3
+ __version__ = "0.1.0"
alloy_cli/build.py ADDED
@@ -0,0 +1,201 @@
1
+ """Render the CMake build tree and drive cmake + ninja.
2
+
3
+ Users never see CMake: the tree lives in .alloy/build-tree (gitignored,
4
+ regenerated every run). `alloy export cmake` will later emit a standalone
5
+ copy for people who want to own their build.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import shutil
11
+ import subprocess
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from .emit.common import EmitError
16
+ from .project import Project
17
+
18
+ # Core name -> GCC -mcpu flag; fallback per architecture.
19
+ _CPU_BY_CORE = {
20
+ "cm0": "cortex-m0",
21
+ "cm0plus": "cortex-m0plus",
22
+ "cm3": "cortex-m3",
23
+ "cm4": "cortex-m4",
24
+ "cm7": "cortex-m7",
25
+ "cm33": "cortex-m33",
26
+ }
27
+ _CPU_BY_ARCH = {
28
+ "armv6m": "cortex-m0plus",
29
+ "armv7m": "cortex-m3",
30
+ "armv7em": "cortex-m4",
31
+ "armv8m_base": "cortex-m23",
32
+ "armv8m_main": "cortex-m33",
33
+ }
34
+
35
+
36
+ def _arch_ns(chip: dict[str, Any]) -> str:
37
+ arch = chip["cores"][0]["arch"]
38
+ if arch.startswith("armv"):
39
+ return "cortex_m"
40
+ if arch.startswith("xtensa"):
41
+ return "xtensa"
42
+ raise EmitError(f"unsupported architecture {arch}")
43
+
44
+
45
+ def _xtensa_prefix() -> str:
46
+ if found := shutil.which("xtensa-esp-elf-gcc"):
47
+ return str(Path(found).with_name("xtensa-esp-elf-"))
48
+ candidate = Path.home() / ".alloy/tools/xtensa-esp-elf/bin/xtensa-esp-elf-gcc"
49
+ if candidate.exists():
50
+ return str(candidate.with_name("xtensa-esp-elf-"))
51
+ raise EmitError(
52
+ "xtensa-esp-elf toolchain not found — looked on PATH and ~/.alloy/tools/xtensa-esp-elf"
53
+ )
54
+
55
+
56
+ def _cpu_flags(chip: dict[str, Any]) -> str:
57
+ if _arch_ns(chip) == "xtensa":
58
+ # The unified xtensa-esp-elf toolchain is multi-core: -mdynconfig
59
+ # selects the concrete core (ESP32 = LX6 little-endian); without it
60
+ # GCC emits generic BIG-endian objects that esptool rejects. The flag
61
+ # must stay RELATIVE to match the multilib table (esp32/libgcc.a);
62
+ # macOS hardened dlopen then needs XTENSA_GNU_CONFIG (absolute) set
63
+ # in the environment — see _build_env().
64
+ return f"-mdynconfig=xtensa_{chip['family']}.so -mlongcalls"
65
+ core = chip["cores"][0]
66
+ cpu = _CPU_BY_CORE.get(core["name"]) or _CPU_BY_ARCH.get(core["arch"])
67
+ if cpu is None:
68
+ raise EmitError(f"no CPU mapping for core {core['name']} / arch {core['arch']}")
69
+ # Soft-float everywhere for now, even on FPU parts: hard-float needs a
70
+ # CPACR enable in the arch startup before the first FP instruction.
71
+ # TODO(arch): enable CP10/CP11 on armv7em, then honor core.fpu here.
72
+ return f"-mcpu={cpu} -mthumb -mfloat-abi=soft"
73
+
74
+
75
+ def _toolchain_cmake(chip: dict[str, Any], cpu_flags: str) -> str:
76
+ if _arch_ns(chip) == "xtensa":
77
+ prefix = _xtensa_prefix()
78
+ processor = "xtensa"
79
+ cc, cxx = f"{prefix}gcc", f"{prefix}g++"
80
+ else:
81
+ processor = "arm"
82
+ cc, cxx = "arm-none-eabi-gcc", "arm-none-eabi-g++"
83
+ return f"""# GENERATED by alloy — DO NOT EDIT
84
+ set(CMAKE_SYSTEM_NAME Generic)
85
+ set(CMAKE_SYSTEM_PROCESSOR {processor})
86
+ set(CMAKE_C_COMPILER "{cc}")
87
+ set(CMAKE_CXX_COMPILER "{cxx}")
88
+ set(CMAKE_ASM_COMPILER "{cc}")
89
+ set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
90
+ set(CMAKE_C_FLAGS_INIT "{cpu_flags}")
91
+ set(CMAKE_CXX_FLAGS_INIT "{cpu_flags}")
92
+ set(CMAKE_ASM_FLAGS_INIT "{cpu_flags}")
93
+ set(CMAKE_EXE_LINKER_FLAGS_INIT "{cpu_flags}")
94
+ """
95
+
96
+
97
+ def _cmakelists(project: Project, chip: dict[str, Any], sources: list[Path],
98
+ runtime_sources: list[Path]) -> str:
99
+ gen = project.gen_dir
100
+ gen_sources = [gen / "board.cpp"]
101
+ for optional in ("vector_table.c", "boot2.c", "irq_data.c"):
102
+ if (gen / optional).exists():
103
+ gen_sources.append(gen / optional)
104
+ src_list = "\n ".join(
105
+ f'"{p}"' for p in [*sources, *gen_sources, *runtime_sources]
106
+ )
107
+ # newlib nano/nosys specs are an ARM-newlib convention; the xtensa
108
+ # toolchain links its own newlib without them.
109
+ specs = "" if _arch_ns(chip) == "xtensa" else """
110
+ --specs=nano.specs
111
+ --specs=nosys.specs"""
112
+ return f"""# GENERATED by alloy — DO NOT EDIT
113
+ cmake_minimum_required(VERSION 3.25)
114
+ project({project.name} C CXX ASM)
115
+
116
+ set(CMAKE_C_STANDARD 11)
117
+ set(CMAKE_CXX_STANDARD 23)
118
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
119
+ set(CMAKE_C_EXTENSIONS OFF)
120
+ set(CMAKE_CXX_EXTENSIONS OFF)
121
+ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
122
+
123
+ add_executable({project.name}.elf
124
+ {src_list}
125
+ )
126
+
127
+ target_include_directories({project.name}.elf PRIVATE
128
+ "{project.alloy_root / 'src'}"
129
+ "{gen}"
130
+ )
131
+
132
+ target_compile_options({project.name}.elf PRIVATE
133
+ -Os -g3
134
+ -ffunction-sections -fdata-sections
135
+ -Wall -Wextra
136
+ $<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
137
+ $<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
138
+ $<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
139
+ )
140
+
141
+ target_link_options({project.name}.elf PRIVATE
142
+ -T "{gen / 'linker.ld'}"
143
+ -nostartfiles
144
+ -Wl,--gc-sections
145
+ -Wl,-Map={project.name}.map{specs}
146
+ )
147
+ """
148
+
149
+
150
+ def build(project: Project, chip: dict[str, Any]) -> Path:
151
+ sources = sorted((project.root / "src").rglob("*.cpp")) + \
152
+ sorted((project.root / "src").rglob("*.c"))
153
+ if not sources:
154
+ raise EmitError(f"no sources under {project.root / 'src'}")
155
+
156
+ arch_dir = project.alloy_root / "src" / "alloy" / "arch" / _arch_ns(chip)
157
+ runtime_sources = sorted(arch_dir.glob("*.cpp")) + sorted(arch_dir.glob("*.S"))
158
+
159
+ tree = project.build_dir
160
+ tree.mkdir(parents=True, exist_ok=True)
161
+ (tree / "toolchain.cmake").write_text(_toolchain_cmake(chip, _cpu_flags(chip)))
162
+ (tree / "CMakeLists.txt").write_text(_cmakelists(project, chip, sources, runtime_sources))
163
+
164
+ env = None
165
+ if _arch_ns(chip) == "xtensa":
166
+ import os # noqa: PLC0415
167
+
168
+ dynconfig = (Path(_xtensa_prefix()).parent.parent / "lib" /
169
+ f"xtensa_{chip['family']}.so")
170
+ if not dynconfig.exists():
171
+ raise EmitError(f"toolchain dynconfig missing: {dynconfig}")
172
+ env = os.environ | {"XTENSA_GNU_CONFIG": str(dynconfig)}
173
+
174
+ out = tree / "out"
175
+ subprocess.run(
176
+ ["cmake", "-G", "Ninja", "-S", str(tree), "-B", str(out),
177
+ f"-DCMAKE_TOOLCHAIN_FILE={tree / 'toolchain.cmake'}",
178
+ "-DCMAKE_BUILD_TYPE=MinSizeRel"],
179
+ check=True, env=env,
180
+ )
181
+ subprocess.run(["cmake", "--build", str(out)], check=True, env=env)
182
+
183
+ elf = out / f"{project.name}.elf"
184
+ if not elf.exists():
185
+ raise EmitError(f"build finished but {elf} is missing")
186
+
187
+ size_tool = (
188
+ f"{_xtensa_prefix()}size" if _arch_ns(chip) == "xtensa"
189
+ else shutil.which("arm-none-eabi-size")
190
+ )
191
+ if size_tool:
192
+ subprocess.run([size_tool, str(elf)], check=False)
193
+
194
+ # clangd support out of the box.
195
+ cc_json = out / "compile_commands.json"
196
+ link = project.root / "compile_commands.json"
197
+ if cc_json.exists():
198
+ link.unlink(missing_ok=True)
199
+ link.symlink_to(cc_json)
200
+
201
+ return elf
alloy_cli/cli.py ADDED
@@ -0,0 +1,351 @@
1
+ """alloy CLI.
2
+
3
+ Commands:
4
+ new scaffold a portable project (the scaffold IS the CI-built blink)
5
+ boards list boards (--json for IDE integrations)
6
+ gen regenerate .alloy/generated from the device database
7
+ build gen + render CMake tree + compile
8
+ flash build + program the board
9
+ monitor bidirectional serial monitor
10
+ run flash + monitor
11
+ clean remove per-board build trees
12
+ set-board change the board in alloy.toml
13
+ setup verify/install toolchains into ~/.alloy/tools
14
+ debug-info debug-server facts for a board (--json)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import subprocess
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ from alloy_devices.loader import load_database
25
+
26
+ from . import __version__
27
+ from .build import build
28
+ from .emit import generate
29
+ from .emit.common import EmitError
30
+ from .project import Project, ProjectError, load_project
31
+
32
+ _MAIN_CPP = """\
33
+ // Portable hello — blink AND echo, so the board never looks dead after a
34
+ // flash. Identical bytes on every supported board; zero #ifdefs.
35
+ #include <alloy/board.hpp>
36
+
37
+ #include <cstdint>
38
+
39
+ int main() {
40
+ board::init();
41
+ auto uart = board::debug_uart::open({.baud = board::debug_uart_baud});
42
+ uart.write("alloy hello: blinking + echoing\\r\\n");
43
+
44
+ std::uint32_t last_toggle = alloy::uptime_ms();
45
+ while (true) {
46
+ std::uint8_t byte{};
47
+ if (uart.read(byte)) {
48
+ uart.write(byte);
49
+ }
50
+ if (alloy::uptime_ms() - last_toggle >= 500u) {
51
+ board::led.toggle();
52
+ last_toggle = alloy::uptime_ms();
53
+ }
54
+ }
55
+ }
56
+ """
57
+
58
+ _ALLOY_TOML = """\
59
+ [project]
60
+ name = "{name}"
61
+
62
+ [board]
63
+ id = "{board}"
64
+
65
+ [alloy]
66
+ root = "{root}"
67
+ """
68
+
69
+ _GITIGNORE = """\
70
+ .alloy/
71
+ compile_commands.json
72
+ """
73
+
74
+
75
+ def _project(args: argparse.Namespace) -> Project:
76
+ return load_project(
77
+ Path(getattr(args, "project", ".") or "."),
78
+ board_override=getattr(args, "board", None),
79
+ )
80
+
81
+
82
+ def cmd_new(args: argparse.Namespace) -> int:
83
+ from .project import _find_alloy_root # noqa: PLC0415
84
+
85
+ target = Path(args.name)
86
+ if target.exists():
87
+ print(f"error: {target} already exists", file=sys.stderr)
88
+ return 1
89
+ alloy_root = _find_alloy_root(Path.cwd())
90
+ boards_dir = alloy_root / "boards"
91
+ if not (boards_dir / args.board / "board.json").exists():
92
+ known = sorted(p.name for p in boards_dir.iterdir() if (p / "board.json").exists())
93
+ print(f"error: unknown board '{args.board}' — known: {', '.join(known)}", file=sys.stderr)
94
+ return 1
95
+ (target / "src").mkdir(parents=True)
96
+ from .project import packaged_alloy_root # noqa: PLC0415
97
+
98
+ if alloy_root == packaged_alloy_root():
99
+ # Wheel installs are relocatable: resolution finds the package at
100
+ # runtime; baking a version-specific site-packages path would rot.
101
+ toml_text = _ALLOY_TOML.split("[alloy]")[0].format(
102
+ name=target.name, board=args.board
103
+ )
104
+ else:
105
+ toml_text = _ALLOY_TOML.format(
106
+ name=target.name, board=args.board, root=alloy_root
107
+ )
108
+ (target / "alloy.toml").write_text(toml_text)
109
+ (target / "src" / "main.cpp").write_text(_MAIN_CPP)
110
+ (target / ".gitignore").write_text(_GITIGNORE)
111
+ print(f"created {target}/ (board: {args.board}, framework: {alloy_root})")
112
+ print(f"next: cd {target} && alloy run")
113
+ return 0
114
+
115
+
116
+ def cmd_boards(args: argparse.Namespace) -> int:
117
+ import json # noqa: PLC0415
118
+
119
+ project_dir = Path(getattr(args, "project", ".") or ".")
120
+ try:
121
+ project = load_project(project_dir)
122
+ boards_dir = project.alloy_root / "boards"
123
+ except ProjectError:
124
+ from .project import _find_alloy_root # noqa: PLC0415
125
+
126
+ boards_dir = _find_alloy_root(project_dir.resolve()) / "boards"
127
+ rows = []
128
+ for board_json in sorted(boards_dir.glob("*/board.json")):
129
+ board = json.loads(board_json.read_text())
130
+ rows.append(board)
131
+ if getattr(args, "json", False):
132
+ print(json.dumps({
133
+ "schema": "alloy.boards.v1",
134
+ "boards": [
135
+ {
136
+ "id": b["id"],
137
+ "name": b.get("name", ""),
138
+ "chip": b["chip"],
139
+ "probe": b.get("probe", {}).get("kind"),
140
+ "roles": sorted(b.get("roles", {})),
141
+ }
142
+ for b in rows
143
+ ],
144
+ }, indent=2))
145
+ return 0
146
+ for b in rows:
147
+ print(f"{b['id']:24} {b.get('name', ''):32} chip={b['chip']}")
148
+ return 0
149
+
150
+
151
+ def cmd_gen(args: argparse.Namespace) -> int:
152
+ project = _project(args)
153
+ written = generate(project)
154
+ print(f"generated {len(written)} file(s) -> {project.gen_dir}")
155
+ return 0
156
+
157
+
158
+ def cmd_build(args: argparse.Namespace) -> int:
159
+ project = _project(args)
160
+ generate(project)
161
+ board = project.load_board()
162
+ db = load_database(project.devices_root)
163
+ chip = db.chips[board["chip"]]
164
+ elf = build(project, chip)
165
+ print(f"\nbuilt {elf}")
166
+ return 0
167
+
168
+
169
+ def cmd_flash(args: argparse.Namespace) -> int:
170
+ from .flash import flash # noqa: PLC0415
171
+
172
+ project = _project(args)
173
+ generate(project)
174
+ board = project.load_board()
175
+ db = load_database(project.devices_root)
176
+ chip = db.chips[board["chip"]]
177
+ elf = build(project, chip)
178
+ runner = flash(board, chip, elf)
179
+ print(f"\nflashed {elf.name} via {runner}")
180
+ return 0
181
+
182
+
183
+ def cmd_monitor(args: argparse.Namespace) -> int:
184
+ from .monitor import monitor # noqa: PLC0415
185
+
186
+ project = _project(args)
187
+ monitor(project.load_board())
188
+ return 0
189
+
190
+
191
+ def cmd_run(args: argparse.Namespace) -> int:
192
+ from .emit.common import EmitError # noqa: PLC0415
193
+ from .monitor import monitor # noqa: PLC0415
194
+
195
+ rc = cmd_flash(args)
196
+ if rc != 0:
197
+ return rc
198
+ project = _project(args)
199
+ try:
200
+ monitor(project.load_board())
201
+ except EmitError as exc:
202
+ print(f"note: {exc}")
203
+ return 0
204
+
205
+
206
+ def cmd_clean(args: argparse.Namespace) -> int:
207
+ import shutil # noqa: PLC0415
208
+
209
+ root = Path(getattr(args, "project", ".") or ".").resolve()
210
+ if not (root / "alloy.toml").exists():
211
+ print(f"error: {root} is not an alloy project (no alloy.toml)", file=sys.stderr)
212
+ return 1
213
+ base = root / ".alloy"
214
+ targets = [base] if args.all else (
215
+ [base / "build-tree" / args.board, base / "generated" / args.board]
216
+ if getattr(args, "board", None)
217
+ else [base / "build-tree"]
218
+ )
219
+ removed = 0
220
+ for target in targets:
221
+ if target.exists():
222
+ shutil.rmtree(target)
223
+ removed += 1
224
+ print(f"removed {target}")
225
+ if removed == 0:
226
+ print("nothing to clean")
227
+ return 0
228
+
229
+
230
+ def cmd_set_board(args: argparse.Namespace) -> int:
231
+ import re # noqa: PLC0415
232
+
233
+ root = Path(getattr(args, "project", ".") or ".").resolve()
234
+ toml_path = root / "alloy.toml"
235
+ if not toml_path.exists():
236
+ print(f"error: {root} is not an alloy project (no alloy.toml)", file=sys.stderr)
237
+ return 1
238
+ # Validate against known boards first.
239
+ project = load_project(root)
240
+ boards_dir = project.alloy_root / "boards"
241
+ if not (boards_dir / args.board_id / "board.json").exists():
242
+ known = sorted(p.name for p in boards_dir.iterdir() if (p / "board.json").exists())
243
+ print(f"error: unknown board '{args.board_id}' — known: {', '.join(known)}",
244
+ file=sys.stderr)
245
+ return 1
246
+ text = toml_path.read_text()
247
+ new_text, n = re.subn(
248
+ r'(\[board\]\s*\n\s*id\s*=\s*")[^"]*(")',
249
+ rf"\g<1>{args.board_id}\g<2>",
250
+ text,
251
+ count=1,
252
+ )
253
+ if n != 1:
254
+ print("error: could not locate [board] id in alloy.toml", file=sys.stderr)
255
+ return 1
256
+ toml_path.write_text(new_text)
257
+ print(f"board set to {args.board_id}")
258
+ return 0
259
+
260
+
261
+ def cmd_setup(args: argparse.Namespace) -> int:
262
+ from .toolchains import setup # noqa: PLC0415
263
+
264
+ families = set(args.family) if args.family else None
265
+ return setup(families, check_only=args.check,
266
+ json_out=args.json, json_progress=args.json_progress)
267
+
268
+
269
+ def cmd_debug_info(args: argparse.Namespace) -> int:
270
+ import json # noqa: PLC0415
271
+
272
+ from .debug import debug_info # noqa: PLC0415
273
+
274
+ project = _project(args)
275
+ board = project.load_board()
276
+ db = load_database(project.devices_root)
277
+ chip = db.chips[board["chip"]]
278
+ elf = project.build_dir / "out" / f"{project.name}.elf"
279
+ info = debug_info(board, chip, elf if elf.exists() else None)
280
+ info["schema"] = "alloy.debug_info.v1"
281
+ if getattr(args, "json", False):
282
+ print(json.dumps(info, indent=2))
283
+ else:
284
+ for key, value in info.items():
285
+ print(f"{key:14} {value}")
286
+ return 0
287
+
288
+
289
+ def main() -> None:
290
+ parser = argparse.ArgumentParser(prog="alloy", description=__doc__)
291
+ parser.add_argument("--version", action="version", version=__version__)
292
+ sub = parser.add_subparsers(dest="command", required=True)
293
+
294
+ p_new = sub.add_parser("new", help="scaffold a new project")
295
+ p_new.add_argument("name")
296
+ p_new.add_argument("--board", required=True)
297
+ p_new.set_defaults(func=cmd_new)
298
+
299
+ p_boards = sub.add_parser("boards", help="list known boards")
300
+ p_boards.add_argument("--project", default=".")
301
+ p_boards.add_argument("--json", action="store_true")
302
+ p_boards.set_defaults(func=cmd_boards)
303
+
304
+ p_clean = sub.add_parser("clean", help="remove per-board build trees")
305
+ p_clean.add_argument("--project", default=".")
306
+ p_clean.add_argument("--board", help="clean only this board's trees")
307
+ p_clean.add_argument("--all", action="store_true", help="remove the whole .alloy/")
308
+ p_clean.set_defaults(func=cmd_clean)
309
+
310
+ p_setb = sub.add_parser("set-board", help="change the board in alloy.toml")
311
+ p_setb.add_argument("board_id")
312
+ p_setb.add_argument("--project", default=".")
313
+ p_setb.set_defaults(func=cmd_set_board)
314
+
315
+ p_setup = sub.add_parser("setup", help="verify/install toolchains")
316
+ p_setup.add_argument("--family", action="append",
317
+ help="limit to a chip family (repeatable)")
318
+ p_setup.add_argument("--check", action="store_true", help="report only")
319
+ p_setup.add_argument("--json", action="store_true")
320
+ p_setup.add_argument("--json-progress", action="store_true")
321
+ p_setup.set_defaults(func=cmd_setup)
322
+
323
+ p_dbg = sub.add_parser("debug-info", help="debug-server facts for a board")
324
+ p_dbg.add_argument("--project", default=".")
325
+ p_dbg.add_argument("--board")
326
+ p_dbg.add_argument("--json", action="store_true")
327
+ p_dbg.set_defaults(func=cmd_debug_info)
328
+
329
+ for cmd, func in (("gen", cmd_gen), ("build", cmd_build), ("flash", cmd_flash),
330
+ ("monitor", cmd_monitor), ("run", cmd_run)):
331
+ p = sub.add_parser(cmd)
332
+ p.add_argument("--project", default=".")
333
+ p.add_argument("--board", help="override the board declared in alloy.toml")
334
+ p.set_defaults(func=func)
335
+
336
+ args = parser.parse_args()
337
+ try:
338
+ sys.exit(args.func(args))
339
+ except (ProjectError, EmitError) as exc:
340
+ print(f"error: {exc}", file=sys.stderr)
341
+ sys.exit(1)
342
+ except subprocess.CalledProcessError as exc:
343
+ cmd = exc.cmd[0] if isinstance(exc.cmd, list) else str(exc.cmd)
344
+ print(f"error: {cmd} failed (exit {exc.returncode})", file=sys.stderr)
345
+ sys.exit(1)
346
+ except KeyboardInterrupt:
347
+ sys.exit(130)
348
+
349
+
350
+ if __name__ == "__main__":
351
+ main()
alloy_cli/debug.py ADDED
@@ -0,0 +1,61 @@
1
+ """Debug-server facts per board — consumed by IDE integrations.
2
+
3
+ The openocd interface/target maps live HERE (flash.py imports them): one
4
+ source of truth for tool-integration knowledge. `debug_info` emits what a
5
+ Cortex-Debug launch.json needs; unsupported families say so honestly.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ # OpenOCD script maps (tool integration, not silicon facts).
14
+ OPENOCD_TARGET = {
15
+ "stm32g0": "stm32g0x",
16
+ "stm32f4": "stm32f4x",
17
+ "stm32f7": "stm32f7x",
18
+ "stm32g4": "stm32g4x",
19
+ "same70": "atsamv",
20
+ "rp2040": "rp2040",
21
+ }
22
+
23
+ OPENOCD_INTERFACE = {
24
+ "stlink": "stlink",
25
+ "cmsis-dap": "cmsis-dap",
26
+ }
27
+
28
+
29
+ def debug_info(board: dict[str, Any], chip: dict[str, Any],
30
+ elf: Path | None) -> dict[str, Any]:
31
+ probe = board.get("probe") or {}
32
+ kind = probe.get("kind", "")
33
+ interface = OPENOCD_INTERFACE.get(kind)
34
+ target = OPENOCD_TARGET.get(chip["family"])
35
+
36
+ info: dict[str, Any] = {
37
+ "board": board["id"],
38
+ "chip": f"{chip['vendor']}/{chip['part']}",
39
+ "elf": str(elf) if elf else None,
40
+ "svd": None, # SVD export is a future phase
41
+ }
42
+ if interface and target:
43
+ info.update(
44
+ supported=True,
45
+ servertype="openocd",
46
+ interface_cfg=f"interface/{interface}.cfg",
47
+ target_cfg=f"target/{target}.cfg",
48
+ device=chip["part"],
49
+ chip_id=probe.get("chip_id"),
50
+ gdb="arm-none-eabi-gdb",
51
+ )
52
+ else:
53
+ reason = (
54
+ "ESP32 interactive debug needs openocd-esp32 + xtensa gdb (future phase)"
55
+ if kind == "esptool"
56
+ else "BOOTSEL boards need a debug probe wired to SWD (picoprobe/debugprobe)"
57
+ if kind == "bootsel"
58
+ else f"no openocd mapping for probe kind '{kind}' / family '{chip['family']}'"
59
+ )
60
+ info.update(supported=False, reason=reason)
61
+ return info