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,626 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan monitor` -- open a serial console to the attached board.
|
|
3
|
+
|
|
4
|
+
Port of `scripts/alp_cli/monitor.py` (73 lines): a thin front door over
|
|
5
|
+
pyserial's `miniterm`. Port comes from `--port`; baud from `--baud` (default
|
|
6
|
+
115200, the SDK-wide console default).
|
|
7
|
+
|
|
8
|
+
There is no safe cross-platform guess for the port itself (COMx vs
|
|
9
|
+
`/dev/ttyUSBx` vs `/dev/cu.*`), so when no port is given -- or the requested
|
|
10
|
+
one does not exist -- this command lists every serial port pyserial can see
|
|
11
|
+
and refuses instead of hanging on a wrong device.
|
|
12
|
+
|
|
13
|
+
"Does not exist" is the stated rule, and until tan-cli#569 the code was
|
|
14
|
+
stricter than it: the gate tested membership in `comports()`, which refuses
|
|
15
|
+
every port pyserial can OPEN but does not ENUMERATE. `_port_is_usable` is now
|
|
16
|
+
the gate, with three accepting arms -- enumerated (plus the `\\\\.\\` alias of
|
|
17
|
+
tan-cli#701), a character device on this host (`/dev/serial/by-id/...`
|
|
18
|
+
symlinks, the case #569 was filed on), or one of pyserial's own URL schemes
|
|
19
|
+
(`socket://`, `rfc2217://`, ... , which have no local device path at all).
|
|
20
|
+
|
|
21
|
+
Board-context port resolution -- filling in `--port` from the current project
|
|
22
|
+
instead of asking for it -- is deliberately NOT implemented here, and it is
|
|
23
|
+
not simply unstarted (tan-cli#255): the build-plan already carries a
|
|
24
|
+
`slices[].debug.console` selector per slice (`build-plan-v1.schema.json`,
|
|
25
|
+
issue #610 §4; computed here too, at `tan/planner/buildplan.py::_slice_debug`,
|
|
26
|
+
and independently in alp-sdk's own `scripts/alp_orchestrate/buildplan.py`),
|
|
27
|
+
resolving to `"uart"` / `"ram"` / `"linux"` / `null`. That is a console
|
|
28
|
+
BACKEND CLASS, not a port: it says a slice's console is a UART (as opposed to
|
|
29
|
+
a RAM console read over SWD, or a Linux tty), never which host-visible device
|
|
30
|
+
that UART shows up as. Nothing in `board.yaml` or the build-plan carries a
|
|
31
|
+
VID:PID, serial number, or platform-specific device path for a board's
|
|
32
|
+
console UART, so `debug.console == "uart"` still leaves every USB-serial
|
|
33
|
+
adapter on the bench indistinguishable to this host OS -- reading it would not
|
|
34
|
+
let this command fill in `--port`. Teach this verb to read a real per-board
|
|
35
|
+
physical-port fact once metadata carries one; `debug.console` alone is not
|
|
36
|
+
that fact.
|
|
37
|
+
|
|
38
|
+
**No alp-sdk checkout required, unlike `model`.** The oracle's `monitor.py`
|
|
39
|
+
imports nothing from alp-sdk beyond `alp_cli._workspace.python_exe`, itself
|
|
40
|
+
just `sys.executable` -- the running interpreter. This port does NOT read
|
|
41
|
+
`sys.executable` directly, though: under PyInstaller `sys.executable` IS
|
|
42
|
+
`tan` itself, so spawning it would just re-enter this CLI instead of
|
|
43
|
+
launching miniterm -- the same reasoning `tan.core.sdk_discovery`'s `_planner_python`
|
|
44
|
+
and `generate_cmd.py` already carry, spelled out there so it need not be
|
|
45
|
+
re-argued per call site. This port reuses that same function, a PATH name
|
|
46
|
+
(`python`/`python3`) never `sys.executable`, when frozen or when
|
|
47
|
+
`sys.executable` is empty (an embedded interpreter can report ""); the
|
|
48
|
+
running interpreter is still preferred otherwise, since it is guaranteed to
|
|
49
|
+
have `serial` importable already. Either way no SDK root is resolved, so
|
|
50
|
+
`tan monitor` no longer requires a resolvable alp-sdk checkout the way the
|
|
51
|
+
retired Rust forwarder did (`crates/tan-cli/src/commands/sdk_cli.rs` resolves
|
|
52
|
+
one unconditionally for every forward, `monitor` included, purely as an
|
|
53
|
+
artifact of sharing one function with `model`/`new-som`/`faultdecode`) -- a
|
|
54
|
+
deliberate, documented improvement, not a regression: `monitor` never read
|
|
55
|
+
anything an SDK root would supply.
|
|
56
|
+
|
|
57
|
+
**Exit code on a failed miniterm run is `RuntimeFailure` (1) regardless of the
|
|
58
|
+
child's own exit code** -- mirroring the shipped Rust forwarder
|
|
59
|
+
(`sdk_cli::run`'s `s.code().unwrap_or(1)` branch always maps to
|
|
60
|
+
`ExitCode::RuntimeFailure`), which is the customer-facing contract today, NOT
|
|
61
|
+
the oracle's literal `raise SystemExit(rc)` passthrough of whatever code
|
|
62
|
+
miniterm returned. The actual child code still reaches the issue message.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
from __future__ import annotations
|
|
66
|
+
|
|
67
|
+
import json
|
|
68
|
+
import os
|
|
69
|
+
import stat
|
|
70
|
+
import subprocess
|
|
71
|
+
import sys
|
|
72
|
+
import tempfile
|
|
73
|
+
from enum import Enum
|
|
74
|
+
from pathlib import Path
|
|
75
|
+
|
|
76
|
+
import typer
|
|
77
|
+
|
|
78
|
+
from tan.core.sdk_discovery import _planner_python
|
|
79
|
+
from tan.core import console_filter as console_filter_mod
|
|
80
|
+
from tan.core import serial_url
|
|
81
|
+
from tan.core.subprocess_env import spawn_env
|
|
82
|
+
from tan.envelope import Envelope, Issue, Project, emit
|
|
83
|
+
from tan.exit_codes import ExitCode
|
|
84
|
+
from tan.output_format import FORMAT_HELP, OutputFormat
|
|
85
|
+
|
|
86
|
+
#: The SDK-wide console default, matching `monitor.py::DEFAULT_BAUD`.
|
|
87
|
+
DEFAULT_BAUD = 115200
|
|
88
|
+
|
|
89
|
+
#: `data.schemaVersion` for this command's payload.
|
|
90
|
+
DATA_SCHEMA_VERSION = "1"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ConsoleFilter(str, Enum):
|
|
94
|
+
"""Console output filters. `colors` (tan's own, default) keeps SGR colour
|
|
95
|
+
sequences and neutralises every other escape/control byte; `default`,
|
|
96
|
+
`nocontrol`, `printable` are miniterm's own stripping filters. `direct`
|
|
97
|
+
passes the device's bytes to the terminal unmodified and is UNSAFE for an
|
|
98
|
+
untrusted target (OSC 52 clipboard writes, title changes, screen games)."""
|
|
99
|
+
|
|
100
|
+
COLORS = "colors"
|
|
101
|
+
DIRECT = "direct"
|
|
102
|
+
DEFAULT = "default"
|
|
103
|
+
NOCONTROL = "nocontrol"
|
|
104
|
+
PRINTABLE = "printable"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class MonitorError(Exception):
|
|
108
|
+
"""A refusal whose issue code and exit code are already decided."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, code: str, message: str, exit_code: ExitCode, data: dict) -> None:
|
|
111
|
+
super().__init__(message)
|
|
112
|
+
self.code = code
|
|
113
|
+
self.message = message
|
|
114
|
+
self.exit_code = exit_code
|
|
115
|
+
self.data = data
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _pyserial_missing() -> MonitorError:
|
|
119
|
+
"""The one spelling of "pyserial is not installed".
|
|
120
|
+
|
|
121
|
+
The hint names the EXTRA rather than the bare distribution because that is
|
|
122
|
+
the supported way to get it: pyserial is declared in
|
|
123
|
+
`[project.optional-dependencies] monitor`, not in `dependencies`. A frozen
|
|
124
|
+
`--onefile` build resolves it at BUILD time, so a customer holding a binary
|
|
125
|
+
built without the extra cannot pip-install their way out -- hence the second
|
|
126
|
+
sentence, which is the only actionable thing to tell them.
|
|
127
|
+
"""
|
|
128
|
+
return MonitorError(
|
|
129
|
+
"monitor.pyserial-missing",
|
|
130
|
+
"pyserial is required for `tan monitor`. Install it with "
|
|
131
|
+
'`pip install "./python[monitor]"` from a tan-cli checkout (tan-cli is '
|
|
132
|
+
"not on PyPI), or `pip install pyserial`. A frozen `tan` binary bundles "
|
|
133
|
+
"it at build time, so a binary built without that extra cannot gain it here.",
|
|
134
|
+
ExitCode.RUNTIME_FAILURE,
|
|
135
|
+
{"schemaVersion": DATA_SCHEMA_VERSION},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
#: Contract-harness seam ONLY (tan-cli#1165) -- never a documented,
|
|
140
|
+
#: `--help`-visible flag. `_available_ports()`'s real source, pyserial's own
|
|
141
|
+
#: `list_ports.comports()`, enumerates whatever serial hardware happens to be
|
|
142
|
+
#: physically attached to the host running it, so no golden envelope can pin a
|
|
143
|
+
#: non-empty, deterministic `data.availablePorts` (the field
|
|
144
|
+
#: `contract/envelopes/monitor-no-port` exists to freeze) without depending on
|
|
145
|
+
#: the recording machine's own hardware -- and on a CI runner with nothing
|
|
146
|
+
#: plugged in, `data.availablePorts` would record as `[]` forever, pinning
|
|
147
|
+
#: nothing the issue asked for. Set to a JSON-encoded `[[device, description],
|
|
148
|
+
#: ...]` array, this REPLACES the pyserial enumeration outright, in the exact
|
|
149
|
+
#: `[(device, description)]` shape every regular CALLER (`_refuse_listing_ports`,
|
|
150
|
+
#: `_port_is_usable`) already expects, so none of THOSE need to know the seam
|
|
151
|
+
#: exists. `_run_monitor` is the one exception: it also checks this variable
|
|
152
|
+
#: directly, to skip its own "pyserial is importable" precheck (see that
|
|
153
|
+
#: function's comment) -- without that second check this golden would depend
|
|
154
|
+
#: on whether pyserial happens to be installed in whatever environment replays
|
|
155
|
+
#: it, the exact host-dependence this seam exists to remove. See
|
|
156
|
+
#: `contract/envelopes/monitor-no-port/PROVENANCE.txt` for how a golden arms
|
|
157
|
+
#: it via `env.json`.
|
|
158
|
+
_TEST_PORTS_ENV = "TAN_MONITOR_TEST_PORTS_JSON"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _available_ports() -> list[tuple[str, str]]:
|
|
162
|
+
"""`[(device, description)]` for every serial port pyserial can see.
|
|
163
|
+
|
|
164
|
+
The import is guarded HERE, not only at the caller, because this is the one
|
|
165
|
+
choke point every port-listing path routes through -- and because
|
|
166
|
+
`_run_monitor`'s precheck is deliberately skipped on a FROZEN build (there
|
|
167
|
+
is no `sys.executable` worth validating there). On a `--onefile` binary
|
|
168
|
+
built without the `monitor` extra this line is therefore the FIRST place
|
|
169
|
+
pyserial is touched, and it is reached IN-PROCESS before any child is
|
|
170
|
+
spawned. Left unguarded the ImportError escaped as an unexpected exception
|
|
171
|
+
and surfaced as `monitor.internal-failure` at exit 5 -- "tan has a bug" --
|
|
172
|
+
for what is simply an optional dependency the customer never installed.
|
|
173
|
+
|
|
174
|
+
`_TEST_PORTS_ENV`, when set, short-circuits all of the above -- see its own
|
|
175
|
+
comment. A malformed value (bad JSON, the wrong shape) is a harness/fixture
|
|
176
|
+
bug, not a customer-facing one -- the same "do not let this become
|
|
177
|
+
`monitor.internal-failure`" reasoning the ImportError guard above states
|
|
178
|
+
for itself -- so it is swallowed and falls through to the real enumeration
|
|
179
|
+
below rather than escaping as an unexpected exception.
|
|
180
|
+
"""
|
|
181
|
+
fake = os.environ.get(_TEST_PORTS_ENV)
|
|
182
|
+
if fake is not None:
|
|
183
|
+
try:
|
|
184
|
+
return [(str(device), str(description)) for device, description in json.loads(fake)]
|
|
185
|
+
except (ValueError, TypeError):
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
from serial.tools import list_ports # noqa: PLC0415 (optional at runtime)
|
|
190
|
+
except ImportError as err:
|
|
191
|
+
raise _pyserial_missing() from err
|
|
192
|
+
|
|
193
|
+
return [(p.device, p.description or "") for p in list_ports.comports()]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
#: Windows' documented device-namespace prefix. `\\.\COM38` is the spelling
|
|
197
|
+
#: Microsoft documents for COM10 and above, and pyserial's win32 backend opens
|
|
198
|
+
#: it -- `serial/serialwin32.py` hands any string that does not
|
|
199
|
+
#: `startswith('COM')` straight to `CreateFile`. What it is NOT is a spelling
|
|
200
|
+
#: `comports()` ever REPORTS: `serial/tools/list_ports_windows.py` builds each
|
|
201
|
+
#: `ListPortInfo` from the registry `PortName`, which is the bare `COM38`. So a
|
|
202
|
+
#: membership test against `comports()` refuses a port pyserial would have
|
|
203
|
+
#: opened, while listing that same port in its own "not found" message
|
|
204
|
+
#: (tan-cli#701).
|
|
205
|
+
_WINDOWS_DEVICE_PREFIX = "\\\\.\\"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _port_aliases(port: str) -> set[str]:
|
|
209
|
+
"""Every spelling of `port` that `comports()` might report it under.
|
|
210
|
+
|
|
211
|
+
Deliberately narrow: this resolves a spelling pyserial itself accepts back
|
|
212
|
+
to the bare device name, rather than widening the gate. A port that is
|
|
213
|
+
genuinely absent is still refused -- see
|
|
214
|
+
`test_a_unc_port_whose_bare_form_is_absent_is_still_refused`.
|
|
215
|
+
"""
|
|
216
|
+
aliases = {port}
|
|
217
|
+
if port.startswith(_WINDOWS_DEVICE_PREFIX):
|
|
218
|
+
aliases.add(port[len(_WINDOWS_DEVICE_PREFIX):])
|
|
219
|
+
return aliases
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _url_handler_prefixes() -> frozenset[str]:
|
|
223
|
+
"""pyserial's own URL schemes, read from `serial.urlhandler` rather than
|
|
224
|
+
hardcoded here (tan-cli#569).
|
|
225
|
+
|
|
226
|
+
`serial.serial_for_url` dispatches on these, and miniterm -- which this
|
|
227
|
+
command spawns -- goes through it. `comports()` enumerates none of them, so
|
|
228
|
+
a membership test refuses every one, and for `socket://`/`rfc2217://` there
|
|
229
|
+
is no device path to fall back on: the port does not exist locally at all.
|
|
230
|
+
|
|
231
|
+
Derived by listing `serial/urlhandler/protocol_*.py`, measured on pyserial
|
|
232
|
+
3.5 as `alt://`, `cp2110://`, `hwgrep://`, `loop://`, `rfc2217://`,
|
|
233
|
+
`socket://`, `spy://`. Read from the installed package so a pyserial that
|
|
234
|
+
adds or drops one moves this set with it. An empty result (no such package)
|
|
235
|
+
yields an empty set, which leaves the gate exactly as strict as before.
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
import pkgutil # noqa: PLC0415
|
|
239
|
+
import serial.urlhandler # noqa: PLC0415
|
|
240
|
+
except ImportError:
|
|
241
|
+
return frozenset()
|
|
242
|
+
return frozenset(
|
|
243
|
+
f"{module.name[len('protocol_'):]}://"
|
|
244
|
+
for module in pkgutil.iter_modules(serial.urlhandler.__path__)
|
|
245
|
+
if module.name.startswith("protocol_")
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _is_openable_device(port: str) -> bool:
|
|
250
|
+
"""Whether `port` is a character device on this host.
|
|
251
|
+
|
|
252
|
+
THE case tan-cli#569 was filed on: `/dev/serial/by-id/usb-..._-if00`
|
|
253
|
+
symlinks. `serial.tools.list_ports` reports raw nodes (`/dev/ttyACM0`) and
|
|
254
|
+
never the by-id path, while `serial.Serial()` opens the symlink fine --
|
|
255
|
+
measured on this host against a real Artery AT32 adapter. On a bench with
|
|
256
|
+
two identical adapters whose `ttyACM` ordering swaps across reboots, the
|
|
257
|
+
by-id path is the only stable name for the intended board, so refusing it
|
|
258
|
+
forces the operator onto the unstable one.
|
|
259
|
+
|
|
260
|
+
`os.stat` follows symlinks, which is the point. Any `OSError` -- absent
|
|
261
|
+
path, permission, an embedded NUL -- reads as "not a device", so a
|
|
262
|
+
genuinely missing port is still refused rather than becoming a traceback.
|
|
263
|
+
Windows reaches this too and answers False for `COM7` (no stat-able
|
|
264
|
+
character device there), which is correct: `_port_aliases` is what covers
|
|
265
|
+
Windows, and this must not quietly widen that platform's gate.
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
return stat.S_ISCHR(os.stat(port).st_mode)
|
|
269
|
+
except (OSError, ValueError):
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _port_is_usable(port: str, enumerated: set[str]) -> bool:
|
|
274
|
+
"""The tan-cli#569 gate: openable, not merely enumerated.
|
|
275
|
+
|
|
276
|
+
Three accepting arms -- enumerated (including the `\\\\.\\` alias of
|
|
277
|
+
tan-cli#701), a character device on this host, or one of pyserial's own URL
|
|
278
|
+
schemes. A name matching none of the three is still refused, which is the
|
|
279
|
+
only reason this function is worth having over `True`.
|
|
280
|
+
"""
|
|
281
|
+
if _port_aliases(port) & enumerated:
|
|
282
|
+
return True
|
|
283
|
+
if _is_openable_device(port):
|
|
284
|
+
return True
|
|
285
|
+
return any(port.startswith(scheme) for scheme in _url_handler_prefixes())
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _ports_data(ports: list[tuple[str, str]]) -> list[dict[str, str]]:
|
|
289
|
+
return [{"device": device, "description": description} for device, description in ports]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _refuse_listing_ports(reason: str) -> MonitorError:
|
|
293
|
+
"""Port of `monitor.py::_die_listing_ports`: the reason plus every serial
|
|
294
|
+
port pyserial can see, folded into one issue message so `--format json`
|
|
295
|
+
carries the same information the oracle prints line-by-line to stderr."""
|
|
296
|
+
ports = _available_ports()
|
|
297
|
+
if ports:
|
|
298
|
+
listing = "; ".join(f"{d} {desc}".rstrip() for d, desc in ports)
|
|
299
|
+
message = f"{reason} -- available serial ports: {listing}"
|
|
300
|
+
else:
|
|
301
|
+
message = f"{reason} -- no serial ports detected on this host."
|
|
302
|
+
return MonitorError(
|
|
303
|
+
"monitor.no-port",
|
|
304
|
+
message,
|
|
305
|
+
ExitCode.RUNTIME_FAILURE,
|
|
306
|
+
{"schemaVersion": DATA_SCHEMA_VERSION, "availablePorts": _ports_data(ports)},
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _child_stdout(json_mode: bool):
|
|
311
|
+
"""What miniterm's stdout is wired to (tan-cli#491 defect 6).
|
|
312
|
+
|
|
313
|
+
TEXT mode: `None`, i.e. inherit -- board traffic on tan's stdout is the
|
|
314
|
+
whole point of an interactive console, and redirecting it would break
|
|
315
|
+
`tan monitor > board.log`.
|
|
316
|
+
|
|
317
|
+
`--format json`: the board's bytes must NOT land on stdout. pyserial's
|
|
318
|
+
miniterm writes every received byte through `Console.write` to its own
|
|
319
|
+
`sys.stdout`, so an inherited stdout puts board traffic AHEAD of the
|
|
320
|
+
envelope and a consumer's whole-stdout `JSON.parse` fails on an `ok: true`,
|
|
321
|
+
exit-0 run -- reproduced end to end against a real pty. Same rule
|
|
322
|
+
`flash_cmd` states for itself: nothing but the single JSON envelope may
|
|
323
|
+
reach stdout under `--format json`. The traffic is kept, on stderr, where
|
|
324
|
+
this command's own `monitor: <port> @ <baud>` banner and miniterm's own
|
|
325
|
+
banner already go.
|
|
326
|
+
|
|
327
|
+
`sys.__stderr__`, not `sys.stderr`: under `--format json` `cli.main` binds
|
|
328
|
+
`sys.stderr` to a `_TeeStderr`, which implements only `write`/`flush`/
|
|
329
|
+
`getvalue` -- it has no `fileno()`, and `subprocess` needs a real one to
|
|
330
|
+
hand the child. `sys.__stderr__` is the interpreter's original stream and
|
|
331
|
+
is unaffected by that rebinding. It can still be `None` (pythonw, an
|
|
332
|
+
embedded interpreter) or closed, hence the guard; `DEVNULL` is the last
|
|
333
|
+
resort, because dropping the board's bytes is bad and putting them on
|
|
334
|
+
stdout is the defect.
|
|
335
|
+
"""
|
|
336
|
+
if not json_mode:
|
|
337
|
+
return None
|
|
338
|
+
stream = sys.__stderr__
|
|
339
|
+
try:
|
|
340
|
+
if stream is not None and stream.fileno() >= 0:
|
|
341
|
+
return stream
|
|
342
|
+
except (AttributeError, OSError, ValueError):
|
|
343
|
+
pass
|
|
344
|
+
return subprocess.DEVNULL
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _spawn_console(python: str, port: str, baud: int, console_filter: str, json_mode: bool) -> int:
|
|
348
|
+
"""Run the plain console child (`python -c <bootstrap>`), returning its exit code."""
|
|
349
|
+
try:
|
|
350
|
+
# Empty cwd: `-c` puts the cwd on sys.path, so a `serial/` planted in the
|
|
351
|
+
# project dir would be imported instead of pyserial (tan-cli#1317). It
|
|
352
|
+
# also neutralises empty/relative PYTHONPATH entries (they resolve
|
|
353
|
+
# against this empty directory).
|
|
354
|
+
with tempfile.TemporaryDirectory(prefix="tan-monitor-") as empty:
|
|
355
|
+
return subprocess.run(
|
|
356
|
+
[
|
|
357
|
+
python,
|
|
358
|
+
"-c",
|
|
359
|
+
console_filter_mod.BOOTSTRAP,
|
|
360
|
+
"--filter",
|
|
361
|
+
console_filter,
|
|
362
|
+
# The spawn runs from an empty cwd, so a relative device path
|
|
363
|
+
# must be made absolute first.
|
|
364
|
+
os.path.abspath(port) if os.path.exists(port) else port,
|
|
365
|
+
str(baud),
|
|
366
|
+
],
|
|
367
|
+
stdout=_child_stdout(json_mode),
|
|
368
|
+
env=spawn_env(),
|
|
369
|
+
cwd=empty,
|
|
370
|
+
).returncode
|
|
371
|
+
except OSError as err:
|
|
372
|
+
raise MonitorError(
|
|
373
|
+
"monitor.launch-failed",
|
|
374
|
+
f"failed to launch `{python} -c <miniterm bootstrap>`: {err}",
|
|
375
|
+
ExitCode.RUNTIME_FAILURE,
|
|
376
|
+
{"schemaVersion": DATA_SCHEMA_VERSION, "port": port, "baud": baud},
|
|
377
|
+
) from err
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _run_monitor(
|
|
381
|
+
port: str | None,
|
|
382
|
+
baud: int,
|
|
383
|
+
json_mode: bool,
|
|
384
|
+
break_opts: tuple[bytes, bytes, float] | None = None,
|
|
385
|
+
non_interactive: bool = False,
|
|
386
|
+
console_filter: str = "colors",
|
|
387
|
+
capture_opts: tuple | None = None,
|
|
388
|
+
action_spec: object | None = None,
|
|
389
|
+
) -> tuple[dict, list[Issue], ExitCode]:
|
|
390
|
+
# Frozen (PyInstaller) or an embedded interpreter with no reportable
|
|
391
|
+
# `sys.executable`: fall back to a PATH name, mirroring
|
|
392
|
+
# `tan.core.sdk_discovery._planner_python` -- NOT `sys.executable`, which under a
|
|
393
|
+
# PyInstaller freeze IS `tan` itself and would just re-enter this CLI.
|
|
394
|
+
using_this_interpreter = not getattr(sys, "frozen", False) and bool(sys.executable)
|
|
395
|
+
python = (
|
|
396
|
+
sys.executable
|
|
397
|
+
if using_this_interpreter
|
|
398
|
+
else _planner_python(str(Path.cwd()), None)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# `_TEST_PORTS_ENV` set means `_available_ports()` never touches real
|
|
402
|
+
# pyserial for this run (see its own comment), so this precheck -- whose
|
|
403
|
+
# only job is proving pyserial resolves in THIS interpreter -- would just
|
|
404
|
+
# make a contract golden depend on whether pyserial happens to be
|
|
405
|
+
# installed in whatever environment ran it, the exact host-dependence the
|
|
406
|
+
# seam exists to remove.
|
|
407
|
+
if using_this_interpreter and os.environ.get(_TEST_PORTS_ENV) is None:
|
|
408
|
+
# This precheck only proves the interpreter about to be spawned --
|
|
409
|
+
# THIS one -- has pyserial. It says nothing about a PATH `python`
|
|
410
|
+
# resolved via `_planner_python()`, so skip it there; a missing
|
|
411
|
+
# pyserial in the child surfaces as the child's own reported failure.
|
|
412
|
+
try:
|
|
413
|
+
import serial # noqa: F401, PLC0415 (validates pyserial is installed)
|
|
414
|
+
except ImportError as err:
|
|
415
|
+
raise _pyserial_missing() from err
|
|
416
|
+
|
|
417
|
+
if port is None:
|
|
418
|
+
raise _refuse_listing_ports("no --port given")
|
|
419
|
+
problem = serial_url.port_url_problem(port)
|
|
420
|
+
if problem is not None:
|
|
421
|
+
raise MonitorError(
|
|
422
|
+
"monitor.bad-port",
|
|
423
|
+
f"bad --port: {problem}",
|
|
424
|
+
ExitCode.VALIDATION_FAILURE,
|
|
425
|
+
{"schemaVersion": DATA_SCHEMA_VERSION, "port": port},
|
|
426
|
+
)
|
|
427
|
+
if not _port_is_usable(port, {device for device, _ in _available_ports()}):
|
|
428
|
+
raise _refuse_listing_ports(f"port '{port}' not found")
|
|
429
|
+
|
|
430
|
+
if capture_opts is not None:
|
|
431
|
+
from tan.commands import monitor_session # noqa: PLC0415 (only on --capture)
|
|
432
|
+
|
|
433
|
+
return monitor_session.run_capture(port, baud, capture_opts, break_opts, action_spec)
|
|
434
|
+
|
|
435
|
+
if break_opts is not None:
|
|
436
|
+
from tan.commands import monitor_session # noqa: PLC0415 (only on --break-uboot)
|
|
437
|
+
|
|
438
|
+
return monitor_session.run(
|
|
439
|
+
port, baud, json_mode, break_opts, non_interactive, console_filter
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
print(f"monitor: {port} @ {baud} (Ctrl+] to quit)", file=sys.stderr)
|
|
443
|
+
rc = _spawn_console(python, port, baud, console_filter, json_mode)
|
|
444
|
+
|
|
445
|
+
data = {"schemaVersion": DATA_SCHEMA_VERSION, "port": port, "baud": baud}
|
|
446
|
+
if rc != 0:
|
|
447
|
+
return (
|
|
448
|
+
data,
|
|
449
|
+
[
|
|
450
|
+
Issue(
|
|
451
|
+
"monitor.failed",
|
|
452
|
+
"error",
|
|
453
|
+
f"`tan monitor` exited with code {rc} (see log above).",
|
|
454
|
+
)
|
|
455
|
+
],
|
|
456
|
+
ExitCode.RUNTIME_FAILURE,
|
|
457
|
+
)
|
|
458
|
+
return data, [], ExitCode.SUCCESS
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def monitor(
|
|
462
|
+
port: str = typer.Option(
|
|
463
|
+
None,
|
|
464
|
+
"--port",
|
|
465
|
+
help="Serial port (COM7, /dev/ttyUSB0, /dev/cu.usbmodem...).",
|
|
466
|
+
),
|
|
467
|
+
baud: int = typer.Option(
|
|
468
|
+
DEFAULT_BAUD, "--baud", show_default=True, help="Baud rate."
|
|
469
|
+
),
|
|
470
|
+
break_uboot: bool = typer.Option(
|
|
471
|
+
False,
|
|
472
|
+
"--break-uboot",
|
|
473
|
+
help="After opening the port, send the autoboot interrupt key repeatedly "
|
|
474
|
+
"until the U-Boot prompt appears or --break-timeout passes, then continue "
|
|
475
|
+
"in the interactive console on the same open port. With --non-interactive "
|
|
476
|
+
"it stops after the break-in instead (exit 0 if caught); the console "
|
|
477
|
+
"itself needs a terminal. --non-interactive is a shared flag (see "
|
|
478
|
+
"its own help line). Power-cycle the board yourself; works over "
|
|
479
|
+
"rfc2217:// and socket:// URLs. Use rfc2217:// against ser2net: socket:// "
|
|
480
|
+
"to a telnet/RFC2217 port delivers IAC negotiation bytes as data and "
|
|
481
|
+
"adds about 4 s.",
|
|
482
|
+
),
|
|
483
|
+
break_key: str = typer.Option(
|
|
484
|
+
None,
|
|
485
|
+
"--break-key",
|
|
486
|
+
help="With --break-uboot: key that interrupts autoboot (default: a space; "
|
|
487
|
+
"escapes \\xNN \\r \\n \\t \\\\ allowed).",
|
|
488
|
+
),
|
|
489
|
+
prompt: str = typer.Option(
|
|
490
|
+
None, "--prompt", help="With --break-uboot: prompt that ends it (default: '=> ')."
|
|
491
|
+
),
|
|
492
|
+
break_timeout: float = typer.Option(
|
|
493
|
+
None,
|
|
494
|
+
"--break-timeout",
|
|
495
|
+
help="With --break-uboot: seconds to keep sending the key (default: 30).",
|
|
496
|
+
),
|
|
497
|
+
capture: bool = typer.Option(
|
|
498
|
+
False,
|
|
499
|
+
"--capture",
|
|
500
|
+
help="Headless capture instead of an interactive console: no TTY needed, "
|
|
501
|
+
"works over pyserial URLs, stores raw bytes. Needs --duration and/or "
|
|
502
|
+
"--until; combine with --break-uboot to break in first. Never prompts "
|
|
503
|
+
"(--non-interactive is accepted and has no further effect).",
|
|
504
|
+
),
|
|
505
|
+
duration: float = typer.Option(
|
|
506
|
+
None, "--duration", help="With --capture: seconds to read (default 30 with --until)."
|
|
507
|
+
),
|
|
508
|
+
until: str = typer.Option(
|
|
509
|
+
None,
|
|
510
|
+
"--until",
|
|
511
|
+
help="With --capture: stop at the first line (or unterminated partial line, "
|
|
512
|
+
"e.g. a prompt) matching this regex; the envelope carries it. A line "
|
|
513
|
+
"longer than 4 KiB is searched on its last 4 KiB (complete or partial). A "
|
|
514
|
+
"regex cannot be interrupted inside one search. No match in time is an error.",
|
|
515
|
+
),
|
|
516
|
+
log: str = typer.Option(None, "--log", help="With --capture: write the raw bytes to this file."),
|
|
517
|
+
send: list[str] = typer.Option(
|
|
518
|
+
None, "--send", metavar="TEXT",
|
|
519
|
+
help="With --capture: write TEXT to the console (repeatable, in order; escapes "
|
|
520
|
+
"\\xNN \\r \\n \\t \\\\, no newline added). All items go out as one burst, at the "
|
|
521
|
+
"start unless --send-after or --send-on says when.",
|
|
522
|
+
),
|
|
523
|
+
send_after: float = typer.Option(
|
|
524
|
+
None, "--send-after", metavar="SECONDS", help="With --send: send the burst this long after the start."
|
|
525
|
+
),
|
|
526
|
+
send_on: str = typer.Option(
|
|
527
|
+
None, "--send-on", metavar="REGEX",
|
|
528
|
+
help="With --send: send the burst on the first line (or partial line, e.g. a prompt) "
|
|
529
|
+
"matching this regex. Excludes --send-after.",
|
|
530
|
+
),
|
|
531
|
+
send_gap: float = typer.Option(
|
|
532
|
+
None, "--send-gap", metavar="SECONDS", help="With --send: pause between items (default 0.2)."
|
|
533
|
+
),
|
|
534
|
+
reopen_at: int = typer.Option(
|
|
535
|
+
None, "--reopen-at", metavar="BAUD",
|
|
536
|
+
help="With --on: change the baud (in place, else close and reopen) on the first complete "
|
|
537
|
+
"line matching --on, then keep capturing. Works over rfc2217:// and local serial.",
|
|
538
|
+
),
|
|
539
|
+
on: str = typer.Option(None, "--on", metavar="REGEX", help="With --reopen-at: the trigger regex."),
|
|
540
|
+
console_filter: ConsoleFilter = typer.Option(
|
|
541
|
+
None,
|
|
542
|
+
"--filter",
|
|
543
|
+
help="Console output filter (default: colors): colors (colours render, every other escape "
|
|
544
|
+
"is neutralised), default/nocontrol/printable (strip control codes), "
|
|
545
|
+
"direct (raw bytes to the terminal: unsafe for untrusted targets). "
|
|
546
|
+
"colors cannot stop same-colour (invisible) text or \\b/\\r overdrawing.",
|
|
547
|
+
),
|
|
548
|
+
output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
|
|
549
|
+
project: str = typer.Option(None, "--project", hidden=True),
|
|
550
|
+
board_yaml: str = typer.Option(None, "--board-yaml", hidden=True),
|
|
551
|
+
sdk_root: str = typer.Option(None, "--sdk-root", hidden=True),
|
|
552
|
+
target: str = typer.Option(None, "--target", hidden=True),
|
|
553
|
+
all_targets: bool = typer.Option(False, "--all", hidden=True),
|
|
554
|
+
verbose: bool = typer.Option(False, "--verbose", hidden=True),
|
|
555
|
+
quiet: bool = typer.Option(False, "--quiet", hidden=True),
|
|
556
|
+
no_color: bool = typer.Option(False, "--no-color", hidden=True),
|
|
557
|
+
non_interactive: bool = typer.Option(
|
|
558
|
+
False, "--non-interactive", help="With --break-uboot: stop after the break-in."
|
|
559
|
+
),
|
|
560
|
+
ci: bool = typer.Option(False, "--ci", hidden=True),
|
|
561
|
+
) -> None:
|
|
562
|
+
"""Open a serial console to the board."""
|
|
563
|
+
# The ten options above are clap's `GlobalArgs` members (`global = true`)
|
|
564
|
+
# that the oracle accepts on EVERY verb, `monitor` included, and never
|
|
565
|
+
# reads for this one -- confirmed live (`tan.exe monitor --non-interactive
|
|
566
|
+
# --ci --target zephyr-conf --all --project . --board-yaml x --sdk-root x
|
|
567
|
+
# --port COM7` reaches the identical "port not found" failure a bare
|
|
568
|
+
# `tan.exe monitor --port COM7` does). Declared here purely so the argv
|
|
569
|
+
# SURFACE matches: `tan monitor --sdk-root <path> --port COM7` exited 2 as
|
|
570
|
+
# a Click "No such option" usage error without this, breaking any caller
|
|
571
|
+
# (or saved script) forwarding the global set unconditionally -- unlike
|
|
572
|
+
# `model`/`new-som`/`faultdecode`, `monitor` never resolves an SDK root at
|
|
573
|
+
# all (see the module docstring), so `--project`/`--board-yaml`/
|
|
574
|
+
# `--sdk-root` are genuinely unread here too, not merely deferred. Hidden
|
|
575
|
+
# from `--help` because they do nothing. Same port-wide gap as
|
|
576
|
+
# `clean_cmd.clean`/`new_som_cmd.new_som`.
|
|
577
|
+
del project, board_yaml, sdk_root, target, all_targets
|
|
578
|
+
del verbose, quiet, no_color, ci
|
|
579
|
+
json_mode = output_format == "json"
|
|
580
|
+
|
|
581
|
+
def finish(data: dict, issues: list[Issue], exit_code: ExitCode) -> None:
|
|
582
|
+
if json_mode:
|
|
583
|
+
emit(
|
|
584
|
+
Envelope(
|
|
585
|
+
"monitor", Project(root=None, board_yaml=None), data, issues, exit_code
|
|
586
|
+
)
|
|
587
|
+
)
|
|
588
|
+
else:
|
|
589
|
+
for issue in issues:
|
|
590
|
+
print(f"monitor: {issue.message}", file=sys.stderr)
|
|
591
|
+
raise typer.Exit(int(exit_code))
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
from tan.commands import monitor_session # noqa: PLC0415 (validation only)
|
|
595
|
+
|
|
596
|
+
opts = monitor_session.break_opts(break_uboot, break_key, prompt, break_timeout)
|
|
597
|
+
cap = monitor_session.capture_opts(
|
|
598
|
+
capture, duration, until, log, console_filter is not None
|
|
599
|
+
)
|
|
600
|
+
from tan.commands import monitor_actions # noqa: PLC0415 (validation only)
|
|
601
|
+
|
|
602
|
+
spec = monitor_actions.action_opts(
|
|
603
|
+
capture, send, send_after, send_on, send_gap, reopen_at, on
|
|
604
|
+
)
|
|
605
|
+
data, issues, exit_code = _run_monitor(
|
|
606
|
+
port, baud, json_mode, opts, non_interactive,
|
|
607
|
+
console_filter.value if console_filter else "colors", cap, spec
|
|
608
|
+
)
|
|
609
|
+
except MonitorError as err:
|
|
610
|
+
finish(err.data, [Issue(err.code, "error", err.message)], err.exit_code)
|
|
611
|
+
return
|
|
612
|
+
except Exception as err: # noqa: BLE001 -- the envelope IS the error contract
|
|
613
|
+
finish(
|
|
614
|
+
{"schemaVersion": DATA_SCHEMA_VERSION},
|
|
615
|
+
[
|
|
616
|
+
Issue(
|
|
617
|
+
"monitor.internal-failure",
|
|
618
|
+
"error",
|
|
619
|
+
f"monitor failed unexpectedly: {type(err).__name__}: {err}",
|
|
620
|
+
)
|
|
621
|
+
],
|
|
622
|
+
ExitCode.INTERNAL_FAILURE,
|
|
623
|
+
)
|
|
624
|
+
return
|
|
625
|
+
|
|
626
|
+
finish(data, issues, exit_code)
|