tan-cli 0.7.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 (330) hide show
  1. tan/__init__.py +0 -0
  2. tan/__main__.py +120 -0
  3. tan/cli.py +939 -0
  4. tan/command_table.py +265 -0
  5. tan/commands/__init__.py +8 -0
  6. tan/commands/bootstrap_cmd.py +4856 -0
  7. tan/commands/bootstrap_patches.py +164 -0
  8. tan/commands/bootstrap_toolchain_fetch.py +155 -0
  9. tan/commands/bootstrap_toolchain_pin.py +36 -0
  10. tan/commands/build/__init__.py +1 -0
  11. tan/commands/build/configure_inputs.py +277 -0
  12. tan/commands/build/execute.py +1852 -0
  13. tan/commands/build/host_python.py +59 -0
  14. tan/commands/build/link_stale.py +78 -0
  15. tan/commands/build/manifest.py +428 -0
  16. tan/commands/build/materialise.py +81 -0
  17. tan/commands/build/token_substitution.py +381 -0
  18. tan/commands/build/toolchain.py +482 -0
  19. tan/commands/build/user_defines_wipe.py +115 -0
  20. tan/commands/build/workspace_patches.py +65 -0
  21. tan/commands/build_cmd.py +2236 -0
  22. tan/commands/build_output.py +331 -0
  23. tan/commands/clean_cmd.py +1183 -0
  24. tan/commands/completion_cmd.py +786 -0
  25. tan/commands/debug_config_cmd.py +2425 -0
  26. tan/commands/diff_cmd.py +940 -0
  27. tan/commands/doctor_cmd.py +4767 -0
  28. tan/commands/examples_cmd.py +644 -0
  29. tan/commands/explain_cmd.py +1319 -0
  30. tan/commands/faultdecode_cmd.py +931 -0
  31. tan/commands/flash_atoc_guard.py +240 -0
  32. tan/commands/flash_cmd.py +5731 -0
  33. tan/commands/flash_mram_guard.py +150 -0
  34. tan/commands/flash_ram.py +578 -0
  35. tan/commands/flash_raw.py +583 -0
  36. tan/commands/generate_cmd.py +1456 -0
  37. tan/commands/image_cmd.py +753 -0
  38. tan/commands/init_cmd.py +1740 -0
  39. tan/commands/inspect_cmd.py +377 -0
  40. tan/commands/kconfig_cmd.py +734 -0
  41. tan/commands/model_cmd.py +1514 -0
  42. tan/commands/model_device_cmd.py +351 -0
  43. tan/commands/model_device_live.py +234 -0
  44. tan/commands/model_host_cmd.py +430 -0
  45. tan/commands/model_zoo_cmd.py +363 -0
  46. tan/commands/monitor_actions.py +125 -0
  47. tan/commands/monitor_cmd.py +626 -0
  48. tan/commands/monitor_session.py +505 -0
  49. tan/commands/new_som_cmd.py +1522 -0
  50. tan/commands/pinmux_cmd.py +568 -0
  51. tan/commands/presets_cmd.py +1123 -0
  52. tan/commands/probe_cmd.py +421 -0
  53. tan/commands/reset_cmd.py +354 -0
  54. tan/commands/reset_confirm.py +168 -0
  55. tan/commands/run_cmd.py +637 -0
  56. tan/commands/scaffold_cmd.py +490 -0
  57. tan/commands/sdk_cmd.py +1723 -0
  58. tan/commands/size_cmd.py +1020 -0
  59. tan/commands/support_bundle_cmd.py +1073 -0
  60. tan/commands/trace_cmd.py +341 -0
  61. tan/commands/validate_cmd.py +2009 -0
  62. tan/commands/west_forward_cmd.py +736 -0
  63. tan/commands/workspace_patch_check.py +296 -0
  64. tan/core/__init__.py +1 -0
  65. tan/core/alp_conf_pregen.py +100 -0
  66. tan/core/artifact_provenance.py +240 -0
  67. tan/core/atoc_guard.py +276 -0
  68. tan/core/atoc_guard_messages.py +232 -0
  69. tan/core/atoc_replacement.py +118 -0
  70. tan/core/atomic_write.py +178 -0
  71. tan/core/board_context.py +351 -0
  72. tan/core/board_diagnostic.py +111 -0
  73. tan/core/board_files.py +221 -0
  74. tan/core/board_validator.py +542 -0
  75. tan/core/board_validator_compat.py +196 -0
  76. tan/core/board_validator_run.py +129 -0
  77. tan/core/board_validator_schema.py +186 -0
  78. tan/core/board_validator_skew.py +46 -0
  79. tan/core/board_yaml_edit.py +295 -0
  80. tan/core/board_yaml_pos.py +118 -0
  81. tan/core/bootstrap.py +2572 -0
  82. tan/core/build_plan.py +625 -0
  83. tan/core/consent.py +104 -0
  84. tan/core/console_filter.py +245 -0
  85. tan/core/debug_launch.py +1803 -0
  86. tan/core/devicetree_lint.py +168 -0
  87. tan/core/dir_removal.py +229 -0
  88. tan/core/doctor_git.py +172 -0
  89. tan/core/doctor_libraries.py +253 -0
  90. tan/core/doctor_render.py +185 -0
  91. tan/core/doctor_scope.py +66 -0
  92. tan/core/doctor_setools.py +237 -0
  93. tan/core/doctor_stale.py +180 -0
  94. tan/core/document_guards.py +723 -0
  95. tan/core/dp_id.py +97 -0
  96. tan/core/elf_load.py +63 -0
  97. tan/core/error_catalog.py +171 -0
  98. tan/core/example_catalog.py +499 -0
  99. tan/core/example_facets.py +151 -0
  100. tan/core/faultdecode.py +521 -0
  101. tan/core/flash_plan.py +2501 -0
  102. tan/core/flow_d_report.py +381 -0
  103. tan/core/flow_d_uptime.py +164 -0
  104. tan/core/fs_confine.py +130 -0
  105. tan/core/global_flags.py +230 -0
  106. tan/core/host_python.py +210 -0
  107. tan/core/image_bundle.py +247 -0
  108. tan/core/inert.py +147 -0
  109. tan/core/jlink_binary.py +128 -0
  110. tan/core/jlink_probe.py +481 -0
  111. tan/core/jsonc_splice.py +404 -0
  112. tan/core/launch_provenance.py +257 -0
  113. tan/core/link_refusal.py +76 -0
  114. tan/core/metadata_schema.py +376 -0
  115. tan/core/model_check.py +227 -0
  116. tan/core/model_device.py +449 -0
  117. tan/core/model_doctor.py +198 -0
  118. tan/core/model_host.py +48 -0
  119. tan/core/model_list.py +112 -0
  120. tan/core/model_zoo.py +353 -0
  121. tan/core/module_template.py +340 -0
  122. tan/core/mram_link.py +87 -0
  123. tan/core/new_som_ospi.py +94 -0
  124. tan/core/os_class.py +159 -0
  125. tan/core/patch_paths.py +87 -0
  126. tan/core/pending.py +40 -0
  127. tan/core/plain_zephyr_plan.py +138 -0
  128. tan/core/plan_exec.py +373 -0
  129. tan/core/plan_tokens.py +600 -0
  130. tan/core/probe.py +155 -0
  131. tan/core/probe_plan.py +237 -0
  132. tan/core/proxy.py +168 -0
  133. tan/core/publish.py +31 -0
  134. tan/core/python_floor.py +178 -0
  135. tan/core/ram_run.py +694 -0
  136. tan/core/ram_watch.py +231 -0
  137. tan/core/raw_write.py +160 -0
  138. tan/core/reset_plan.py +56 -0
  139. tan/core/run.py +161 -0
  140. tan/core/scaffold.py +2203 -0
  141. tan/core/scaffold_selftest_identity.py +225 -0
  142. tan/core/sdk_default_registry.py +565 -0
  143. tan/core/sdk_discovery.py +996 -0
  144. tan/core/sdk_removal.py +465 -0
  145. tan/core/serial_actions.py +175 -0
  146. tan/core/serial_capture.py +185 -0
  147. tan/core/serial_url.py +45 -0
  148. tan/core/setools.py +608 -0
  149. tan/core/setools_scratch.py +243 -0
  150. tan/core/shapes.py +194 -0
  151. tan/core/size.py +610 -0
  152. tan/core/som_buildability.py +204 -0
  153. tan/core/som_schema_version.py +62 -0
  154. tan/core/subprocess_env.py +201 -0
  155. tan/core/sysbuild_image.py +94 -0
  156. tan/core/system_manifest.py +827 -0
  157. tan/core/text_layout.py +73 -0
  158. tan/core/timestamp.py +182 -0
  159. tan/core/tool_lookup.py +383 -0
  160. tan/core/toolchain_pin.py +117 -0
  161. tan/core/toolchain_provision.py +1224 -0
  162. tan/core/trusted_path.py +134 -0
  163. tan/core/uboot_breakin.py +154 -0
  164. tan/core/uri_reference.py +415 -0
  165. tan/core/user_defines.py +152 -0
  166. tan/core/venv.py +332 -0
  167. tan/core/west_patches.py +143 -0
  168. tan/core/west_workspace_refusal.py +159 -0
  169. tan/core/zephyr_env.py +124 -0
  170. tan/env.py +185 -0
  171. tan/envelope.py +647 -0
  172. tan/exit_codes.py +12 -0
  173. tan/lazy_typer.py +185 -0
  174. tan/model/__init__.py +2 -0
  175. tan/model/_gen_fixture.py +132 -0
  176. tan/model/adapters/__init__.py +116 -0
  177. tan/model/adapters/cpu.py +26 -0
  178. tan/model/adapters/deepx.py +94 -0
  179. tan/model/adapters/drpai.py +242 -0
  180. tan/model/adapters/ethos_u.py +951 -0
  181. tan/model/adapters/executorch.py +55 -0
  182. tan/model/analyze.py +694 -0
  183. tan/model/build.py +304 -0
  184. tan/model/check.py +682 -0
  185. tan/model/manifest.py +625 -0
  186. tan/model/measure.py +216 -0
  187. tan/model/npu_ops_validate.py +130 -0
  188. tan/model/package.py +125 -0
  189. tan/model/perf.py +635 -0
  190. tan/model/perf_apply.py +721 -0
  191. tan/model/prep.py +193 -0
  192. tan/model/sram_fit.py +502 -0
  193. tan/model/targets.py +374 -0
  194. tan/model/tensorio.py +287 -0
  195. tan/net.py +73 -0
  196. tan/output_format.py +213 -0
  197. tan/planner/__init__.py +219 -0
  198. tan/planner/aperture.py +202 -0
  199. tan/planner/buildplan.py +1092 -0
  200. tan/planner/camera_owner.py +192 -0
  201. tan/planner/cameras.py +132 -0
  202. tan/planner/carveout.py +755 -0
  203. tan/planner/cli.py +274 -0
  204. tan/planner/headers.py +315 -0
  205. tan/planner/kconfig.py +2435 -0
  206. tan/planner/kconfig_symbols.py +473 -0
  207. tan/planner/libraries.py +677 -0
  208. tan/planner/link_target.py +237 -0
  209. tan/planner/linux_ownership.py +216 -0
  210. tan/planner/loader.py +1525 -0
  211. tan/planner/manifest.py +116 -0
  212. tan/planner/memory.py +161 -0
  213. tan/planner/memregion.py +56 -0
  214. tan/planner/models.py +533 -0
  215. tan/planner/orchestrator.py +874 -0
  216. tan/planner/ownership.py +203 -0
  217. tan/planner/partition.py +1160 -0
  218. tan/planner/paths.py +32 -0
  219. tan/planner/plain_slice.py +139 -0
  220. tan/planner/project_emit/__init__.py +60 -0
  221. tan/planner/project_emit/bom_netlist.py +375 -0
  222. tan/planner/project_emit/dts.py +580 -0
  223. tan/planner/project_emit/hw_info.py +221 -0
  224. tan/planner/project_emit/native_sim.py +136 -0
  225. tan/planner/project_emit/west_libs.py +213 -0
  226. tan/planner/project_loader.py +264 -0
  227. tan/planner/sdk_capability.py +203 -0
  228. tan/planner/sdk_compat.py +395 -0
  229. tan/planner/secure.py +350 -0
  230. tan/planner/slugs.py +394 -0
  231. tan/planner/som_metadata.py +325 -0
  232. tan/planner/strict_loaders.py +131 -0
  233. tan/planner/template.py +1037 -0
  234. tan/planner/template_pins.py +535 -0
  235. tan/planner/template_rewrite.py +714 -0
  236. tan/planner/topology.py +208 -0
  237. tan/planner/validate.py +365 -0
  238. tan/planner/whole_device_alias.py +67 -0
  239. tan/planner/zephyr_board.py +3510 -0
  240. tan/planner_cli.py +97 -0
  241. tan/planner_emit.py +617 -0
  242. tan/planner_root.py +119 -0
  243. tan/soc_ref.py +58 -0
  244. tan/templates/__init__.py +34 -0
  245. tan/templates/vendored/MANIFEST.md +1862 -0
  246. tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
  247. tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
  248. tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
  249. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
  250. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
  251. tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
  252. tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
  253. tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
  254. tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
  255. tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
  256. tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
  257. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
  258. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
  259. tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
  260. tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
  261. tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
  262. tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
  263. tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
  264. tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
  265. tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
  266. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
  267. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
  268. tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
  269. tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
  270. tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
  271. tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
  272. tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
  273. tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
  274. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
  275. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
  276. tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
  277. tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
  278. tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
  279. tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
  280. tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
  281. tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
  282. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
  283. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
  284. tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
  285. tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
  286. tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
  287. tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
  288. tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
  289. tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
  290. tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
  291. tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
  292. tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
  293. tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
  294. tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
  295. tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
  296. tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
  297. tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
  298. tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
  299. tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
  300. tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
  301. tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
  302. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
  303. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
  304. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
  305. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
  306. tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
  307. tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
  308. tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
  309. tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
  310. tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
  311. tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
  312. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
  313. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
  314. tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
  315. tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
  316. tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
  317. tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
  318. tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
  319. tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
  320. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
  321. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
  322. tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
  323. tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
  324. tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
  325. tan/version.py +102 -0
  326. tan_cli-0.7.0.dist-info/METADATA +125 -0
  327. tan_cli-0.7.0.dist-info/RECORD +330 -0
  328. tan_cli-0.7.0.dist-info/WHEEL +5 -0
  329. tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
  330. tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/planner/paths.py ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Filesystem roots for the planner package -- the bottom leaf.
4
+
5
+ `REPO`, `METADATA_ROOT`, and the schema paths. A near-dependency-free leaf so
6
+ any sibling -- topology, the loader, the carve-out / partition resolvers -- can
7
+ import a path root directly instead of reaching back into the package __init__
8
+ (which would cycle). Extracting these is the #285 paths seam that lets
9
+ topology.py drop its lazy `BOARD_SCHEMA` import.
10
+
11
+ RELOCATED (was alp-sdk `scripts/alp_orchestrate/paths.py`): the roots used to be
12
+ derived from this file's own location -- three parents up landed on the SDK
13
+ checkout. Inside `tan` that walk lands on the `tan` package, so the root is now
14
+ an explicit binding resolved by `tan.planner_root.sdk_root()`, which raises if
15
+ the package was imported before a root was bound. `REPO` still means *the
16
+ alp-sdk checkout*, never tan's own tree: `metadata/**`, `firmware/`,
17
+ `zephyr/sysbuild/` and the git commit all come from the SDK (ADR-0017 -- the
18
+ generators relocated, the facts did not).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from tan.planner_root import sdk_root
24
+
25
+ # Evaluated at import time, and deliberately so: siblings take
26
+ # `metadata_root: Path = METADATA_ROOT` as a *default argument*, which binds
27
+ # here too. `sdk_root()` raising on an unbound import is what keeps that from
28
+ # becoming a silently-wrong metadata tree.
29
+ REPO = sdk_root()
30
+ METADATA_ROOT = REPO / "metadata"
31
+ BOARD_SCHEMA = METADATA_ROOT / "schemas" / "board.schema.json"
32
+ BOARD_PRESET_SCHEMA = METADATA_ROOT / "schemas" / "board-preset.schema.json"
@@ -0,0 +1,139 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The `system-manifest.yaml` a `board.yaml`-less Zephyr build writes (tan-cli#1370).
3
+
4
+ `tan build --board <zephyr-board-target>` has no `board.yaml` for the planner to
5
+ project a manifest from. This builds the same document the planner would have
6
+ written for ONE slice -- same schema, same `flash_method`/`flash_args` (composed
7
+ by the planner's own `_slice_flash_recipe`, not re-spelled here) -- from the SoM
8
+ preset whose `topology.<core>.board` names the target.
9
+
10
+ The planner core id (`m55_he`) is NOT the Zephyr cpucluster (`rtss_he`) the board
11
+ qualifier ends in: it comes from the preset's topology key, so no core name is
12
+ spelled in this module. An unknown target (`native_sim`, a non-Alp board) still
13
+ gets a one-slice manifest, with no SoC facts to carry.
14
+
15
+ Imports of the planner are local: the planner freezes its SDK root at import
16
+ time, so callers must `bind_sdk_root` first.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ #: A manifest slice linked below the SoC flash base runs from RAM only.
24
+ RAM_RUN_ONLY = "ram_run_only"
25
+ #: The flash_args a RAM-run-only slice keeps: the wrong-board preflight pair.
26
+ _RAM_RUN_KEYS = ("expect_dpidr", "jlink_device")
27
+
28
+
29
+ def _find_preset(board: str, metadata_root: Path) -> tuple[dict, str] | None:
30
+ from .loader import _load_yaml
31
+
32
+ for path in sorted((metadata_root / "e1m_modules").glob("*.yaml")):
33
+ try:
34
+ preset = _load_yaml(path)
35
+ except Exception: # noqa: BLE001 -- an unreadable preset is not THIS target's
36
+ continue
37
+ for core_id, topo in sorted((preset.get("topology") or {}).items()):
38
+ if isinstance(topo, dict) and topo.get("board") == board:
39
+ return preset, str(core_id)
40
+ return None
41
+
42
+
43
+ def plain_core_id(board: str, metadata_root: Path) -> str | None:
44
+ """The planner core id of the SoM topology entry that names `board`."""
45
+ found = _find_preset(board, metadata_root)
46
+ return found[1] if found else None
47
+
48
+
49
+ def plain_system_manifest(
50
+ board: str, metadata_root: Path | None, *, fallback_core_id: str, elf_path: str | None = None
51
+ ) -> str:
52
+ """The manifest YAML for one plain Zephyr slice, status `pending`.
53
+
54
+ `elf_path`, when it points at an ELF whose lowest load address is below the
55
+ SoC's `soc_flash_base`, marks the slice `ram_run_only`."""
56
+ import yaml
57
+
58
+ from tan.core.elf_load import lowest_load_address
59
+
60
+ from .loader import (
61
+ _enforce_flow_d_preflight_pair, _jlink_flash_device_declared, _load_json, _resolve_flow_d_preflight,
62
+ _resolve_jlink_flash_device, _resolve_slot0_load_address,
63
+ _resolve_variant_debug, _silicon_to_soc_path, _slice_from_resolved)
64
+ from .models import Slice
65
+ from .validate import _enforce_os_matches_core_class
66
+ from .orchestrator import _slice_flash_recipe
67
+
68
+ found = _find_preset(board, metadata_root) if metadata_root is not None else None
69
+ hw_info: dict[str, Any] = dict.fromkeys(
70
+ ("sku", "som_hw_rev", "board_name", "board_hw_rev", "silicon"))
71
+ hw_info["sku"] = ""
72
+ flash_base: int | None = None
73
+ if found is None:
74
+ slice_ = Slice(core_id=fallback_core_id, os="zephyr", board=board)
75
+ else:
76
+ preset, core_id = found
77
+ topo = preset["topology"][core_id]
78
+ soc_spec = _load_json(_silicon_to_soc_path(preset["silicon"], metadata_root))
79
+ debug = _resolve_variant_debug(preset, soc_spec)
80
+ declared = _jlink_flash_device_declared(debug)
81
+ flash_device = _resolve_jlink_flash_device(debug)
82
+ expect_dpidr, jlink_device = _resolve_flow_d_preflight(debug, core_id)
83
+ slice_ = _slice_from_resolved(
84
+ core_id,
85
+ {"os": "zephyr", "board": board, "toolchain": topo.get("toolchain")},
86
+ jlink_flash_device=flash_device,
87
+ jlink_flash_device_declared=declared,
88
+ expect_dpidr=expect_dpidr,
89
+ jlink_device=jlink_device,
90
+ slot0_load_address=(
91
+ _resolve_slot0_load_address(preset, core_id)
92
+ if (declared or flash_device is not None) else None),
93
+ )
94
+ # The same refusals the planned route applies per slice (loader.py).
95
+ core_type = next(
96
+ (str(c.get("type") or "") for c in (soc_spec.get("cores") or [])
97
+ if c.get("id") == core_id), "")
98
+ _enforce_flow_d_preflight_pair(slice_, debug, preset["sku"])
99
+ _enforce_os_matches_core_class(slice_, core_type)
100
+ hw_info.update(sku=preset["sku"], silicon=preset.get("silicon"))
101
+ base = soc_spec.get("soc_flash_base")
102
+ flash_base = int(base) if isinstance(base, int) else None
103
+
104
+ entry = slice_.to_manifest_entry()
105
+ if found is None:
106
+ # No SoM preset names this target: there is no recipe to compose, and a
107
+ # guessed one could program the wrong thing. `tan flash` skips a slice
108
+ # with no flash_method, naming this reason.
109
+ entry["flash_method"] = "none"
110
+ entry["flash_args"] = {}
111
+ entry["reason"] = (
112
+ f"no SoM preset in the SDK names board target `{board}`; "
113
+ "no flash recipe is known for it")
114
+ elif flash_base is not None:
115
+ # The SoC has a flash window, so WHERE the image is linked decides how it
116
+ # may be run. An image tan cannot classify must never inherit the MRAM
117
+ # recipe (slot0_load_address / jlink_flash_device): refuse it as RAM-only.
118
+ # TODO(tan-cli#1360): also set `slice_.link_target = "itcm"` and import
119
+ # RAM_RUN_ONLY_METHOD from `tan.core.link_refusal` once #1360 is in this
120
+ # base, so `tan flash` refuses the method by name.
121
+ low = lowest_load_address(elf_path) if elf_path else None
122
+ if low is None or low < flash_base:
123
+ _, args = _slice_flash_recipe(slice_)
124
+ entry["flash_method"] = RAM_RUN_ONLY
125
+ entry["flash_args"] = {k: v for k, v in (args or {}).items() if k in _RAM_RUN_KEYS}
126
+ if low is None:
127
+ entry["reason"] = (
128
+ "the built ELF could not be classified (missing, unreadable, or "
129
+ "no loadable segment); not treated as a flash image")
130
+ doc = {
131
+ "schema_version": 1,
132
+ "generated_by": "tan build --board",
133
+ "hw_info": hw_info,
134
+ "slices": [entry],
135
+ "ipc": [],
136
+ "helper_mcus": [],
137
+ "boot_order": [],
138
+ }
139
+ return yaml.safe_dump(doc, sort_keys=False, default_flow_style=False)
@@ -0,0 +1,60 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """board.yaml emitters whose `--emit` front door is `scripts/alp_project.py`.
3
+
4
+ RELOCATED (was alp-sdk `scripts/alp_project_emit/`). The package keeps its shape
5
+ so the move stays reviewable as a move: `dts.py`, `native_sim.py`, `hw_info.py`,
6
+ `west_libs.py` and `bom_netlist.py` are the same files with their imports
7
+ repointed at `tan.planner`'s own leaves.
8
+
9
+ Two things alp-sdk's package root carried did NOT come across, because nothing
10
+ these five modules do reaches them and a second copy would be a second source of
11
+ truth:
12
+
13
+ * `_PERIPHERAL_KCONFIG` -- `slugs.peripheral_kconfig(metadata_root)` already
14
+ reads `metadata/registries/peripheral-kconfig.json` here. It takes the
15
+ project's own root since alp-sdk#1485; there is no module-level table to
16
+ copy any more.
17
+ * `_CHIP_SUBSYSTEMS` -- already in `slugs.py`, with its I-26 justification.
18
+
19
+ `composed-route-table` came across too: `bom_netlist.py` carries
20
+ `_emit_composed_route_table` alongside `_emit_carrier_netlist`, sharing the same
21
+ `_composed_route_rows` helper.
22
+
23
+ What DOES live here is the one fact genuinely shared by two leaves: the canonical
24
+ E1M / E1M-X GPIO pad order, which `dts.py` (`--emit dts-overlay`) and
25
+ `native_sim.py` (`--emit native-sim-overlay`) must agree on byte for byte -- the
26
+ "Devicetree / overlay invariant" documented in the SDK's `e1m_pinout.h` /
27
+ `e1m_x_pinout.h`. Kept here, not duplicated in either leaf, so the two overlays
28
+ cannot drift.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ __all__ = ["_e1m_gpio_canonical", "_e1m_x_gpio_canonical"]
34
+
35
+
36
+ def _e1m_gpio_canonical() -> list[str]:
37
+ """Return the 52 ALP_E1M_GPIO_<suffix> names in canonical index order."""
38
+ names: list[str] = [f"IO{n}" for n in range(26)] # 0..25
39
+ names += [f"PWM{n}" for n in range(8)] # 26..33
40
+ for e in range(4): # 34..41
41
+ names += [f"ENC{e}_X", f"ENC{e}_Y"]
42
+ names += [f"ADC{n}" for n in range(8)] # 42..49
43
+ names += ["DAC0", "DAC1"] # 50..51
44
+ return names
45
+
46
+
47
+ def _e1m_x_gpio_canonical() -> list[str]:
48
+ """Return the 99 ALP_E1M_X_GPIO_<suffix> names in canonical index order."""
49
+ names: list[str] = [f"IO{n}" for n in range(36)] # 0..35
50
+ names += [f"PWM{n}" for n in range(8)] # 36..43
51
+ for e in range(4): # 44..51
52
+ names += [f"ENC{e}_X", f"ENC{e}_Y"]
53
+ names += [f"ADC{n}" for n in range(8)] # 52..59
54
+ names += ["DAC0", "DAC1"] # 60..61
55
+ names += ["I2C2_SDA", "I2C2_SCL", "I2C3_SDA", "I2C3_SCL"]
56
+ names += ["SPI2_MISO", "SPI2_MOSI", "SPI2_SCLK", "SPI2_CS0", "SPI2_CS1"]
57
+ names += ["CAN1_H", "CAN1_L"]
58
+ names += [f"LCD_B{n}" for n in range(24)]
59
+ names += ["LCD_HSYNC", "LCD_VSYNC"] # 97..98
60
+ return names
@@ -0,0 +1,375 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Carrier netlist / BOM emission (`--emit carrier-netlist` /
3
+ `--emit composed-route-table`).
4
+
5
+ RELOCATED (was alp-sdk `scripts/alp_project_emit/bom_netlist.py`) -- see this
6
+ package's `__init__.py` for the move's contract.
7
+
8
+ `_emit_composed_route_table` relocated alongside `_emit_carrier_netlist`
9
+ (tan-cli composed-route-table front door): the two share `_composed_route_rows`
10
+ in full -- that is where the hw_rev pad-route override actually happens -- so a
11
+ second copy of that resolution could not exist to drift.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from ..loader import _load_yaml
21
+ from ..project_loader import (
22
+ _compose_route,
23
+ _hwrev_pad_route_overrides,
24
+ _resolve_pad_routes,
25
+ )
26
+ from ..som_metadata import _resolve_silicon_variant
27
+
28
+
29
+ # ---------------------------------------------------------------------
30
+ # Carrier route / netlist emitters
31
+ # ---------------------------------------------------------------------
32
+ #
33
+ # `composed-route-table` stays as the original debug surface. The
34
+ # route-row helper below is shared by the production `carrier-netlist`
35
+ # contract so the two views cannot drift on hw_rev pad-route overrides.
36
+
37
+
38
+ def _composed_route_rows(
39
+ project: dict[str, Any],
40
+ sku_preset: dict[str, Any],
41
+ board_preset: dict[str, Any] | None,
42
+ metadata_root: Path,
43
+ ) -> tuple[list[dict[str, Any]], str | None, str | None]:
44
+ """Return composed route rows plus the selected hw_rev / variant.
45
+
46
+ Rows cover every E1M pad named by the board and every SoM-only pad
47
+ that has a dispatch route. Board-defined rows preserve YAML order;
48
+ SoM-only rows are sorted by E1M ID for deterministic output.
49
+ """
50
+ pad_routes = _resolve_pad_routes(sku_preset)
51
+
52
+ # Apply the selected board revision's pad-route overrides on top of the
53
+ # base (production-rev) pad_routes, so the composed table -- and thus
54
+ # `--emit composed-route-table` -- differs by hw_rev. The rev comes from
55
+ # the board's `som.hw_rev`, falling back to the SoM's `default_hw_rev`.
56
+ hw_rev = ((project.get("som") or {}).get("hw_rev")
57
+ or sku_preset.get("default_hw_rev"))
58
+ for ov in _hwrev_pad_route_overrides(project["som"]["sku"], hw_rev,
59
+ metadata_root):
60
+ pad_routes[ov["e1m"]] = ov
61
+
62
+ # Resolve silicon variant order_code for the top-level summary field.
63
+ variant = _resolve_silicon_variant(sku_preset, metadata_root)
64
+ silicon_variant_str = variant["order_code"] if variant else None
65
+
66
+ # Collect board-side entries, preserving the sub-category name.
67
+ # Build a mapping: e1m_id -> (category, entry_dict).
68
+ # When the same E1M pad appears multiple times (e.g. E1M_PWM1 maps to
69
+ # both EVK_PWM_LED_BLUE and EVK_ARD_PWM1 in the EVK YAML) we emit one
70
+ # row per board entry so no information is lost.
71
+ board_entries: list[tuple[str, dict[str, Any]]] = []
72
+ seen_from_board: set[str] = set()
73
+ if board_preset is not None:
74
+ e1m_routes = board_preset.get("e1m_routes") or {}
75
+ for category, entries in e1m_routes.items():
76
+ if not isinstance(entries, list):
77
+ continue
78
+ for entry in entries:
79
+ if not isinstance(entry, dict):
80
+ continue
81
+ e1m = entry.get("e1m")
82
+ if not isinstance(e1m, str):
83
+ continue
84
+ board_entries.append((category, entry))
85
+ seen_from_board.add(e1m)
86
+
87
+ # Also include SoM-only pads (in pad_routes but not in board).
88
+ som_only_pads = sorted(set(pad_routes.keys()) - seen_from_board)
89
+
90
+ routes: list[dict[str, Any]] = []
91
+
92
+ # Board-defined entries first (preserves YAML order).
93
+ for category, c_entry in board_entries:
94
+ e1m = c_entry["e1m"]
95
+ composed = _compose_route(e1m, c_entry, pad_routes)
96
+ row: dict[str, Any] = {"e1m": e1m, "board_category": category}
97
+ row["board_macro"] = composed.get("board_macro")
98
+ row["board_role"] = composed.get("board_role")
99
+ if "board_doc" in composed:
100
+ row["board_doc"] = composed["board_doc"]
101
+ # active_low is a board-side flag, not surfaced by _compose_route;
102
+ # read it directly from the board entry.
103
+ active_low = c_entry.get("active_low")
104
+ if active_low is not None:
105
+ row["active_low"] = bool(active_low)
106
+ row["dispatch"] = composed.get("dispatch", "direct")
107
+ if "dispatch_pin" in composed:
108
+ row["dispatch_pin"] = composed["dispatch_pin"]
109
+ if "som_doc" in composed:
110
+ row["som_doc"] = composed["som_doc"]
111
+ routes.append(row)
112
+
113
+ # SoM-only pads (not assigned a board role in this board YAML).
114
+ for e1m in som_only_pads:
115
+ composed = _compose_route(e1m, None, pad_routes)
116
+ row = {
117
+ "e1m": e1m,
118
+ "board_category": None,
119
+ "board_macro": None,
120
+ "board_role": None,
121
+ "dispatch": composed.get("dispatch", "direct"),
122
+ }
123
+ if "dispatch_pin" in composed:
124
+ row["dispatch_pin"] = composed["dispatch_pin"]
125
+ if "som_doc" in composed:
126
+ row["som_doc"] = composed["som_doc"]
127
+ routes.append(row)
128
+
129
+ return routes, hw_rev, silicon_variant_str
130
+
131
+
132
+ def _manifest_path(kind: str, item_id: str, metadata_root: Path) -> Path:
133
+ return metadata_root / kind / f"{item_id}.yaml"
134
+
135
+
136
+ def _load_optional_manifest(kind: str, item_id: str,
137
+ metadata_root: Path) -> dict[str, Any] | None:
138
+ path = _manifest_path(kind, item_id, metadata_root)
139
+ if not path.is_file():
140
+ return None
141
+ return _load_yaml(path)
142
+
143
+
144
+ def _passive_rows(passives: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
145
+ rows: list[dict[str, Any]] = []
146
+ for passive in passives or []:
147
+ if not isinstance(passive, dict):
148
+ continue
149
+ row: dict[str, Any] = {
150
+ "role": passive.get("role"),
151
+ "value": passive.get("value"),
152
+ "net": passive.get("net"),
153
+ "refdes_prefix": passive.get("refdes_prefix"),
154
+ }
155
+ rows.append({k: v for k, v in row.items() if v is not None})
156
+ return rows
157
+
158
+
159
+ def _chip_bom_row(item_id: str, manifest: dict[str, Any],
160
+ manifest_relpath: str) -> dict[str, Any]:
161
+ physical = manifest.get("physical") or {}
162
+ caveats: list[str] = []
163
+ if not physical:
164
+ caveats.append("missing_physical")
165
+ elif physical.get("visibility") == "internal":
166
+ caveats.append("physical_detail_internal")
167
+ if len(manifest.get("mpn_population") or []) > 1:
168
+ caveats.append("mpn_population_candidates")
169
+
170
+ row: dict[str, Any] = {
171
+ "item_id": item_id,
172
+ "kind": "chip",
173
+ "scope": "carrier",
174
+ "source": manifest_relpath,
175
+ "display_name": manifest.get("display_name"),
176
+ "vendor": manifest.get("vendor"),
177
+ "mpn_population": manifest.get("mpn_population") or [],
178
+ "bus": manifest.get("bus"),
179
+ "quantity": None,
180
+ "physical": {
181
+ "refdes_prefix": physical.get("refdes_prefix"),
182
+ "package": physical.get("package"),
183
+ "footprint": physical.get("footprint"),
184
+ "visibility": physical.get("visibility"),
185
+ "provenance": physical.get("provenance"),
186
+ },
187
+ "passives": _passive_rows(physical.get("passives")),
188
+ }
189
+ row["physical"] = {k: v for k, v in row["physical"].items()
190
+ if v is not None}
191
+ if caveats:
192
+ row["caveats"] = caveats
193
+ return row
194
+
195
+
196
+ def _block_bom_row(item_id: str, manifest: dict[str, Any],
197
+ manifest_relpath: str) -> dict[str, Any]:
198
+ realizations = [
199
+ r for r in manifest.get("realizations") or []
200
+ if isinstance(r, dict)
201
+ ]
202
+ realization = realizations[0] if realizations else {}
203
+ caveats: list[str] = []
204
+ if not realizations:
205
+ caveats.append("missing_realization")
206
+ elif len(realizations) > 1:
207
+ caveats.append("multiple_realizations")
208
+ if realization.get("visibility") == "internal":
209
+ caveats.append("physical_detail_internal")
210
+ if not realization.get("parts"):
211
+ caveats.append("no_concrete_parts")
212
+
213
+ row: dict[str, Any] = {
214
+ "item_id": item_id,
215
+ "kind": "block",
216
+ "scope": "carrier",
217
+ "source": manifest_relpath,
218
+ "display_name": manifest.get("display_name"),
219
+ "quantity": None,
220
+ "realization": {
221
+ "id": realization.get("id"),
222
+ "physical_form": realization.get("physical_form"),
223
+ "visibility": realization.get("visibility"),
224
+ },
225
+ "parts": realization.get("parts") or [],
226
+ "passives": _passive_rows(realization.get("passives")),
227
+ }
228
+ row["realization"] = {k: v for k, v in row["realization"].items()
229
+ if v is not None}
230
+ if caveats:
231
+ row["caveats"] = caveats
232
+ return row
233
+
234
+
235
+ def _carrier_bom_rows(
236
+ board_preset: dict[str, Any] | None,
237
+ metadata_root: Path,
238
+ ) -> list[dict[str, Any]]:
239
+ """Build carrier BOM rows from board `populated: true`.
240
+
241
+ `populated:` is a logical population map, not a line-item BOM with
242
+ refdes or count, so rows deliberately leave `quantity` null unless a
243
+ future metadata field makes it authoritative.
244
+ """
245
+ rows: list[dict[str, Any]] = []
246
+ if board_preset is None:
247
+ return rows
248
+
249
+ populated = board_preset.get("populated") or {}
250
+ for item_id in sorted(k for k, v in populated.items() if v is True):
251
+ chip = _load_optional_manifest("chips", item_id, metadata_root)
252
+ if chip is not None:
253
+ rows.append(_chip_bom_row(
254
+ item_id, chip, f"metadata/chips/{item_id}.yaml"))
255
+ continue
256
+
257
+ block = _load_optional_manifest("blocks", item_id, metadata_root)
258
+ if block is not None:
259
+ rows.append(_block_bom_row(
260
+ item_id, block, f"metadata/blocks/{item_id}.yaml"))
261
+ continue
262
+
263
+ rows.append({
264
+ "item_id": item_id,
265
+ "kind": "unknown",
266
+ "scope": "carrier",
267
+ "source": None,
268
+ "quantity": None,
269
+ "caveats": ["missing_manifest"],
270
+ })
271
+ return rows
272
+
273
+
274
+ def _route_to_net(row: dict[str, Any]) -> dict[str, Any]:
275
+ net_id = row.get("board_macro") or row["e1m"]
276
+ endpoints: list[dict[str, Any]] = [
277
+ {"kind": "e1m", "ref": row["e1m"]},
278
+ ]
279
+ if row.get("board_macro"):
280
+ endpoints.append({"kind": "board-macro", "ref": row["board_macro"]})
281
+ if row.get("dispatch") and row["dispatch"] != "direct":
282
+ endpoint: dict[str, Any] = {
283
+ "kind": "som-dispatch",
284
+ "ref": row["dispatch"],
285
+ }
286
+ if "dispatch_pin" in row:
287
+ endpoint["pin"] = row["dispatch_pin"]
288
+ endpoints.append(endpoint)
289
+
290
+ net: dict[str, Any] = {
291
+ "net_id": net_id,
292
+ "e1m": row["e1m"],
293
+ "board_category": row.get("board_category"),
294
+ "board_macro": row.get("board_macro"),
295
+ "board_role": row.get("board_role"),
296
+ "dispatch": row.get("dispatch", "direct"),
297
+ "endpoints": endpoints,
298
+ }
299
+ for key in ("board_doc", "active_low", "dispatch_pin", "som_doc"):
300
+ if key in row:
301
+ net[key] = row[key]
302
+ caveats = []
303
+ if row.get("board_macro") is None:
304
+ caveats.append("som_only_no_carrier_role")
305
+ if caveats:
306
+ net["caveats"] = caveats
307
+ return net
308
+
309
+
310
+ def _emit_carrier_netlist(
311
+ project: dict[str, Any],
312
+ sku_preset: dict[str, Any],
313
+ board_preset: dict[str, Any] | None,
314
+ metadata_root: Path,
315
+ ) -> str:
316
+ """Emit the Studio-facing carrier netlist + BOM handoff contract.
317
+
318
+ This is intentionally not a KiCad, Gerber, or layout artifact. It
319
+ exposes only public carrier-facing facts derivable from board.yaml,
320
+ board presets, chip/block manifests, and SoM pad dispatch metadata.
321
+ """
322
+ routes, hw_rev, silicon_variant_str = _composed_route_rows(
323
+ project, sku_preset, board_preset, metadata_root)
324
+ board_name = (board_preset or {}).get("name") or project.get("name")
325
+ result: dict[str, Any] = {
326
+ "schema_version": 1,
327
+ "kind": "alp.carrier_netlist",
328
+ "generated_by": "scripts/alp_project.py --emit carrier-netlist",
329
+ "board": board_name,
330
+ "som": project["som"]["sku"],
331
+ "hw_rev": hw_rev,
332
+ "silicon_variant": silicon_variant_str,
333
+ "nets": [_route_to_net(row) for row in routes],
334
+ "bom": {
335
+ "carrier": _carrier_bom_rows(board_preset, metadata_root),
336
+ },
337
+ "caveats": [
338
+ "carrier_handoff_not_pcb_layout",
339
+ "no_kicad_or_gerber_output",
340
+ "som_internals_excluded",
341
+ "quantity_null_when_board_populated_has_no_count",
342
+ ],
343
+ }
344
+ return json.dumps(result, indent=2) + "\n"
345
+
346
+
347
+ def _emit_composed_route_table(
348
+ project: dict[str, Any],
349
+ sku_preset: dict[str, Any],
350
+ board_preset: dict[str, Any] | None,
351
+ metadata_root: Path,
352
+ ) -> str:
353
+ """Emit a JSON summary of the fully-composed pad route table for
354
+ the current (board x SoM) pair.
355
+
356
+ The table is derived by calling _resolve_pad_routes() (SoM side) and
357
+ _compose_route() (join with board side) for every E1M pad that
358
+ appears in either the board's e1m_routes: block or the SoM's
359
+ pad_routes: block.
360
+
361
+ Pads that only appear in the SoM's pad_routes: block (i.e. no
362
+ board-side role assigned) are included with null board_* fields
363
+ so the table is complete for the SoM-standalone scenario.
364
+ """
365
+ routes, hw_rev, silicon_variant_str = _composed_route_rows(
366
+ project, sku_preset, board_preset, metadata_root)
367
+ board_name = (board_preset or {}).get("name") or project.get("name")
368
+ result: dict[str, Any] = {
369
+ "board": board_name,
370
+ "som": project["som"]["sku"],
371
+ "hw_rev": hw_rev,
372
+ "silicon_variant": silicon_variant_str,
373
+ "routes": routes,
374
+ }
375
+ return json.dumps(result, indent=2) + "\n"