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.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- 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)
|