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
@@ -0,0 +1,351 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan model run --device` / `ab --device` (tan-cli#1287, on-device tier).
3
+
4
+ The target runs a benchmark app (alp-sdk's `examples/aen/aen-inference-energy`
5
+ speaks the console protocol `tan.core.model_device` parses) and prints
6
+ per-window timing and, when the board has the monitor IC, power samples. This
7
+ module turns that console text into the same `result` envelope the host tier
8
+ fills, with `tier: device`; deployment and capture belong to tan's own flash and
9
+ monitor paths, so they enter through ONE seam, `LIVE_FLOW`:
10
+
11
+ * `--capture FILE` ingests a console capture taken by whoever ran the board
12
+ (the bench's `ram-run.sh`, `tan monitor`, a `ram_console_buf` dump). It needs
13
+ no hardware, no extra and no onnxruntime -- parsing is stdlib.
14
+ * Without `--capture`, `--device` calls `LIVE_FLOW(...)`, which RAM-runs the
15
+ already-built `diagnostics.link: itcm` project through `tan flash --ram`'s own
16
+ Flow C and returns the RAM console (`tan.commands.model_device_live`); the row
17
+ then carries `source: live`. `capture_via` composes any deploy/read pair with
18
+ the same coded failures.
19
+
20
+ powerMj is always `null` on this tier (energy is reported in `energy`, with its
21
+ scope label, from usable active+idle sample pairs only). peakSramKib is
22
+ ENERGY-CFG `sram_peak_bytes` / 1024 and `model` falls back to ENERGY-CFG `model`
23
+ when no model file is named (tan-cli#1404); both are `null` if the app omits them.
24
+ Refusals are plain `Issue(...)` returns so the registry gate sees the codes.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import os
30
+ import stat
31
+ from collections.abc import Callable
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ from tan.commands.build_output import ProjectContext, resolve_project_context
36
+ from tan.commands.model_device_live import LiveOptions, LiveRefusal, LiveRun, live_console, preflight
37
+ from tan.commands.model_host_cmd import (
38
+ ab_empty_data,
39
+ resolve_model_path,
40
+ run_empty_data,
41
+ )
42
+ from tan.core.model_device import (
43
+ NOISE_SIGMA,
44
+ DeviceCaptureError,
45
+ parse_console,
46
+ run_result_from_capture,
47
+ )
48
+ from tan.envelope import Issue, Project, SdkInfo
49
+ from tan.exit_codes import ExitCode
50
+ from tan.model.measure import compare
51
+
52
+ Result = tuple[Project, SdkInfo | None, dict, list[Issue], ExitCode]
53
+
54
+ #: Largest console capture read from disk (a 64 KiB RAM console is the app's own
55
+ #: ceiling; this only stops a wrong file being slurped).
56
+ MAX_CAPTURE_BYTES = 8 * 1024 * 1024
57
+
58
+ #: Longest `consoleText` carried in a live row, in CHARACTERS (not bytes). A larger
59
+ #: console is trimmed (`consoleTruncated: true`); the saved
60
+ #: `model-console-*.txt` always holds all of it. Over the cap the TAIL is kept --
61
+ #: `LATENCY-RESULT` is printed last.
62
+ MAX_CONSOLE_TEXT_CHARS = 64 * 1024
63
+
64
+ #: `(context, model_label, options) -> console text | Issue`: RAM-run the project
65
+ #: through Flow C and return its console (`model_device_live.live_console`). A seam
66
+ #: so a test can substitute a deploy that needs no probe.
67
+ LIVE_FLOW: Callable[[ProjectContext, str | None, LiveOptions], LiveRun | LiveRefusal] = live_console
68
+
69
+
70
+ def capture_via(
71
+ deploy: Callable[[], Issue | None], read_console: Callable[[], str | Issue]
72
+ ) -> str | Issue:
73
+ """Run `deploy()` (an `Issue` means it failed) then `read_console()`;
74
+ the first failure is returned verbatim, so flash/monitor refusals keep their
75
+ own codes."""
76
+ failed = deploy()
77
+ if failed is not None:
78
+ return failed
79
+ return read_console()
80
+
81
+
82
+ def _read_capture(context: ProjectContext, raw: str, role: str) -> str | Issue:
83
+ """The capture file's text. Only a regular file is read -- never a FIFO,
84
+ device or /proc entry -- and at most `MAX_CAPTURE_BYTES + 1` bytes of it, so
85
+ a file that grows or lies about its size cannot be slurped."""
86
+ path = Path(raw)
87
+ if not path.is_absolute():
88
+ path = Path(context.workspace_root) / path
89
+ try:
90
+ fd = os.open(path, os.O_RDONLY | getattr(os, "O_NONBLOCK", 0))
91
+ with os.fdopen(fd, "rb") as handle:
92
+ if not stat.S_ISREG(os.fstat(handle.fileno()).st_mode):
93
+ return Issue("model.device-capture-invalid", "error", f"{role} {path} is not a regular file.")
94
+ data = handle.read(MAX_CAPTURE_BYTES + 1)
95
+ except OSError as err:
96
+ return Issue("model.device-capture-missing", "error", f"Cannot read {role} {path}: {err}")
97
+ if len(data) > MAX_CAPTURE_BYTES:
98
+ return Issue("model.device-capture-invalid", "error", f"{role} {path} is larger than {MAX_CAPTURE_BYTES} bytes.")
99
+ return data.decode("utf-8", errors="replace")
100
+
101
+
102
+ def _console_for(
103
+ context: ProjectContext, capture: str | None, label: str | None, role: str, live: LiveOptions
104
+ ) -> LiveRun | LiveRefusal:
105
+ if capture:
106
+ text = _read_capture(context, capture, role)
107
+ return LiveRefusal([text]) if isinstance(text, Issue) else LiveRun(text, [], None)
108
+ return LIVE_FLOW(context, label, live)
109
+
110
+
111
+ def _row(
112
+ label: str | None, text: str, run: LiveRun | None = None, project_path: str | None = None
113
+ ) -> tuple[dict, Any] | Issue:
114
+ """The `result` row for one console capture, or the `model.device-capture-invalid` refusal."""
115
+ try:
116
+ result, energy, diag = run_result_from_capture(parse_console(text))
117
+ except (DeviceCaptureError, KeyError, ValueError, TypeError, ArithmeticError, AttributeError,
118
+ RecursionError) as err:
119
+ return Issue(
120
+ "model.device-capture-invalid", "error",
121
+ f"Unusable device capture: {err}; if the app had not finished, raise --wait.",
122
+ )
123
+ size = None
124
+ if label:
125
+ try:
126
+ size = os.stat(label).st_size
127
+ except OSError:
128
+ size = None
129
+ live = run is not None and run.flash is not None
130
+ extra = {"source": "capture"}
131
+ if live:
132
+ extra = {
133
+ "source": "live", "project": project_path, "flash": run.flash,
134
+ "consoleText": text[-MAX_CONSOLE_TEXT_CHARS:],
135
+ "consoleTruncated": len(text) > MAX_CONSOLE_TEXT_CHARS,
136
+ }
137
+ return {
138
+ **extra,
139
+ "model": label or diag.get("model"),
140
+ "backend": result.backend,
141
+ "tier": "device",
142
+ # The app's own LATENCY-RESULT ms_per_inference when printed (scope
143
+ # "device-latency-result"), else the median over active windows of
144
+ # (window span / inferences completed); the latter always stays in
145
+ # diagnostics.windowLatencyMs.
146
+ "latencyMs": result.latency_ms,
147
+ "latencyScope": diag["latencyScope"],
148
+ "outputArgmax": None,
149
+ "peakSramKib": result.peak_sram_kib,
150
+ # Always null on the device tier: energy is reported in `energy`, with
151
+ # its own scope label, never as a bare number.
152
+ "powerMj": None,
153
+ # Inferences timed across the active windows (NOT a repeat count).
154
+ "runs": result.runs,
155
+ "sizeBytes": size,
156
+ "energy": None if energy is None else {
157
+ "valueMjPerInference": energy.value_mj_per_inference,
158
+ "spreadMj": energy.spread_mj,
159
+ "rails": list(energy.rails),
160
+ "nInferences": energy.n_inferences,
161
+ "windowMs": energy.window_ms,
162
+ "sampleCount": energy.sample_count,
163
+ "source": energy.source,
164
+ "scope": energy.scope,
165
+ },
166
+ "diagnostics": diag,
167
+ }, result
168
+
169
+
170
+ def _degraded(row: dict, label: str) -> list[Issue]:
171
+ diag = row["diagnostics"]
172
+ unresolved = diag.get("unresolvedEnergy")
173
+ issues = [] if unresolved is None else [_unresolved_issue(unresolved, label)]
174
+ # The device's own unresolvable-delta line is the unresolved issue's evidence; not repeated here.
175
+ warn = [w for w in diag["warnLines"] if unresolved is None or w != unresolved["deviceLine"]]
176
+ notes = [*diag["werrLines"], *warn, *diag["skippedPairs"]]
177
+ if diag.get("energyNote"):
178
+ notes.append(diag["energyNote"])
179
+ if not diag["npuDispatched"]:
180
+ notes.append("the app reported npu_dispatched=false (NPU did not run the model)")
181
+ if notes:
182
+ issues.append(
183
+ Issue("model.device-capture-degraded", "warning", f"{label}: " + "; ".join(notes[:5]))
184
+ )
185
+ return issues
186
+
187
+
188
+ def _unresolved_issue(unresolved: dict, label: str) -> Issue:
189
+ """tan-cli#1387: the energy delta is below the rail's noise, so `energy` is null."""
190
+ spread = unresolved["spreadMj"]
191
+ spread_text = "no spread (one pair)" if spread is None else f"spread {spread:.6g} mJ"
192
+ why = (
193
+ f"the device reported `{unresolved['deviceLine']}`"
194
+ if unresolved["verdict"] == "device"
195
+ else f"the mean is not {NOISE_SIGMA:g} standard errors above zero over "
196
+ f"{unresolved['pairs']} pair(s)"
197
+ )
198
+ return Issue(
199
+ "model.device-energy-unresolved", "warning",
200
+ f"{label}: energy on rail `{unresolved['rail']}` is not resolvable -- mean "
201
+ f"{unresolved['valueMjPerInference']:.6g} mJ/inference, {spread_text}; {why}. "
202
+ "`energy` is null (the raw values are in diagnostics.unresolvedEnergy); "
203
+ "latency is unaffected. The inference load may be below this rail's noise, "
204
+ "or the rail may be the wrong one.",
205
+ )
206
+
207
+
208
+ def _device_label(context: ProjectContext, source: str | None) -> str | None | Issue:
209
+ """The model label: the file given (it must exist), or `None`."""
210
+ if not source:
211
+ return None
212
+ resolved = resolve_model_path(context, source, "run")
213
+ return resolved if isinstance(resolved, Issue) else resolved.as_posix()
214
+
215
+
216
+ def run_device_run(
217
+ *, context: ProjectContext, source: str | None, capture: str | None, live: LiveOptions | None = None
218
+ ) -> Result:
219
+ project, sdk = context.project(), context.sdk
220
+ data = run_empty_data()
221
+
222
+ def refuse(issue: Issue) -> Result:
223
+ return project, sdk, data, [issue], ExitCode.VALIDATION_FAILURE
224
+
225
+ label = _device_label(context, source)
226
+ if isinstance(label, Issue):
227
+ return refuse(label)
228
+ live = live or LiveOptions()
229
+ run = _console_for(context, capture, label, "the run", live)
230
+ if isinstance(run, LiveRefusal):
231
+ if run.flash is not None:
232
+ data["flash"] = run.flash
233
+ return project, sdk, data, run.issues, run.exit_code
234
+ built = _row(label, run.text, run, context.workspace_root)
235
+ if isinstance(built, Issue):
236
+ if run.flash is not None:
237
+ data["flash"] = run.flash # the board WAS loaded and reset
238
+ return project, sdk, data, [*run.issues, built], ExitCode.VALIDATION_FAILURE
239
+ row, _ = built
240
+ data["model"] = row["model"]
241
+ data["result"] = row
242
+ return project, sdk, data, [*run.issues, *_degraded(row, "device run")], ExitCode.SUCCESS
243
+
244
+
245
+ def _note_loaded(data: dict, flashes: dict, role: str, flash: dict | None) -> None:
246
+ """Refused `ab`: report every side that had already RAM-loaded and reset the board."""
247
+ if flash is not None:
248
+ flashes[role.lower()] = flash
249
+ if flashes:
250
+ data["flash"] = dict(flashes)
251
+
252
+
253
+ def _preflight_both(contexts: list[ProjectContext], live: LiveOptions) -> Issue | None:
254
+ """tan-cli#1522: both projects' preflight (board.yaml, manifest, ITCM slice)
255
+ up front -- B's refusal must not come after A already reset the board."""
256
+ for ctx in contexts:
257
+ refused = preflight(ctx, live)
258
+ if refused is not None:
259
+ return refused
260
+ return None
261
+
262
+
263
+ def _scope_mismatch(a_row: dict, b_row: dict) -> list[Issue]:
264
+ """tan-cli#1522: latencies measured differently (a window span is an upper
265
+ bound) make `faster`/`latencyRatio` not like-for-like."""
266
+ if a_row["latencyScope"] == b_row["latencyScope"]:
267
+ return []
268
+ return [Issue(
269
+ "model.device-ab-scope-mismatch", "warning",
270
+ f"A's latency is {a_row['latencyScope']} but B's is {b_row['latencyScope']}: "
271
+ "the two are not like-for-like (a window span includes the energy polling and is an "
272
+ "upper bound), so `faster` and `latencyRatio` are not a reliable comparison; "
273
+ "print LATENCY-RESULT on both sides.",
274
+ )]
275
+
276
+
277
+ def _second_context(context: ProjectContext, capture: str | None,
278
+ against_capture: str | None, live: LiveOptions) -> ProjectContext | Issue:
279
+ """B's project context, or the refusal for a mixed/underspecified A-vs-B."""
280
+ if bool(capture) != bool(against_capture):
281
+ return Issue(
282
+ "model.device-ab-mixed-sources", "error",
283
+ "`ab --device` takes both sides live (--against-project, --confirm) or both from "
284
+ "captures (--capture and --against-capture), not one of each; nothing was run.",
285
+ )
286
+ context_b = context
287
+ if not against_capture and live.against_project:
288
+ context_b = resolve_project_context(live.against_project, None, live.sdk_root)
289
+ if not against_capture and context_b is context:
290
+ return Issue(
291
+ "model.device-live-needs-two-projects", "error",
292
+ "`ab --device` without --against-capture runs each project in turn: pass the second "
293
+ "project with --against-project (nothing was run).",
294
+ )
295
+ return context_b
296
+
297
+
298
+ def run_device_ab(
299
+ *,
300
+ context: ProjectContext,
301
+ source: str | None,
302
+ against: str | None,
303
+ capture: str | None,
304
+ against_capture: str | None,
305
+ live: LiveOptions | None = None,
306
+ ) -> Result:
307
+ project, sdk = context.project(), context.sdk
308
+ data = ab_empty_data()
309
+ rows: list[tuple[dict, Any]] = []
310
+ issues: list[Issue] = []
311
+ flashes: dict[str, dict] = {}
312
+ live = live or LiveOptions()
313
+ context_b = _second_context(context, capture, against_capture, live)
314
+ if isinstance(context_b, Issue):
315
+ return project, sdk, data, [context_b], ExitCode.VALIDATION_FAILURE
316
+ sides = (("A", source, capture, context), ("B", against, against_capture, context_b))
317
+ # Both labels first: a missing model file must refuse before A loads the board.
318
+ labels = [_device_label(ctx, raw) for _, raw, _, ctx in sides]
319
+ for label in labels:
320
+ if isinstance(label, Issue):
321
+ return project, sdk, data, [label], ExitCode.VALIDATION_FAILURE
322
+ if not capture:
323
+ refused = _preflight_both([ctx for _r, _raw, _c, ctx in sides], live)
324
+ if refused is not None:
325
+ return project, sdk, data, [refused], ExitCode.VALIDATION_FAILURE
326
+ for (role, _raw, cap, ctx), label in zip(sides, labels):
327
+ run = _console_for(ctx, cap, label, f"model {role}", live)
328
+ if isinstance(run, LiveRefusal):
329
+ _note_loaded(data, flashes, role, run.flash)
330
+ return project, sdk, data, [*issues, *run.issues], run.exit_code
331
+ built = _row(label, run.text, run, ctx.workspace_root)
332
+ if isinstance(built, Issue):
333
+ _note_loaded(data, flashes, role, run.flash)
334
+ return project, sdk, data, [*issues, *run.issues, built], ExitCode.VALIDATION_FAILURE
335
+ rows.append(built)
336
+ if run.flash is not None:
337
+ flashes[role.lower()] = run.flash
338
+ issues.extend(run.issues)
339
+ issues.extend(_degraded(built[0], f"device run {role}"))
340
+ (a_row, a_res), (b_row, b_res) = rows
341
+ cmp = compare(a_res, b_res)
342
+ data["a"], data["b"] = a_row, b_row
343
+ issues.extend(_scope_mismatch(a_row, b_row))
344
+ data["comparison"] = {
345
+ "faster": cmp.faster,
346
+ "latencyRatio": cmp.latency_ratio,
347
+ "aLatencyMs": cmp.a_latency_ms,
348
+ "bLatencyMs": cmp.b_latency_ms,
349
+ "sizeDeltaBytes": None,
350
+ }
351
+ return project, sdk, data, issues, ExitCode.SUCCESS
@@ -0,0 +1,234 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The live tier of `tan model run --device` / `ab --device` (tan-cli#1287).
3
+
4
+ `live_console` RAM-runs an already-built `diagnostics.link: itcm` project through
5
+ AEN Flow C -- the same `flash_cmd._run(ram=True, ram_console=True)` that
6
+ `tan flash --ram --ram-console` calls, in-process, never a subprocess of tan --
7
+ and returns the `ram_console_buf` text for `model_device_cmd` to hand to the
8
+ capture parser. The benchmark app (alp-sdk `examples/aen/aen-inference-latency`
9
+ or `aen-inference-energy`) must have the model baked in: tan does not build,
10
+ convert or embed it here.
11
+
12
+ `--ram` is the only write path used, so MRAM is never touched. Every flash
13
+ refusal is passed through with its own `flash.*` code; the refusals this module
14
+ adds are plain `Issue(...)` returns so the registry gate sees the codes.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import time
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+
24
+ from tan.commands import flash_cmd
25
+ from tan.commands.build_output import ProjectContext
26
+ from tan.core.jlink_probe import is_valid_usb_path
27
+ from tan.core.link_refusal import RAM_RUN_ONLY_METHOD
28
+ from tan.core.flash_plan import ManifestError, confirm_gate_note, parse_system_manifest
29
+ from tan.envelope import Issue
30
+ from tan.exit_codes import ExitCode
31
+
32
+ DEFAULT_CORE = "m55_he"
33
+ DEFAULT_WAIT_S = 1.5
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class LiveOptions:
38
+ """The flags a live device run forwards to Flow C."""
39
+
40
+ core: str = DEFAULT_CORE
41
+ wait: float = DEFAULT_WAIT_S
42
+ confirm: bool = False
43
+ probe_serial: str | None = None
44
+ probe_usb_path: str | None = None
45
+ jlink: str | None = None
46
+ sdk_root: str | None = None
47
+ against_project: str | None = None
48
+ #: The user's own `--board-yaml`, forwarded to Flow C; `None` when left at the default.
49
+ board_yaml: str | None = None
50
+
51
+ def any_set(self) -> bool:
52
+ return self != LiveOptions(sdk_root=self.sdk_root, board_yaml=self.board_yaml)
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class LiveRun:
57
+ """A successful RAM-run: the console, EVERY `flash.*` issue Flow C raised
58
+ (warnings such as `flash.probe-unverified` included -- they qualify the
59
+ measurement), and the `flash` provenance block for the result row."""
60
+
61
+ text: str
62
+ issues: list[Issue]
63
+ flash: dict | None = None
64
+ #: Where the raw console was saved (`<build>/flash-logs/model-console-<ts>.txt`), if it was.
65
+ console_path: str | None = None
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class LiveRefusal:
70
+ """A refusal: the issues to report (Flow C's own, verbatim and in order) and
71
+ the exit code -- Flow C's `ExitCode` is kept, a model-side refusal is a
72
+ validation failure."""
73
+
74
+ issues: list[Issue]
75
+ exit_code: ExitCode = ExitCode.VALIDATION_FAILURE
76
+ #: The `flash` provenance block when the image WAS RAM-loaded and reset before
77
+ #: the refusal (tan-cli#1417); `None` when nothing reached the board.
78
+ flash: dict | None = None
79
+
80
+
81
+ def _refuse(issue: Issue, *before: Issue) -> LiveRefusal:
82
+ return LiveRefusal([*before, issue])
83
+
84
+
85
+ def preflight(context: ProjectContext, live: LiveOptions) -> Issue | None:
86
+ """The project must be built for RAM-run: board.yaml, a manifest, and a
87
+ `ram_run_only` (ITCM-linked) slice for the chosen core."""
88
+ if not Path(context.board_yaml).is_file():
89
+ return Issue(
90
+ "model.device-no-project", "error",
91
+ f"No project at {context.workspace_root} (board.yaml not found at {context.board_yaml}); "
92
+ "`--device` without --capture needs a built diagnostics.link: itcm project (--project).",
93
+ )
94
+ if live.probe_usb_path is not None and not is_valid_usb_path(live.probe_usb_path):
95
+ return Issue(
96
+ "model.device-probe-invalid", "error",
97
+ f"{live.probe_usb_path!r} is not a USB port path like 3-4.2 (<bus>-<port>[.<port>...]).",
98
+ )
99
+ manifest_path = Path(context.workspace_root) / "build" / "system-manifest.yaml"
100
+ try:
101
+ manifest = parse_system_manifest(manifest_path.read_text(encoding="utf-8"))
102
+ except (OSError, UnicodeDecodeError, ManifestError) as err:
103
+ return Issue(
104
+ "model.device-no-manifest", "error",
105
+ f"No usable system-manifest.yaml at {manifest_path} ({err}); run `tan build --project "
106
+ f"{context.workspace_root}` for the benchmark app first.",
107
+ )
108
+ if not any(s.core_id == live.core and s.flash_method == RAM_RUN_ONLY_METHOD for s in manifest.slices):
109
+ return Issue(
110
+ "model.device-no-itcm-slice", "error",
111
+ f"The manifest has no RAM-run-only slice for core {live.core}: the benchmark app must be "
112
+ "built with board.yaml `diagnostics.link: itcm` (an ITCM-linked image, the only kind "
113
+ "`tan flash --ram` runs).",
114
+ )
115
+ return None
116
+
117
+
118
+ def _provenance(entry: dict, live: LiveOptions, console_path: str | None = None) -> dict:
119
+ jlink = entry.get("jlink") or {}
120
+ block = {
121
+ "core": live.core,
122
+ "wait": live.wait,
123
+ "transcriptPath": jlink.get("transcriptPath"),
124
+ "attachedCore": jlink.get("attachedCore"),
125
+ }
126
+ if jlink.get("dpidr"):
127
+ block["dpidr"] = jlink["dpidr"]
128
+ if console_path:
129
+ block["consolePath"] = console_path
130
+ return block
131
+
132
+
133
+ def _save_console(
134
+ context: ProjectContext, text: object, build_root: object = None
135
+ ) -> tuple[str | None, list[Issue]]:
136
+ """Write the raw RAM console next to the Flow C transcript
137
+ (`<build>/flash-logs/model-console-<UTC ts>.txt`, never overwritten) so
138
+ `latencyMs` can be checked against the app's own `LATENCY-RESULT`. A write
139
+ failure is a warning: the measurement is still valid. `build_root` is the one
140
+ Flow C itself used (so the file sits beside its transcript, nested layouts
141
+ included); a blank console is not saved."""
142
+ if not isinstance(text, str) or not text.strip():
143
+ return None, []
144
+ root = Path(build_root) if isinstance(build_root, str) and build_root else Path(context.workspace_root) / "build"
145
+ logs = root / "flash-logs"
146
+ base = time.strftime("model-console-%Y%m%dT%H%M%SZ", time.gmtime())
147
+ try:
148
+ logs.mkdir(parents=True, exist_ok=True)
149
+ n = 0
150
+ while True:
151
+ path = logs / (f"{base}.txt" if n == 0 else f"{base}-{n}.txt")
152
+ try:
153
+ with open(path, "x", encoding="utf-8", newline="\n") as handle:
154
+ handle.write(text)
155
+ return str(path), []
156
+ except FileExistsError:
157
+ n += 1
158
+ except OSError as err:
159
+ return None, [Issue(
160
+ "model.device-console-unsaved", "warning",
161
+ f"The raw RAM console could not be saved under {logs}: {err}",
162
+ )]
163
+
164
+
165
+ def live_console(context: ProjectContext, label: str | None, live: LiveOptions) -> LiveRun | LiveRefusal:
166
+ """RAM-run the project and return its console plus Flow C's issues, or the refusal."""
167
+ refused = preflight(context, live)
168
+ if refused is not None:
169
+ return _refuse(refused)
170
+ code, data, issues, _lines, _sdk = flash_cmd._run(
171
+ app_path=context.workspace_root,
172
+ build_root_arg=None,
173
+ sdk_root_arg=live.sdk_root,
174
+ board_yaml=live.board_yaml,
175
+ core=live.core,
176
+ helper=None,
177
+ dry_run=False,
178
+ skip_missing_tools=False,
179
+ capture=True,
180
+ cwd=context.workspace_root,
181
+ confirm_flag=live.confirm,
182
+ probe_serial=live.probe_serial,
183
+ probe_usb_path=live.probe_usb_path,
184
+ jlink_path=live.jlink,
185
+ ram=True,
186
+ ram_console=True,
187
+ ram_wait=live.wait,
188
+ )
189
+ entries = data.get("entries") or []
190
+ entry = entries[0] if entries else {}
191
+ flash_issues = list(issues)
192
+ if entry.get("status") == "planned":
193
+ # First, so a consumer reading issues[0] sees the actionable refusal; Flow C's
194
+ # own `flash.nothing-flashed` / `flash.confirm-required` follow it.
195
+ return LiveRefusal([Issue(
196
+ "model.device-confirm-required", "error",
197
+ "A live device run RAM-loads the image and resets the whole device (including the Secure "
198
+ "Enclave); nothing was run: "
199
+ f"{confirm_gate_note('--confirm was not given', 'run on the device')}.",
200
+ ), *flash_issues])
201
+ failed = int(code) != 0 or entry.get("status") == "failed"
202
+ if failed:
203
+ if not any(i.severity == "error" for i in flash_issues):
204
+ flash_issues.append(Issue(
205
+ "model.device-flash-failed", "error",
206
+ f"The Flow C RAM-run failed: {entry.get('message') or 'no detail reported'}",
207
+ ))
208
+ # tan-cli#1486: the registry documents `model.device-flash-failed` as
209
+ # VALIDATION_FAILURE; a Flow C error issue passed through keeps Flow C's own code.
210
+ synthesised = not any(i.severity == "error" for i in issues)
211
+ if synthesised:
212
+ exit_code = ExitCode.VALIDATION_FAILURE
213
+ else:
214
+ exit_code = ExitCode(int(code)) if int(code) else ExitCode.RUNTIME_FAILURE
215
+ # tan-cli#1497: a failure AFTER the image was loaded and the device reset
216
+ # (Flow C's `ram.loaded`) still happened to the board: keep the provenance.
217
+ loaded = bool((entry.get("ram") or {}).get("loaded"))
218
+ return LiveRefusal(flash_issues, exit_code,
219
+ flash=_provenance(entry, live) if loaded else None)
220
+ console = entry.get("ramConsole") or {}
221
+ text = console.get("text")
222
+ console_path, save_issues = _save_console(context, text, data.get("buildRoot"))
223
+ flash = _provenance(entry, live, console_path)
224
+ if not isinstance(text, str) or not text.strip():
225
+ which = console.get("selected")
226
+ why = (
227
+ "the build selected the UART console, which a RAM-run cannot read"
228
+ if which == "uart" else f"nothing was printed in the {live.wait:g}s wait"
229
+ )
230
+ return LiveRefusal([*flash_issues, Issue(
231
+ "model.device-console-empty", "error",
232
+ f"The RAM console is empty ({why}); raise --wait or check the benchmark app.",
233
+ )], flash=flash)
234
+ return LiveRun(text, [*flash_issues, *save_issues], flash, console_path)