tan-cli 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. tan/__init__.py +0 -0
  2. tan/__main__.py +120 -0
  3. tan/cli.py +939 -0
  4. tan/command_table.py +265 -0
  5. tan/commands/__init__.py +8 -0
  6. tan/commands/bootstrap_cmd.py +4856 -0
  7. tan/commands/bootstrap_patches.py +164 -0
  8. tan/commands/bootstrap_toolchain_fetch.py +155 -0
  9. tan/commands/bootstrap_toolchain_pin.py +36 -0
  10. tan/commands/build/__init__.py +1 -0
  11. tan/commands/build/configure_inputs.py +277 -0
  12. tan/commands/build/execute.py +1852 -0
  13. tan/commands/build/host_python.py +59 -0
  14. tan/commands/build/link_stale.py +78 -0
  15. tan/commands/build/manifest.py +428 -0
  16. tan/commands/build/materialise.py +81 -0
  17. tan/commands/build/token_substitution.py +381 -0
  18. tan/commands/build/toolchain.py +482 -0
  19. tan/commands/build/user_defines_wipe.py +115 -0
  20. tan/commands/build/workspace_patches.py +65 -0
  21. tan/commands/build_cmd.py +2236 -0
  22. tan/commands/build_output.py +331 -0
  23. tan/commands/clean_cmd.py +1183 -0
  24. tan/commands/completion_cmd.py +786 -0
  25. tan/commands/debug_config_cmd.py +2425 -0
  26. tan/commands/diff_cmd.py +940 -0
  27. tan/commands/doctor_cmd.py +4767 -0
  28. tan/commands/examples_cmd.py +644 -0
  29. tan/commands/explain_cmd.py +1319 -0
  30. tan/commands/faultdecode_cmd.py +931 -0
  31. tan/commands/flash_atoc_guard.py +240 -0
  32. tan/commands/flash_cmd.py +5731 -0
  33. tan/commands/flash_mram_guard.py +150 -0
  34. tan/commands/flash_ram.py +578 -0
  35. tan/commands/flash_raw.py +583 -0
  36. tan/commands/generate_cmd.py +1456 -0
  37. tan/commands/image_cmd.py +753 -0
  38. tan/commands/init_cmd.py +1740 -0
  39. tan/commands/inspect_cmd.py +377 -0
  40. tan/commands/kconfig_cmd.py +734 -0
  41. tan/commands/model_cmd.py +1514 -0
  42. tan/commands/model_device_cmd.py +351 -0
  43. tan/commands/model_device_live.py +234 -0
  44. tan/commands/model_host_cmd.py +430 -0
  45. tan/commands/model_zoo_cmd.py +363 -0
  46. tan/commands/monitor_actions.py +125 -0
  47. tan/commands/monitor_cmd.py +626 -0
  48. tan/commands/monitor_session.py +505 -0
  49. tan/commands/new_som_cmd.py +1522 -0
  50. tan/commands/pinmux_cmd.py +568 -0
  51. tan/commands/presets_cmd.py +1123 -0
  52. tan/commands/probe_cmd.py +421 -0
  53. tan/commands/reset_cmd.py +354 -0
  54. tan/commands/reset_confirm.py +168 -0
  55. tan/commands/run_cmd.py +637 -0
  56. tan/commands/scaffold_cmd.py +490 -0
  57. tan/commands/sdk_cmd.py +1723 -0
  58. tan/commands/size_cmd.py +1020 -0
  59. tan/commands/support_bundle_cmd.py +1073 -0
  60. tan/commands/trace_cmd.py +341 -0
  61. tan/commands/validate_cmd.py +2009 -0
  62. tan/commands/west_forward_cmd.py +736 -0
  63. tan/commands/workspace_patch_check.py +296 -0
  64. tan/core/__init__.py +1 -0
  65. tan/core/alp_conf_pregen.py +100 -0
  66. tan/core/artifact_provenance.py +240 -0
  67. tan/core/atoc_guard.py +276 -0
  68. tan/core/atoc_guard_messages.py +232 -0
  69. tan/core/atoc_replacement.py +118 -0
  70. tan/core/atomic_write.py +178 -0
  71. tan/core/board_context.py +351 -0
  72. tan/core/board_diagnostic.py +111 -0
  73. tan/core/board_files.py +221 -0
  74. tan/core/board_validator.py +542 -0
  75. tan/core/board_validator_compat.py +196 -0
  76. tan/core/board_validator_run.py +129 -0
  77. tan/core/board_validator_schema.py +186 -0
  78. tan/core/board_validator_skew.py +46 -0
  79. tan/core/board_yaml_edit.py +295 -0
  80. tan/core/board_yaml_pos.py +118 -0
  81. tan/core/bootstrap.py +2572 -0
  82. tan/core/build_plan.py +625 -0
  83. tan/core/consent.py +104 -0
  84. tan/core/console_filter.py +245 -0
  85. tan/core/debug_launch.py +1803 -0
  86. tan/core/devicetree_lint.py +168 -0
  87. tan/core/dir_removal.py +229 -0
  88. tan/core/doctor_git.py +172 -0
  89. tan/core/doctor_libraries.py +253 -0
  90. tan/core/doctor_render.py +185 -0
  91. tan/core/doctor_scope.py +66 -0
  92. tan/core/doctor_setools.py +237 -0
  93. tan/core/doctor_stale.py +180 -0
  94. tan/core/document_guards.py +723 -0
  95. tan/core/dp_id.py +97 -0
  96. tan/core/elf_load.py +63 -0
  97. tan/core/error_catalog.py +171 -0
  98. tan/core/example_catalog.py +499 -0
  99. tan/core/example_facets.py +151 -0
  100. tan/core/faultdecode.py +521 -0
  101. tan/core/flash_plan.py +2501 -0
  102. tan/core/flow_d_report.py +381 -0
  103. tan/core/flow_d_uptime.py +164 -0
  104. tan/core/fs_confine.py +130 -0
  105. tan/core/global_flags.py +230 -0
  106. tan/core/host_python.py +210 -0
  107. tan/core/image_bundle.py +247 -0
  108. tan/core/inert.py +147 -0
  109. tan/core/jlink_binary.py +128 -0
  110. tan/core/jlink_probe.py +481 -0
  111. tan/core/jsonc_splice.py +404 -0
  112. tan/core/launch_provenance.py +257 -0
  113. tan/core/link_refusal.py +76 -0
  114. tan/core/metadata_schema.py +376 -0
  115. tan/core/model_check.py +227 -0
  116. tan/core/model_device.py +449 -0
  117. tan/core/model_doctor.py +198 -0
  118. tan/core/model_host.py +48 -0
  119. tan/core/model_list.py +112 -0
  120. tan/core/model_zoo.py +353 -0
  121. tan/core/module_template.py +340 -0
  122. tan/core/mram_link.py +87 -0
  123. tan/core/new_som_ospi.py +94 -0
  124. tan/core/os_class.py +159 -0
  125. tan/core/patch_paths.py +87 -0
  126. tan/core/pending.py +40 -0
  127. tan/core/plain_zephyr_plan.py +138 -0
  128. tan/core/plan_exec.py +373 -0
  129. tan/core/plan_tokens.py +600 -0
  130. tan/core/probe.py +155 -0
  131. tan/core/probe_plan.py +237 -0
  132. tan/core/proxy.py +168 -0
  133. tan/core/publish.py +31 -0
  134. tan/core/python_floor.py +178 -0
  135. tan/core/ram_run.py +694 -0
  136. tan/core/ram_watch.py +231 -0
  137. tan/core/raw_write.py +160 -0
  138. tan/core/reset_plan.py +56 -0
  139. tan/core/run.py +161 -0
  140. tan/core/scaffold.py +2203 -0
  141. tan/core/scaffold_selftest_identity.py +225 -0
  142. tan/core/sdk_default_registry.py +565 -0
  143. tan/core/sdk_discovery.py +996 -0
  144. tan/core/sdk_removal.py +465 -0
  145. tan/core/serial_actions.py +175 -0
  146. tan/core/serial_capture.py +185 -0
  147. tan/core/serial_url.py +45 -0
  148. tan/core/setools.py +608 -0
  149. tan/core/setools_scratch.py +243 -0
  150. tan/core/shapes.py +194 -0
  151. tan/core/size.py +610 -0
  152. tan/core/som_buildability.py +204 -0
  153. tan/core/som_schema_version.py +62 -0
  154. tan/core/subprocess_env.py +201 -0
  155. tan/core/sysbuild_image.py +94 -0
  156. tan/core/system_manifest.py +827 -0
  157. tan/core/text_layout.py +73 -0
  158. tan/core/timestamp.py +182 -0
  159. tan/core/tool_lookup.py +383 -0
  160. tan/core/toolchain_pin.py +117 -0
  161. tan/core/toolchain_provision.py +1224 -0
  162. tan/core/trusted_path.py +134 -0
  163. tan/core/uboot_breakin.py +154 -0
  164. tan/core/uri_reference.py +415 -0
  165. tan/core/user_defines.py +152 -0
  166. tan/core/venv.py +332 -0
  167. tan/core/west_patches.py +143 -0
  168. tan/core/west_workspace_refusal.py +159 -0
  169. tan/core/zephyr_env.py +124 -0
  170. tan/env.py +185 -0
  171. tan/envelope.py +647 -0
  172. tan/exit_codes.py +12 -0
  173. tan/lazy_typer.py +185 -0
  174. tan/model/__init__.py +2 -0
  175. tan/model/_gen_fixture.py +132 -0
  176. tan/model/adapters/__init__.py +116 -0
  177. tan/model/adapters/cpu.py +26 -0
  178. tan/model/adapters/deepx.py +94 -0
  179. tan/model/adapters/drpai.py +242 -0
  180. tan/model/adapters/ethos_u.py +951 -0
  181. tan/model/adapters/executorch.py +55 -0
  182. tan/model/analyze.py +694 -0
  183. tan/model/build.py +304 -0
  184. tan/model/check.py +682 -0
  185. tan/model/manifest.py +625 -0
  186. tan/model/measure.py +216 -0
  187. tan/model/npu_ops_validate.py +130 -0
  188. tan/model/package.py +125 -0
  189. tan/model/perf.py +635 -0
  190. tan/model/perf_apply.py +721 -0
  191. tan/model/prep.py +193 -0
  192. tan/model/sram_fit.py +502 -0
  193. tan/model/targets.py +374 -0
  194. tan/model/tensorio.py +287 -0
  195. tan/net.py +73 -0
  196. tan/output_format.py +213 -0
  197. tan/planner/__init__.py +219 -0
  198. tan/planner/aperture.py +202 -0
  199. tan/planner/buildplan.py +1092 -0
  200. tan/planner/camera_owner.py +192 -0
  201. tan/planner/cameras.py +132 -0
  202. tan/planner/carveout.py +755 -0
  203. tan/planner/cli.py +274 -0
  204. tan/planner/headers.py +315 -0
  205. tan/planner/kconfig.py +2435 -0
  206. tan/planner/kconfig_symbols.py +473 -0
  207. tan/planner/libraries.py +677 -0
  208. tan/planner/link_target.py +237 -0
  209. tan/planner/linux_ownership.py +216 -0
  210. tan/planner/loader.py +1525 -0
  211. tan/planner/manifest.py +116 -0
  212. tan/planner/memory.py +161 -0
  213. tan/planner/memregion.py +56 -0
  214. tan/planner/models.py +533 -0
  215. tan/planner/orchestrator.py +874 -0
  216. tan/planner/ownership.py +203 -0
  217. tan/planner/partition.py +1160 -0
  218. tan/planner/paths.py +32 -0
  219. tan/planner/plain_slice.py +139 -0
  220. tan/planner/project_emit/__init__.py +60 -0
  221. tan/planner/project_emit/bom_netlist.py +375 -0
  222. tan/planner/project_emit/dts.py +580 -0
  223. tan/planner/project_emit/hw_info.py +221 -0
  224. tan/planner/project_emit/native_sim.py +136 -0
  225. tan/planner/project_emit/west_libs.py +213 -0
  226. tan/planner/project_loader.py +264 -0
  227. tan/planner/sdk_capability.py +203 -0
  228. tan/planner/sdk_compat.py +395 -0
  229. tan/planner/secure.py +350 -0
  230. tan/planner/slugs.py +394 -0
  231. tan/planner/som_metadata.py +325 -0
  232. tan/planner/strict_loaders.py +131 -0
  233. tan/planner/template.py +1037 -0
  234. tan/planner/template_pins.py +535 -0
  235. tan/planner/template_rewrite.py +714 -0
  236. tan/planner/topology.py +208 -0
  237. tan/planner/validate.py +365 -0
  238. tan/planner/whole_device_alias.py +67 -0
  239. tan/planner/zephyr_board.py +3510 -0
  240. tan/planner_cli.py +97 -0
  241. tan/planner_emit.py +617 -0
  242. tan/planner_root.py +119 -0
  243. tan/soc_ref.py +58 -0
  244. tan/templates/__init__.py +34 -0
  245. tan/templates/vendored/MANIFEST.md +1862 -0
  246. tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
  247. tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
  248. tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
  249. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
  250. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
  251. tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
  252. tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
  253. tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
  254. tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
  255. tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
  256. tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
  257. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
  258. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
  259. tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
  260. tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
  261. tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
  262. tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
  263. tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
  264. tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
  265. tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
  266. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
  267. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
  268. tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
  269. tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
  270. tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
  271. tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
  272. tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
  273. tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
  274. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
  275. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
  276. tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
  277. tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
  278. tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
  279. tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
  280. tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
  281. tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
  282. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
  283. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
  284. tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
  285. tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
  286. tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
  287. tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
  288. tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
  289. tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
  290. tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
  291. tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
  292. tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
  293. tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
  294. tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
  295. tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
  296. tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
  297. tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
  298. tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
  299. tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
  300. tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
  301. tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
  302. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
  303. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
  304. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
  305. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
  306. tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
  307. tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
  308. tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
  309. tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
  310. tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
  311. tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
  312. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
  313. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
  314. tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
  315. tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
  316. tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
  317. tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
  318. tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
  319. tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
  320. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
  321. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
  322. tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
  323. tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
  324. tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
  325. tan/version.py +102 -0
  326. tan_cli-0.7.0.dist-info/METADATA +125 -0
  327. tan_cli-0.7.0.dist-info/RECORD +330 -0
  328. tan_cli-0.7.0.dist-info/WHEEL +5 -0
  329. tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
  330. tan_cli-0.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,354 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan reset` -- ONE bare nRESET pulse through J-Link (tan-cli#1452).
3
+
4
+ Bench recovery from a non-waking STOP is a single clean pulse: `r0`, a pulse
5
+ width (default 100 ms), `r1`. This is not the pin reset `tan flash` runs
6
+ (`RSetType 2; r; g`, with retries): no connect, no halt, no connect-under-reset, no retry
7
+ storm, so the target is not woken out of STOP and the VBAT/BKRAM evidence
8
+ survives. See `tan.core.reset_plan` for the script rules.
9
+
10
+ Probe handling is `tan probe`'s: the trusted J-Link binary (`resolve_jlink`),
11
+ `--probe-serial` / `--probe-usb-path` and the `ShowEmuList` verification before
12
+ the spawn, through `flash_cmd._execute`. `JLINK_RUN_PLACE` is read from the
13
+ environment of THIS invocation, so the board-farm shim that wraps `JLinkExe`
14
+ picks the place per command, exactly as for `tan flash`; the envelope reports
15
+ the value in `data.place`.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import re
21
+ import time
22
+ import sys
23
+ from typing import Any, Callable
24
+
25
+ import typer
26
+
27
+ from tan.commands import flash_cmd as fc
28
+ from tan.commands import flash_raw
29
+ from tan.commands import reset_confirm as rc_mod
30
+ from tan.commands.monitor_cmd import MonitorError
31
+ from tan.core import reset_plan as rp
32
+ from tan.core import trusted_path
33
+ from tan.core.flash_plan import FlashPlan, FlashPlanError
34
+ from tan.core.global_flags import accept_global_flags
35
+ from tan.core.jlink_probe import HANDSHAKE_PREFIX, USB_PATH_ENV, parse_handshakes
36
+ from tan.core.jlink_binary import ENV_OVERRIDE as JLINK_ENV, resolve_jlink
37
+ from tan.envelope import Envelope, Issue
38
+ from tan.exit_codes import ExitCode
39
+ from tan.output_format import FORMAT_HELP, OutputFormat, resolve_format
40
+
41
+ COMMAND = "reset"
42
+ PLACE_ENV = "JLINK_RUN_PLACE"
43
+ _SCHEMA_VERSION = fc._DATA_SCHEMA_VERSION
44
+ _SCRIPT_PREFIX = "tan-reset-"
45
+ #: Commander output that means the pulse did not happen, even when the closing
46
+ #: "Script processing completed" banner is also printed.
47
+ _FAILURE = re.compile(r"FAILED|Cannot connect|Could not open")
48
+ _Result = tuple[ExitCode, dict[str, Any], list[Issue], list[str]]
49
+
50
+
51
+ def _fail(data: dict[str, Any], issue: Issue, rc=ExitCode.RUNTIME_FAILURE) -> _Result:
52
+ return rc, data, [issue], [f"reset: {issue.message}"]
53
+
54
+
55
+ def _select(data, probe_serial, probe_usb_path, jlink_path, project_dir, enumerate_probes):
56
+ """`(guard, echo, serial)` or a refusal `_Result`."""
57
+ ctx = fc._Context(
58
+ sku="", build_root="", sdk_root="", dry_run=False, skip_missing_tools=False,
59
+ force_confirm=False, capture=True, probe_serial=probe_serial,
60
+ probe_usb_path=probe_usb_path, project_dir=project_dir, jlink_path=jlink_path,
61
+ **({"enumerate_probes": enumerate_probes} if enumerate_probes is not None else {}),
62
+ )
63
+ flash_args, selection, snapshot = fc._flow_d_probe_selection({}, ctx)
64
+ echo = fc._probe_echo(selection, ctx)
65
+ if echo:
66
+ data["probe"] = echo
67
+ if selection is not None and selection.refusal_code is not None:
68
+ return _fail(data, Issue(f"flash.probe-{selection.refusal_code}", "error", selection.refusal or ""))
69
+ guard = None
70
+ if selection is not None and (selection.serial is not None or selection.visible is None):
71
+ guard = fc._ProbeGuard(selection, snapshot, ctx.enumerate_probes, echo,
72
+ script_prefix=_SCRIPT_PREFIX)
73
+ return guard, flash_args.get("jlink_serial")
74
+
75
+
76
+ def _spawn(guard, script, exe, usb_path, fast):
77
+ """One J-Link Commander spawn. `fast` (a masking wrapper is in use, named by
78
+ JLINK_RUN_PLACE, and --probe-usb-path was given): no ShowEmuList pass before
79
+ the pulse -- the wrapper itself refuses a TAN_PROBE_USB_PATH that is not its
80
+ place's port (exit 96) before opening any probe, and the pulse is judged
81
+ afterwards by the TAN_PROBE_ISOLATED_USB_PATH handshake. Otherwise the
82
+ pre-spawn probe guard of `tan flash` runs. The argv is the proven one
83
+ (`-NoGui 1 -CommanderScript`, plus -ExitOnError 1): no -if, no -autoconnect,
84
+ no -device; Commander opens the probe lazily on `r0`."""
85
+ argv = ("JLinkExe", "-NoGui", "1", "-ExitOnError", "1", "-CommanderScript")
86
+ if not fast:
87
+ return fc._execute(
88
+ FlashPlan(argv=argv, ok_message="", jlink_script=script),
89
+ True, None, None, guard, jlink_exe=exe, script_prefix=_SCRIPT_PREFIX,
90
+ )
91
+ return fc._spawn_jlink(
92
+ list(argv), script, True, fc._FLASH_TIMEOUT_S, None, None, exe,
93
+ extra_env={USB_PATH_ENV: usb_path}, script_prefix=_SCRIPT_PREFIX,
94
+ )
95
+
96
+
97
+ def _verdict(data, out, guard, fast, usb_path) -> Issue | None:
98
+ """The refusal / failure for a finished spawn, else `None`."""
99
+ if guard is not None and guard.tripped and not fast:
100
+ return Issue(f"flash.probe-{guard.tripped_code}", "error", guard.tripped)
101
+ text = f"{out.stdout}\n{out.stderr}"
102
+ if not out.success or "Script processing completed" not in text or _FAILURE.search(text):
103
+ tail = fc._capture_tail(out) or "J-Link did not complete the script"
104
+ return Issue("reset.failed", "error", f"the nRESET pulse did not complete: {tail}")
105
+ if fast:
106
+ seen = parse_handshakes(text)
107
+ if seen != [usb_path]:
108
+ return Issue(
109
+ "flash.probe-verify-failed", "error",
110
+ f"the pulse ran, but the wrapper did not attest isolation to {usb_path} "
111
+ f"({HANDSHAKE_PREFIX} lines: {', '.join(seen) or 'none'}); it may have reached "
112
+ "the wrong board",
113
+ )
114
+ data.setdefault("probe", {})["isolation"] = f"wrapper-attested:{usb_path}"
115
+ return None
116
+
117
+
118
+ def _finish_pulse(data, ser, confirm, pulse_ms, initial=b"", exit_ts=None, stuck=None) -> _Result:
119
+ place = f" on place {data['place']}" if data["place"] else ""
120
+ line = f"reset: nRESET pulsed for {pulse_ms} ms{place}"
121
+ if ser is None:
122
+ note = Issue("reset.boot-not-confirmed", "info",
123
+ "pulse sent; boot not confirmed (pass --confirm-console PORT --expect REGEX)")
124
+ return ExitCode.SUCCESS, data, [note], [line + "; boot not confirmed"]
125
+ if stuck is not None: # never run a second reader beside a stuck one
126
+ data["console"] = {"port": confirm.port, "observed": False, "error": str(stuck)}
127
+ else:
128
+ data["console"] = rc_mod.observe(ser, confirm, initial, exit_ts)
129
+ data["resetObserved"] = bool(data["console"]["observed"])
130
+ if "error" in data["console"]:
131
+ data["resetObserved"] = "unknown"
132
+ issue = Issue("reset.console-read-failed", "error",
133
+ f"pulse sent, but reading {confirm.port} failed ({data['console']['error']}): "
134
+ "reset not confirmed either way")
135
+ return _fail(data, issue)
136
+ if not data["resetObserved"]:
137
+ issue = Issue("reset.boot-not-observed", "error",
138
+ f"pulse sent, but no console line matched --expect within the {confirm.window_s}s "
139
+ f"confirm window on {confirm.port}: the board did not visibly reboot"
140
+ + (f" (a late match at {data['console']['lateMatchAtSeconds']}s is not a reset)"
141
+ if "lateMatchAtSeconds" in data["console"] else ""))
142
+ return _fail(data, issue)
143
+ return ExitCode.SUCCESS, data, [], [line + "; boot observed"]
144
+
145
+
146
+ def _run(
147
+ pulse_ms: int,
148
+ probe_serial: str | None,
149
+ probe_usb_path: str | None,
150
+ jlink_path: str | None,
151
+ project_dir: str,
152
+ enumerate_probes: Callable[[], Any] | None = None,
153
+ confirm: "rc_mod.ConfirmSpec | None" = None,
154
+ ) -> _Result:
155
+ data: dict[str, Any] = {
156
+ "schemaVersion": _SCHEMA_VERSION, "pulseMs": pulse_ms,
157
+ "place": os.environ.get(PLACE_ENV) or None, "writes": False,
158
+ "resetObserved": "unknown",
159
+ }
160
+ try:
161
+ rp.check_pulse_ms(pulse_ms)
162
+ except rp.ResetArgError as err:
163
+ return _fail(data, Issue("reset.bad-argument", "error", str(err)), ExitCode.VALIDATION_FAILURE)
164
+ place = data["place"]
165
+ if place:
166
+ # A reservation place is named: the ONLY J-Link program tan may spawn is the trusted
167
+ # wrapper. Refuse before any spawn rather than run a raw ShowEmuList that enumerates
168
+ # other places' probes.
169
+ wrapper, why = trusted_path.configured_wrapper()
170
+ if wrapper is None:
171
+ data["wrapper"] = {"trusted": False, "reason": why}
172
+ return _fail(data, Issue(
173
+ "reset.wrapper-required", "error",
174
+ f"{PLACE_ENV} is set but no trusted wrapper is configured ({why}): set "
175
+ "TAN_JLINK_WRAPPER to the absolute path of the board-farm JLinkExe shim. "
176
+ "Nothing was spawned."))
177
+ from_wrapper_env = not jlink_path
178
+ jlink_path = jlink_path or wrapper # an explicit --jlink wins, and is checked below
179
+ else:
180
+ from_wrapper_env = False
181
+ found = resolve_jlink(jlink_path, project_dir=project_dir)
182
+ exe = found.path if found is not None else None
183
+ # resolve_jlink() labels any path handed to it as "the --jlink flag"; name the real
184
+ # origin when tan itself defaulted to TAN_JLINK_WRAPPER (tan-cli#1467).
185
+ source = "TAN_JLINK_WRAPPER" if (found and from_wrapper_env) else (found.source if found else None)
186
+ data["jlink"] = {"binary": exe, "binarySource": source}
187
+ if exe is None:
188
+ return _fail(data, Issue("reset.failed", "error", fc._NO_TRUSTED_JLINK))
189
+ trusted, reason = trusted_path.is_configured_wrapper(exe)
190
+ data["wrapper"] = {"trusted": trusted, "reason": reason}
191
+ if place and not trusted:
192
+ return _fail(data, Issue(
193
+ "reset.wrapper-required", "error",
194
+ f"{PLACE_ENV} is set but the J-Link program is not the configured wrapper ({reason}); "
195
+ "nothing was spawned."))
196
+ if place:
197
+ # The wrapper's holder check is `<host>/<user>`, identical for every session of one
198
+ # user, so a pulse here could reset a board ANOTHER session holds (tan-cli#1457). Demand
199
+ # exactly what `tan flash --raw` demands: this session's lease nonce, labgrid showing
200
+ # us as the holder of this acquisition, and --probe-usb-path equal to the place's swd
201
+ # port. Refused before any spawn.
202
+ refusal, _verified = flash_raw._reservation_refusal(place, exe, probe_usb_path)
203
+ if refusal is not None:
204
+ return _fail(data, Issue(
205
+ "reset.reservation-required", "error",
206
+ f"refusing to pulse nRESET without a provably held bench reservation: {refusal} "
207
+ "Nothing was spawned."))
208
+ # Single-spawn mode only when the J-Link program IS the configured wrapper: a real SEGGER
209
+ # JLinkExe given JLINK_RUN_PLACE would skip ShowEmuList and pulse whichever probe
210
+ # enumerates first.
211
+ fast = bool(data["place"]) and probe_usb_path is not None and trusted
212
+ try:
213
+ picked = _select(data, probe_serial, probe_usb_path, jlink_path, project_dir, enumerate_probes)
214
+ if isinstance(picked, tuple) and len(picked) == 4:
215
+ return picked
216
+ guard, serial = picked
217
+ script = rp.pulse_script(None if fast else serial, pulse_ms)
218
+ except (FlashPlanError, rp.ResetArgError) as err:
219
+ return _fail(data, Issue("reset.failed", "error", str(err)))
220
+ data["script"] = fc._DISABLE_FW_UPDATE.splitlines() + script.splitlines()
221
+ data["singleSpawn"] = fast
222
+ ser = None
223
+ started = time.monotonic()
224
+ if confirm is not None:
225
+ try:
226
+ ser = rc_mod.open_console(confirm)
227
+ except MonitorError as err:
228
+ return _fail(data, Issue("reset.console-open-failed", "error", err.message))
229
+ drain = rc_mod.Drain(ser) if ser is not None else None
230
+ try:
231
+ spawn_started = time.monotonic()
232
+ out = _spawn(guard, script, exe, probe_usb_path, fast)
233
+ exit_ts = time.monotonic()
234
+ stuck = None
235
+ initial: list[bytes] = []
236
+ if drain is not None:
237
+ try:
238
+ initial = drain.stop(exit_ts)
239
+ except rc_mod.DrainStuck as err:
240
+ stuck = err
241
+ data["timing"] = {
242
+ "prepSeconds": round(spawn_started - started, 3),
243
+ "jlinkSpawnSeconds": round(time.monotonic() - spawn_started, 3),
244
+ "note": "jlinkSpawnSeconds includes any wrapper preamble before the pulse itself",
245
+ }
246
+ problem = _verdict(data, out, guard, fast, probe_usb_path)
247
+ if problem is not None:
248
+ return _fail(data, problem)
249
+ return _finish_pulse(data, ser, confirm, pulse_ms, b"".join(initial), exit_ts, stuck)
250
+ finally:
251
+ if drain is not None:
252
+ try:
253
+ drain.stop()
254
+ except rc_mod.DrainStuck:
255
+ pass
256
+ if ser is not None:
257
+ try:
258
+ ser.close()
259
+ except Exception: # noqa: BLE001 -- observe() already closed it
260
+ pass
261
+
262
+
263
+ def reset(
264
+ ctx: typer.Context,
265
+ project: str = typer.Option(None, "--project", metavar="PATH", help="Project root (defaults to '.')."),
266
+ pulse_ms: int = typer.Option(
267
+ rp.DEFAULT_PULSE_MS, "--pulse-ms", metavar="MS", show_default=True,
268
+ help=f"How long nRESET is held low ({rp.MIN_PULSE_MS}..{rp.MAX_PULSE_MS})."),
269
+ probe_serial: str = typer.Option(None, "--probe-serial", metavar="SN", help="J-Link serial for this run."),
270
+ probe_usb_path: str = typer.Option(
271
+ None, "--probe-usb-path", metavar="BUS-PORT",
272
+ help="Select the J-Link at this USB port path (e.g. 3-4.2). Without JLINK_RUN_PLACE it is "
273
+ "verified (ShowEmuList) before the JLinkExe spawn, as `tan flash` does. With JLINK_RUN_PLACE it "
274
+ "must equal the leased place's swd port (checked against labgrid before any spawn), there is "
275
+ "no ShowEmuList, and the wrapper's TAN_PROBE_ISOLATED_USB_PATH handshake is checked only "
276
+ "AFTER the pulse has run (TAN_PROBE_USB_PATH is exported for the masking wrapper)."),
277
+ confirm_console: str = typer.Option(
278
+ None, "--confirm-console", metavar="PORT",
279
+ help="Serial port (or rfc2217:// URL) to watch for the reboot; opened before the pulse. "
280
+ "Needs --expect. Without it the result is resetObserved: unknown."),
281
+ expect: str = typer.Option(
282
+ None, "--expect", metavar="REGEX",
283
+ help="With --confirm-console: a line printed after reset; only bytes that arrive after "
284
+ "J-Link exits count."),
285
+ confirm_baud: int = typer.Option(None, "--confirm-baud", metavar="BAUD", help="Console baud (default 115200)."),
286
+ confirm_timeout: float = typer.Option(
287
+ None, "--confirm-timeout", metavar="SECONDS", help="Overall read limit for --expect (default 30)."),
288
+ confirm_window: float = typer.Option(
289
+ None, "--confirm-window", metavar="SECONDS",
290
+ help="The FIRST --expect match must arrive this soon after J-Link exits (default 3); a later "
291
+ "one is recorded as lateMatchAtSeconds and is not a reset."),
292
+ jlink: str = typer.Option(
293
+ None, "--jlink", metavar="PATH",
294
+ help=f"The J-Link Commander binary (otherwise {JLINK_ENV}, PATH, then a SEGGER install root; "
295
+ "never the project .venv). With JLINK_RUN_PLACE set, TAN_JLINK_WRAPPER (the absolute path of "
296
+ "the board-farm shim) is used instead and any other program is refused (reset.wrapper-required)."),
297
+ output_format: OutputFormat = typer.Option(None, "--format", help=FORMAT_HELP),
298
+ ) -> None:
299
+ """ONE bare nRESET pulse through J-Link: `r0`, wait --pulse-ms, `r1`. No connect, no halt, no
300
+ connect-under-reset, no retry; it does not flash. Set JLINK_RUN_PLACE (with --probe-usb-path) in the
301
+ environment to pick the board-farm place for this command; that requires the session lease
302
+ (reset.reservation-required otherwise).
303
+
304
+ \b
305
+ tan reset --probe-usb-path 3-4.2
306
+ JLINK_RUN_PLACE=aen-evk-02 tan reset --probe-usb-path 3-4.2 --pulse-ms 200 --format json
307
+ tan reset --probe-usb-path 3-4.2 --confirm-console rfc2217://gw:4001 --expect 'Booting Zephyr'
308
+ (pick a banner printed some time AFTER reset: text emitted before J-Link exits is not counted)
309
+ (with JLINK_RUN_PLACE: also needs TAN_JLINK_WRAPPER, this session's TAN_LEASE_NONCE from
310
+ scripts/bench/tan-lease.sh, and labgrid-client showing you as the holder)
311
+ (the reset is only claimed with --confirm-console; otherwise resetObserved is "unknown")
312
+ """
313
+ json_mode = resolve_format(output_format, ctx.obj, choices=OutputFormat) == "json"
314
+ probe_serial = probe_serial if isinstance(probe_serial, str) else None
315
+ probe_usb_path = probe_usb_path if isinstance(probe_usb_path, str) else None
316
+ if probe_usb_path is not None and not fc.is_valid_usb_path(probe_usb_path):
317
+ raise typer.BadParameter(
318
+ f"{probe_usb_path!r} is not a USB port path like 3-4.2 (<bus>-<port>[.<port>...])",
319
+ param_hint="--probe-usb-path",
320
+ )
321
+ bad_port = None
322
+ confirm = None
323
+ try:
324
+ confirm = rc_mod.confirm_spec(confirm_console, expect, confirm_baud, confirm_timeout, confirm_window)
325
+ except rc_mod.BadPort as err:
326
+ bad_port = err
327
+ except rc_mod.ConfirmError as err:
328
+ raise typer.BadParameter(str(err)) from err
329
+ cwd = fc.workspace_root(project)
330
+ project_obj = fc._resolve_project(cwd, None)
331
+ try:
332
+ if bad_port is not None:
333
+ exit_code, data, issues, lines = _fail(
334
+ {"schemaVersion": _SCHEMA_VERSION, "port": confirm_console},
335
+ Issue("reset.bad-port", "error", str(bad_port)), ExitCode.VALIDATION_FAILURE)
336
+ else:
337
+ exit_code, data, issues, lines = _run(
338
+ pulse_ms, probe_serial, probe_usb_path,
339
+ jlink if isinstance(jlink, str) else None, cwd, confirm=confirm,
340
+ )
341
+ except Exception as err: # noqa: BLE001 -- a tan bug is reported as one, with an envelope
342
+ exit_code = ExitCode.INTERNAL_FAILURE
343
+ data = {"schemaVersion": _SCHEMA_VERSION}
344
+ issues = [Issue("reset.internal-failure", "error", f"{type(err).__name__}: {err}")]
345
+ lines = ["reset: internal failure"]
346
+ if json_mode:
347
+ fc.emit(Envelope(COMMAND, project_obj, data, issues, exit_code))
348
+ else:
349
+ for line in lines:
350
+ print(line, file=sys.stderr if exit_code else sys.stdout)
351
+ raise typer.Exit(int(exit_code))
352
+
353
+
354
+ reset = accept_global_flags(reset)
@@ -0,0 +1,168 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan reset --confirm-console PORT --expect REGEX` (tan-cli#1452).
3
+
4
+ A J-Link `r0`/`r1` pulse that exits 0 proves only that Commander ran; it says
5
+ nothing about the board rebooting (a target in STOP can ignore it). This is the
6
+ evidence: the console is opened BEFORE the pulse (so an `rfc2217://` session is
7
+ already up), and once J-Link has exited the input queue is dropped and the port
8
+ is read until a line matches `--expect` or `--confirm-timeout` passes. Only
9
+ bytes that arrive after J-Link exits count, so a line the board printed before
10
+ the pulse cannot confirm it. Boot text emitted in the second between `r1` and
11
+ Commander's exit is therefore not seen: pick a banner printed some time after
12
+ reset.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import threading
17
+ import time
18
+ from dataclasses import dataclass
19
+ from typing import Any
20
+
21
+ from tan.core import serial_capture, serial_url
22
+
23
+ DEFAULT_BAUD = 115200
24
+ DEFAULT_TIMEOUT_S = 30.0
25
+ DEFAULT_WINDOW_S = 3.0
26
+
27
+
28
+ class ConfirmError(ValueError):
29
+ """A bad `--confirm-*` / `--expect` combination."""
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ConfirmSpec:
34
+ port: str
35
+ expect: Any # compiled regex
36
+ baud: int
37
+ timeout_s: float
38
+ window_s: float = DEFAULT_WINDOW_S
39
+
40
+
41
+ class BadPort(ConfirmError):
42
+ """`--confirm-console` is a malformed serial URL."""
43
+
44
+
45
+ def confirm_spec(
46
+ port: str | None,
47
+ expect: str | None,
48
+ baud: int | None,
49
+ timeout_s: float | None,
50
+ window_s: float | None = None,
51
+ ):
52
+ """`None` when no confirmation was asked for; `ConfirmError` on a bad combination."""
53
+ if all(v is None for v in (port, expect, baud, timeout_s, window_s)):
54
+ return None
55
+ if port is None or expect is None:
56
+ raise ConfirmError("--confirm-console and --expect must be given together")
57
+ problem = serial_url.port_url_problem(port)
58
+ if problem is not None:
59
+ raise BadPort(f"bad --confirm-console: {problem}")
60
+ try:
61
+ pattern = serial_capture.compile_until(expect)
62
+ except ValueError as err:
63
+ raise ConfirmError(str(err).replace("--until", "--expect")) from err
64
+ baud = DEFAULT_BAUD if baud is None else baud
65
+ timeout_s = DEFAULT_TIMEOUT_S if timeout_s is None else timeout_s
66
+ window_s = DEFAULT_WINDOW_S if window_s is None else window_s
67
+ if baud <= 0 or not timeout_s > 0 or not window_s > 0:
68
+ raise ConfirmError(
69
+ "--confirm-baud, --confirm-timeout and --confirm-window must be greater than 0"
70
+ )
71
+ return ConfirmSpec(port, pattern, baud, timeout_s, window_s)
72
+
73
+
74
+ class DrainStuck(RuntimeError):
75
+ """The background reader did not return from its read; the console is left alone."""
76
+
77
+
78
+ class Drain:
79
+ """The ONLY reader of the console while J-Link runs.
80
+
81
+ Everything it reads is time-stamped. Stale rfc2217 lines in flight when the
82
+ pulse is sent arrive before J-Link exits, so they are dropped; `stop(exit_ts)`
83
+ hands back only the chunks stamped at or after `exit_ts`, which the caller
84
+ feeds to the capture as its first data. The thread may be inside a 0.1 s
85
+ `read()` when `stop()` is called: it is joined, never abandoned, so a banner
86
+ that arrives (measured: 0.088 s after exit) during that read is kept, and two
87
+ readers never run at once."""
88
+
89
+ def __init__(self, ser, clock=time.monotonic) -> None:
90
+ self._ser = ser
91
+ self._clock = clock # injectable so tests need no wall-clock races
92
+ self._halt = threading.Event()
93
+ self._chunks: list[tuple[float, bytes]] = []
94
+ self._stopped = False
95
+ self._thread = threading.Thread(target=self._run, daemon=True)
96
+ self._thread.start()
97
+
98
+ def _run(self) -> None:
99
+ while not self._halt.is_set():
100
+ try:
101
+ got = self._ser.read(max(1, int(getattr(self._ser, "in_waiting", 0) or 0)))
102
+ except Exception: # noqa: BLE001 -- observe() reports a dead link
103
+ return
104
+ if got:
105
+ self._chunks.append((self._clock(), got))
106
+ else:
107
+ time.sleep(0.005)
108
+
109
+ def stop(self, exit_ts: float | None = None, timeout: float = 2.0) -> list[bytes]:
110
+ """Halt the reader and return the chunks received at/after `exit_ts`
111
+ (none when `exit_ts` is None). Idempotent. `DrainStuck` if the reader is
112
+ still inside `read()` after `timeout`."""
113
+ if self._stopped:
114
+ return []
115
+ self._halt.set()
116
+ self._thread.join(timeout=timeout)
117
+ if self._thread.is_alive():
118
+ raise DrainStuck(f"the console reader did not stop within {timeout}s")
119
+ self._stopped = True
120
+ if exit_ts is None:
121
+ return []
122
+ return [data for ts, data in self._chunks if ts >= exit_ts]
123
+
124
+
125
+ def open_console(spec: ConfirmSpec):
126
+ """Open the console (before the pulse). `MonitorError` on failure."""
127
+ from tan.commands import monitor_session # noqa: PLC0415 (pyserial is optional)
128
+
129
+ return monitor_session.open_port(spec.port, spec.baud, capture=True)
130
+
131
+
132
+ def observe(
133
+ ser, spec: ConfirmSpec, initial: bytes = b"", exit_ts: float | None = None, clock=time.monotonic
134
+ ) -> dict[str, Any]:
135
+ """After J-Link exited: wait for `--expect`, starting with `initial` (what the
136
+ `Drain` stamped at or after `exit_ts`). The queue is NOT flushed here: a flush
137
+ could discard a banner that is already waiting.
138
+ Closes `ser`. Returns the `data.console` block. The FIRST matching line must arrive within
139
+ `window_s` of J-Link exiting: a later one (e.g. an RTC-alarm wake out of STOP) is recorded as
140
+ `lateMatchAtSeconds` and is not a reset."""
141
+ try:
142
+ t0 = clock()
143
+ res = serial_capture.capture(
144
+ ser, duration_s=max(spec.timeout_s, spec.window_s), until=spec.expect, initial=initial,
145
+ clock=clock,
146
+ )
147
+ # Latency from J-Link's exit, not from when this capture began.
148
+ total = res.elapsed_s + (t0 - exit_ts if exit_ts is not None else 0.0)
149
+ latency = round(total, 3) if res.matched else None
150
+ in_window = res.matched and total <= spec.window_s
151
+ block = {
152
+ "port": spec.port, "baud": spec.baud, "observed": in_window,
153
+ "windowSeconds": spec.window_s, "matchLatencySeconds": latency if in_window else None,
154
+ "matchedLine": res.matched_line if in_window else None,
155
+ "elapsedSeconds": round(total, 3),
156
+ "bytesSeen": res.bytes_seen, "bytesSeenTail": res.tail,
157
+ }
158
+ if res.matched and not in_window:
159
+ block["lateMatchAtSeconds"] = latency
160
+ block["lateMatchedLine"] = res.matched_line
161
+ return block
162
+ except OSError as err:
163
+ return {"port": spec.port, "baud": spec.baud, "observed": False, "error": str(err)}
164
+ finally:
165
+ try:
166
+ ser.close()
167
+ except OSError:
168
+ pass