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/core/ram_run.py ADDED
@@ -0,0 +1,694 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Pure logic for AEN Flow C -- a J-Link ITCM RAM-run (tan-cli#1313).
3
+
4
+ Flow C loads a Zephyr image into ITCM over SWD, points the core at its reset
5
+ handler, lets it run and reads `ram_console_buf` back. **Nothing touches MRAM.**
6
+ The proven raw helper is alp-sdk `scripts/bench/aen/ram-run.sh` (+ `reread.sh`);
7
+ this module is its decision half, with no IO beyond reading the ELF bytes it is
8
+ given:
9
+
10
+ * a minimal ELF32 reader (no `pyelftools`: tan declares no such dependency and
11
+ the toolchain's `readelf`/`nm` are not guaranteed to be on the host) -- the
12
+ LOAD segments and the symbol table;
13
+ * [`plan_ram_image`]: the load address DERIVED from the LOAD segment with the
14
+ lowest `p_paddr` among those with a nonzero `p_filesz` (never "the first LOAD
15
+ segment", which is often a zero-FileSiz `.bss` in DTCM -- loading there splats
16
+ live RAM), the refusals the script makes (MRAM-linked image, implausible base,
17
+ vector table that disagrees with the ELF entry), and the initial SP/PC read
18
+ from the vector table;
19
+ * the J-Link Commander scripts for the load session and the console read;
20
+ * [`decode_console`]: the `mem8` dump -> text, exactly the script's awk.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import re
25
+ import struct
26
+ from collections.abc import Sequence
27
+ from dataclasses import dataclass
28
+
29
+ #: Registered issue codes (`contract/issue-codes.json`).
30
+ CODE_NOT_RAM_LINKED = "flash.ram-image-not-ram-linked"
31
+ CODE_CONSOLE_SYMBOL_MISSING = "flash.ram-console-symbol-missing"
32
+ CODE_FAILED = "flash.ram-failed"
33
+ CODE_CORE_UNSUPPORTED = "flash.ram-core-unsupported"
34
+ CODE_CORE_MISMATCH = "flash.ram-core-mismatch"
35
+ CODE_CORE_UNCONFIRMED = "flash.ram-core-unconfirmed"
36
+
37
+ #: The RAM console buffer symbol (`CONFIG_RAM_CONSOLE`).
38
+ #: SRAM0's local aperture base on the AEN parts (shared with `core.size`).
39
+ SRAM0_BASE = 0x02000000
40
+
41
+ CONSOLE_SYMBOL = "ram_console_buf"
42
+
43
+ #: Largest region one `mem8` command reads (bench-env.sh `bench_mem8_chunks`).
44
+ MEM8_CHUNK = 65536
45
+ #: Upper bound on the console read, so a corrupt symbol size cannot ask for MiBs.
46
+ MAX_CONSOLE_BYTES = 64 * 1024
47
+ #: ELF reader bounds: a symbol table this large, or one whose entries are smaller than
48
+ #: an Elf32_Sym, is not a Zephyr image and is not trusted for an address.
49
+ MAX_SYMBOLS = 200_000
50
+ MAX_SECTIONS = 4096
51
+ ELF32_SYM_SIZE = 16
52
+ #: What the script reads when the symbol carries no size.
53
+ DEFAULT_CONSOLE_BYTES = 0x600
54
+
55
+ MRAM_BASE = 0x80000000
56
+
57
+
58
+ class RamRunError(Exception):
59
+ """A Flow C refusal. `code` is the registered issue code."""
60
+
61
+ def __init__(self, message: str, code: str = CODE_FAILED) -> None:
62
+ super().__init__(message)
63
+ self.code = code
64
+
65
+
66
+ # ── ELF32 ───────────────────────────────────────────────────────────────────
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class Segment:
71
+ paddr: int
72
+ vaddr: int
73
+ filesz: int
74
+ memsz: int
75
+ #: ELF `p_flags` (PF_X = 1). Defaults to R+X so hand-built segments stay executable.
76
+ flags: int = 5
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class ElfImage:
81
+ entry: int
82
+ segments: tuple[Segment, ...]
83
+ #: name -> (address, size)
84
+ symbols: dict[str, tuple[int, int]]
85
+ #: names the symbol table defines more than once at different addresses (static
86
+ #: symbols of the same name in several files); `symbols` keeps the first of them.
87
+ ambiguous: frozenset[str] = frozenset()
88
+
89
+
90
+ def parse_elf(data: bytes) -> ElfImage:
91
+ """Parse the parts of a little-endian ELF32 that Flow C needs. Raises
92
+ `RamRunError` for anything else (a 64-bit or big-endian file, a truncated
93
+ one): guessing at a load address from a file we cannot read is how a
94
+ resident app's stack gets overwritten."""
95
+ if len(data) < 52 or data[:4] != b"\x7fELF":
96
+ raise RamRunError("not an ELF file")
97
+ if data[4] != 1 or data[5] != 1:
98
+ raise RamRunError("only little-endian ELF32 images are supported")
99
+ try:
100
+ (_t, _m, _v, entry, phoff, shoff, _f, _eh, phentsize, phnum, shentsize, shnum, _si) = (
101
+ struct.unpack_from("<HHIIIIIHHHHHH", data, 16)
102
+ )
103
+ segments = []
104
+ for i in range(phnum):
105
+ off = phoff + i * phentsize
106
+ p_type, _o, p_vaddr, p_paddr, p_filesz, p_memsz, p_flags = struct.unpack_from(
107
+ "<IIIIIII", data, off
108
+ )
109
+ if p_type == 1: # PT_LOAD
110
+ segments.append(Segment(p_paddr, p_vaddr, p_filesz, p_memsz, p_flags))
111
+ symbols, ambiguous = _symbols(data, shoff, shentsize, shnum)
112
+ except struct.error as err:
113
+ raise RamRunError(f"truncated ELF file ({err})") from err
114
+ return ElfImage(entry, tuple(segments), symbols, ambiguous)
115
+
116
+
117
+ def _symbols(
118
+ data: bytes, shoff: int, shentsize: int, shnum: int
119
+ ) -> tuple[dict[str, tuple[int, int]], frozenset[str]]:
120
+ out: dict[str, tuple[int, int]] = {}
121
+ ambiguous: set[str] = set()
122
+ if not shoff or not shnum:
123
+ return out, frozenset()
124
+ if shnum > MAX_SECTIONS:
125
+ raise RamRunError(f"the ELF declares {shnum} sections; refusing to parse it")
126
+ headers = [
127
+ struct.unpack_from("<IIIIIIIIII", data, shoff + i * shentsize) for i in range(shnum)
128
+ ]
129
+ for _name, sh_type, _fl, _addr, sh_off, sh_size, sh_link, _info, _align, sh_entsize in headers:
130
+ if sh_type != 2: # SHT_SYMTAB
131
+ continue
132
+ if sh_entsize < ELF32_SYM_SIZE:
133
+ raise RamRunError(
134
+ f"the ELF symbol table declares an entry size of {sh_entsize} B "
135
+ f"(an Elf32_Sym is {ELF32_SYM_SIZE}); refusing to read symbols from it"
136
+ )
137
+ if sh_size // sh_entsize > MAX_SYMBOLS:
138
+ raise RamRunError(
139
+ f"the ELF symbol table has {sh_size // sh_entsize} entries (cap {MAX_SYMBOLS})"
140
+ )
141
+ if sh_link >= len(headers):
142
+ continue
143
+ str_off, str_size = headers[sh_link][4], headers[sh_link][5]
144
+ strtab = data[str_off : str_off + str_size]
145
+ for i in range(sh_size // sh_entsize):
146
+ st_name, st_value, st_size, _info2, _other, _shndx = struct.unpack_from(
147
+ "<IIIBBH", data, sh_off + i * sh_entsize
148
+ )
149
+ end = strtab.find(b"\0", st_name)
150
+ name = strtab[st_name : end if end >= 0 else None].decode("ascii", "replace")
151
+ if name:
152
+ if out.setdefault(name, (st_value, st_size)) != (st_value, st_size):
153
+ ambiguous.add(name)
154
+ return out, frozenset(ambiguous)
155
+
156
+
157
+ # ── the image plan ──────────────────────────────────────────────────────────
158
+
159
+
160
+ @dataclass(frozen=True)
161
+ class RamImage:
162
+ base: int
163
+ entry: int # PC to run from, thumb bit cleared
164
+ initial_sp: int
165
+ reset_vector: int
166
+ size: int
167
+ console: tuple[int, int] | None # (address, size) of ram_console_buf
168
+
169
+
170
+ #: The cores tan will RAM-run. The HP core's debug AP selection (AP 0x00200000 vs the
171
+ #: HE's 0x00300000 on evk-01) is not bench-proven through tan, so a slice other than
172
+ #: the HE core is refused rather than risking loading HE's recipe into the wrong core.
173
+ SUPPORTED_CORE = "M55_HE"
174
+
175
+ #: Per-core ITCM global aliases (the recipe's load bases): HE 0x58000000, HP 0x50000000.
176
+ _ITCM_GLOBAL = {"M55_HE": 0x58000000, "M55_HP": 0x50000000}
177
+
178
+
179
+ @dataclass(frozen=True)
180
+ class Apertures:
181
+ """Where a core's code and data may live, with SIZES from the SoC metadata's
182
+ `sram_banks_kb` (never invented): `code` / `data` are `(base, size)` lists."""
183
+
184
+ code: tuple[tuple[int, int], ...]
185
+ data: tuple[tuple[int, int], ...]
186
+
187
+
188
+ def check_core(core_id: str, base: int) -> None:
189
+ """Refuse a slice that is not the HE core (`flash.ram-core-unsupported`), and a
190
+ base that is the OTHER core's ITCM alias (`flash.ram-core-mismatch`): an image
191
+ linked for the HP core's 0x50000000 aperture on the HE slice would be loaded into
192
+ whichever core the probe happens to be attached to."""
193
+ core = core_id.upper()
194
+ if core != SUPPORTED_CORE:
195
+ raise RamRunError(
196
+ f"tan flash --ram supports only the M55 HE core for now; '{core_id}' is not "
197
+ "it. The HP core's debug access port selection is not bench-proven through "
198
+ "tan, and loading the HE recipe into the wrong core is not recoverable by a "
199
+ "retry. Use the raw alp-sdk helper for the HP core.",
200
+ CODE_CORE_UNSUPPORTED,
201
+ )
202
+ for other, alias in _ITCM_GLOBAL.items():
203
+ if other != core and base == alias:
204
+ raise RamRunError(
205
+ f"the image is linked for {other}'s ITCM alias 0x{alias:X} but the slice is "
206
+ f"{core}: it would run on whichever core the probe is attached to. Rebuild "
207
+ "it for the HE core.",
208
+ CODE_CORE_MISMATCH,
209
+ )
210
+
211
+
212
+ def apertures_for(core_id: str, banks_kib: Sequence[tuple[str, float]]) -> Apertures | None:
213
+ """The HE core's apertures from a SoC variant's `sram_banks_kb` (`(name, KiB)`):
214
+ its own ITCM bank at the local `0x0` and the global `0x58000000` alias, its DTCM
215
+ bank at the architectural local `0x20000000`, and `SRAM0` at `0x02000000`.
216
+ `None` when the metadata names no ITCM/DTCM bank for the core -- the caller then
217
+ REFUSES; a size is never guessed."""
218
+ token = core_id.upper()
219
+ itcm = dtcm = sram0 = None
220
+ for name, kib in banks_kib:
221
+ up = name.upper()
222
+ size = int(kib * 1024)
223
+ if token in up and "ITCM" in up and itcm is None:
224
+ itcm = size
225
+ elif token in up and "DTCM" in up and dtcm is None:
226
+ dtcm = size
227
+ elif up == "SRAM0" and sram0 is None:
228
+ sram0 = size
229
+ if itcm is None or dtcm is None or token not in _ITCM_GLOBAL:
230
+ return None
231
+ code = [(0x0, itcm), (_ITCM_GLOBAL[token], itcm)]
232
+ data = [(0x20000000, dtcm)]
233
+ if sram0 is not None:
234
+ code.append((SRAM0_BASE, sram0))
235
+ data.append((SRAM0_BASE, sram0))
236
+ return Apertures(tuple(code), tuple(data))
237
+
238
+
239
+ def _within(address: int, length: int, spans: Sequence[tuple[int, int]]) -> bool:
240
+ return any(base <= address and address + length <= base + size for base, size in spans)
241
+
242
+
243
+ def _plausible_base(base: int) -> bool:
244
+ return base in (0x0, 0x50000000, 0x58000000) or SRAM0_BASE <= base < 0x03000000
245
+
246
+
247
+ def plan_ram_image(
248
+ elf: ElfImage,
249
+ binary: bytes,
250
+ *,
251
+ core_id: str | None = None,
252
+ apertures: Apertures | None = None,
253
+ ) -> RamImage:
254
+ """Decide whether `binary` (the raw `zephyr.bin` beside `elf`) can be
255
+ RAM-run and where. Refuses with [`CODE_NOT_RAM_LINKED`] when it is linked
256
+ for MRAM or any base that is not an ITCM/SRAM target, and with
257
+ [`CODE_FAILED`] when the pieces disagree."""
258
+ loadable = [s for s in elf.segments if s.filesz]
259
+ if not loadable:
260
+ raise RamRunError("the ELF has no LOAD segment with file content")
261
+ base = min(s.paddr for s in loadable)
262
+ if base >= MRAM_BASE:
263
+ raise RamRunError(
264
+ f"the image is slot0/MRAM-linked (LOAD segment at 0x{base:X}); loading it "
265
+ "re-enters the ALREADY-RESIDENT MRAM image, not the freshly built one. "
266
+ "Rebuild with the Flow C ITCM retarget (aen-flowc-itcm.conf + "
267
+ "aen-flowc-itcm.overlay) or flash it with Flow D.",
268
+ CODE_NOT_RAM_LINKED,
269
+ )
270
+ if not _plausible_base(base):
271
+ raise RamRunError(
272
+ f"derived load base 0x{base:X} is not a Flow C target (expected 0x0, the ITCM "
273
+ "global alias 0x50000000/0x58000000, or SRAM 0x02xxxxxx) -- a DTCM/data "
274
+ "segment was picked, which would corrupt live RAM. Refusing to load.",
275
+ CODE_NOT_RAM_LINKED,
276
+ )
277
+ if core_id is not None:
278
+ check_core(core_id, base)
279
+ if len(binary) < 8:
280
+ raise RamRunError("zephyr.bin is too small to hold a vector table")
281
+ if apertures is not None and not _within(base, len(binary), apertures.code):
282
+ raise RamRunError(
283
+ f"the {len(binary)} B image at 0x{base:X} does not fit any of the core's code "
284
+ "apertures (" + ", ".join(f"0x{b:X}+0x{n:X}" for b, n in apertures.code) + "; sizes "
285
+ "from the SoC metadata) -- loading it would run past the TCM into live memory.",
286
+ CODE_NOT_RAM_LINKED,
287
+ )
288
+ span = max(s.paddr + s.filesz for s in loadable) - base
289
+ if len(binary) < span:
290
+ raise RamRunError(
291
+ f"zephyr.bin is {len(binary)} B but the ELF's LOAD segments span {span} B from "
292
+ f"0x{base:X} -- the binary and the ELF are from different builds."
293
+ )
294
+ sp, reset = struct.unpack_from("<II", binary, 0)
295
+ entry = reset & ~1
296
+ if entry != elf.entry & ~1:
297
+ raise RamRunError(
298
+ f"the vector table's reset handler (0x{reset:X}) and the ELF entry point "
299
+ f"(0x{elf.entry:X}) disagree -- refusing to guess the PC."
300
+ )
301
+ if sp == 0 or sp >= MRAM_BASE:
302
+ raise RamRunError(f"the vector table's initial SP 0x{sp:X} is not a RAM address.")
303
+ console = None
304
+ sym = elf.symbols.get(CONSOLE_SYMBOL)
305
+ if sym is not None:
306
+ addr, size = sym
307
+ length = min(size or DEFAULT_CONSOLE_BYTES, MAX_CONSOLE_BYTES)
308
+ if apertures is not None and not _within(addr, length, apertures.data):
309
+ raise RamRunError(
310
+ f"{CONSOLE_SYMBOL} (0x{addr:X}+0x{length:X}) is outside the core's DTCM/SRAM "
311
+ "apertures (" + ", ".join(f"0x{b:X}+0x{n:X}" for b, n in apertures.data)
312
+ + ") -- refusing to read an address the ELF should not have put there."
313
+ )
314
+ console = (addr, length)
315
+ return RamImage(base, entry, sp, reset, len(binary), console)
316
+
317
+
318
+ # ── J-Link scripts ──────────────────────────────────────────────────────────
319
+
320
+
321
+ _SAFE_TOKEN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
322
+
323
+
324
+ def preamble(serial: str | None, speed: int, device: str) -> list[str]:
325
+ """Everything up to and including `connect`, shared by both sessions. `serial` and
326
+ `device` must be plain tokens and `speed` an int, or this refuses -- they are
327
+ the only free-form values in either script."""
328
+ if not isinstance(speed, int) or isinstance(speed, bool) or speed <= 0:
329
+ raise RamRunError(f"J-Link speed {speed!r} is not a positive integer")
330
+ for label, value in (("serial", serial), ("device", device)):
331
+ if value is not None and not _SAFE_TOKEN.match(value):
332
+ raise RamRunError(f"the J-Link {label} {value!r} is not a plain token")
333
+ lines = [f"SelectEmuBySN {serial}"] if serial else []
334
+ return [*lines, "si SWD", f"speed {speed}", f"device {device}", "connect"]
335
+
336
+
337
+ def load_script(
338
+ pre: Sequence[str],
339
+ binary_path: str,
340
+ image: RamImage,
341
+ after_go: Sequence[str] = (),
342
+ hold_ms: int = 0,
343
+ ) -> str:
344
+ # Every address below is an `int` rendered with `0x%X`, never a string; the
345
+ # caller passes only a path it staged itself (and already validated).
346
+ """`connect; halt; loadbin; setpc; go` -- the proven Flow C load session.
347
+ `loadbin` resets the core and re-reads the vector table (SP); `setpc` enters
348
+ the reset handler. No MRAM address appears anywhere. `after_go` are extra read-only
349
+ lines run in the same session once the image is going (`tan.core.ram_watch`).
350
+
351
+ `hold_ms > 0` (experimental hold mode, `TAN_FLASH_RAM_HOLD=1`, tan-cli#1372) keeps the
352
+ session open for that long after `go` with a plain `Sleep <ms>`: no halt, no register
353
+ or DEMCR/DWT write follows `go` (a halt after go+Sleep leaves the core incoherent,
354
+ alp-sdk#2076)."""
355
+ if any(c in binary_path for c in "\r\n\0"):
356
+ raise RamRunError("the image path carries a control character")
357
+ return "\n".join(
358
+ [*pre, "halt", f"loadbin {binary_path} 0x{image.base:X}", f"setpc 0x{image.entry:X}", "go", *after_go,
359
+ *([f"Sleep {int(hold_ms)}"] if hold_ms > 0 else []), "exit"]
360
+ ) + "\n"
361
+
362
+
363
+ def mem8_lines(address: int, size: int) -> list[str]:
364
+ out, off = [], 0
365
+ while off < size:
366
+ chunk = min(MEM8_CHUNK, size - off)
367
+ out.append(f"mem8 0x{address + off:X}, 0x{chunk:X}")
368
+ off += chunk
369
+ return out
370
+
371
+
372
+ def read_script(pre: Sequence[str], address: int, size: int) -> str:
373
+ return "\n".join([*pre, *mem8_lines(address, size), "exit"]) + "\n"
374
+
375
+
376
+ _DUMP_LINE = re.compile(r"^([0-9A-Fa-f]+) = ((?:[0-9A-Fa-f]{2}\s*)+)$")
377
+
378
+
379
+ def parse_dump(transcript: str) -> dict[int, bytes]:
380
+ """Every `ADDR = xx xx ...` line of a `mem8` transcript, concatenated per
381
+ contiguous run, keyed by the address of the run's first byte."""
382
+ runs: dict[int, bytearray] = {}
383
+ cursor: tuple[int, int] | None = None # (run start, next address)
384
+ for raw in transcript.splitlines():
385
+ match = _DUMP_LINE.match(raw.strip())
386
+ if not match:
387
+ continue
388
+ addr = int(match.group(1), 16)
389
+ data = bytes(int(b, 16) for b in match.group(2).split())
390
+ if cursor is not None and cursor[1] == addr:
391
+ runs[cursor[0]].extend(data)
392
+ cursor = (cursor[0], addr + len(data))
393
+ else:
394
+ runs[addr] = bytearray(data)
395
+ cursor = (addr, addr + len(data))
396
+ return {k: bytes(v) for k, v in runs.items()}
397
+
398
+
399
+ def read_back(transcript: str, address: int, size: int) -> bytes:
400
+ """The `size` bytes at `address` out of a `mem8` transcript, or raise
401
+ `RamRunError` when any part is missing (J-Link can drop one chunk's dump
402
+ silently, alp-sdk#2313)."""
403
+ buf = bytearray(size)
404
+ have = bytearray(size)
405
+ for start, data in parse_dump(transcript).items():
406
+ lo = max(start, address)
407
+ hi = min(start + len(data), address + size)
408
+ if lo < hi:
409
+ buf[lo - address : hi - address] = data[lo - start : hi - start]
410
+ have[lo - address : hi - address] = b"\x01" * (hi - lo)
411
+ if not all(have):
412
+ missing = have.index(0)
413
+ raise RamRunError(
414
+ f"the RAM console read is incomplete: no dump for 0x{address + missing:X} "
415
+ f"(of {size} bytes at 0x{address:X})"
416
+ )
417
+ return bytes(buf)
418
+
419
+
420
+ def decode_console(data: bytes) -> str:
421
+ """The script's awk: stop after more than four consecutive NULs, skip a NUL
422
+ run, newline for CR/LF, printable ASCII kept, everything else dropped."""
423
+ out: list[str] = []
424
+ nul = 0
425
+ for b in data:
426
+ if b == 0:
427
+ nul += 1
428
+ if nul > 4:
429
+ break
430
+ continue
431
+ nul = 0
432
+ if b in (10, 13):
433
+ out.append("\n")
434
+ elif 32 <= b < 127:
435
+ out.append(chr(b))
436
+ return "".join(out)
437
+
438
+
439
+ # ── transcript checks ───────────────────────────────────────────────────────
440
+
441
+ _CONNECT_FAILURES = ("Cannot connect", "Could not connect", "Failed to power up DAP")
442
+
443
+
444
+ def _window(transcript: str, echo: str) -> str | None:
445
+ """What J-Link printed between the echo of `echo` and the next prompt, or
446
+ `None` when the command was never echoed (a stub, or a quiet build)."""
447
+ lines = transcript.splitlines()
448
+ for i, line in enumerate(lines):
449
+ if line.strip() == f"J-Link>{echo}":
450
+ out = []
451
+ for follow in lines[i + 1 :]:
452
+ if follow.startswith("J-Link>"):
453
+ break
454
+ out.append(follow)
455
+ return "\n".join(out)
456
+ return None
457
+
458
+
459
+ def check_session(transcript: str, *, loadbin: bool) -> str | None:
460
+ """The refusals `ram-run.sh` makes on a session transcript: a connect that
461
+ did not happen, a session that never reached `Script processing
462
+ completed.` (the J-Link process died mid-way -- NOT a reported command
463
+ failure), a `loadbin` that did not say `O.K.`, or a rejected `setpc`/`go`.
464
+
465
+ For the LOAD session (`loadbin=True`) the transcript must PROVE the load: the
466
+ `loadbin` echo with an `O.K.` after it, a `setpc` echo with nothing but blank
467
+ lines after it, and a `go` echo with at most the memory-map banner after it. A
468
+ transcript that does not echo them cannot confirm the load -- a STALE image
469
+ already in ITCM could otherwise boot and be read back as this run's -- so it is
470
+ refused ("cannot confirm the load"). `None` when the transcript is clean."""
471
+ for marker in _CONNECT_FAILURES:
472
+ if marker in transcript:
473
+ return f"J-Link reported `{marker}`"
474
+ if "Script processing completed." not in transcript:
475
+ return (
476
+ "the J-Link transcript has no 'Script processing completed.' line -- the "
477
+ "process crashed, was killed or was truncated before finishing"
478
+ )
479
+ if not loadbin:
480
+ return None
481
+ echoes = {
482
+ word: next(
483
+ (ln[len("J-Link>") :] for ln in transcript.splitlines() if ln.startswith(f"J-Link>{word} ")
484
+ or ln.strip() == f"J-Link>{word}"),
485
+ None,
486
+ )
487
+ for word in ("loadbin", "setpc", "go")
488
+ }
489
+ missing = [w for w, e in echoes.items() if e is None]
490
+ if missing:
491
+ return (
492
+ "cannot confirm the load: the J-Link transcript has no echo of "
493
+ + ", ".join(missing) + " (expected `J-Link>loadbin ...`, `J-Link>setpc ...`, `J-Link>go`)"
494
+ )
495
+ load = _window(transcript, echoes["loadbin"])
496
+ if load is None or "O.K." not in load.split():
497
+ return "loadbin did not report 'O.K.' -- refusing to treat this as a fresh load"
498
+ bad = _window(transcript, echoes["setpc"])
499
+ if bad and bad.strip():
500
+ return f"setpc was rejected: {bad.strip()}"
501
+ go = _window(transcript, echoes["go"]) or ""
502
+ if any(
503
+ ln.strip()
504
+ and ln.strip() != "Script processing completed."
505
+ and not re.fullmatch(r"Memory map '.*' is active", ln.strip())
506
+ for ln in go.splitlines()
507
+ ):
508
+ return f"go was rejected: {go.strip()}"
509
+ return None
510
+
511
+
512
+ _AP_ADDR = re.compile(r"AP\[(\d+)\]\s*\(APAddr\s+(0x[0-9A-Fa-f]+)\)")
513
+ _AP_CORE_FOUND = re.compile(r"AP\[(\d+)\]:\s*Core found")
514
+ _CPUID = re.compile(r"CPUID register:\s*(0x[0-9A-Fa-f]+)")
515
+ _FOUND = re.compile(r"^Found Cortex-M\S*.*$", re.MULTILINE)
516
+
517
+ #: The debug access ports of the two M55s on the Ensemble E8, from the bench: HE AP
518
+ #: 0x00300000, HP AP 0x00200000 (alp-sdk scripts/bench/aen/openocd-ram-run.sh:16-17;
519
+ #: changelog.d/2037-openocd-m55he-bench-core-selection.md:4 "0x00200000 is the M55-HP,
520
+ #: not the HE, and 0x00300000 is the HE"; changelog.d/2025.md:31 `AP[3] (APAddr
521
+ #: 0x00300000)` is the AHB-AP carrying the M55 debug).
522
+ HE_AP_ADDR = 0x00300000
523
+ HP_AP_ADDR = 0x00200000
524
+
525
+
526
+ def attached_core(transcript: str) -> dict | None:
527
+ """Which core J-Link attached to, from its own `connect` banner: the AP that reports
528
+ `AP[n]: Core found` (with its `APAddr`), the `CPUID register` and the `Found
529
+ Cortex-M55 ...` line. The `APAddr` of the Core-found AP is what tells HE from HP --
530
+ the `Found Cortex-M55 r1p0` line alone is identical for both. `None` when the
531
+ transcript names no Core-found AP. More than one distinct Core-found AP is
532
+ reported as `multiple`."""
533
+ addrs = {int(m.group(1)): m.group(2) for m in _AP_ADDR.finditer(transcript)}
534
+ found = [int(m.group(1)) for m in _AP_CORE_FOUND.finditer(transcript)]
535
+ cpuid = _CPUID.search(transcript)
536
+ line = _FOUND.search(transcript)
537
+ if not found and not cpuid and not line:
538
+ return None
539
+ ports = sorted(set(found))
540
+ out: dict = {
541
+ "coreFoundAp": ports[0] if len(ports) == 1 else (ports or None),
542
+ "apAddr": addrs.get(ports[0]) if len(ports) == 1 else None,
543
+ "cpuid": cpuid.group(1) if cpuid else None,
544
+ "found": line.group(0).strip() if line else None,
545
+ }
546
+ if len(ports) > 1:
547
+ out["multiple"] = [addrs.get(p) for p in ports]
548
+ return out
549
+
550
+
551
+ def ap_verdict(attached: dict | None) -> str:
552
+ """`he` / `hp` from the Core-found AP's address; `multiple` / `unidentified` otherwise."""
553
+ if not attached:
554
+ return "unidentified"
555
+ if "multiple" in attached:
556
+ # More than one Core-found AP. If ANY of them is the HP's, that is HP evidence
557
+ # (non-overridable); otherwise it is merely unplaceable.
558
+ for addr in attached["multiple"]:
559
+ try:
560
+ if addr is not None and int(addr, 16) == HP_AP_ADDR:
561
+ return "hp"
562
+ except ValueError:
563
+ continue
564
+ return "multiple"
565
+ try:
566
+ addr = int(attached["apAddr"], 16)
567
+ except (TypeError, ValueError):
568
+ return "unidentified"
569
+ return {HE_AP_ADDR: "he", HP_AP_ADDR: "hp"}.get(addr, "unidentified")
570
+
571
+
572
+ # ── which core did the generic attach land on? (tan-cli#1354) ───────────────
573
+ #
574
+ # J-Link's "Found Cortex-M55 r1p0" is the same line for the HE and the HP core, and a
575
+ # generic `Cortex-M55` attach takes whichever M55 access port it finds. The DECISION
576
+ # rests on the access port: the AP that reports `AP[n]: Core found`, whose APAddr is
577
+ # 0x00300000 for the HE and 0x00200000 for the HP.
578
+ #
579
+ # CORROBORATION is a single read-only `mem32` pair: a core's LOCAL ITCM at 0x0 is its own
580
+ # global window, HE 0x58000000 (alp-sdk metadata/socs/alif/ensemble/e8.json
581
+ # `itcm_global_base` 1476395008 at line 106; docs/aen-bench-bringup.md:20 "loadAddress=
582
+ # 0x50000000 = HP ITCM global, vs HE's 0x58000000"). The HP window 0x50000000 is NEVER
583
+ # read, and this is MEASURED, not cautious. Bench round 8 (2026-10-07, evk-02, AEN803
584
+ # 2026W36-0001, read-only A/B): A2 = `mem32 0x0` + `mem32 0x58000000` ONLY -> A3 `h` OK
585
+ # (DHCSR 00130003). A4 = the full check INCLUDING `mem32 0x50000000` -> A5/A6 `h` FAILS
586
+ # (`WARNING: CPU could not be halted`, DHCSR 00110003) and the core stays unhaltable until a
587
+ # PIN reset (C0-C2 recovered it); the read itself returned words with no error, and the later
588
+ # load only worked through the fallback chain ending in `SYSRESETREQ has confused core` ->
589
+ # VECTRESET. The shipped script is exactly the A2 reads: the local ITCM 0x0 and the HE
590
+ # window, nothing else.
591
+
592
+ HE_ALIAS = _ITCM_GLOBAL["M55_HE"]
593
+ CORE_CHECK_WORDS = 4
594
+
595
+ #: Halt/reset trouble J-Link reports in a load transcript. The Flow D reset markers plus
596
+ #: the ones a RAM-run's halt-then-loadbin hits first: a core that would not halt, a core
597
+ #: J-Link could not find, and the SYSRESETREQ-confused fallback (bench round 8).
598
+ RAM_RUN_TROUBLE_MARKERS = (
599
+ "CPU could not be halted",
600
+ "Could not find core",
601
+ "SYSRESETREQ has confused core",
602
+ "Failed to halt CPU",
603
+ "CPU is not halted",
604
+ "Reset: Failed",
605
+ "CPU may have not been reset",
606
+ )
607
+
608
+
609
+ def trouble_markers(transcript: str) -> tuple[str, ...]:
610
+ """Every halt/reset-trouble marker present in `transcript`, in marker order."""
611
+ return tuple(m for m in RAM_RUN_TROUBLE_MARKERS if m in transcript)
612
+
613
+ _MEM32_LINE = re.compile(r"^([0-9A-Fa-f]{8}) = ((?:[0-9A-Fa-f]{8}(?:\s+|$))+)$")
614
+
615
+
616
+ def core_check_script(pre: Sequence[str]) -> str:
617
+ """The read-only identification session: `connect` (no halt, no loadbin), then
618
+ `mem32` of the local ITCM and of the HE global alias ONLY. Addresses and the word
619
+ count are ints rendered with `0x%X`."""
620
+ reads = [f"mem32 0x{a:X}, 0x{CORE_CHECK_WORDS:X}" for a in (0x0, HE_ALIAS)]
621
+ return "\n".join([*pre, *reads, "exit"]) + "\n"
622
+
623
+
624
+ def parse_mem32(transcript: str, address: int, count: int = CORE_CHECK_WORDS) -> tuple[int, ...] | None:
625
+ """The `count` 32-bit words J-Link dumped at `address`, or `None` when the dump is
626
+ missing, short, or not contiguous from `address` (an unreadable window prints
627
+ `Could not read memory.` and no dump line). Strict: a line that does not match
628
+ `ADDR = WWWWWWWW ...` is ignored, never guessed at."""
629
+ words: dict[int, int] = {}
630
+ for raw in transcript.splitlines():
631
+ match = _MEM32_LINE.match(raw.strip())
632
+ if not match:
633
+ continue
634
+ base = int(match.group(1), 16)
635
+ for i, tok in enumerate(match.group(2).split()):
636
+ words.setdefault(base + 4 * i, int(tok, 16))
637
+ wanted = [address + 4 * i for i in range(count)]
638
+ if not all(a in words for a in wanted):
639
+ return None
640
+ return tuple(words[a] for a in wanted)
641
+
642
+
643
+ def core_verdict(local: tuple[int, ...] | None, he: tuple[int, ...] | None) -> str:
644
+ """The ITCM corroboration alone: `match` when the local view equals the HE window,
645
+ `disagree` when both read and differ, `unreadable` when either could not be read."""
646
+ if local is None or he is None:
647
+ return "unreadable"
648
+ return "match" if local == he else "disagree"
649
+
650
+
651
+ def combine_verdicts(ap: str, itcm: str) -> str:
652
+ """The decision. ONLY the access port can say HE: `he` needs `ap == he` and an ITCM
653
+ that matches or is silent. The ITCM never confirms on its own.
654
+
655
+ * `ap == he`, ITCM `disagree` -> `conflict` (a local view that is NOT the HE window
656
+ on an HE-looking attach is suspicious, and not overridable);
657
+ * `ap == hp` -> `hp` (an ITCM that matches the HE window against an HP access port is
658
+ reported as `conflict-hp`: still HP evidence, still never overridable);
659
+ * no AP evidence (`unidentified` / `multiple`) -> `unidentified`."""
660
+ if ap == "he":
661
+ return "conflict" if itcm == "disagree" else "he"
662
+ if ap == "hp":
663
+ return "conflict-hp" if itcm == "match" else "hp"
664
+ return "unidentified"
665
+
666
+
667
+ #: Verdicts `--assume-he` may override: the evidence is MISSING, not contrary. Anything
668
+ #: that involves HP evidence, or a contradiction on an HE attach, is never overridable.
669
+ OVERRIDABLE_VERDICTS = frozenset({"unidentified", "unreadable"})
670
+
671
+
672
+ def core_check(transcript: str) -> tuple[str, dict]:
673
+ """`(verdict, evidence)` for a [`core_check_script`] transcript: the Core-found AP
674
+ (decisive) combined with the ITCM words (corroboration). `evidence` is the envelope's
675
+ `ram.coreCheck` body."""
676
+ local = parse_mem32(transcript, 0x0)
677
+ he = parse_mem32(transcript, HE_ALIAS)
678
+
679
+ def hexed(words):
680
+ return None if words is None else [f"0x{w:08X}" for w in words]
681
+
682
+ attached = attached_core(transcript)
683
+ ap = ap_verdict(attached)
684
+ itcm = core_verdict(local, he)
685
+ evidence = {
686
+ "ap": attached,
687
+ "apVerdict": ap,
688
+ "itcmWords": {
689
+ "local0x00000000": hexed(local),
690
+ f"he0x{HE_ALIAS:08X}": hexed(he),
691
+ },
692
+ "itcmVerdict": itcm,
693
+ }
694
+ return combine_verdicts(ap, itcm), evidence