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/kconfig.py ADDED
@@ -0,0 +1,2435 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Per-slice Kconfig (alp.conf) emitter.
4
+
5
+ `_slice_alp_conf` renders a Zephyr slice's Kconfig fragment off the parsed model
6
+ + the resolved capabilities / chip-driver / library / peripheral enables;
7
+ `_emit_extra_library_profile` renders an extra `libraries:` profile fragment.
8
+ Extracted as the #285 kconfig emit seam. The slug / peripheral-Kconfig /
9
+ chip-subsystem tables come from the slugs.py leaf and the per-library
10
+ HW-accelerator matcher from the libraries.py layer -- both plain module-scope
11
+ imports now. In alp-sdk they were lazy in-body imports of `alp_project` /
12
+ `alp_project_emit` (a cycle-break there, since `alp_project` imported this
13
+ module), and after the relocation they were the last thing making an emit from
14
+ `tan` load alp-sdk's Python at all. Per-core `libraries:` Kconfig is read from
15
+ each library's manifest via `_per_core_library_kconfig`.
16
+
17
+ `_slice_alp_conf` itself is a thin composer over a set of `_emit_*` section
18
+ helpers (console / SoM capability / chip driver / subsystem / library /
19
+ inference / cross-core shmem cache / memory / power / storage / OTA /
20
+ diagnostics), each returning that section's `list[str]` fragment. Split out
21
+ mechanically as the #673 Phase-1 kconfig split -- behavior-preserving,
22
+ byte-identical emit output; see `check_emit_snapshots.py`.
23
+
24
+ This module is a pure string templater: it never runs kconfiglib/west, so
25
+ every emitted `CONFIG_*` line must be provable correct from this module's own
26
+ YAML/JSON inputs + its own `zephyr/kconfigs/*.kconfig` tree -- it cannot ask
27
+ the real Kconfig solver whether a symbol's dependency chain resolves.
28
+ Consequently the emitted alp.conf's Kconfig symbol set is coupled to the
29
+ alp-sdk module version that generated it, the same way `west.yml` deliberately
30
+ pins one Zephyr/module release at a time (docs/zephyr-version-policy.md); an
31
+ alp.conf generated by one alp-sdk version is not a promised-compatible input
32
+ to a Zephyr tree built against another. Detecting that cross-version skew
33
+ cleanly (rather than a Kconfig "assign to undefined symbol" abort) is #855's
34
+ job, not this module's -- this module only owns intra-version dependency
35
+ gating (see #874).
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import re
41
+ from functools import lru_cache
42
+ from pathlib import Path
43
+ from typing import Any, Optional
44
+
45
+ import yaml
46
+
47
+ from . import cameras as _cameras
48
+ from . import libraries as _library_layer
49
+ from . import sdk_compat as _sdk_compat
50
+ from .link_target import applies_to as link_applies_to
51
+ from .loader import _library_alias_table
52
+ from .models import BoardProject, OrchestratorError, Slice
53
+ from .ownership import project_m33_overlay
54
+ from .paths import REPO
55
+ from .partition import resolve_storage_partitions
56
+ from .slugs import (
57
+ _BLOCK_SLUGS,
58
+ _CHIP_SUBSYSTEMS,
59
+ _board_define_slug,
60
+ _is_tbd,
61
+ _slugs_from_helper_firmware,
62
+ _slugs_from_on_module,
63
+ _som_define_slug,
64
+ peripheral_kconfig,
65
+ )
66
+ from .som_metadata import (
67
+ resolve_capabilities,
68
+ silicon_to_kconfig,
69
+ som_unpopulated_capabilities,
70
+ )
71
+
72
+
73
+ def _emit_extra_library_profile(
74
+ name: str,
75
+ profile_rel: str,
76
+ project: "BoardProject",
77
+ ) -> list[str]:
78
+ """Walk an extra_libraries `profile:` file and emit the per-class
79
+ first-match Kconfig lines for the active SoM.
80
+
81
+ Mirrors the curated-library `integration.zephyr.hw_backends` matcher
82
+ (folded into each `metadata/libraries/<lib>.yaml` manifest): each
83
+ accelerator class declares a `priority:` list whose entries carry
84
+ `silicon:` / `soc_family:` / `requires_cap:` matchers plus a
85
+ `kconfig:` directive. Entries marked `status: planned` or
86
+ `status: stub` are metadata only and are not emitted as active
87
+ build claims. First matching implemented entry per class wins; an
88
+ `sw_fallback:` block with `kconfig:` is always emitted at the end
89
+ so the build has a SW floor when no HW backend matches.
90
+
91
+ Returns the list of emitted lines (each line is a complete
92
+ `CONFIG_*=y` or `# ...` comment); empty list when the profile
93
+ parses but nothing matches and there's no sw_fallback.
94
+
95
+ Failures (malformed YAML, missing keys) emit a single `#`-prefixed
96
+ diagnostic comment so the customer sees the failure in the slice's
97
+ alp.conf rather than silent drop-out.
98
+ """
99
+ # No longer a divergence: alp-sdk#1961 landed as alp-sdk PR #2005 and
100
+ # this repo's #1241 re-sync (to alp-sdk 15b2f32c) converged the two
101
+ # copies, so the RELOCATED marker that stood here is retired with
102
+ # tan-cli#1122. Upstream now spells these three lines identically --
103
+ # `.resolve()` dropped (nothing downstream needs a real path, only the
104
+ # read does) and `UnicodeDecodeError` in the clause, because it is a
105
+ # `ValueError` not an `OSError` and `read_text(encoding="utf-8")` would
106
+ # otherwise escape a function contracted never to raise.
107
+ try:
108
+ profile_path = REPO / profile_rel
109
+ doc = yaml.safe_load(profile_path.read_text(encoding="utf-8"))
110
+ except (OSError, UnicodeDecodeError, yaml.YAMLError) as e:
111
+ return [f"# extra_libraries[{name}] profile parse failed: {e}"]
112
+ if not isinstance(doc, dict):
113
+ return [f"# extra_libraries[{name}] profile is not a mapping"]
114
+
115
+ # Match keys come from the active SoM. Mirror
116
+ # libraries._SOC_FAMILY_TOKEN so extra_libraries entries can
117
+ # share existing curated-profile match keys when desired.
118
+ silicon_ref = project.som_preset.get("silicon") or ""
119
+ family = (project.som_preset.get("family") or "").lower()
120
+ soc_family_token: Optional[str] = None
121
+ if family.startswith("alif-ensemble"):
122
+ soc_family_token = "alif_ensemble"
123
+ elif family.startswith("renesas-rzv2n"):
124
+ soc_family_token = "renesas_rzv2n"
125
+
126
+ # resolve_capabilities merges SoC-JSON defaults + SoM overrides.
127
+ capabilities = resolve_capabilities(project.som_preset, project.effective_metadata_root())
128
+
129
+ def _cap_truthy(cap_name: str) -> bool:
130
+ v = capabilities.get(cap_name)
131
+ if v is None:
132
+ return False
133
+ if isinstance(v, bool):
134
+ return v
135
+ if isinstance(v, int):
136
+ return v > 0
137
+ s = str(v).lower()
138
+ if s in ("true", "yes"):
139
+ return True
140
+ if s in ("false", "no", "null", "none", "0", ""):
141
+ return False
142
+ try:
143
+ return int(s) > 0
144
+ except ValueError:
145
+ return False
146
+
147
+ def _entry_matches(e: dict[str, Any]) -> bool:
148
+ status = str(e.get("status", "implemented")).strip().lower()
149
+ if status in {"planned", "stub"}:
150
+ return False
151
+ sili = e.get("silicon")
152
+ sf = e.get("soc_family")
153
+ cap = e.get("requires_cap")
154
+ if sili is not None and sili != silicon_ref:
155
+ return False
156
+ if sf is not None and sf != soc_family_token:
157
+ return False
158
+ if cap is not None and not _cap_truthy(str(cap)):
159
+ return False
160
+ return True
161
+
162
+ out: list[str] = []
163
+ accelerators = doc.get("accelerators") or []
164
+ if isinstance(accelerators, list):
165
+ for cls in accelerators:
166
+ if not isinstance(cls, dict):
167
+ continue
168
+ cls_name = cls.get("class") or "<unknown>"
169
+ for p in (cls.get("priority") or []):
170
+ if not isinstance(p, dict):
171
+ continue
172
+ if not _entry_matches(p):
173
+ continue
174
+ kc = p.get("kconfig")
175
+ if isinstance(kc, str) and kc:
176
+ out.append(f"{kc} # {name} / {cls_name}")
177
+ break
178
+
179
+ sw = doc.get("sw_fallback")
180
+ if isinstance(sw, dict):
181
+ kc = sw.get("kconfig")
182
+ if isinstance(kc, str) and kc:
183
+ out.append(f"{kc} # {name} / sw_fallback")
184
+
185
+ return out
186
+
187
+
188
+ # --------------------------------------------------------------------
189
+ # Console backend auto-selection (issue: generic console).
190
+ #
191
+ # The Alp SDK is a generic, multi-OS SoM layer: the same example can be
192
+ # built for a Zephyr core (Cortex-M) or a Linux core (Cortex-A / Yocto),
193
+ # and the console backend that "printf lands on" differs per OS. Rather
194
+ # than force every app author to hand-pick console Kconfig (the old AEN
195
+ # bench examples hard-coded a RAM console, which then fought the board's
196
+ # real UART console), the generator selects it automatically from the
197
+ # slice's `os:` and lets the SoM manufacturer / customer override via a
198
+ # single `diagnostics.console:` knob in board.yaml.
199
+ #
200
+ # auto (default) -> os: zephyr => alp (UART console)
201
+ # os: yocto/ubuntu => linux (stdio/tty console)
202
+ # other => none
203
+ # alp | uart -> Zephyr UART console (the module's console UART,
204
+ # e.g. E1M edge UART0; the board layer owns the
205
+ # `zephyr,console` DT chosen + pinmux).
206
+ # ram -> Zephyr RAM console (bench flow: read the buffer
207
+ # over SWD when no serial line is available).
208
+ # linux -> Linux console (nothing to emit on the Zephyr side;
209
+ # meaningful only on a yocto slice).
210
+ # none | off -> emit nothing; inherit whatever the board provides.
211
+ # --------------------------------------------------------------------
212
+ _CONSOLE_ALIASES = {
213
+ "alp": "uart",
214
+ "uart": "uart",
215
+ "serial": "uart",
216
+ "ram": "ram",
217
+ "swd": "ram",
218
+ "linux": "linux",
219
+ "stdio": "linux",
220
+ "none": "none",
221
+ "off": "none",
222
+ }
223
+
224
+
225
+ def _resolve_console(value: Optional[str], os_: str,
226
+ hw_console: bool = True) -> str:
227
+ """Resolve the effective console backend for a slice.
228
+
229
+ ``value`` is the raw ``diagnostics.console:`` knob (or ``None`` /
230
+ ``"auto"`` for the OS-derived default); ``os_`` is the slice OS.
231
+ ``hw_console`` is the SoM-topology fact (``topology.<id>.hw_console``):
232
+ a headless core (``False``) has no hardware UART console, so the
233
+ AUTO-resolved Zephyr default must be ``"none"`` (inherit the board
234
+ default) instead of ``"uart"`` -- emitting ``CONFIG_UART_CONSOLE=y``
235
+ on a core whose board has no serial driver is an "assigned y but got n"
236
+ Kconfig error, fatal to the Zephyr build (issue #717). An explicit
237
+ ``diagnostics.console:`` still wins for a headless core (the caller may
238
+ upgrade a headless AUTO to ``"ram"`` via ``diagnostics.sim_console``,
239
+ #686). Returns one of ``"uart"``, ``"ram"``, ``"linux"``, ``"none"``.
240
+ """
241
+ v = (value or "auto").strip().lower()
242
+ if v == "auto":
243
+ if os_ == "zephyr":
244
+ return "uart" if hw_console else "none"
245
+ if os_ in ("yocto", "ubuntu"):
246
+ return "linux"
247
+ return "none"
248
+ return _CONSOLE_ALIASES.get(v, "none")
249
+
250
+
251
+ #: Floor for `CONFIG_RAM_CONSOLE_BUFFER_SIZE` when `diagnostics.console: ram`
252
+ #: selects the RAM console. An app that sets a larger size in its own
253
+ #: `prj.conf` keeps it (see `_app_ram_console_size`). Only `prj.conf` is
254
+ #: read: a size set in `boards/<board>.conf`, `prj_<board>.conf` or an app
255
+ #: `EXTRA_CONF_FILE` is not seen and still loses to this floor.
256
+ _RAM_CONSOLE_MIN_SIZE = 2048
257
+
258
+ _RAM_CONSOLE_SIZE_RE = re.compile(
259
+ r"^\s*CONFIG_RAM_CONSOLE_BUFFER_SIZE\s*=\s*(0[xX][0-9a-fA-F]+|\d+)\s*(?:#.*)?$")
260
+
261
+
262
+ def _app_ram_console_size(project: BoardProject, slice_: Slice) -> int:
263
+ """`CONFIG_RAM_CONSOLE_BUFFER_SIZE` the slice's own `prj.conf` sets, or 0.
264
+
265
+ The generated alp.conf is merged AFTER prj.conf, so a bare assignment
266
+ would clobber (and shrink) an app-set size -- the app's console then
267
+ wraps (tan-cli#1401). Best effort: no source dir, no app, or an
268
+ unreadable prj.conf all read as "app sets nothing". Limit: only
269
+ `prj.conf` is read, so sizes set in `boards/<board>.conf`,
270
+ `prj_<board>.conf` or an app `EXTRA_CONF_FILE` still lose.
271
+ """
272
+ if project.source_dir is None or not slice_.app:
273
+ return 0
274
+ from .orchestrator import _zephyr_app_dir # lazy: orchestrator imports us
275
+ try:
276
+ prj = _zephyr_app_dir(slice_.app, project.source_dir) / "prj.conf"
277
+ text = prj.read_text(encoding="utf-8")
278
+ except (OSError, OrchestratorError, UnicodeDecodeError):
279
+ return 0
280
+ size = 0
281
+ for line in text.splitlines():
282
+ m = _RAM_CONSOLE_SIZE_RE.match(line)
283
+ if m:
284
+ # Last assignment wins, like Kconfig. Kconfig reads an int symbol
285
+ # as hex with a 0x prefix, else base 10 -- `016384` is 16384,
286
+ # where int(x, 0) would raise ValueError.
287
+ tok = m.group(1)
288
+ size = int(tok, 16) if tok[:2] in ("0x", "0X") else int(tok, 10)
289
+ return size
290
+
291
+
292
+ def _emit_zephyr_console(console: str, sim_console: bool = False,
293
+ ram_size: int = _RAM_CONSOLE_MIN_SIZE) -> list[str]:
294
+ """Kconfig lines for a Zephyr slice's resolved console backend.
295
+
296
+ ``sim_console`` distinguishes a RAM console selected explicitly via
297
+ ``diagnostics.console: ram`` (bench SWD flow) from one auto-selected
298
+ for a headless core by ``diagnostics.sim_console: true`` (studio sim
299
+ streams the ring over ``uart_socket``); the emitted Kconfig is
300
+ identical, only the teaching comment differs.
301
+ """
302
+ if console == "uart":
303
+ return [
304
+ "# Console: Alp UART console (auto-selected for os: zephyr).",
305
+ "# printf/LOG land on the module's console UART (the board layer",
306
+ "# owns the `zephyr,console` DT chosen + pinmux -- e.g. E1M edge",
307
+ "# UART0). Override in board.yaml with `diagnostics.console: ram`",
308
+ "# (SWD bench flow) or `none` (inherit the board default).",
309
+ "CONFIG_SERIAL=y",
310
+ "CONFIG_CONSOLE=y",
311
+ "CONFIG_UART_CONSOLE=y",
312
+ "CONFIG_UART_INTERRUPT_DRIVEN=y",
313
+ "",
314
+ ]
315
+ if console == "ram":
316
+ header = (
317
+ [
318
+ "# Console: RAM console (board.yaml `diagnostics.sim_console: "
319
+ "true`).",
320
+ "# This core has no hardware UART console -- the studio",
321
+ "# simulator streams `ram_console_buf` over its `uart_socket`.",
322
+ ]
323
+ if sim_console
324
+ else [
325
+ "# Console: RAM console (board.yaml `diagnostics.console: "
326
+ "ram`).",
327
+ "# No serial line on this bench -- read `ram_console_buf` "
328
+ "over SWD.",
329
+ ]
330
+ )
331
+ return header + [
332
+ "# LOG is routed to printk so the RAM backend still captures it.",
333
+ "CONFIG_CONSOLE=y",
334
+ "CONFIG_RAM_CONSOLE=y",
335
+ f"CONFIG_RAM_CONSOLE_BUFFER_SIZE={ram_size}",
336
+ "CONFIG_UART_CONSOLE=n",
337
+ "",
338
+ ]
339
+ # "linux" on a Zephyr slice is meaningless, and "none" means inherit
340
+ # the board default -- emit nothing in both cases.
341
+ return []
342
+
343
+
344
+ _LINUX_WIRELESS_PROVIDER_PREFIXES = ("murata_", "cyw")
345
+
346
+
347
+ def _wireless_provider(project: BoardProject) -> Optional[str]:
348
+ """Return the SoM's on-module Wi-Fi/BLE provider slug, if known."""
349
+ om = project.som_preset.get("on_module") or {}
350
+ provider = om.get("wifi_ble")
351
+ if not isinstance(provider, str):
352
+ return None
353
+ provider = provider.strip()
354
+ # Case-insensitive: "tbd"/"Tbd" are the same hand-typed placeholder as
355
+ # a bare "TBD" (alp-sdk #1048); `provider` is already whitespace-
356
+ # stripped above.
357
+ if not provider or provider.upper() == "TBD":
358
+ return None
359
+ return provider
360
+
361
+
362
+ def _is_linux_wireless_provider(provider: Optional[str]) -> bool:
363
+ """True when Wi-Fi/BLE data paths are owned by the Linux BSP stack."""
364
+ if provider is None:
365
+ return False
366
+ return provider.startswith(_LINUX_WIRELESS_PROVIDER_PREFIXES)
367
+
368
+
369
+ def _zephyr_iot_kconfig(
370
+ project: BoardProject,
371
+ slice_: Slice,
372
+ resolved_chip_state: dict[str, bool],
373
+ ) -> list[str]:
374
+ """Kconfig lines for ``cores.<id>.iot`` on a Zephyr slice.
375
+
376
+ ``resolved_chip_state`` is the same som -> board-populated -> project-chips
377
+ resolved on/off map ``_emit_chips`` prints from (see
378
+ ``_resolve_chip_states``) -- the CC3501E bridge backend
379
+ (``ALP_SDK_WIFI_CC3501E`` / ``ALP_SDK_BLE_CC3501E``) ``depends on
380
+ ALP_SDK_CHIP_CC3501E``, so a board variant that turns the chip off via
381
+ ``populated: { cc3501e: false }`` must not also get a Wi-Fi/BLE
382
+ ``=y`` line that silently resolves to n (issue #874).
383
+ """
384
+ iot = slice_.iot or {}
385
+ if not any(iot.get(k) for k in ("wifi", "mqtt", "tls", "ble")):
386
+ return []
387
+
388
+ provider = _wireless_provider(project)
389
+ lines: list[str] = [
390
+ f"# IoT features declared on core `{slice_.core_id}` "
391
+ "(board.yaml `cores.*.iot`)",
392
+ ]
393
+ if provider:
394
+ lines.append(
395
+ f"# Wireless provider resolved from `{project.sku}` "
396
+ f"on_module.wifi_ble: {provider}.")
397
+ else:
398
+ lines.append("# Wireless provider is TBD/absent; using generic Zephyr stack gates.")
399
+
400
+ emitted: set[str] = set()
401
+
402
+ def add(line: str) -> None:
403
+ if line.startswith("CONFIG_"):
404
+ if line in emitted:
405
+ return
406
+ emitted.add(line)
407
+ lines.append(line)
408
+
409
+ def add_network_base() -> None:
410
+ add("CONFIG_NETWORKING=y")
411
+ add("CONFIG_NET_IPV4=y")
412
+ add("CONFIG_NET_SOCKETS=y")
413
+
414
+ linux_owned = _is_linux_wireless_provider(provider)
415
+
416
+ if iot.get("wifi"):
417
+ if provider == "cc3501e":
418
+ if resolved_chip_state.get("cc3501e"):
419
+ add("# Wi-Fi: AEN CC3501E bridge backend, not Zephyr wifi_mgmt.")
420
+ add("CONFIG_ALP_SDK_WIFI_CC3501E=y")
421
+ else:
422
+ add("# Wi-Fi: SoM wireless provider is CC3501E, but this board")
423
+ add("# does not populate the chip (board.yaml `populated.cc3501e: "
424
+ "false`);")
425
+ add("# no Wi-Fi backend is emitted for this slice.")
426
+ elif linux_owned:
427
+ add("# Wi-Fi: provider data path is Linux/BSP-owned on this SoM;")
428
+ add("# no Zephyr wifi_mgmt backend is emitted for this slice.")
429
+ else:
430
+ add("# Wi-Fi: generic Zephyr wifi_mgmt station path.")
431
+ add_network_base()
432
+ add("CONFIG_WIFI=y")
433
+ add("CONFIG_NET_MGMT=y")
434
+ add("CONFIG_NET_MGMT_EVENT=y")
435
+ add("CONFIG_NET_L2_WIFI_MGMT=y")
436
+ add("CONFIG_ALP_SDK_IOT_WIFI=y")
437
+
438
+ if iot.get("mqtt"):
439
+ add("# MQTT: Zephyr mqtt_client over the active socket provider.")
440
+ add_network_base()
441
+ add("CONFIG_NET_TCP=y")
442
+ add("CONFIG_MQTT_LIB=y")
443
+ add("CONFIG_ALP_SDK_IOT_MQTT=y")
444
+
445
+ if iot.get("tls"):
446
+ add("# TLS: credential store + TLS-capable protocol clients.")
447
+ # TLS_CREDENTIALS needs the networking base even when neither
448
+ # `wifi:` nor `mqtt:` is also set on this slice -- omitting it left
449
+ # CONFIG_TLS_CREDENTIALS=y silently resolving to n (unmet
450
+ # NETWORKING/NET_SOCKETS deps), issue #874. add_network_base()
451
+ # dedupes against the wifi/mqtt branches above.
452
+ add_network_base()
453
+ add("CONFIG_TLS_CREDENTIALS=y")
454
+ if iot.get("mqtt"):
455
+ add("CONFIG_MQTT_LIB_TLS=y")
456
+
457
+ if iot.get("ble"):
458
+ if provider == "cc3501e":
459
+ if resolved_chip_state.get("cc3501e"):
460
+ add("# BLE: AEN CC3501E bridge backend, not Zephyr HCI.")
461
+ add("CONFIG_ALP_SDK_BLE_CC3501E=y")
462
+ else:
463
+ add("# BLE: SoM wireless provider is CC3501E, but this board")
464
+ add("# does not populate the chip (board.yaml `populated.cc3501e: "
465
+ "false`);")
466
+ add("# no BLE backend is emitted for this slice.")
467
+ elif linux_owned:
468
+ add("# BLE: provider data path is Linux/BSP-owned on this SoM;")
469
+ add("# no Zephyr BT-host backend is emitted for this slice.")
470
+ else:
471
+ add("# BLE: generic Zephyr BT host path.")
472
+ add("CONFIG_BT=y")
473
+ add("CONFIG_BT_PERIPHERAL=y")
474
+ add("CONFIG_BT_CENTRAL=y")
475
+ add("CONFIG_ALP_SDK_BLE=y")
476
+
477
+ lines.append("")
478
+ return lines
479
+
480
+
481
+ def _yocto_iot_lines(project: BoardProject, slice_: Slice) -> list[str]:
482
+ """local.conf lines for ``cores.<id>.iot`` on a Yocto slice."""
483
+ iot = slice_.iot or {}
484
+ if not any(iot.get(k) for k in ("wifi", "mqtt", "tls", "ble")):
485
+ return []
486
+
487
+ provider = _wireless_provider(project)
488
+ lines: list[str] = [
489
+ "",
490
+ f"# IoT features declared on core `{slice_.core_id}` "
491
+ "(board.yaml `cores.*.iot`)",
492
+ ]
493
+ if provider:
494
+ lines.append(
495
+ f"# Wireless provider resolved from `{project.sku}` "
496
+ f"on_module.wifi_ble: {provider}.")
497
+ else:
498
+ lines.append("# Wireless provider is TBD/absent; emit generic Linux userland.")
499
+
500
+ image_pkgs: list[str] = []
501
+ packageconfig: list[str] = []
502
+
503
+ def add_unique(seq: list[str], value: str) -> None:
504
+ if value not in seq:
505
+ seq.append(value)
506
+
507
+ linux_owned = _is_linux_wireless_provider(provider)
508
+
509
+ if iot.get("wifi"):
510
+ if provider == "cc3501e":
511
+ lines.append("# Wi-Fi: CC3501E is controlled by the Zephyr/companion path.")
512
+ else:
513
+ if linux_owned:
514
+ lines.append(
515
+ "# Wi-Fi: Linux owns this provider's SDIO/firmware path; "
516
+ "BSP/machine recipes supply kernel/firmware packages.")
517
+ add_unique(image_pkgs, "wpa-supplicant")
518
+ add_unique(image_pkgs, "iw")
519
+ add_unique(image_pkgs, "wireless-regdb")
520
+
521
+ if iot.get("ble"):
522
+ if provider == "cc3501e":
523
+ lines.append("# BLE: CC3501E is controlled by the Zephyr/companion path.")
524
+ else:
525
+ if linux_owned:
526
+ lines.append(
527
+ "# BLE: Linux owns this provider's HCI path; BSP/machine "
528
+ "recipes supply controller firmware.")
529
+ add_unique(image_pkgs, "bluez5")
530
+
531
+ if iot.get("mqtt"):
532
+ add_unique(packageconfig, "mqtt")
533
+
534
+ if iot.get("tls"):
535
+ add_unique(packageconfig, "security")
536
+ add_unique(image_pkgs, "ca-certificates")
537
+
538
+ if image_pkgs:
539
+ lines.append(f'IMAGE_INSTALL:append = " {" ".join(image_pkgs)}"')
540
+ if packageconfig:
541
+ lines.append(
542
+ f'PACKAGECONFIG:append:pn-alp-sdk = " {" ".join(packageconfig)}"')
543
+
544
+ return lines
545
+
546
+
547
+ def _emit_baseline(slice_: Slice, diagnostics: dict[str, Any]) -> list[str]:
548
+ """Baseline Kconfig lines: the fragment header comment + the core
549
+ log/print enables every Zephyr slice gets regardless of what it
550
+ declares in board.yaml.
551
+ """
552
+ lines: list[str] = []
553
+ lines.append("# Auto-generated by scripts/alp_orchestrate.py "
554
+ "-- do not edit.")
555
+ lines.append(f"# Per-core Kconfig fragment for slice "
556
+ f"`{slice_.core_id}` ({slice_.os}).")
557
+ lines.append("")
558
+ lines.append("CONFIG_ALP_SDK=y")
559
+ lines.append("CONFIG_LOG=y")
560
+ lines.append("CONFIG_PRINTK=y")
561
+ if diagnostics.get("last_error", True):
562
+ lines.append("CONFIG_THREAD_LOCAL_STORAGE=y")
563
+ log_level = diagnostics.get("log_level")
564
+ if log_level is not None:
565
+ log_level_kc = {
566
+ "error": 1, "warn": 2, "info": 3, "debug": 4, "trace": 4,
567
+ }
568
+ if log_level in log_level_kc:
569
+ lines.append(f"CONFIG_LOG_DEFAULT_LEVEL={log_level_kc[log_level]}")
570
+ lines.append("")
571
+ return lines
572
+
573
+
574
+ def _emit_console(diagnostics: dict[str, Any], slice_: Slice,
575
+ project: Optional[BoardProject] = None) -> list[str]:
576
+ """Console backend, auto-selected from the slice OS (overridable via
577
+ board.yaml `diagnostics.console:`). A Zephyr slice defaults to the
578
+ Alp UART console so `west build && attach a terminal` just works;
579
+ a customer on a serial-less bench flips to `ram`.
580
+
581
+ `diagnostics.sim_console: true` (issue #686) upgrades the AUTO-resolved
582
+ console of a HEADLESS core (`hw_console: false` -- no HW UART, e.g. the
583
+ RZ/V2N M33 system-manager) to the RAM console, so the studio simulator's
584
+ `uart_socket` isn't silent. Precedence: an explicit `diagnostics.
585
+ console:` always wins; a core that has a UART console is simulated
586
+ through its real UART node and needs no Kconfig change.
587
+ """
588
+ raw = diagnostics.get("console")
589
+ console = _resolve_console(raw, slice_.os, slice_.hw_console)
590
+ auto = (raw or "auto").strip().lower() == "auto"
591
+ # A headless AUTO core resolves to "none" (above); `sim_console: true`
592
+ # upgrades it to the RAM console so the studio simulator's uart_socket
593
+ # isn't silent (#686). Precedence: an explicit `diagnostics.console:`
594
+ # (auto == False) always wins and is never upgraded.
595
+ sim = bool(diagnostics.get("sim_console")) and auto \
596
+ and not slice_.hw_console and console == "none"
597
+ if sim:
598
+ console = "ram"
599
+ # `diagnostics.link: itcm` (tan-cli#1350): a Flow C RAM-run's default
600
+ # observable is the RAM console, so an AUTO console is promoted to it. An
601
+ # explicit `console: uart` is NOT promoted: it keeps the board UART console
602
+ # and `link_target.itcm_conf` emits no RAM-console bits for it
603
+ # (tan-cli#1374). linux/none are refused by the loader (alp is a uart alias).
604
+ if auto and link_applies_to(diagnostics, slice_):
605
+ console = "ram"
606
+ ram_size = _RAM_CONSOLE_MIN_SIZE
607
+ if console == "ram" and project is not None:
608
+ ram_size = max(ram_size, _app_ram_console_size(project, slice_))
609
+ return _emit_zephyr_console(console, sim_console=sim, ram_size=ram_size)
610
+
611
+
612
+ # Alif "high-perf"/"high-efficiency" M55 pair -> the "M55-HP"/"M55-HE"
613
+ # short form the boot banner and Alif's own docs use, in place of the
614
+ # fuller "Cortex-M55" every other core type gets (_core_display_label).
615
+ _M55_SUBTYPE_SUFFIX = {"high-perf": "HP", "high-efficiency": "HE"}
616
+
617
+
618
+ def _core_display_label(core: dict[str, Any]) -> str:
619
+ """Boot-banner label for one `soc_spec["cores"]` entry.
620
+
621
+ "Cortex-<X>" from the `type` field for the general case (e.g.
622
+ "cortex-a32" -> "Cortex-A32"); the Alif M55 HP/HE pair gets the
623
+ shorter "M55-HP"/"M55-HE" form instead, because that IS the name
624
+ Alif's own docs and the AEN board trees use for them -- printing
625
+ "Cortex-M55-HE" nobody else calls it that would be its own kind of
626
+ dishonesty. Falls back to the bare (uppercased) type string when it
627
+ is not a "cortex-*" type at all (e.g. an NPU-only or vendor-specific
628
+ core class no SoC spec in the catalogue uses today).
629
+ """
630
+ ctype = str(core.get("type") or "").strip()
631
+ subtype = str(core.get("subtype") or "").strip().lower()
632
+ if ctype == "cortex-m55" and subtype in _M55_SUBTYPE_SUFFIX:
633
+ return f"M55-{_M55_SUBTYPE_SUFFIX[subtype]}"
634
+ if ctype.lower().startswith("cortex-"):
635
+ return "Cortex-" + ctype.split("-", 1)[1].upper()
636
+ return ctype.upper() if ctype else "core"
637
+
638
+
639
+ def _soc_cpu_complement(soc_spec: dict[str, Any], active_core_id: Optional[str]) -> str:
640
+ """Pre-formatted core list for `CONFIG_ALP_SDK_SOC_CPUS`: every core
641
+ the SoC JSON declares, the active one marked and listed first --
642
+ e.g. "M55-HE @160MHz (active) + M55-HP @400MHz + 2x Cortex-A32
643
+ @800MHz". A core with no `freq_mhz` (e.g. an internal-companion NPU
644
+ core with no independent clock in the SoC JSON) prints its label with
645
+ no `@...MHz` suffix rather than a fabricated frequency.
646
+ """
647
+ cores = list(soc_spec.get("cores") or [])
648
+ cores.sort(key=lambda c: 0 if c.get("id") == active_core_id else 1)
649
+ parts: list[str] = []
650
+ for core in cores:
651
+ label = _core_display_label(core)
652
+ count = core.get("count") or 1
653
+ freq = core.get("freq_mhz")
654
+ prefix = f"{int(count)}x " if isinstance(count, (int, float)) and count > 1 else ""
655
+ freq_str = f" @{int(freq)}MHz" if isinstance(freq, (int, float)) else ""
656
+ marker = " (active)" if core.get("id") == active_core_id else ""
657
+ # `|<cluster>` lets alp_banner.c move the marker to the core the
658
+ # image is actually built for (alp-sdk#2469) -- see
659
+ # src/zephyr/alp_soc_cpus.h.
660
+ cluster = core.get("zephyr_cpucluster")
661
+ tag = f"|{cluster}" if cluster else ""
662
+ parts.append(f"{prefix}{label}{freq_str}{marker}{tag}")
663
+ return " + ".join(parts)
664
+
665
+
666
+ def _soc_npu_complement(soc_spec: dict[str, Any]) -> str:
667
+ """Pre-formatted NPU list for `CONFIG_ALP_SDK_SOC_NPUS`, e.g.
668
+ "Ethos-U85 + 2x Ethos-U55" -- same-`type` NPU instances (e.g. the
669
+ E8's two Ethos-U55s, one per M55 core) are grouped into a single
670
+ "Nx <Label>" entry rather than repeated, in the order their `type`
671
+ first appears in `soc_spec["npus"]`.
672
+ """
673
+ order: list[str] = []
674
+ counts: dict[str, int] = {}
675
+ for npu in (soc_spec.get("npus") or []):
676
+ ntype = str(npu.get("type") or "").strip()
677
+ if not ntype:
678
+ continue
679
+ label = "-".join(seg.capitalize() for seg in ntype.split("-"))
680
+ if label not in counts:
681
+ order.append(label)
682
+ counts[label] = counts.get(label, 0) + 1
683
+ return " + ".join(
684
+ f"{counts[label]}x {label}" if counts[label] > 1 else label
685
+ for label in order)
686
+
687
+
688
+ def _soc_display_name(soc_spec: dict[str, Any]) -> str:
689
+ """Vendor + family + part for `CONFIG_ALP_SDK_SOC_NAME`, e.g. "Alif
690
+ Ensemble E8". `vendor` contributes only its first word (Alif
691
+ Semiconductor -> Alif); `part` is dropped when it already restates
692
+ `family` verbatim as a prefix (e.g. family "Ensemble" / part "Ensemble E8"
693
+ would otherwise read "Alif Ensemble Ensemble E8").
694
+ """
695
+ vendor = str(soc_spec.get("vendor") or "").strip()
696
+ family = str(soc_spec.get("family") or "").strip()
697
+ part = str(soc_spec.get("part") or "").strip()
698
+ vendor_short = vendor.split()[0] if vendor else ""
699
+ if family and part and part.startswith(family):
700
+ segments = [vendor_short, part]
701
+ else:
702
+ segments = [vendor_short, family, part]
703
+ return " ".join(s for s in segments if s)
704
+
705
+
706
+ def _emit_soc_summary(project: "BoardProject", slice_: "Slice") -> list[str]:
707
+ """Boot-banner identity + system-summary Kconfig (`CONFIG_ALP_SDK_
708
+ SOC_*` / `CONFIG_ALP_SDK_SOM_*_MBIT`, `zephyr/kconfigs/core.kconfig`).
709
+
710
+ Every field here is transcribed, never invented, from data this
711
+ project already resolved: the SoC spec JSON (`project.soc_spec` --
712
+ `cores[]`, `npus[]`, `soc_ram_kb`, `soc_flash_mb`, the on-die SRAM +
713
+ MRAM the SoC itself carries) for the CONFIG_ALP_SDK_SOC_* facts, and
714
+ the SoM preset's `memory:` block (off-SoC OSPI RAM/flash the SKU's
715
+ BOM actually populates -- distinct from the SoC facts above: two SKUs
716
+ on the identical PCB/silicon can differ here, e.g. E1M-AEN803's 512
717
+ Mbit HyperRAM + 256 Mbit NOR vs E1M-AEN801, which populates neither)
718
+ for CONFIG_ALP_SDK_SOM_{DRAM,FLASH}_MBIT.
719
+
720
+ `alp_banner.c` prefers these over its devicetree fallback (one
721
+ image's chosen sram/flash REGION size, not the SoM's actual
722
+ hardware) whenever `CONFIG_ALP_SDK_SOC_CPUS` is non-empty -- see that
723
+ file's module docstring. A project whose SoC spec is missing a field
724
+ (no `npus`, no `soc_ram_kb`, ...) still gets whatever IS known; only
725
+ `CONFIG_ALP_SDK_SOC_NAME`/`_CPUS` gate the whole banner section on the
726
+ C side, so those two are the only ones this function must guarantee
727
+ non-empty whenever `project.soc_spec` resolves at all.
728
+ """
729
+ soc_spec = project.soc_spec or {}
730
+ lines: list[str] = [
731
+ "# Boot-banner SoC identity + system summary (from the SoC spec "
732
+ "JSON + SoM preset `memory:`; see alp_banner.c).",
733
+ ]
734
+
735
+ name = _soc_display_name(soc_spec)
736
+ if name:
737
+ lines.append(f'CONFIG_ALP_SDK_SOC_NAME="{name}"')
738
+
739
+ cpus = _soc_cpu_complement(soc_spec, slice_.core_id)
740
+ if cpus:
741
+ lines.append(f'CONFIG_ALP_SDK_SOC_CPUS="{cpus}"')
742
+
743
+ npus = _soc_npu_complement(soc_spec)
744
+ if npus:
745
+ lines.append(f'CONFIG_ALP_SDK_SOC_NPUS="{npus}"')
746
+
747
+ soc_ram_kb = soc_spec.get("soc_ram_kb")
748
+ if isinstance(soc_ram_kb, (int, float)) and soc_ram_kb > 0:
749
+ lines.append(f"CONFIG_ALP_SDK_SOC_SRAM_KB={int(soc_ram_kb)}")
750
+
751
+ soc_flash_mb = soc_spec.get("soc_flash_mb")
752
+ if isinstance(soc_flash_mb, (int, float)) and soc_flash_mb > 0:
753
+ lines.append(f"CONFIG_ALP_SDK_SOC_MRAM_KB={int(round(soc_flash_mb * 1024))}")
754
+
755
+ # Off-SoC OSPI memory the SoM SKU actually populates (som-preset
756
+ # `memory:` -- a MODULE fact, not a SoC one; see the SKU contrast
757
+ # above). `is_tbd`/absent/non-positive all mean "don't claim it".
758
+ mem = project.som_preset.get("memory") or {}
759
+ dram_mbit = mem.get("dram_mbit")
760
+ if isinstance(dram_mbit, (int, float)) and not _is_tbd(dram_mbit) and dram_mbit > 0:
761
+ lines.append(f"CONFIG_ALP_SDK_SOM_DRAM_MBIT={int(dram_mbit)}")
762
+ flash_mbit = mem.get("flash_mbit")
763
+ if isinstance(flash_mbit, (int, float)) and not _is_tbd(flash_mbit) and flash_mbit > 0:
764
+ lines.append(f"CONFIG_ALP_SDK_SOM_FLASH_MBIT={int(flash_mbit)}")
765
+
766
+ lines.append("")
767
+ return lines
768
+
769
+
770
+ def _emit_som_caps(
771
+ project: "BoardProject",
772
+ silicon: Optional[str],
773
+ kconfig: Optional[str],
774
+ ) -> list[str]:
775
+ """SoM-capability Kconfig lines: HW-manifest EEPROM wiring, the
776
+ resolved SoM-silicon Kconfig symbol, and the cross-EVK board facade /
777
+ `<alp/soc_caps.h>` capability-restriction selector.
778
+ """
779
+ lines: list[str] = []
780
+
781
+ # HW manifest EEPROM: the SoM preset provides the default bus,
782
+ # while board.yaml `features.hw_info.eeprom` can pin the exact
783
+ # app/project address and manifest offset. Harmless on examples
784
+ # without that bus wired: the read returns NOT_READY and the banner
785
+ # says so.
786
+ hw_info_eeprom = project.hw_info_eeprom_config()
787
+ if hw_info_eeprom is not None:
788
+ source = hw_info_eeprom.get("source")
789
+ if source == "features.hw_info.eeprom":
790
+ lines.append(
791
+ "# HW manifest EEPROM from board.yaml `features.hw_info.eeprom`;")
792
+ else:
793
+ lines.append("# On-module HW manifest EEPROM -> portable bus 0 (alp-i2c0);")
794
+ lines.append("# enables alp_hw_info_read() + the console banner manifest line.")
795
+ lines.append(
796
+ "CONFIG_ALP_SDK_HW_INFO_EEPROM_I2C_BUS_ID="
797
+ f"{hw_info_eeprom['bus_id']}")
798
+ if source == "features.hw_info.eeprom":
799
+ lines.append(
800
+ "CONFIG_ALP_SDK_HW_INFO_EEPROM_ADDR_7BIT="
801
+ f"0x{hw_info_eeprom['addr_7bit']:02x}")
802
+ lines.append(
803
+ "CONFIG_ALP_SDK_HW_INFO_EEPROM_OFFSET="
804
+ f"{hw_info_eeprom['offset']}")
805
+ lines.append("")
806
+
807
+ # Build-time hw_rev this project resolved -- lets the boot banner warn
808
+ # when the LIVE EEPROM manifest disagrees (issue #1853). Emitted
809
+ # unconditionally (not scoped to the `hw_info_eeprom` block above): a
810
+ # SKU with no on_module.eeprom today still gets the
811
+ # symbol, so it isn't silently dropped if that SKU gains an EEPROM
812
+ # later, and it stays harmless meanwhile (alp_hw_info_read() never
813
+ # returns ALP_OK without a bus, so the banner's compare never runs).
814
+ # Same `hw_rev or ... or "unknown"` fallback chain as
815
+ # alp_project_emit/hw_info.py's `_emit_hw_info_h` (project.hw_rev is
816
+ # already `som.hw_rev or default_hw_rev`; add the same "unknown"
817
+ # floor here so the two resolvers can't disagree on the empty case).
818
+ # Emit the composed board designator (`2626-r2`), not the bare revision key,
819
+ # because that is what the module's EEPROM identity carries -- and this symbol
820
+ # exists only to be compared against it. Getting this wrong is not cosmetic:
821
+ # CONFIG_ALP_SDK_HW_REV_MISMATCH_FATAL promotes the banner's mismatch warning
822
+ # to k_panic(), so a write side that composes and a compare side that does not
823
+ # would halt boot on a correctly provisioned module.
824
+ #
825
+ # project.hw_rev itself stays the BARE key -- loader.py passes it to
826
+ # family_revision_known() as a lookup key into hw_revisions, where a composed
827
+ # value would not resolve.
828
+ _som_hw_rev = project.hw_rev
829
+ if _som_hw_rev:
830
+ from .loader import _sku_family_dir
831
+ _fam = _sku_family_dir(project.sku)
832
+ if _fam:
833
+ _som_hw_rev = _sdk_compat.board_designator(
834
+ _sdk_compat.load_family_table(project.effective_metadata_root(), _fam),
835
+ _som_hw_rev)
836
+ lines.append(f'CONFIG_ALP_SDK_SOM_HW_REV="{_som_hw_rev or "unknown"}"')
837
+ lines.append("")
838
+
839
+ if kconfig:
840
+ lines.append(f"# SoM silicon ({silicon} via {project.sku})")
841
+ lines.append(f"CONFIG_{kconfig}=y")
842
+ lines.append("")
843
+
844
+ # Cross-EVK board facade selector (<alp/board.h>).
845
+ # Emitted only when the project resolves to a named board preset
846
+ # (e.g. `preset: e1m-x-evk` -> board_name "E1M-X-EVK").
847
+ # CONFIG_COMPILER_OPT passes the define into every source file the
848
+ # Zephyr build compiles, so the facade header can resolve the
849
+ # correct EVK-specific BOARD_* aliases without per-app prj.conf
850
+ # entries or extra_args. Inline boards (no preset/name) skip this;
851
+ # native_sim testcases that do need the define set it via extra_args.
852
+ # Per-SKU capability-restriction gate for <alp/soc_caps.h>: emitted
853
+ # ONLY when the SoM preset declares `silicon_capabilities.unpopulated`
854
+ # (no SKU in the current catalogue does), so unrestricted SKUs keep a
855
+ # byte-identical fragment. Folded into the same CONFIG_COMPILER_OPT
856
+ # string as the board facade define because that Kconfig is
857
+ # single-value (last write wins, not additive).
858
+ som_def = ""
859
+ if som_unpopulated_capabilities(project.som_preset):
860
+ som_def = f"-DALP_SOM_{_som_define_slug(project.sku)}"
861
+ if project.board_name:
862
+ # Cross-EVK board facade selector for <alp/board.h>. NOTE:
863
+ # CONFIG_COMPILER_OPT is a single-value Kconfig string (last write
864
+ # wins, not additive) -- an app must NOT also set it in prj.conf or
865
+ # this -D is silently dropped; pass extra defines another way.
866
+ lines.append("# Cross-EVK board facade selector (<alp/board.h>);"
867
+ " CONFIG_COMPILER_OPT is single-value (do not also set in prj.conf).")
868
+ if som_def:
869
+ lines.append("# + ALP_SOM_* capability-restriction gate: this SKU "
870
+ "leaves silicon capabilities unpopulated "
871
+ "(SoM preset silicon_capabilities.unpopulated).")
872
+ opt = f"-DALP_BOARD_{_board_define_slug(project.board_name)}"
873
+ if som_def:
874
+ opt = f"{opt} {som_def}"
875
+ lines.append(f'CONFIG_COMPILER_OPT="{opt}"')
876
+ lines.append("")
877
+ elif som_def:
878
+ lines.append("# ALP_SOM_* capability-restriction gate (<alp/soc_caps.h>);"
879
+ " CONFIG_COMPILER_OPT is single-value (do not also set in prj.conf).")
880
+ lines.append(f'CONFIG_COMPILER_OPT="{som_def}"')
881
+ lines.append("")
882
+
883
+ return lines
884
+
885
+
886
+ def _slug_kconfig(slug: str) -> str:
887
+ """Map a chip/block slug to its Kconfig symbol (CHIP_ or BLOCK_)."""
888
+ kind = "BLOCK" if slug in _BLOCK_SLUGS else "CHIP"
889
+ return f"CONFIG_ALP_SDK_{kind}_{slug.upper()}"
890
+
891
+
892
+ def _resolve_chip_states(project: "BoardProject") -> dict[str, bool]:
893
+ """Final resolved on/off state for every chip/block slug the project
894
+ touches -- the single source of truth `_emit_chips` prints its
895
+ `CONFIG_ALP_SDK_CHIP_*=` lines FROM (so the printed line and this map
896
+ can never disagree by construction) and any other emitter (e.g.
897
+ `_zephyr_iot_kconfig`'s CC3501E Wi-Fi/BLE gate) consults before gating
898
+ a dependent symbol (issue #874).
899
+
900
+ Precedence: on-module presence is the physical baseline (always
901
+ True). An explicit `populated: {chip: true/false}` entry then applies
902
+ the board's stated population, on-module chip or not. Finally, a
903
+ project-declared `chips:` entry can still turn a board-DNI'd chip ON
904
+ -- the documented escape hatch for a project that wires up a sensor
905
+ the board preset doesn't populate by default (e.g. `ai/cold-chain-
906
+ monitor`'s `chips: [bme280]` overriding the EVK preset's `populated:
907
+ {bme280: false}`) -- UNLESS the chip is ALSO on-module: an on-module
908
+ (SoM-intrinsic) component is fixed at SoM-fab time (see the `som:`
909
+ block's own description in board.schema.json) and re-listing it in
910
+ `chips:` is a redundant declaration, not a fresh override, so it must
911
+ not silently flip a board's `populated: {chip: false}` back on for a
912
+ chip ALSO claimed on-module (issue #874 follow-up: the triple-overlap
913
+ case -- on-module + populated:false + project `chips:` all naming the
914
+ same chip -- used to let the project-chips pass win regardless,
915
+ re-creating the exact WIFI_CC3501E=y-against-chip=n contradiction
916
+ #874 fixed).
917
+ """
918
+ states: dict[str, bool] = {}
919
+
920
+ som_chips: set[str] = set()
921
+ om = project.som_preset.get("on_module") or {}
922
+ for slug in _slugs_from_on_module(om):
923
+ som_chips.add(slug)
924
+ hf = project.som_preset.get("helper_firmware") or []
925
+ for slug in _slugs_from_helper_firmware(hf):
926
+ som_chips.add(slug)
927
+ for chip in som_chips:
928
+ states[chip] = True
929
+
930
+ populated: dict[str, bool] = dict(
931
+ (project.board_preset or {}).get("populated") or {})
932
+ for chip, on in populated.items():
933
+ states[chip] = bool(on)
934
+
935
+ for chip in (project.chips or []):
936
+ if chip in som_chips:
937
+ continue
938
+ states[chip] = True
939
+
940
+ return states
941
+
942
+
943
+ def _chip_has_driver(slug: str) -> bool:
944
+ """True when `chips/<slug>/` exists, i.e. there is something to compile.
945
+
946
+ This is the same criterion `_slugs_from_on_module` already applies to
947
+ `ospi_memories` and `hyperram`, whose docstring records the reason:
948
+ those parts have "no `chips/<part>/` driver, so their MPNs are NOT
949
+ extracted as chip slugs (emitting them as `CONFIG_ALP_SDK_CHIP_<X>`
950
+ would trip Zephyr's undefined-symbol guard)". The rule is applied there
951
+ per-field; it has to hold per-chip too, because a scalar `on_module:`
952
+ field can name a real chip manifest that has no driver behind it.
953
+
954
+ Driver presence is checked on disk rather than read from the manifest's
955
+ `driver_status:`, because the directory is what the Kconfig declaration
956
+ actually points at -- every entry in `zephyr/kconfigs/chips.kconfig`
957
+ reads "Compile chips/<part>/<part>.c". A status string can drift from
958
+ the tree; the directory cannot.
959
+
960
+ `REPO` is the BOUND alp-sdk checkout (`tan.planner_root`), not tan's own
961
+ tree -- `chips/` is a fact and facts did not relocate (ADR-0017).
962
+ """
963
+ return (REPO / "chips" / slug).is_dir()
964
+
965
+
966
+ @lru_cache(maxsize=1)
967
+ def _declared_chip_symbols() -> frozenset[str]:
968
+ """Every `ALP_SDK_CHIP_*` symbol the BOUND alp-sdk actually declares.
969
+
970
+ Parsed from the SDK's own `zephyr/**/*.kconfig` rather than inferred, so
971
+ this is the same fact Zephyr resolves against. Measured on alp-sdk `dev`:
972
+ 80 declarations, all in `zephyr/kconfigs/chips.kconfig`, matching the 80
973
+ `chips/<slug>/` directories exactly.
974
+
975
+ An empty result means the SDK's kconfigs could not be read at all; the
976
+ caller treats that as "cannot verify" and stays silent rather than
977
+ refusing every build on a layout this parser does not understand.
978
+ """
979
+ declared: set[str] = set()
980
+ for path in sorted((REPO / "zephyr").rglob("*.kconfig")):
981
+ try:
982
+ text = path.read_text(encoding="utf-8", errors="replace")
983
+ except OSError:
984
+ continue
985
+ declared.update(re.findall(r"^config (ALP_SDK_CHIP_\w+)", text, re.M))
986
+ return frozenset(declared)
987
+
988
+
989
+ def _assert_chip_symbols_declared(slugs: "list[str]") -> None:
990
+ """Refuse to write a `CONFIG_` line the bound SDK cannot resolve (#728).
991
+
992
+ `_chip_has_driver` decides what to emit from `chips/<slug>/` on disk.
993
+ That is the right *intent* -- the directory is what the declaration
994
+ compiles -- but it is an inference, and Zephyr resolves the declaration.
995
+ When the two disagree, the emitted line reaches Kconfig as
996
+
997
+ alp.conf:28: warning: attempt to assign the value 'y' to the
998
+ undefined symbol ALP_SDK_CHIP_DP83825
999
+ error: Aborting due to Kconfig warnings
1000
+
1001
+ which blames the app's generated config for a planner/SDK skew and takes
1002
+ out `tan build` for the whole SoM. That is how tan-cli#728 presented:
1003
+ released `tan 0.5.1`, whose vendored planner predates alp-sdk#1241/#1322,
1004
+ against an alp-sdk `dev` whose `ethernet_phy: dp83825` made an undriven
1005
+ chip reachable from `on_module:` for the first time.
1006
+
1007
+ A released binary cannot be retro-fixed, so this does not repair 0.5.1 --
1008
+ it stops the NEXT skew of this class from being discovered by Zephyr. The
1009
+ refusal names the symbol and the SDK, so the reader knows which pair
1010
+ disagrees rather than hunting a `CONFIG_` line they never wrote.
1011
+
1012
+ Silent when the SDK declares nothing this parser recognises: an
1013
+ unreadable or restructured kconfig tree is a reason to stay out of the
1014
+ way, not to refuse every build. Verification is the goal, not gatekeeping
1015
+ on a layout assumption.
1016
+ """
1017
+ declared = _declared_chip_symbols()
1018
+ if not declared:
1019
+ return
1020
+ undeclared = sorted(
1021
+ s for s in slugs
1022
+ if (sym := _slug_kconfig(s).removeprefix("CONFIG_")).startswith("ALP_SDK_CHIP_")
1023
+ and sym not in declared
1024
+ )
1025
+ if not undeclared:
1026
+ return
1027
+ names = ", ".join(f"{_slug_kconfig(s)} (chip `{s}`)" for s in undeclared)
1028
+ raise OrchestratorError(
1029
+ f"the bound alp-sdk at {REPO} declares no Kconfig symbol for "
1030
+ f"{names}. Emitting it would write an assignment Zephyr cannot "
1031
+ f"resolve, failing the build with `attempt to assign the value 'y' "
1032
+ f"to the undefined symbol` and blaming the generated alp.conf. This "
1033
+ f"means tan's planner and this SDK disagree about which chips have "
1034
+ f"drivers -- usually a tan too old for the SDK (tan-cli#728). Update "
1035
+ f"tan, or bind an alp-sdk matching this tan."
1036
+ )
1037
+
1038
+
1039
+ def _emit_chips(
1040
+ project: "BoardProject",
1041
+ chip_subsystems_table: dict[str, tuple[str, ...]],
1042
+ ) -> tuple[list[str], set[str], dict[str, bool]]:
1043
+ """SoM-intrinsic + board-populated + project-declared chip drivers.
1044
+
1045
+ Every `CONFIG_ALP_SDK_CHIP_*=` / `CONFIG_ALP_SDK_BLOCK_*=` line is
1046
+ printed FROM `_resolve_chip_states`'s resolved map (single source of
1047
+ truth, issue #874) and printed EXACTLY ONCE regardless of how many of
1048
+ the three sources below mention it -- a chip already printed by an
1049
+ earlier block is skipped by a later one rather than re-asserted with
1050
+ a value that could disagree.
1051
+
1052
+ Returns the emitted Kconfig lines, the accumulated set of Zephyr
1053
+ subsystem names the enabled chip drivers require -- fed into
1054
+ `_emit_subsystems` alongside the slice's `peripherals:` array -- and
1055
+ the resolved chip on/off map.
1056
+ """
1057
+ lines: list[str] = []
1058
+ chip_subsystems: set[str] = set()
1059
+ resolved_chip_state = _resolve_chip_states(project)
1060
+ printed: set[str] = set()
1061
+
1062
+ def _emit_block(header: str, candidates) -> None:
1063
+ chips = sorted(c for c in set(candidates) if c not in printed)
1064
+ if not chips:
1065
+ return
1066
+ _assert_chip_symbols_declared(chips)
1067
+ lines.append(header)
1068
+ for chip in chips:
1069
+ on = resolved_chip_state.get(chip, False)
1070
+ lines.append(f"{_slug_kconfig(chip)}={'y' if on else 'n'}")
1071
+ printed.add(chip)
1072
+ if on:
1073
+ for s in chip_subsystems_table.get(chip, ()):
1074
+ chip_subsystems.add(s)
1075
+ lines.append("")
1076
+
1077
+ # ----------------------------------------------------------------
1078
+ # SoM-intrinsic chip drivers — derived from on_module: + helper_firmware:
1079
+ # in the SoM preset. These are NOT declared by the customer; they
1080
+ # are determined by which SoM SKU the project targets. Swapping
1081
+ # `som.sku:` from E1M-V2N101 to E1M-AEN801 automatically swaps
1082
+ # the on-module chip set without any board.yaml changes.
1083
+ # ----------------------------------------------------------------
1084
+ som_chips: set[str] = set()
1085
+ om = project.som_preset.get("on_module") or {}
1086
+ if om:
1087
+ for slug in _slugs_from_on_module(om):
1088
+ som_chips.add(slug)
1089
+ hf = project.som_preset.get("helper_firmware") or []
1090
+ for slug in _slugs_from_helper_firmware(hf):
1091
+ som_chips.add(slug)
1092
+
1093
+ # A CONFIG_ALP_SDK_CHIP_<X> line is only meaningful when the symbol
1094
+ # exists, and the symbol only exists when there is a driver to compile:
1095
+ # every declaration in zephyr/kconfigs/chips.kconfig reads "Compile
1096
+ # chips/<part>/<part>.c". A manifest whose driver_status says no such
1097
+ # driver exists therefore has no symbol, and emitting the line anyway
1098
+ # writes a CONFIG_ assignment Zephyr cannot resolve (alp-sdk#1241).
1099
+ #
1100
+ # Measured over metadata/chips/*.yaml when this was added: 77 of 81
1101
+ # chips carry a declared symbol -- every `complete`, `partial` and
1102
+ # `stub` one, each with a real chips/<part>/ directory. The only
1103
+ # undeclared ones are the four `planned` Murata modules, which no SoM
1104
+ # populates. `ethernet_phy: dp83825` made an undriven chip reachable
1105
+ # from on_module: for the first time, which is how this surfaced.
1106
+ #
1107
+ # The chip stays in metadata -- it is real, populated hardware, and the
1108
+ # manifest is what documents the ADR-0023 layer-3 gap. It just does not
1109
+ # get a driver-selection Kconfig it has no driver for.
1110
+ som_chips = {s for s in som_chips if _chip_has_driver(s)}
1111
+
1112
+ _emit_block(
1113
+ f"# SoM-intrinsic chip drivers (from `{project.sku}` "
1114
+ f"on_module + helper_firmware)", som_chips)
1115
+
1116
+ # ----------------------------------------------------------------
1117
+ # Board-populated chip drivers. Single source: the resolved
1118
+ # board_preset dict, which comes from either the shared
1119
+ # metadata/boards/<preset>.yaml or the project's inline
1120
+ # top-level fields (synthesised by the loader). No project-level
1121
+ # override merge -- the schema's `oneOf` rule rejects mixing.
1122
+ #
1123
+ # Chip drivers compile per-chip via the
1124
+ # zephyr_library_sources_ifdef(CONFIG_ALP_SDK_CHIP_<NAME> ...)
1125
+ # (or CONFIG_ALP_SDK_BLOCK_<NAME> for the `button_led` / `pdm_mic`
1126
+ # SDK-level block helpers) entries in zephyr/CMakeLists.txt.
1127
+ #
1128
+ # We emit the chip-driver block on every zephyr slice — Kconfig
1129
+ # dedupes when multiple per-core fragments overlay onto the same
1130
+ # base. This keeps `--core <id>` invocations self-sufficient
1131
+ # (the slice carries everything it needs to compile) without
1132
+ # depending on cross-slice ordering.
1133
+ # ----------------------------------------------------------------
1134
+ populated: dict[str, bool] = dict(
1135
+ (project.board_preset or {}).get("populated") or {})
1136
+ _emit_block(
1137
+ "# Board-populated chip drivers (from the resolved "
1138
+ "board definition)", populated.keys())
1139
+
1140
+ # Project-declared chips (board.yaml top-level `chips:` array).
1141
+ # Adds chip drivers the application links directly via
1142
+ # <alp/chips/<name>.h> -- e.g. when the project plugs an external
1143
+ # sensor into a board's headers that's not in the board's
1144
+ # `populated:` table, or turns ON a chip the board preset DNIs by
1145
+ # default (`_resolve_chip_states` already folded that override into
1146
+ # the resolved value the "Board-populated" block above printed, so
1147
+ # there is nothing left to print here for such a chip -- this block
1148
+ # only has fresh lines for chips `populated:` never mentioned at
1149
+ # all). A chip that's ALSO on-module never reaches here with a
1150
+ # different value than the SoM-intrinsic block already printed
1151
+ # (`_resolve_chip_states` deliberately does not let a project
1152
+ # `chips:` entry override an on-module chip's resolved state).
1153
+ _emit_block(
1154
+ "# Project-declared chips (board.yaml `chips:` array)",
1155
+ project.chips or [])
1156
+
1157
+ return lines, chip_subsystems, resolved_chip_state
1158
+
1159
+
1160
+ def _emit_subsystems(
1161
+ project: BoardProject, slice_: Slice, chip_subsystems: set[str]
1162
+ ) -> list[str]:
1163
+ """Zephyr subsystems required by the slice: union of (chip-driver-
1164
+ required subsystems, from `_emit_chips`) and (this core's
1165
+ `peripherals:` array, which adds to the union per spec §4.6).
1166
+ """
1167
+ lines: list[str] = []
1168
+ periph_kconfig = peripheral_kconfig(project.effective_metadata_root())
1169
+ periph_subsystems: set[str] = set()
1170
+ for periph in slice_.peripherals or []:
1171
+ periph_subsystems.update(periph_kconfig.get(periph, ()))
1172
+ all_subsystems = chip_subsystems | periph_subsystems
1173
+ if all_subsystems:
1174
+ lines.append(f"# Zephyr subsystems required on core "
1175
+ f"`{slice_.core_id}` (chip drivers + peripherals)")
1176
+ for s in sorted(all_subsystems):
1177
+ lines.append(f"CONFIG_{s}=y")
1178
+ lines.append("")
1179
+ return lines
1180
+
1181
+
1182
+ def _per_core_library_kconfig(
1183
+ lib: str,
1184
+ manifest: Optional[dict],
1185
+ metadata_root: Path) -> Optional[list[str]]:
1186
+ """Base-Kconfig lines for a per-core `libraries:` token, read from the
1187
+ library's ADR 0018 manifest (metadata/libraries/<canonical>.yaml) rather
1188
+ than a hand-maintained table (WS6-c #610 §6).
1189
+
1190
+ `manifest` is the already-resolved document from the ADR-0018 library
1191
+ layer (`libraries.resolve_selection`); pass it so the per-core channel
1192
+ reads exactly what the layer validated instead of re-reading the file
1193
+ behind the layer's back (alplabai/tan-cli#555). Pass `None` and the
1194
+ token is resolved to its canonical manifest through the
1195
+ metadata/library-aliases-v1.json alias table (read from `metadata_root`,
1196
+ the SAME tree `load_board_yaml(..., metadata_root=...)` resolved this
1197
+ project against -- #1485) and loaded here.
1198
+
1199
+ The emitted set is the union of the manifest's
1200
+ `integration.zephyr.kconfig` (the upstream module-enable line(s)) and its
1201
+ `integration.zephyr.hw_backends.sw_fallback.kconfig` (the SDK SW floor),
1202
+ module-enable first and the SW-fallback marker last, deduped -- computed
1203
+ by `libraries.zephyr_library_kconfig`, the same derivation the
1204
+ project-wide channel (`libraries.zephyr_kconfig_lines`) calls, so a
1205
+ library's base Kconfig set cannot drift between the two declaration
1206
+ channels (#1359).
1207
+
1208
+ Returns None when no manifest resolves (caller emits the pending-wire TODO).
1209
+ """
1210
+ doc = manifest
1211
+ if doc is None:
1212
+ alias = _library_alias_table(metadata_root)
1213
+ canonical = alias.get(lib, lib)
1214
+ path = metadata_root / "libraries" / f"{canonical}.yaml"
1215
+ if not path.is_file():
1216
+ return None
1217
+ doc = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
1218
+ return _library_layer.zephyr_library_kconfig(doc)
1219
+
1220
+
1221
+ def _emit_libraries(
1222
+ project: "BoardProject",
1223
+ slice_: Slice,
1224
+ ) -> list[str]:
1225
+ """Per-core `libraries:`, curated third-party `libraries:` (ADR 0018),
1226
+ and the open-set `extra_libraries:` escape hatch (v0.6 P2.1).
1227
+
1228
+ Per-core Kconfig is resolved from each library's manifest
1229
+ (`_per_core_library_kconfig`), not a hand-maintained enable table.
1230
+
1231
+ Both the project-wide and the core-scoped declaration channels go through
1232
+ the SAME ADR-0018 layer: `libraries.resolve_selection(..., slice_=)` is
1233
+ what refuses an unknown name, a failed `requires:` constraint or a library
1234
+ that cannot be wired on this target, and it is resolved FIRST so that
1235
+ refusal lands before a single line is emitted (alplabai/tan-cli#555).
1236
+ """
1237
+ lines: list[str] = []
1238
+
1239
+ # name -> validated manifest for every library in scope on this slice.
1240
+ resolved: dict[str, dict] = {}
1241
+ if slice_.os == "zephyr":
1242
+ resolved = dict(_library_layer.resolve_selection(
1243
+ project, project.effective_metadata_root(), slice_=slice_))
1244
+
1245
+ if slice_.libraries:
1246
+ lines.append(f"# Libraries declared on core "
1247
+ f"`{slice_.core_id}`")
1248
+ for lib in sorted(slice_.libraries):
1249
+ kcs = _per_core_library_kconfig(
1250
+ lib, resolved.get(lib), project.effective_metadata_root())
1251
+ if kcs is None:
1252
+ lines.append(
1253
+ f"# TODO: wire library '{lib}' once its v0.4 enable lands")
1254
+ else:
1255
+ for kc in kcs:
1256
+ lines.append(kc)
1257
+ lines.append("")
1258
+
1259
+ # Project-wide curated third-party libraries (top-level `libraries:`,
1260
+ # ADR 0018). Emitted from the metadata/libraries/<name>.yaml manifests;
1261
+ # resolve_selection() has already rejected any incompatible selection.
1262
+ # Guard keeps a project with no `libraries:` byte-identical.
1263
+ library_lines = _library_layer.zephyr_kconfig_lines(
1264
+ project, slice_, project.effective_metadata_root())
1265
+ if library_lines:
1266
+ lines.append("# Curated third-party libraries "
1267
+ "(project `libraries:`, ADR 0018)")
1268
+ lines.extend(library_lines)
1269
+ lines.append("")
1270
+
1271
+ # ----------------------------------------------------------------
1272
+ # Open-set extra_libraries (v0.6 P2.1). Each entry declares either
1273
+ # inline `kconfig:` lines (emitted verbatim) or a `profile:` path
1274
+ # to a hw-backends.yaml-style file we walk with the same silicon /
1275
+ # soc_family / requires_cap matcher as the curated libraries.
1276
+ # _validate_consistency() guarantees exactly-one of kconfig/profile
1277
+ # and the profile file resolves, so the per-entry checks here are
1278
+ # just for emit-time safety.
1279
+ # ----------------------------------------------------------------
1280
+ if slice_.extra_libraries:
1281
+ lines.append(f"# Extra libraries declared on core "
1282
+ f"`{slice_.core_id}` (open-set escape hatch)")
1283
+ for entry in sorted(slice_.extra_libraries,
1284
+ key=lambda e: e.get("name", "")):
1285
+ name = entry.get("name", "<unnamed>")
1286
+ kc_lines = entry.get("kconfig")
1287
+ profile = entry.get("profile")
1288
+ if kc_lines:
1289
+ lines.append(f"# extra_libraries[{name}] (inline kconfig)")
1290
+ for kc in kc_lines:
1291
+ lines.append(str(kc))
1292
+ elif profile:
1293
+ lines.append(f"# extra_libraries[{name}] (profile: {profile})")
1294
+ profile_lines = _emit_extra_library_profile(
1295
+ name, profile, project)
1296
+ if profile_lines:
1297
+ lines.extend(profile_lines)
1298
+ else:
1299
+ lines.append(
1300
+ f"# (no matching backend for "
1301
+ f"silicon={project.som_preset.get('silicon')} "
1302
+ f"in {profile})")
1303
+ lines.append("")
1304
+
1305
+ return lines
1306
+
1307
+
1308
+ def _slice_wants_inference(project: "BoardProject", slice_: Slice) -> bool:
1309
+ """True when this slice genuinely uses `<alp/inference.h>`.
1310
+
1311
+ Two independent signals, either sufficient: `cores.<id>.inference:`
1312
+ declared (app-level tuning, e.g. `default_arena_kib:` -- the signal
1313
+ every inference example under examples/ai, examples/audio,
1314
+ examples/camera-vision declares), OR the slice's `libraries:` scope
1315
+ (project-wide AND core-scoped, via `libraries.scoped_names` -- #1359
1316
+ follow-up: reading `slice_.libraries` alone missed a project-wide
1317
+ `libraries: [tflite-micro]` entry) pulls in `tflite-micro` (resolved
1318
+ through the same alias table `_per_core_library_kconfig` uses -- a
1319
+ `cores:`-scoped top-level `libraries:` entry folds into
1320
+ `slice_.libraries` at load time, see `loader.py::_normalize_libraries`).
1321
+ The library signal exists because `cores.<id>.inference:` is app-level
1322
+ TUNING, not a declaration of intent -- an app that never overrides the
1323
+ arena default has no reason to write the block, and #874's adversarial
1324
+ follow-up found exactly that false negative (`examples/camera-vision/
1325
+ ai-object-detection-realtime`, `libraries: tflite-micro` with no
1326
+ `inference:` block). A slice with NEITHER signal stays clean (no
1327
+ unconditional emit returns, issue #874 item 4).
1328
+ """
1329
+ if slice_.inference:
1330
+ return True
1331
+ alias = _library_alias_table(project.effective_metadata_root())
1332
+ return any(alias.get(lib, lib) == "tflite-micro"
1333
+ for lib in _library_layer.scoped_names(project, slice_=slice_))
1334
+
1335
+
1336
+ def _emit_inference(
1337
+ project: "BoardProject",
1338
+ slice_: Slice,
1339
+ silicon: Optional[str],
1340
+ ) -> list[str]:
1341
+ """Inference dispatcher enables, driven entirely by the SoM preset's
1342
+ `capabilities:` matrix + SoC-JSON `cores[]` -- NOT by board.yaml.
1343
+ The customer never picks *which backend* at build time; the SDK
1344
+ compiles in every dispatcher the silicon supports, and apps choose
1345
+ per-handle at runtime via alp_inference_open(.backend=...).
1346
+ CPU/TFLM is the universal SW fallback and is on whenever this
1347
+ section emits at all.
1348
+
1349
+ *Whether* the section emits anything is gated on `_slice_wants_inference`
1350
+ (declared `cores.<id>.inference:` tuning OR a `libraries:` pull of
1351
+ `tflite-micro`). Emitting the backend switches unconditionally on
1352
+ every zephyr slice used to put a misleading `=y` line (silently
1353
+ resolving to n -- BACKEND_TFLM's `depends on TENSORFLOW_LITE_MICRO &&
1354
+ CPP` are genuinely-external symbols not every slice's prj.conf sets)
1355
+ in front of apps that never call alp_inference_open() at all
1356
+ (issue #874).
1357
+
1358
+ Two layers of variant detail are emitted alongside the umbrella
1359
+ switches so the SDK driver code + (upstream) kernel libraries
1360
+ compile against the right per-silicon variant, each only alongside
1361
+ the ALP_SDK_* parent it `depends on` in
1362
+ zephyr/kconfigs/iot-audio-inference.kconfig (issue #874 item 3):
1363
+
1364
+ - CONFIG_ALP_SDK_INFERENCE_ETHOS_U_VARIANT_{U55,U85}=y -- derived
1365
+ from the silicon capability counts (ethos_u{55,85}_count, resolved
1366
+ from the SoC JSON npus[]), the single source for which NPUs the
1367
+ part carries. U85 carries Arm's larger MAC array + TensorOptimized
1368
+ kernels; U55 carries the smaller MAC + reference kernels.
1369
+ U55/U85 depend on BACKEND_ETHOS_U_AEN.
1370
+
1371
+ - CONFIG_ALP_SDK_INFERENCE_TFLM_KERNEL_{NEON,HELIUM,REF}=y -- picked
1372
+ from the SoC JSON's `cores[<slice.core_id>].vector_extension`
1373
+ so the CPU-side TFLM kernel set matches the target core's SIMD
1374
+ reality (NEON on A-cluster, Helium MVE on M55, scalar / REF
1375
+ on baseline M33 / M4). Per-core in case a single SoM hosts
1376
+ multiple core classes (E7 = A32 + M55, all three Helium /
1377
+ Neon flavours). All three depend on BACKEND_TFLM.
1378
+ """
1379
+ if not _slice_wants_inference(project, slice_):
1380
+ return []
1381
+
1382
+ lines: list[str] = []
1383
+
1384
+ # resolve_capabilities merges SoC-JSON defaults with SoM overrides
1385
+ # so silicon-determined caps (ethos_u55_count, drp_ai, ...) resolve
1386
+ # even when removed from the SoM YAML (capability unification, slice 3b).
1387
+ capabilities = resolve_capabilities(project.som_preset, project.effective_metadata_root())
1388
+ inference_lines: list[str] = [
1389
+ # BACKEND_TFLM depends on TENSORFLOW_LITE_MICRO && CPP -- genuinely
1390
+ # external (upstream Zephyr / tflite-micro module) symbols, not
1391
+ # ALP_SDK_*. west.yml pulls the tflite-micro module in
1392
+ # unconditionally (no west group gate; see west.yml's
1393
+ # name-allowlist), so both resolve here rather than requiring every
1394
+ # inference app to hand-write them in its own prj.conf (#874; see
1395
+ # also #855 for cross-alp-sdk-version symbol skew). This is
1396
+ # effective on an alp-sdk-topdir west workspace (west.yml's own
1397
+ # manifest); a workspace initialised off a bare upstream
1398
+ # `zephyr` manifest (the repo's own twister CI, a plain
1399
+ # `west init -m .../zephyr`) has no tflite-micro module at all,
1400
+ # so TENSORFLOW_LITE_MICRO degrades gracefully to n (a Kconfig
1401
+ # "assigned y but got n" warning, not fatal -- the inference
1402
+ # dispatcher falls back to ALP_SDK_INFERENCE_SW_FALLBACK there).
1403
+ "CONFIG_CPP=y",
1404
+ # tflite-micro's headers require C++17 (e.g. tensorflow/lite/array.h
1405
+ # uses std::conditional_t / std::decay_t). Zephyr's C++ standard
1406
+ # `choice STD_CPP` defaults to C++11, so a TFLM build compiled at
1407
+ # -std=c++11 fails. Emit C++17 here whenever inference (TFLM) is
1408
+ # wanted so every inference project builds without hand-editing:
1409
+ # real NPU apps previously set CONFIG_STD_CPP17=y in their own
1410
+ # prj.conf; the emit now guarantees it on every platform (native_sim
1411
+ # + real silicon alike), matching what the AEN NPU builds already use.
1412
+ "CONFIG_STD_CPP17=y",
1413
+ "CONFIG_TENSORFLOW_LITE_MICRO=y",
1414
+ "CONFIG_ALP_SDK_INFERENCE_BACKEND_TFLM=y",
1415
+ ]
1416
+
1417
+ # ---- G-2 -- CPU-class TFLM kernel selector --------------------
1418
+ # Look up this slice's core in the SoC spec; pick exactly one of
1419
+ # NEON / HELIUM / REF based on the vector_extension field. Defaults
1420
+ # to REF when the SoC JSON is silent (paper-correct on the scalar
1421
+ # M33s -- V2N m33_sm).
1422
+ #
1423
+ # `c.get("type")` is UNVALIDATED against `soc-spec-v1.schema.json`'s own
1424
+ # `"type": {"type": "string"}` -- the identical gap `presets_cmd.
1425
+ # core_type_lookup` and `topology.core_os_topology` closed for
1426
+ # tan-cli#957, same class, this call site missed by that sweep
1427
+ # (tan-cli#962): a schema-invalid SoC JSON (hand-authored, mid-
1428
+ # `porting-a-new-som`, or corrupted) with a truthy non-string `type`
1429
+ # reaches `.lower()` here and raises `AttributeError`, aborting `tan
1430
+ # build` for the whole slice on any board that reaches this branch
1431
+ # (`_slice_wants_inference` -- examples/ai, examples/audio,
1432
+ # examples/camera-vision). `isinstance`-guard to the same `""`
1433
+ # unresolved sentinel a missing `type` already produces, matching
1434
+ # `core_type_lookup`'s `c["type"] if isinstance(c.get("type"), str)
1435
+ # else ""`.
1436
+ tflm_kernel_kc: str = "CONFIG_ALP_SDK_INFERENCE_TFLM_KERNEL_REF=y"
1437
+ for c in (project.soc_spec.get("cores") or []):
1438
+ if c.get("id") != slice_.core_id:
1439
+ continue
1440
+ raw_vec = c.get("vector_extension")
1441
+ vec = raw_vec.lower() if isinstance(raw_vec, str) else ""
1442
+ raw_type = c.get("type")
1443
+ ctype = raw_type.lower() if isinstance(raw_type, str) else ""
1444
+ if vec == "neon" or ctype.startswith("cortex-a"):
1445
+ tflm_kernel_kc = "CONFIG_ALP_SDK_INFERENCE_TFLM_KERNEL_NEON=y"
1446
+ elif vec == "helium":
1447
+ tflm_kernel_kc = "CONFIG_ALP_SDK_INFERENCE_TFLM_KERNEL_HELIUM=y"
1448
+ else:
1449
+ tflm_kernel_kc = "CONFIG_ALP_SDK_INFERENCE_TFLM_KERNEL_REF=y"
1450
+ break
1451
+ inference_lines.append(tflm_kernel_kc)
1452
+
1453
+ # ---- G-1 -- per-variant Ethos-U selector ---------------------
1454
+ # Which Ethos-U variants this SoM carries -- derived from the
1455
+ # silicon-determined capability counts (ethos_u{55,85}_count, resolved
1456
+ # from the SoC JSON npus[] via resolve_capabilities). This is the single
1457
+ # source: an on-die NPU cannot be depopulated at the SoM level, so the SoM
1458
+ # preset does NOT restate the variant list.
1459
+ ethos_variants: set[str] = set()
1460
+ if (capabilities.get("ethos_u55_count") or 0) > 0:
1461
+ ethos_variants.add("u55")
1462
+ if (capabilities.get("ethos_u85_count") or 0) > 0:
1463
+ ethos_variants.add("u85")
1464
+ ethos_present = bool(ethos_variants)
1465
+ if ethos_present:
1466
+ # Per-silicon Ethos-U backend (Slice 3 registry layout):
1467
+ # Alif Ensemble (AEN) -> _BACKEND_ETHOS_U_AEN
1468
+ # Parent-gating (#874 item 3): the variant switches `depend on` the
1469
+ # parent backend (zephyr/kconfigs/iot-audio-inference.kconfig).
1470
+ inference_lines.append("CONFIG_ALP_SDK_INFERENCE_BACKEND_ETHOS_U_AEN=y")
1471
+ allowed_variants = {"u55", "u85"}
1472
+ for v in sorted(ethos_variants & allowed_variants):
1473
+ inference_lines.append(f"CONFIG_ALP_SDK_INFERENCE_ETHOS_U_VARIANT_{v.upper()}=y")
1474
+ # Real Arm Ethos-U driver config -- the silicon-proven pair (bench:
1475
+ # E1M-AEN801/E8, person_detect U85). BACKEND_ETHOS_U_AEN already
1476
+ # `select ETHOS_U if DT_HAS_ARM_ETHOS_U_ENABLED` (silicon-only); these
1477
+ # assignments complete the driver config. The core driver compiles
1478
+ # exactly ONE accelerator config (ETHOS_U_NPU_CONFIG is a Kconfig
1479
+ # `choice`), so pick the single most-capable NPU this silicon carries.
1480
+ # On native_sim (no arm,ethos-u node) ETHOS_U stays off, so the
1481
+ # ETHOS_U*-dependent lines below no-op with a harmless Kconfig
1482
+ # "assigned y but got n" warning (same pattern as TENSORFLOW_LITE_MICRO
1483
+ # above). CONFIG_ETHOS_U_DCACHE=y links the correct-signature dcache
1484
+ # hooks (NOT CONFIG_ARM_ETHOS_U -- hal_alif's stale callback path);
1485
+ # CONFIG_DCACHE=n is the CPU<->NPU SRAM coherence mechanism; the
1486
+ # ethos_u driver's mutex/semaphore need a kernel heap (k_malloc).
1487
+ # Pick the most-capable variant this silicon carries (U85 > U55),
1488
+ # then read its MAC config from the SoC's npus[] `mac_per_cycle` -- NOT a
1489
+ # hardcode. The derived symbol must be a real ETHOS_U_NPU_CONFIG choice
1490
+ # member (hal_ethos_u), else it would silently no-op -- so validate and
1491
+ # fail loudly on a metadata mismatch.
1492
+ if "u85" in ethos_variants:
1493
+ variant_num = "85"
1494
+ else:
1495
+ variant_num = "55"
1496
+ npu_type = f"ethos-u{variant_num}"
1497
+ npus_of_type = [
1498
+ n for n in (project.soc_spec.get("npus") or [])
1499
+ if str(n.get("type", "")).lower() == npu_type and n.get("mac_per_cycle")
1500
+ ]
1501
+ # Size the accelerator for the NPU instance THIS slice's core actually
1502
+ # drives, not the SoC's most-capable one. A part can carry two U55s of
1503
+ # the same variant but different MACs wired to different cores (E3/E5/E7:
1504
+ # a 256-MAC high-perf U55 on M55-HP + a 128-MAC high-efficiency U55 on
1505
+ # M55-HE -- npus[].paired_core in the SoC JSON). A 256-MAC command
1506
+ # stream errors a 128-MAC NPU at invoke (register-proven on E8), so a
1507
+ # blind max() would mis-size the HE slice. Prefer the core-paired
1508
+ # instance; fall back to the most-capable of the variant when the chosen
1509
+ # variant is not core-paired (the E8 U85, a shared SoC-level NPU --
1510
+ # Alif block name NPU_HG) or the SoC JSON predates paired_core.
1511
+ paired = [n["mac_per_cycle"] for n in npus_of_type
1512
+ if n.get("paired_core") == slice_.core_id]
1513
+ macs = [n["mac_per_cycle"] for n in npus_of_type]
1514
+ if paired:
1515
+ mac = paired[0]
1516
+ elif len({*macs}) > 1:
1517
+ # This variant has >1 distinct MAC array on the SoC but none is
1518
+ # paired_core=<this slice's core> -- a silent max() here would
1519
+ # re-introduce the #909 mis-sizing (a 256-MAC command stream errors
1520
+ # a 128-MAC NPU at invoke). Fail loudly, like the _valid_accel
1521
+ # guard below, so a missing/typo'd paired_core is caught at emit.
1522
+ raise ValueError(
1523
+ f"{silicon}: inference slice core {slice_.core_id!r} drives an "
1524
+ f"{npu_type} but the SoC JSON declares MAC variants {sorted({*macs})} "
1525
+ f"and none sets paired_core={slice_.core_id!r} -- add "
1526
+ f"npus[].paired_core so the build sizes the accelerator for this "
1527
+ f"core instead of guessing (see #909).")
1528
+ else:
1529
+ # Single MAC for this variant (or npus[]-less older spec): unambiguous.
1530
+ mac = macs[0] if macs else 256
1531
+ accel = f"ETHOS_U{variant_num}_{mac}"
1532
+ _valid_accel = {
1533
+ "ETHOS_U55_64", "ETHOS_U55_128", "ETHOS_U55_256",
1534
+ "ETHOS_U85_128", "ETHOS_U85_256", "ETHOS_U85_512",
1535
+ "ETHOS_U85_1024", "ETHOS_U85_2048",
1536
+ }
1537
+ if accel not in _valid_accel:
1538
+ raise ValueError(
1539
+ f"{silicon}: derived Ethos-U accelerator {accel!r} "
1540
+ f"(npus[].mac_per_cycle={mac} for {npu_type}) is not a valid "
1541
+ f"ETHOS_U_NPU_CONFIG choice member -- check the SoC JSON npus[]")
1542
+ inference_lines.append("CONFIG_ETHOS_U_DCACHE=y")
1543
+ inference_lines.append(f"CONFIG_{accel}=y")
1544
+ inference_lines.append("CONFIG_DCACHE=n")
1545
+ inference_lines.append("CONFIG_HEAP_MEM_POOL_SIZE=65536")
1546
+ # DRP-AI3 and DEEPX DX-M1 have NO Zephyr Kconfig -- deliberately.
1547
+ # Both engines are A55/Linux-side only (DRP-AI3 via the MERA/TVM
1548
+ # userspace runtime, DX-M1 via libdxrt over the A55's PCIe); an
1549
+ # M-class Zephyr slice cannot drive either (issues #58/#59), so it
1550
+ # gets TFLM only. Their build wiring lives on the cmake-args /
1551
+ # Yocto emit paths (_slice_cmake_args below).
1552
+ # SoM-declared AUTO accelerator order for the .alpmodel tiebreak
1553
+ # (zephyr/CMakeLists.txt maps it to -DALP_SDK_INFERENCE_AUTO_ORDER, the
1554
+ # same define name the Yocto and baremetal builds use).
1555
+ auto_order = _inference_auto_order(project.som_preset)
1556
+ if auto_order:
1557
+ inference_lines.append(
1558
+ f'CONFIG_ALP_SDK_INFERENCE_AUTO_ORDER="{",".join(auto_order)}"')
1559
+ lines.append("# Inference dispatchers (from SoM capabilities -- "
1560
+ "customer does not pick)")
1561
+ lines.extend(inference_lines)
1562
+ lines.append("")
1563
+ return lines
1564
+
1565
+
1566
+ def _emit_cross_core_shmem_cache(
1567
+ project: "BoardProject",
1568
+ slice_: Slice,
1569
+ existing_lines: list[str],
1570
+ ) -> list[str]:
1571
+ """CONFIG_DCACHE=n for a slice sharing a non-cacheable `raw_shmem` or
1572
+ `rpmsg` IPC carve-out with a peer core -- the hardware fact this symbol
1573
+ encodes (each core has its own D-cache; shared SRAM is cross-core
1574
+ incoherent unless the cache is off, or the app does explicit cache
1575
+ maintenance) is NOT specific to Ethos-U inference. Before this, the
1576
+ only place that emitted it was the Ethos-U branch of `_emit_inference`,
1577
+ so a non-inference cross-core project (e.g. `examples/multicore/
1578
+ mproc-mailbox`, an M55-HP<->M55-HE mailbox+shmem roundtrip with no
1579
+ `inference:` block) got no help from the generator and had to
1580
+ hand-write the line in `prj.conf` -- see alp-sdk PR #1080, which found
1581
+ three examples silently reporting `RESULT SKIP` while the debug AP
1582
+ watched their beacons advance: the hardware worked, the stale D-cache
1583
+ just hid it from the software.
1584
+
1585
+ Board-agnostic and keyed off the existing `ipc:` schema field (board-
1586
+ yaml §3.9), not a SoC/vendor check, so it fires for any silicon that
1587
+ declares a matching carve-out:
1588
+
1589
+ - `kind: rpmsg` is covered too (alp-sdk #1088's conservative fix),
1590
+ for the identical reason `raw_shmem` is: `<alp/rpc.h>` has no
1591
+ cache-maintenance layer (no `sys_cache_*` call anywhere under
1592
+ `src/` or `include/`, and no per-channel cache field). A
1593
+ `cacheable: true` rpmsg channel would therefore select a path with
1594
+ no maintenance behind it, so
1595
+ `loader.py` rejects `cacheable: true` on a `rpmsg` entry outright
1596
+ rather than silently honouring it -- any entry that reaches this
1597
+ function is already non-cacheable, and the D-cache goes off
1598
+ unconditionally for its endpoints. The real fix --
1599
+ `sys_cache_data_flush_range` / `sys_cache_data_invd_range` in
1600
+ `<alp/rpc.h>` -- remains open; see alp-sdk #1088.
1601
+ - `kind: mailbox_only` is excluded -- it carries no shared memory
1602
+ (doorbell + signal only), so there is nothing to keep coherent.
1603
+ - `kind: raw_shmem` closes the original #1080 case: `<alp/mproc.h>`'s
1604
+ raw shmem+mailbox primitives have no cache-maintenance layer
1605
+ either, so the SDK's zero-effort-safe default is to turn the
1606
+ D-cache off for every endpoint -- unless the entry opts out with
1607
+ an explicit `cacheable: true`, which is the app declaring it will
1608
+ do its own cache ops instead (board.schema.json
1609
+ `ipc_entry.cacheable`). This opt-out is `raw_shmem`-only; `rpmsg`
1610
+ has no opt-out (see above).
1611
+
1612
+ Must run after `_emit_inference` in `_slice_alp_conf` (the dedup below
1613
+ depends on that order, it does not detect the reverse case): skips
1614
+ emission if a line asserting the same symbol is already present, i.e.
1615
+ the Ethos-U inference branch already asserted it for this slice --
1616
+ kept as its own check rather than threading a bool out of
1617
+ `_emit_inference`, since the two triggers (Ethos-U NPU coherence vs.
1618
+ mproc shmem coherence) are unrelated hardware facts that happen to
1619
+ share one Kconfig symbol.
1620
+ """
1621
+ if any("CONFIG_DCACHE=n" in line for line in existing_lines):
1622
+ return []
1623
+ needs_dcache_off = any(
1624
+ entry.kind in ("raw_shmem", "rpmsg")
1625
+ and slice_.core_id in entry.endpoints
1626
+ and not entry.cacheable
1627
+ for entry in project.ipc
1628
+ )
1629
+ if not needs_dcache_off:
1630
+ return []
1631
+ return [
1632
+ "# board.yaml ipc[].kind: raw_shmem or rpmsg names this core as",
1633
+ "# an endpoint with no explicit cacheable: true opt-out.",
1634
+ "# <alp/mproc.h> (raw_shmem) and <alp/rpc.h> (rpmsg) both have no",
1635
+ "# cache-maintenance layer, so the D-cache must stay off (not",
1636
+ "# just the carve-out's own region) for both sides to see each",
1637
+ "# other's writes without explicit clean/invalidate (PR #1080,",
1638
+ "# #1088).",
1639
+ "CONFIG_DCACHE=n",
1640
+ "",
1641
+ ]
1642
+
1643
+
1644
+ def _emit_memory(slice_: Slice) -> list[str]:
1645
+ """Per-slice memory tuning (board.yaml `cores.<id>.memory:`)."""
1646
+ lines: list[str] = []
1647
+ mem = slice_.memory or {}
1648
+ mem_lines: list[str] = []
1649
+ if mem.get("stack_kib"):
1650
+ mem_lines.append(f"CONFIG_MAIN_STACK_SIZE={int(mem['stack_kib']) * 1024}")
1651
+ if mem.get("isr_stack_kib"):
1652
+ mem_lines.append(f"CONFIG_ISR_STACK_SIZE={int(mem['isr_stack_kib']) * 1024}")
1653
+ if mem.get("heap_kib") is not None:
1654
+ mem_lines.append(f"CONFIG_HEAP_MEM_POOL_SIZE={int(mem['heap_kib']) * 1024}")
1655
+ if mem_lines:
1656
+ lines.append("# Per-slice memory tuning (board.yaml cores.<id>.memory:)")
1657
+ lines.extend(mem_lines)
1658
+ lines.append("")
1659
+ return lines
1660
+
1661
+
1662
+ def _emit_power(slice_: Slice) -> list[str]:
1663
+ """Per-slice power-management profile (board.yaml `cores.<id>.power:`)."""
1664
+ lines: list[str] = []
1665
+ pwr = slice_.power or {}
1666
+ pwr_lines: list[str] = []
1667
+ sleep_mode = (pwr.get("sleep_mode") or "disabled").lower()
1668
+ if sleep_mode != "disabled":
1669
+ pwr_lines.append("CONFIG_PM=y")
1670
+ pwr_lines.append("CONFIG_PM_DEVICE=y")
1671
+ # PM_STATE_* selection is silicon-family-specific; lowest-power
1672
+ # state the hierarchy keeps is mirrored in a single hint flag.
1673
+ pwr_lines.append(f"# Sleep target: {sleep_mode}")
1674
+ for wake in (pwr.get("wakeup_sources") or []):
1675
+ # Wake-source declarations land as hint comments only. Zephyr
1676
+ # marks a device as wakeup-capable via the DT `wakeup-source;`
1677
+ # property + a runtime pm_device_wakeup_enable() call; there is
1678
+ # no top-level CONFIG_PM_DEVICE_WAKE_<SUBSYS> Kconfig symbol, so
1679
+ # emitting one trips the build with `undefined symbol` warnings
1680
+ # (-> kconfig errors -> twister CMake build failure). The real
1681
+ # DT-overlay + runtime-enable plumbing lands per silicon in v0.7;
1682
+ # until then the wake-source list is documented in the generated
1683
+ # alp.conf for the customer to wire by hand.
1684
+ if isinstance(wake, str) and not wake.startswith("E1M_"):
1685
+ pwr_lines.append(
1686
+ f"# wakeup source: {wake} "
1687
+ "(DT wakeup-source; + pm_device_wakeup_enable() pending v0.7)")
1688
+ elif isinstance(wake, str):
1689
+ pwr_lines.append(f"# wakeup source: {wake} (per-silicon Kconfig pending)")
1690
+ if pwr_lines:
1691
+ lines.append("# Per-slice power-management profile (board.yaml cores.<id>.power:)")
1692
+ lines.extend(pwr_lines)
1693
+ lines.append("")
1694
+ return lines
1695
+
1696
+
1697
+ def _emit_storage(project: "BoardProject") -> list[str]:
1698
+ """Storage partitions (board.yaml `storage:` block). Emits per-fs
1699
+ Kconfig for every resolved (non-blocked) partition. Blocked
1700
+ partitions produce a hint comment so the slice build still
1701
+ compiles but the runtime mount fails loudly via the DTS overlay.
1702
+
1703
+ Per-slice ownership of partitions is not modelled today: every
1704
+ Zephyr/baremetal slice gets the union. Kconfig dedupes when
1705
+ multiple per-core fragments overlay onto the same base. v0.7
1706
+ may add `core:` to storage entries for per-slice scoping.
1707
+ """
1708
+ lines: list[str] = []
1709
+ partitions = resolve_storage_partitions(project)
1710
+ ok_partitions = [p for p in partitions if p.status == "ok"]
1711
+ fs_seen: set[str] = set()
1712
+ for p in ok_partitions:
1713
+ fs_seen.add(p.fs)
1714
+ fs_kconfig: list[str] = []
1715
+ if "littlefs" in fs_seen:
1716
+ fs_kconfig.append("CONFIG_FILE_SYSTEM=y")
1717
+ fs_kconfig.append("CONFIG_FILE_SYSTEM_LITTLEFS=y")
1718
+ if "fat" in fs_seen:
1719
+ fs_kconfig.append("CONFIG_FILE_SYSTEM=y")
1720
+ fs_kconfig.append("CONFIG_FAT_FILESYSTEM_ELM=y")
1721
+ if "ext4" in fs_seen:
1722
+ fs_kconfig.append("CONFIG_FILE_SYSTEM=y")
1723
+ fs_kconfig.append("CONFIG_FILE_SYSTEM_EXT2=y")
1724
+ if fs_kconfig or ok_partitions:
1725
+ lines.append("# Storage partitions (board.yaml `storage:`)")
1726
+ # FILE_SYSTEM=y appears twice in the list when multiple fs
1727
+ # types are enabled; dedupe before emit.
1728
+ for kc in sorted(set(fs_kconfig)):
1729
+ lines.append(kc)
1730
+ # Per-partition LITTLEFS partition labels surface as a hint
1731
+ # comment only. Modern Zephyr does NOT define a per-partition
1732
+ # CONFIG_FS_LITTLEFS_PARTITION_<NAME> Kconfig -- the partition
1733
+ # gets matched via the DT `fixed-partitions` node + the
1734
+ # chosen `zephyr,storage-partition` / FIXED_PARTITION_ID()
1735
+ # macro at runtime. Emitting a Kconfig stem trips Zephyr's
1736
+ # "assignment to undefined symbol" warning + aborts the build.
1737
+ for p in ok_partitions:
1738
+ if p.fs == "littlefs":
1739
+ lines.append(
1740
+ f"# partition[{p.name}] -> mount at runtime via "
1741
+ f"FIXED_PARTITION_ID({p.name}_partition)")
1742
+ blocked = [p for p in partitions if p.status == "blocked"]
1743
+ for p in blocked:
1744
+ lines.append(f"# BLOCKED storage[{p.name}]: "
1745
+ f"{p.reason or 'unknown reason'}")
1746
+ lines.append("")
1747
+ return lines
1748
+
1749
+
1750
+ def _split_server_url(url: str) -> tuple[str, Optional[int], Optional[str]]:
1751
+ """Decompose an `ota.server.url` into (host, port, scheme).
1752
+
1753
+ board.yaml's `ota.server.url` is a full URI (`board.schema.json` types it
1754
+ `format: uri`). Zephyr's Hawkbit client wants the three parts SEPARATELY
1755
+ -- a bare host for `zsock_getaddrinfo()` / the TLS SNI name / the HTTP
1756
+ `Host:` header, an int port, and a TLS bool. Handing it the whole URI is
1757
+ a DNS lookup that can never resolve (alplabai/tan-cli#558).
1758
+
1759
+ A value carrying no `://` is taken as an already-bare host and returned
1760
+ verbatim. A `${VAR}` placeholder passes through here too, but
1761
+ `_slice_alp_conf` then refuses it: nothing substitutes a placeholder in a
1762
+ Kconfig fragment (issue #2696, `_refuse_live_kconfig_placeholders`). Host
1763
+ case is preserved (DNS is case-insensitive), so this parses the authority
1764
+ by hand rather than through `urlsplit`, whose `.hostname` lowercases.
1765
+
1766
+ Raises OrchestratorError on anything that cannot be expressed as those
1767
+ three parts rather than emitting a value the client cannot use.
1768
+ """
1769
+ raw = url.strip()
1770
+ if "://" not in raw:
1771
+ return raw, None, None
1772
+ scheme, _, rest = raw.partition("://")
1773
+ scheme = scheme.lower()
1774
+ if scheme not in ("http", "https"):
1775
+ raise OrchestratorError(
1776
+ f"ota.server.url `{url}`: scheme `{scheme}` is not supported by "
1777
+ f"the Zephyr Hawkbit DDI client -- use http:// or https://")
1778
+ authority, sep, path = rest.partition("/")
1779
+ if sep and path not in ("", "/"):
1780
+ raise OrchestratorError(
1781
+ f"ota.server.url `{url}`: the Hawkbit DDI client builds its own "
1782
+ f"request paths (/{{tenant}}/controller/v1/...) and has no knob "
1783
+ f"for a base path, so `/{path}` cannot be honoured -- give the "
1784
+ f"scheme, host and port only")
1785
+ if "@" in authority:
1786
+ raise OrchestratorError(
1787
+ f"ota.server.url `{url}`: userinfo in the URL is not supported -- "
1788
+ f"Hawkbit authenticates with a DDI security token "
1789
+ f"(`ota.server.tenant`), not URL credentials")
1790
+ port: Optional[int] = None
1791
+ host = authority
1792
+ if authority.startswith("["): # bracketed IPv6 literal
1793
+ close = authority.find("]")
1794
+ if close < 0:
1795
+ raise OrchestratorError(
1796
+ f"ota.server.url `{url}`: unterminated IPv6 literal in the host")
1797
+ host = authority[:close + 1]
1798
+ tail = authority[close + 1:]
1799
+ if tail.startswith(":"):
1800
+ port_text = tail[1:]
1801
+ elif tail:
1802
+ raise OrchestratorError(
1803
+ f"ota.server.url `{url}`: unexpected `{tail}` after the "
1804
+ f"IPv6 host literal")
1805
+ else:
1806
+ port_text = ""
1807
+ else:
1808
+ host, _, port_text = authority.partition(":")
1809
+ if port_text:
1810
+ if not port_text.isdigit() or not 1 <= int(port_text) <= 65535:
1811
+ raise OrchestratorError(
1812
+ f"ota.server.url `{url}`: `{port_text}` is not a valid TCP port")
1813
+ port = int(port_text)
1814
+ if not host:
1815
+ raise OrchestratorError(
1816
+ f"ota.server.url `{url}`: no host to resolve")
1817
+ if port is None:
1818
+ port = 443 if scheme == "https" else 80
1819
+ return host, port, scheme
1820
+
1821
+
1822
+ def _hawkbit_server_lines(url: str) -> list[str]:
1823
+ """`CONFIG_HAWKBIT_{SERVER,PORT,USE_TLS}` for one `ota.server.url`.
1824
+
1825
+ Grounded in the pinned Zephyr v4.4.1 tree:
1826
+ * `HAWKBIT_SERVER` -- "User address for the hawkbit server"
1827
+ (subsys/mgmt/hawkbit/Kconfig:68): bare host, fed to
1828
+ `zsock_getaddrinfo()`, the TLS hostname and the `Host:` header.
1829
+ * `HAWKBIT_PORT` -- int, `default 8080` (Kconfig:75).
1830
+ * `HAWKBIT_USE_TLS` -- `depends on NET_SOCKETS_SOCKOPT_TLS`
1831
+ (Kconfig:168), so the socket option has to be enabled alongside it
1832
+ or kconfiglib warns "assigned y but got n" and the Zephyr configure
1833
+ aborts.
1834
+ """
1835
+ host, port, scheme = _split_server_url(url)
1836
+ lines = [f'CONFIG_HAWKBIT_SERVER="{host}"']
1837
+ if port is not None:
1838
+ lines.append(f"CONFIG_HAWKBIT_PORT={port}")
1839
+ if scheme == "https":
1840
+ lines.append("CONFIG_NET_SOCKETS_SOCKOPT_TLS=y")
1841
+ lines.append("CONFIG_HAWKBIT_USE_TLS=y")
1842
+ elif scheme is None:
1843
+ lines.append("# ota.server.url carries no scheme -- taken as a bare "
1844
+ "host; CONFIG_HAWKBIT_PORT / CONFIG_HAWKBIT_USE_TLS keep "
1845
+ "their Zephyr defaults (8080, plaintext).")
1846
+ return lines
1847
+
1848
+
1849
+ # Zephyr's CONFIG_HAWKBIT_POLL_INTERVAL is declared in MINUTES with
1850
+ # `range 1 43200` (subsys/mgmt/hawkbit/Kconfig:29-33) and is multiplied by
1851
+ # SEC_PER_MIN at runtime (subsys/mgmt/hawkbit/hawkbit.c:57).
1852
+ _HAWKBIT_POLL_MIN_MINUTES = 1
1853
+ _HAWKBIT_POLL_MAX_MINUTES = 43200
1854
+
1855
+
1856
+ def _hawkbit_poll_line(poll_s: int) -> str:
1857
+ """`CONFIG_HAWKBIT_POLL_INTERVAL` for one `ota.poll_interval_s`.
1858
+
1859
+ board.yaml states the interval in SECONDS; the Kconfig symbol is in
1860
+ MINUTES. Writing the seconds value through verbatim dilated every fleet's
1861
+ poll period 60x -- the schema's own 1800 s default became 1800 minutes =
1862
+ 30 hours (alplabai/tan-cli#557). Convert, and refuse a value that cannot
1863
+ be expressed in whole minutes or that falls outside Zephyr's declared
1864
+ range, rather than emitting an out-of-range value the Zephyr configure
1865
+ will abort on (or, with the abort relaxed, silently replace with the
1866
+ default of 5).
1867
+ """
1868
+ minutes, remainder = divmod(poll_s, 60)
1869
+ if remainder:
1870
+ raise OrchestratorError(
1871
+ f"ota.poll_interval_s: {poll_s} s cannot be expressed in "
1872
+ f"CONFIG_HAWKBIT_POLL_INTERVAL, which Zephyr declares in whole "
1873
+ f"MINUTES -- use {minutes * 60} or {(minutes + 1) * 60}")
1874
+ if not _HAWKBIT_POLL_MIN_MINUTES <= minutes <= _HAWKBIT_POLL_MAX_MINUTES:
1875
+ raise OrchestratorError(
1876
+ f"ota.poll_interval_s: {poll_s} s is {minutes} min, outside "
1877
+ f"CONFIG_HAWKBIT_POLL_INTERVAL's `range "
1878
+ f"{_HAWKBIT_POLL_MIN_MINUTES} {_HAWKBIT_POLL_MAX_MINUTES}` "
1879
+ f"(minutes) -- use {_HAWKBIT_POLL_MIN_MINUTES * 60} .. "
1880
+ f"{_HAWKBIT_POLL_MAX_MINUTES * 60} s")
1881
+ return f"CONFIG_HAWKBIT_POLL_INTERVAL={minutes}"
1882
+
1883
+
1884
+ def _emit_ota(project: "BoardProject") -> list[str]:
1885
+ """OTA Zephyr client (board.yaml `ota:` block, provider-driven dispatch).
1886
+
1887
+ Resolved design Q on the Zephyr-side OTA client (ADR 0009 follow-up):
1888
+ provider: mender -> Mender-MCU-client (out-of-tree, BSD-3)
1889
+ provider: hawkbit -> Zephyr Hawkbit DDI client (upstream)
1890
+ provider: mcumgr -> Zephyr MCUmgr (upstream; transport is the app's call)
1891
+
1892
+ Yocto slices keep the existing Mender server-mode dispatch in
1893
+ `_slice_local_conf`. This handles the Zephyr side: per-slice
1894
+ Kconfig that compiles the matching client in. Settings (server URL,
1895
+ poll interval, tenant token) thread through Kconfig string values
1896
+ when declared in `ota:`. A `${VAR}` placeholder may only land on a
1897
+ commented line here (the Mender hints): Zephyr does not expand one in a
1898
+ Kconfig fragment, so `_slice_alp_conf` refuses it on a live line
1899
+ (issue #2696).
1900
+ """
1901
+ lines: list[str] = []
1902
+ ota = project.ota or {}
1903
+ provider = (ota.get("provider") or "").lower() if isinstance(ota, dict) else ""
1904
+ if provider and provider in ("mender", "hawkbit", "mcumgr"):
1905
+ lines.append(f"# OTA Zephyr client (board.yaml `ota.provider: {provider}`)")
1906
+ srv = (ota.get("server") or {}) if isinstance(ota.get("server"), dict) else {}
1907
+ poll = ota.get("poll_interval_s")
1908
+ if provider == "mender":
1909
+ # Mender-MCU-client. The west.yml `mender` group is NOT
1910
+ # active by default (the upstream mender-mcu-client
1911
+ # repository is pinned alongside the v0.7 OTA wiring), so
1912
+ # CONFIG_MENDER_* would resolve to undefined-symbol warnings
1913
+ # under twister and abort the build. Emit the settings as
1914
+ # hint comments so the configuration is visible to the
1915
+ # customer (and to anyone reviewing the generated alp.conf)
1916
+ # while keeping the build clean. When the mender west
1917
+ # group flips active the comments here go back to live
1918
+ # CONFIG_* lines in a single commit.
1919
+ lines.append("# Mender-MCU-client wiring is pending the v0.7 OTA module")
1920
+ lines.append("# (mender-mcu-client west group activation).")
1921
+ lines.append("# CONFIG_MENDER_MCU_CLIENT=y")
1922
+ if srv.get("url"):
1923
+ lines.append(f'# CONFIG_MENDER_SERVER_URL="{srv["url"]}"')
1924
+ if srv.get("tenant"):
1925
+ lines.append(f'# CONFIG_MENDER_TENANT_TOKEN="{srv["tenant"]}"')
1926
+ if ota.get("artifact_name"):
1927
+ lines.append(f'# CONFIG_MENDER_ARTIFACT_NAME="{ota["artifact_name"]}"')
1928
+ if isinstance(poll, int) and poll > 0:
1929
+ lines.append(f"# CONFIG_MENDER_UPDATE_POLL_INTERVAL={poll}")
1930
+ elif provider == "hawkbit":
1931
+ # Zephyr upstream's Hawkbit DDI client.
1932
+ lines.append("CONFIG_HAWKBIT=y")
1933
+ lines.append("CONFIG_HAWKBIT_SHELL=y")
1934
+ if srv.get("url"):
1935
+ lines.extend(_hawkbit_server_lines(str(srv["url"])))
1936
+ if isinstance(poll, int) and poll > 0:
1937
+ lines.append(_hawkbit_poll_line(poll))
1938
+ elif provider == "mcumgr":
1939
+ # MCUmgr is upstream; the transport (UART/BLE/UDP) stays the
1940
+ # app's call -- we only enable the base SMP server.
1941
+ lines.append("CONFIG_MCUMGR=y")
1942
+ lines.append("CONFIG_MCUMGR_GRP_IMG=y")
1943
+ lines.append("CONFIG_MCUMGR_GRP_OS=y")
1944
+ lines.append("# MCUmgr transport (UART/BLE/UDP) is the app's call;")
1945
+ lines.append("# enable the matching CONFIG_MCUMGR_TRANSPORT_* in your prj.conf.")
1946
+ lines.append("")
1947
+ return lines
1948
+
1949
+
1950
+ # board.yaml `diagnostics.modules:` level -> the Zephyr log-choice suffix.
1951
+ # Zephyr's per-module choice is <MOD>_LOG_LEVEL_{OFF,ERR,WRN,INF,DBG,DEFAULT}
1952
+ # (subsys/logging/Kconfig.template.log_config); the int <MOD>_LOG_LEVEL is
1953
+ # PROMPTLESS and derived from that choice, so it cannot be assigned from a
1954
+ # fragment at all. Zephyr has no TRACE level -- `trace` maps to DBG, the same
1955
+ # value 4 the old int emit used.
1956
+ _LOG_LEVEL_CHOICE = {
1957
+ "off": "OFF",
1958
+ "error": "ERR",
1959
+ "warn": "WRN",
1960
+ "info": "INF",
1961
+ "debug": "DBG",
1962
+ "trace": "DBG",
1963
+ }
1964
+
1965
+ # board.yaml `diagnostics.modules:` key -> (Zephyr log-module name, the
1966
+ # Kconfig symbol(s) whose `=y` proves the choice symbol is DECLARED -- one
1967
+ # name, or a tuple when the declaration is nested in more than one `if` --,
1968
+ # the symbol the module's log choice `depends on`).
1969
+ #
1970
+ # All three are load-bearing. A module's `<MOD>_LOG_LEVEL_<LEVEL>` choice
1971
+ # symbols exist only inside the `if` block(s) enclosing the module's
1972
+ # `source "...Kconfig.template.log_config"`, and the choice itself
1973
+ # `depends on` a logging gate -- plain `LOG` for the subsys/logging template,
1974
+ # `NET_LOG` for the networking one
1975
+ # (subsys/net/Kconfig.template.log_config.net: `depends on $(module-dep)`).
1976
+ # Both failure modes are real, and they are NOT the same failure:
1977
+ #
1978
+ # * assigning an UNDEFINED symbol is fatal -- alp.conf is loaded as a
1979
+ # handwritten fragment with `warn_assign_undef=True` and
1980
+ # `zephyr/scripts/kconfig/kconfig.py` turns that warning into
1981
+ # `err("Aborting due to Kconfig warnings")`;
1982
+ # * assigning a DEFINED-but-undeclared-here choice symbol is SILENT -- the
1983
+ # configure still exits 0, the override is discarded, and all you get is
1984
+ # `warning: The choice symbol <SYM> ... was selected (set =y), but no
1985
+ # symbol ended up as the choice selection`. Measured on real Zephyr
1986
+ # v4.4.1 (`cmake -DBOARD=native_sim samples/hello_world` + a fragment
1987
+ # carrying CONFIG_LOG=y / CONFIG_MBEDTLS=y / CONFIG_MBEDTLS_LOG_LEVEL_DBG=y:
1988
+ # EXIT=0, and the generated .config has no CONFIG_MBEDTLS_LOG_LEVEL_DBG
1989
+ # line at all). That silent branch is exactly what tan-cli#559 is about,
1990
+ # so the guard columns below have to be EXACT, not approximately right.
1991
+ #
1992
+ # So a line goes live only when EVERY guard symbol AND the logging gate are
1993
+ # already `=y` in this very fragment.
1994
+ #
1995
+ # Every entry below is transcribed from the pinned Zephyr v4.4.1 tree -- the
1996
+ # module name appears there as a literal `module = <NAME>` feeding a
1997
+ # log_config template, and each guard symbol is a real `config`/`menuconfig`
1998
+ # this emitter can put in the fragment. The guard column is the enclosing
1999
+ # `if` chain resolved across the whole `source` chain from Kconfig.zephyr
2000
+ # down, minus the parts a guard already implies (e.g. `if NETWORKING` around
2001
+ # every net_* row is implied by `NET_LOG`, which is declared inside it; the
2002
+ # hidden `BT_LOG` is `default y if LOG && BT`; `FILE_SYSTEM_LIB_LINK` is
2003
+ # `select`ed by `FILE_SYSTEM`).
2004
+ #
2005
+ # Two residual `if`s cannot be expressed here because they are NEGATIVE and
2006
+ # the guard set only knows `=y`: `if !NET_RAW_MODE` (hidden, default n, only
2007
+ # `select`ed by the IEEE-802.15.4 raw drivers) around net_core / net_tcp /
2008
+ # net_ipv4, and the `BT_STACK_SELECTION` choice defaulting to `BT_HCI` around
2009
+ # bluetooth. A board that selects `NET_RAW_MODE` or `BT_CUSTOM` would get the
2010
+ # silent-discard branch above; the SDK enables neither.
2011
+ #
2012
+ # A key that is not in this table (a typo, or a real Zephyr module the SDK
2013
+ # never enables) degrades to a comment; that is the safe direction -- a
2014
+ # missing entry costs a log-level override, a wrong entry costs the override
2015
+ # silently and leaves a Kconfig warning in the build.
2016
+ _LOG_MODULES: dict[str, tuple[str, str | tuple[str, ...], str]] = {
2017
+ # drivers/ -- subsys/logging/Kconfig.template.log_config (`depends on LOG`)
2018
+ "adc": ("ADC", "ADC", "LOG"), # drivers/adc/Kconfig:94
2019
+ "can": ("CAN", "CAN", "LOG"), # drivers/can/Kconfig:16
2020
+ "counter": ("COUNTER", "COUNTER", "LOG"), # drivers/counter/Kconfig:47
2021
+ "dac": ("DAC", "DAC", "LOG"), # drivers/dac/Kconfig:16
2022
+ "display": ("DISPLAY", "DISPLAY", "LOG"), # drivers/display/Kconfig:19
2023
+ "ethernet": ("ETHERNET", "ETH_DRIVER", "LOG"), # drivers/ethernet/Kconfig:23 (enable :6)
2024
+ "flash": ("FLASH", "FLASH", "LOG"), # drivers/flash/Kconfig:226
2025
+ "gpio": ("GPIO", "GPIO", "LOG"), # drivers/gpio/Kconfig:13
2026
+ "i2c": ("I2C", "I2C", "LOG"), # drivers/i2c/Kconfig:188
2027
+ "i2s": ("I2S", "I2S", "LOG"), # drivers/i2s/Kconfig:22
2028
+ "pwm": ("PWM", "PWM", "LOG"), # drivers/pwm/Kconfig:13
2029
+ "rtc": ("RTC", "RTC", "LOG"), # drivers/rtc/Kconfig:11
2030
+ "sensor": ("SENSOR", "SENSOR", "LOG"), # drivers/sensor/Kconfig:14
2031
+ "spi": ("SPI", "SPI", "LOG"), # drivers/spi/Kconfig:102
2032
+ "uart_console": ("UART_CONSOLE", "UART_CONSOLE", "LOG"), # drivers/console/Kconfig:286
2033
+ "watchdog": ("WDT", "WATCHDOG", "LOG"), # drivers/watchdog/Kconfig:43
2034
+ "wifi": ("WIFI", "WIFI", "LOG"), # drivers/wifi/Kconfig:11-12
2035
+ # subsys/
2036
+ "bluetooth": ("BT", "BT", "LOG"), # subsys/bluetooth/Kconfig.logging:17
2037
+ "fs": ("FS", "FILE_SYSTEM", "LOG"), # subsys/fs/Kconfig:144
2038
+ "modbus": ("MODBUS", "MODBUS", "LOG"), # subsys/modbus/Kconfig:94
2039
+ "pm": ("PM", "PM", "LOG"), # subsys/pm/Kconfig:55
2040
+ "pm_device": ("PM_DEVICE", "PM_DEVICE", "LOG"), # subsys/pm/Kconfig:118
2041
+ "tls_credentials": ("TLS_CREDENTIALS", "TLS_CREDENTIALS", "LOG"), # subsys/net/lib/tls_credentials/Kconfig:11
2042
+ "usb_device": ("USB_DEVICE", "USB_DEVICE_STACK", "LOG"), # subsys/usb/device/Kconfig:16
2043
+ # subsys/net -- Kconfig.template.log_config.net (`module-dep = NET_LOG`)
2044
+ "coap": ("COAP", "COAP", "NET_LOG"), # subsys/net/lib/coap/Kconfig:286-287
2045
+ "net_core": ("NET_CORE", "NETWORKING", "NET_LOG"), # subsys/net/ip/Kconfig.debug:48-49
2046
+ # NET_NATIVE is load-bearing: the log_config sits inside `if
2047
+ # NET_NATIVE_IPV4` (Kconfig.ipv4:44), a HIDDEN symbol -- `depends on
2048
+ # NET_NATIVE`, `default y if NET_IPV4` (subsys/net/ip/Kconfig:60-63). So
2049
+ # NET_IPV4=y alone does NOT declare the choice on an offload-only
2050
+ # (NET_NATIVE=n) target; NET_IPV4=y + NET_NATIVE=y does, exactly.
2051
+ "net_ipv4": ("NET_IPV4", ("NET_IPV4", "NET_NATIVE"), "NET_LOG"), # subsys/net/ip/Kconfig.ipv4:191-192
2052
+ "net_l2_ethernet": ("NET_L2_ETHERNET", "NET_L2_ETHERNET", "NET_LOG"), # subsys/net/l2/ethernet/Kconfig:13-14
2053
+ "net_sockets": ("NET_SOCKETS", "NET_SOCKETS", "NET_LOG"), # subsys/net/lib/sockets/Kconfig:443-444
2054
+ "net_tcp": ("NET_TCP", "NET_TCP", "NET_LOG"), # subsys/net/ip/Kconfig.tcp:16-17
2055
+ # modules/
2056
+ # MBEDTLS_DEBUG is the real guard, not MBEDTLS: `module = MBEDTLS` sits
2057
+ # inside `if MBEDTLS_DEBUG` (modules/mbedtls/Kconfig:89), itself nested in
2058
+ # `if MBEDTLS` (:31). MBEDTLS_DEBUG is a prompted bool defaulting n that
2059
+ # this emitter never writes, while CONFIG_MBEDTLS=y IS written (from
2060
+ # metadata/libraries/mbedtls.yaml) by iot-fleet-ota, production-deployment,
2061
+ # rpmsg-v2n, rpmsg-aen and heterogeneous-offload -- so guarding on MBEDTLS
2062
+ # alone made the ONE in-table module the SDK routinely enables emit a line
2063
+ # whose choice symbol is not declared.
2064
+ "mbedtls": ("MBEDTLS", ("MBEDTLS", "MBEDTLS_DEBUG"), "LOG"), # modules/mbedtls/Kconfig:89-93
2065
+ }
2066
+
2067
+
2068
+ def _enabled_symbols(lines: list[str]) -> set[str]:
2069
+ """The Kconfig symbols this fragment has already switched on.
2070
+
2071
+ Reads back the `CONFIG_<SYM>=y` lines built so far (annotations like
2072
+ `CONFIG_X=y # lib / class` included), which is the only authority on what
2073
+ this particular slice compiles in.
2074
+ """
2075
+ out: set[str] = set()
2076
+ for line in lines:
2077
+ text = line.strip()
2078
+ if not text.startswith("CONFIG_") or "=" not in text:
2079
+ continue
2080
+ symbol, _, value = text.partition("=")
2081
+ if value.split("#", 1)[0].strip() == "y":
2082
+ out.add(symbol[len("CONFIG_"):])
2083
+ return out
2084
+
2085
+
2086
+ def _emit_diagnostics(project: "BoardProject",
2087
+ slice_: Slice,
2088
+ emitted: list[str]) -> list[str]:
2089
+ """Per-module log-level overrides (board.yaml `diagnostics.modules:`).
2090
+
2091
+ `board.schema.json` documents this knob as emitting
2092
+ `CONFIG_<MODULE>_LOG_LEVEL_<LEVEL>=y` -- the choice-symbol form. That is
2093
+ what is emitted here. The old `CONFIG_<MODULE>_LOG_LEVEL=<n>` int form
2094
+ could not build for ANY key (alplabai/tan-cli#559): the int symbol is
2095
+ promptless and derived, so Zephyr rejects assigning it even for a real
2096
+ module (`I2C_LOG_LEVEL ... is not directly user-configurable (has no
2097
+ prompt)`), and an unknown key was an assignment to an undefined symbol.
2098
+
2099
+ A live line is emitted only when the module is in `_LOG_MODULES` AND
2100
+ EVERY one of its guard symbols AND its logging gate (`LOG`, or `NET_LOG`
2101
+ for the networking modules) are already `=y` in `emitted` -- the
2102
+ conditions under which the choice symbol is declared at all. A module
2103
+ can carry more than one guard when its log_config template is nested in
2104
+ more than one `if`: `mbedtls` needs MBEDTLS **and** MBEDTLS_DEBUG,
2105
+ `net_ipv4` needs NET_IPV4 **and** NET_NATIVE. Getting that wrong is not
2106
+ caught by the build: the configure still exits 0 and merely warns that
2107
+ the choice symbol "was selected (set =y), but no symbol ended up as the
2108
+ choice selection", so the override vanishes silently -- the same
2109
+ silent-failure class tan-cli#559 is about.
2110
+
2111
+ Everything else (an ALP_* SDK module with no LOG_MODULE_REGISTER yet, a
2112
+ module that lives on another core, a typo) is downgraded to a comment
2113
+ naming the reason, so the fragment always configures.
2114
+ """
2115
+ lines: list[str] = []
2116
+ modules = (project.diagnostics or {}).get("modules") or {}
2117
+ if modules:
2118
+ enabled = _enabled_symbols(emitted)
2119
+ lines.append("# Per-module log-level overrides (board.yaml diagnostics.modules:)")
2120
+ for mod, lvl in sorted(modules.items()):
2121
+ choice = _LOG_LEVEL_CHOICE.get(str(lvl).lower())
2122
+ if choice is None:
2123
+ continue
2124
+ known = _LOG_MODULES.get(str(mod).lower())
2125
+ if known is None:
2126
+ stem = str(mod).upper()
2127
+ reason = (
2128
+ "pending LOG_MODULE_REGISTER on this SDK module"
2129
+ if stem.startswith("ALP_") else
2130
+ f"no Zephyr log module `{mod}` the SDK can wire -- "
2131
+ f"check the spelling")
2132
+ lines.append(f"# CONFIG_{stem}_LOG_LEVEL_{choice}=y ({reason})")
2133
+ continue
2134
+ log_module, guard_symbols, log_gate = known
2135
+ if isinstance(guard_symbols, str):
2136
+ guard_symbols = (guard_symbols,)
2137
+ symbol = f"CONFIG_{log_module}_LOG_LEVEL_{choice}"
2138
+ missing = [g for g in guard_symbols if g not in enabled]
2139
+ if log_gate not in enabled:
2140
+ lines.append(f"# {symbol}=y (its log choice depends on "
2141
+ f"CONFIG_{log_gate}=y, not set on core "
2142
+ f"`{slice_.core_id}`)")
2143
+ elif missing:
2144
+ need = ", ".join(f"CONFIG_{g}=y" for g in missing)
2145
+ lines.append(f"# {symbol}=y (its log choice is declared only "
2146
+ f"under {need}, not set on core "
2147
+ f"`{slice_.core_id}`)")
2148
+ else:
2149
+ lines.append(f"{symbol}=y")
2150
+ lines.append("")
2151
+ return lines
2152
+
2153
+
2154
+ def _slice_alp_conf(project: BoardProject, slice_: Slice) -> str:
2155
+ """Per-core Kconfig fragment for a Zephyr slice.
2156
+
2157
+ Emits the full Kconfig the slice needs: baseline + log + silicon +
2158
+ per-core peripherals/libraries + SoM-intrinsic chip drivers (auto-
2159
+ derived from ``on_module:`` + ``helper_firmware:`` in the SoM preset)
2160
+ + board-populated chip drivers (from board.yaml ``board.populated:``
2161
+ + the board preset) + the Zephyr subsystem enables those chip drivers
2162
+ need (e.g. an enabled ``rv3028c7`` chip driver pulls in ``CONFIG_I2C=y``).
2163
+
2164
+ Swapping ``som.sku:`` in board.yaml automatically changes the SoM-
2165
+ intrinsic chip set with no other board.yaml edits required.
2166
+
2167
+ This is a thin composer: each section below is delegated to a
2168
+ dedicated ``_emit_*`` helper (console / SoM capability / chip driver
2169
+ / subsystem / library / inference / cross-core shmem cache / memory /
2170
+ power / storage / OTA / diagnostics), called in the exact fixed order
2171
+ the fragment has always used. Split out mechanically as the #673
2172
+ Phase-1 kconfig split -- behavior-preserving for every section that
2173
+ predates the cross-core shmem cache one; ``check_emit_snapshots.py``'s
2174
+ fixtures (none of which declare a ``raw_shmem`` ``ipc:`` entry) stay
2175
+ byte-identical.
2176
+ """
2177
+ silicon = project.som_preset.get("silicon")
2178
+ kconfig = silicon_to_kconfig(silicon, project.effective_metadata_root())
2179
+ diagnostics = project.diagnostics
2180
+
2181
+ lines: list[str] = []
2182
+ lines.extend(_emit_baseline(slice_, diagnostics))
2183
+ lines.extend(_emit_console(diagnostics, slice_, project))
2184
+ lines.extend(_emit_soc_summary(project, slice_))
2185
+ lines.extend(_emit_som_caps(project, silicon, kconfig))
2186
+
2187
+ chip_lines, chip_subsystems, resolved_chip_state = _emit_chips(
2188
+ project, _CHIP_SUBSYSTEMS)
2189
+ lines.extend(chip_lines)
2190
+ lines.extend(_emit_subsystems(project, slice_, chip_subsystems))
2191
+
2192
+ iot_lines = _zephyr_iot_kconfig(project, slice_, resolved_chip_state)
2193
+ if iot_lines:
2194
+ lines.extend(iot_lines)
2195
+
2196
+ lines.extend(_emit_libraries(project, slice_))
2197
+ # §D.lib.loader -- per-`libraries:` HW-accelerator backend wiring
2198
+ # (CONFIG_ALP_<LIB>_<BACKEND>=y). This is the single source both the
2199
+ # planner's build-plan `configArtefacts` and `alp_project.py --emit
2200
+ # zephyr-conf --core <id>` (what gen_example_alp_conf.py's pre-generation
2201
+ # for twister / bare `west build` mirrors) share
2202
+ # -- folded in here (2026-07-20) so the two paths cannot silently
2203
+ # diverge on a `libraries:` entry with a hw_backends matcher; see
2204
+ # docs/adr/0020-sdk-owns-build-execution.md addendum.
2205
+ #
2206
+ # Reads BOTH declaration channels (`libraries.scoped_names`), not just
2207
+ # `slice_.libraries`. A library's accelerator support is a property of
2208
+ # the library manifest and the SoM's silicon, not of whether board.yaml
2209
+ # spelled the selection as a bare/project-wide entry or a `cores:`-
2210
+ # scoped one -- the two forms must resolve to the identical hw-backend
2211
+ # set for the same core (alp-sdk #1359: a `cores:` entry was silently a
2212
+ # SUPERSET of the project-wide form, gaining HELIUM/ADC_DMA/etc. that
2213
+ # `cores:` reads as narrowing, not widening).
2214
+ hw_backend_lines = _library_layer._emit_library_hw_backends(
2215
+ _library_layer.scoped_names(project, slice_=slice_), project.sku,
2216
+ project.effective_metadata_root())
2217
+ if hw_backend_lines:
2218
+ lines.append("# §D.lib.loader -- per-library HW-accelerator "
2219
+ "wiring (auto-emitted).")
2220
+ lines.extend(hw_backend_lines)
2221
+ lines.append("")
2222
+ lines.extend(_emit_inference(project, slice_, silicon))
2223
+ lines.extend(_emit_cross_core_shmem_cache(project, slice_, lines))
2224
+ lines.extend(_emit_memory(slice_))
2225
+ lines.extend(_emit_power(slice_))
2226
+ lines.extend(_emit_storage(project))
2227
+ lines.extend(_emit_ota(project))
2228
+ # Passed the fragment built so far: a per-module log-level override is
2229
+ # only a real Kconfig symbol when THIS slice already switched the module
2230
+ # (and CONFIG_LOG) on -- see `_emit_diagnostics`.
2231
+ lines.extend(_emit_diagnostics(project, slice_, lines))
2232
+ # Per-product core ownership: Kconfig for the assignable peripherals this
2233
+ # project assigned to this (M33) core -- the board tree carries the nodes
2234
+ # disabled, so only an owning project enables them.
2235
+ own_kconfig = project_m33_overlay(project, slice_.core_id)[1]
2236
+ if own_kconfig:
2237
+ lines.append("# Assignable peripherals owned by this core (board.yaml "
2238
+ "`ownership:`).")
2239
+ lines.extend(own_kconfig)
2240
+
2241
+ text = "\n".join(lines) + "\n"
2242
+ _refuse_live_kconfig_placeholders(text, slice_.core_id)
2243
+ return text
2244
+
2245
+
2246
+ def _refuse_live_kconfig_placeholders(text: str, core_id: str) -> None:
2247
+ """Refuse a `${NAME}` placeholder on a live line of a Zephyr fragment.
2248
+
2249
+ A board.yaml value written as a placeholder (`ota.server.tenant:
2250
+ "${MENDER_TENANT_TOKEN}"`) is meant for the build host or the device to
2251
+ fill. A Zephyr Kconfig fragment can never do that: the pinned v4.4.1
2252
+ `scripts/kconfig/kconfiglib.py` `_load_config` only unescapes a `.conf`
2253
+ string value -- the `expandvars` call in that file belongs to the Kconfig
2254
+ *source* tokenizer, not the `.conf` loader -- and nothing in Zephyr's CMake
2255
+ expands a fragment either. So a live `CONFIG_HAWKBIT_SERVER="${HOST}"`
2256
+ would build firmware that carries the literal text `${HOST}` as its
2257
+ server name, silently. A commented line (the Mender-MCU-client hint lines
2258
+ in `_emit_ota`) is inert and allowed.
2259
+ """
2260
+ # Imported here, not at the top: a new top-of-file line would shift every
2261
+ # line number that changelog fragments cite in this module.
2262
+ import re
2263
+
2264
+ for line in text.splitlines():
2265
+ if line.lstrip().startswith("#"):
2266
+ continue
2267
+ match = re.search(r"\$\{[^}]*\}", line)
2268
+ if match:
2269
+ raise OrchestratorError(
2270
+ f"core '{core_id}': the Zephyr config would carry the "
2271
+ f"placeholder `{match.group(0)}` literally (`{line.strip()}`) "
2272
+ f"-- Zephyr does not expand environment variables in a "
2273
+ f"Kconfig fragment, so the firmware would use the text "
2274
+ f"`{match.group(0)}` itself. Write the real value in "
2275
+ f"board.yaml, or set it in the app's own prj.conf")
2276
+
2277
+
2278
+ def _inference_auto_order(som_preset: dict) -> list[str]:
2279
+ """The SoM preset's ordered AUTO accelerator preference, best first.
2280
+
2281
+ `inference.auto_order` is the single source: its first entry is the SoM's
2282
+ preferred backend.
2283
+ """
2284
+ return list((som_preset.get("inference") or {}).get("auto_order") or [])
2285
+
2286
+
2287
+ def _slice_local_conf(project: BoardProject, slice_: Slice) -> str:
2288
+ """Per-core local.conf snippet for a Yocto slice."""
2289
+ machine = slice_.machine or f"e1m-{project.sku.lower().replace('e1m-', '')}"
2290
+ lines: list[str] = []
2291
+ lines.append("# Auto-generated by scripts/alp_orchestrate.py "
2292
+ "-- append to local.conf.")
2293
+ lines.append(f"# Per-core slice `{slice_.core_id}` "
2294
+ f"(image: {slice_.image or 'custom'})")
2295
+ # Console: a Yocto slice uses the Linux console (kernel console = the
2296
+ # SoM's debug UART via the `console=` kernel cmdline; userspace printf
2297
+ # goes to stdout/the controlling tty). This is the Linux half of the
2298
+ # generic console auto-selection -- nothing to set in local.conf; the
2299
+ # BSP owns the `console=ttySC*` cmdline. (Zephyr slices get the Alp
2300
+ # UART console instead; see _emit_zephyr_console.)
2301
+ console = _resolve_console((project.diagnostics or {}).get("console"),
2302
+ slice_.os)
2303
+ if console == "linux":
2304
+ lines.append("# Console: Linux console (kernel `console=` cmdline "
2305
+ "-> SoM debug UART; userspace -> stdout/tty).")
2306
+ lines.append(f'MACHINE = "{machine}"')
2307
+ if slice_.peripherals:
2308
+ joined = ", ".join(sorted(slice_.peripherals))
2309
+ lines.append("# Peripherals declared for this Yocto slice are "
2310
+ f"BSP/kernel-owned ({joined}); no Zephyr Kconfig or "
2311
+ "local.conf package knob is emitted here.")
2312
+ iot_lines = _yocto_iot_lines(project, slice_)
2313
+ if iot_lines:
2314
+ lines.extend(iot_lines)
2315
+ # `cameras:` -> ALP_CAMERA_CAM<n>, the variable the kernel bbappend keys
2316
+ # the sensor devicetree include on (same resolver as Zephyr's -DSHIELD).
2317
+ lines.extend(_cameras.yocto_camera_lines(project, slice_))
2318
+ # Curated third-party libraries (top-level `libraries:`, ADR 0018) with a
2319
+ # Yocto integration section -- BOTH the project-wide entries and the ones
2320
+ # scoped to this core. Every recipe name comes from the library's own
2321
+ # `integration.yocto.image_install:`; nothing is derived from the library
2322
+ # name. (alplabai/tan-cli#555: the core-scoped half used to bypass the
2323
+ # library layer entirely and emit `lib-<name>` -- a recipe that RPROVIDES
2324
+ # nothing, so `bitbake alp-image-edge` died on `Nothing RPROVIDES
2325
+ # 'lib-mbedtls'`.) Guard keeps a project with no such libraries
2326
+ # byte-identical.
2327
+ for name in _library_layer.yocto_unwireable(
2328
+ project, slice_, project.effective_metadata_root()):
2329
+ lines.append(f"# library `{name}`: manifest declares no "
2330
+ f"`integration.yocto:` section -- nothing to add to "
2331
+ f"IMAGE_INSTALL on core `{slice_.core_id}`.")
2332
+ library_pkgs = _library_layer.yocto_image_install(
2333
+ project, slice_, project.effective_metadata_root())
2334
+ if library_pkgs:
2335
+ joined = " ".join(library_pkgs)
2336
+ lines.append(f'IMAGE_INSTALL:append = " {joined}"')
2337
+ # SoM-declared AUTO accelerator preference. Read by the alp-sdk recipe
2338
+ # (EXTRA_OECMAKE -> -DALP_SDK_INFERENCE_AUTO_ORDER); only the
2339
+ # .alpmodel selector (alp_model_select) consumes it, as a tiebreak. Weak `?=` so a hand-edited
2340
+ # local.conf wins; emitted only for presets that declare it.
2341
+ auto_order = _inference_auto_order(project.som_preset)
2342
+ if auto_order:
2343
+ lines.append(f'ALP_SDK_INFERENCE_AUTO_ORDER ?= "{",".join(auto_order)}"')
2344
+ if slice_.image:
2345
+ lines.append(f"# bitbake target: {slice_.image}")
2346
+
2347
+ # Mender OTA wiring (board.yaml `ota:` block). Emits Mender
2348
+ # `?=` (weak) assignments so hand-edited local.conf values in
2349
+ # the build dir still win.
2350
+ ota = project.ota or {}
2351
+ if ota.get("provider") == "mender":
2352
+ lines.append("")
2353
+ lines.append("# OTA (board.yaml `ota:` block)")
2354
+ lines.append('INHERIT += "mender-full"')
2355
+ if ota.get("artifact_name"):
2356
+ lines.append(f'MENDER_ARTIFACT_NAME ?= "{ota["artifact_name"]}"')
2357
+ srv = ota.get("server") or {}
2358
+ if srv.get("url"):
2359
+ lines.append(f'MENDER_SERVER_URL ?= "{srv["url"]}"')
2360
+ tenant = str(srv.get("tenant") or "")
2361
+ if "${" in tenant:
2362
+ # A ${NAME} placeholder is never expanded by BitBake from the
2363
+ # host environment, so a self-referencing `?=` would bake the
2364
+ # literal text (or fail to expand). Leave the token to the
2365
+ # documented local.conf override instead.
2366
+ lines.append(
2367
+ "# MENDER_TENANT_TOKEN: set it in conf/local.conf "
2368
+ "(meta-alp-sdk/README.md, Mender step 3); the board.yaml "
2369
+ "placeholder is not expanded by BitBake."
2370
+ )
2371
+ elif tenant:
2372
+ lines.append(f'MENDER_TENANT_TOKEN ?= "{tenant}"')
2373
+ sto = ota.get("storage") or {}
2374
+ if sto.get("device"):
2375
+ lines.append(f'MENDER_STORAGE_DEVICE_BASE ?= "{sto["device"]}"')
2376
+ if sto.get("boot_part_mb"):
2377
+ lines.append(f'MENDER_BOOT_PART_SIZE_MB ?= "{sto["boot_part_mb"]}"')
2378
+ if sto.get("total_size_mb"):
2379
+ lines.append(f'MENDER_STORAGE_TOTAL_SIZE_MB ?= "{sto["total_size_mb"]}"')
2380
+ if ota.get("poll_interval_s"):
2381
+ lines.append(f'MENDER_INVENTORY_INTERVAL ?= "{ota["poll_interval_s"]}"')
2382
+
2383
+ return "\n".join(lines) + "\n"
2384
+
2385
+
2386
+ def _slice_cmake_args(project: BoardProject, slice_: Slice) -> str:
2387
+ """Per-core cmake -D args for a baremetal / yocto slice.
2388
+
2389
+ NPU-dispatch enables (DRP-AI, DEEPX) are driven from the SoM
2390
+ preset's `capabilities:` matrix -- never from board.yaml. On
2391
+ multi-NPU SKUs (V2M101 = DRP-AI3 + DEEPX DX-M1) every available
2392
+ NPU is enabled so apps can dispatch concurrent independent models
2393
+ via alp_inference_open(.backend=...) per-handle.
2394
+ """
2395
+ family = project.som_preset.get("family") or "unknown"
2396
+ # resolve_capabilities merges SoC-JSON defaults with SoM overrides.
2397
+ capabilities = resolve_capabilities(project.som_preset, project.effective_metadata_root())
2398
+ lines: list[str] = []
2399
+ lines.append("# Auto-generated by scripts/alp_orchestrate.py "
2400
+ "-- pass to cmake.")
2401
+ lines.append(f"-DALP_SOM_SKU={project.sku}")
2402
+ lines.append(f"-DALP_SOM_FAMILY={family}")
2403
+ # Per-SKU capability-restriction gate for <alp/soc_caps.h>: emitted
2404
+ # ONLY when the SoM preset declares `silicon_capabilities.unpopulated`,
2405
+ # so every unrestricted SKU's cmake-args output stays byte-identical.
2406
+ if som_unpopulated_capabilities(project.som_preset):
2407
+ lines.append(f"-DALP_SOM_{_som_define_slug(project.sku)}")
2408
+ lines.append(f"-DALP_CORE_ID={slice_.core_id}")
2409
+ # Cross-EVK board facade selector (<alp/board.h>).
2410
+ # Emitted only when the project resolves to a named board preset
2411
+ # (e.g. `preset: e1m-x-evk` -> board_name "E1M-X-EVK").
2412
+ # Inline boards with no preset/name skip this; the facade defaults
2413
+ # to an #error forcing the builder to set it explicitly (e.g. via
2414
+ # extra_args for native_sim testcases).
2415
+ if project.board_name:
2416
+ lines.append(f"-DALP_BOARD_{_board_define_slug(project.board_name)}")
2417
+ if slice_.toolchain:
2418
+ lines.append(f"-DALP_TOOLCHAIN={slice_.toolchain}")
2419
+ # `cameras:` -> the same -DSHIELD the build command carries.
2420
+ shield = _cameras.shield_define_for_build(project, slice_)
2421
+ if shield:
2422
+ lines.append(f"-D{shield}")
2423
+ if capabilities.get("drp_ai"):
2424
+ # Must match the option name in src/yocto/CMakeLists.txt
2425
+ # (ALP_SDK_USE_DRPAI_V2N -- compiles inference_drpai.cpp).
2426
+ lines.append("-DALP_SDK_USE_DRPAI_V2N=ON")
2427
+ if capabilities.get("deepx_dxm1"):
2428
+ lines.append("-DALP_SDK_USE_DEEPX_DXM1=ON")
2429
+ # Project-wide curated third-party libraries (top-level `libraries:`,
2430
+ # ADR 0018) with a baremetal integration section. Guard keeps a project
2431
+ # with no such libraries byte-identical.
2432
+ for arg in _library_layer.baremetal_cmake_args(
2433
+ project, slice_, project.effective_metadata_root()):
2434
+ lines.append(arg)
2435
+ return "\n".join(lines) + "\n"