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,185 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Headless serial capture: pure logic for `tan monitor --capture`
3
+ (tan-cli#1324).
4
+
5
+ Reads from an already-open port-like object (`read(size) -> bytes`, optional
6
+ `in_waiting`) for a fixed duration or until a regex matches, optionally
7
+ teeing the raw bytes to a binary sink. No TTY, no miniterm, no port opening;
8
+ the caller owns the port and the sink. The clock is injected so the timing is
9
+ testable without sleeping.
10
+
11
+ `--until` semantics: the regex is searched in every COMPLETE line (CR/LF
12
+ stripped) exactly once, and in the current UNTERMINATED line (so a prompt such
13
+ as `=> ` matches before any newline arrives), at most the last
14
+ `MAX_SEARCH_CHARS` of it per read. Long complete lines are searched on their
15
+ LAST `MAX_SEARCH_CHARS` too, the same window as the partial line. The deadline is checked between lines, so a
16
+ read that delivers thousands of lines cannot overrun `--duration`. Python's
17
+ `re` cannot be interrupted, so a pathological (catastrophic-backtracking)
18
+ pattern can still stall inside ONE search; the cap on the searched text and
19
+ the per-line deadline check bound the damage, they do not remove it.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import re
25
+ import time
26
+ from collections.abc import Callable
27
+ from dataclasses import dataclass
28
+ from typing import TYPE_CHECKING, BinaryIO, Protocol
29
+
30
+ if TYPE_CHECKING:
31
+ from tan.core.serial_actions import CaptureActions
32
+
33
+ TAIL_BYTES = 256
34
+ #: No single `re.search` is given more than this many characters.
35
+ MAX_SEARCH_CHARS = 4096
36
+ #: A partial (unterminated) line longer than this is truncated from the left
37
+ #: so a binary stream with no newline cannot grow memory without bound.
38
+ _MAX_PENDING = 65536
39
+ #: Duration used when `--until` is given without `--duration`.
40
+ DEFAULT_UNTIL_DURATION_S = 30.0
41
+
42
+
43
+ class ReadPort(Protocol):
44
+ def read(self, size: int = 1, /) -> bytes: ...
45
+
46
+
47
+ class SinkError(Exception):
48
+ """Writing the log failed (kept apart from serial I/O errors)."""
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class CaptureResult:
53
+ matched: bool
54
+ matched_line: str | None
55
+ elapsed_s: float
56
+ bytes_seen: int
57
+ tail: str
58
+
59
+
60
+ def compile_until(pattern: str) -> re.Pattern[str]:
61
+ """Compile `--until`; `ValueError` (with the regex error) if malformed or
62
+ empty, so the caller can map it to one validation issue."""
63
+ if not pattern:
64
+ raise ValueError("--until must not be empty")
65
+ try:
66
+ return re.compile(pattern)
67
+ except re.error as err:
68
+ raise ValueError(f"--until is not a valid regex: {err}") from err
69
+
70
+
71
+ def _text(b: bytes) -> str:
72
+ return b.decode("utf-8", errors="replace")
73
+
74
+
75
+ def _tail_text(buf: bytes) -> str:
76
+ if len(buf) > TAIL_BYTES:
77
+ buf = buf[-TAIL_BYTES:]
78
+ while buf and 0x80 <= buf[0] < 0xC0: # do not start mid-character
79
+ buf = buf[1:]
80
+ return _text(buf)
81
+
82
+
83
+ def capture(
84
+ port: ReadPort,
85
+ *,
86
+ duration_s: float,
87
+ until: re.Pattern[str] | None = None,
88
+ sink: BinaryIO | None = None,
89
+ initial: bytes = b"",
90
+ clock: Callable[[], float] = time.monotonic,
91
+ actions: "CaptureActions | None" = None,
92
+ ) -> CaptureResult:
93
+ """Read until `until` matches, or `duration_s` elapses.
94
+
95
+ On a match the capture stops at the end of the matched line: the rest of
96
+ the same read is not logged or counted. `initial` is data already received before this call (the `--break-uboot`
97
+ tail): it is logged and searched like any other chunk, so a prompt that
98
+ ended the break-in can satisfy `--until`. At least one read happens even
99
+ with a zero duration. A sink failure raises `SinkError`; a port failure
100
+ propagates as the port's own `OSError`.
101
+
102
+ `actions` (tan-cli#1451, `tan.core.serial_actions`): ticked every loop
103
+ iteration and offered each complete line and the current partial line
104
+ BEFORE `until` is tested. A reopen replaces the port, so the loop reads
105
+ `actions.port` rather than `port` when given. Lines are scanned when
106
+ either `until` or a line-triggered action is present.
107
+ """
108
+ start = clock()
109
+ deadline = start + duration_s
110
+ pending = b""
111
+ tail = b""
112
+ seen = 0
113
+ first = True
114
+
115
+ def commit(part: bytes) -> None:
116
+ """Count, tail and log `part`: what the capture reports as received."""
117
+ nonlocal seen, tail
118
+ seen += len(part)
119
+ tail = (tail + part)[-TAIL_BYTES:]
120
+ if sink is not None and part:
121
+ try:
122
+ sink.write(part)
123
+ sink.flush()
124
+ except OSError as err:
125
+ raise SinkError(str(err)) from err
126
+
127
+ def done(matched: bool, line: str | None) -> CaptureResult:
128
+ return CaptureResult(matched, line, clock() - start, seen, _tail_text(tail))
129
+
130
+ while True:
131
+ if first and initial:
132
+ chunk = initial
133
+ else:
134
+ if actions is not None:
135
+ actions.tick()
136
+ src = actions.port if actions is not None else port
137
+ n = max(1, int(getattr(src, "in_waiting", 0) or 0))
138
+ chunk = src.read(n) or b""
139
+ first = False
140
+ scan = until is not None or (actions is not None and actions.spec.watches_lines)
141
+ if chunk and not scan:
142
+ commit(chunk)
143
+ elif chunk:
144
+ before = len(pending)
145
+ *lines, pending = (pending + chunk).split(b"\n")
146
+ pending = pending[-_MAX_PENDING:]
147
+ # A read that returns at or after the deadline is still searched
148
+ # whole (tan-cli#1515). Its size is bounded by `in_waiting` (and
149
+ # each line by MAX_SEARCH_CHARS), so it needs no flood guard. Only
150
+ # scanning that itself overruns the deadline stops, and then only
151
+ # the lines already searched are committed, so the log never holds
152
+ # a line the result says was not searched.
153
+ late = clock() >= deadline
154
+ end = -before # offset of the current line's end within `chunk`
155
+ for raw in lines: # each complete line is new data: search it once
156
+ end += len(raw) + 1
157
+ line = _text(raw).rstrip("\r")[-MAX_SEARCH_CHARS:]
158
+ if actions is not None:
159
+ actions.line(line)
160
+ if until is not None and until.search(line):
161
+ # Stop at the END OF THE MATCHED LINE: bytes after it in the
162
+ # same read are neither logged nor reported.
163
+ commit(chunk[: max(0, end)])
164
+ return done(True, line)
165
+ if not late and clock() >= deadline:
166
+ commit(chunk[: max(0, end)])
167
+ return done(False, None)
168
+ if actions is not None and actions.take_flush():
169
+ # Baud changed: the rest of this read and the partial line
170
+ # are old-baud garbage; keep only up to the matched line.
171
+ commit(chunk[: max(0, end)])
172
+ pending, flushed = b"", True
173
+ break
174
+ else:
175
+ flushed = False
176
+ if flushed:
177
+ continue
178
+ commit(chunk)
179
+ partial = _text(pending).rstrip("\r")[-MAX_SEARCH_CHARS:]
180
+ if partial and actions is not None:
181
+ actions.line(partial, partial=True)
182
+ if partial and until is not None and until.search(partial):
183
+ return done(True, partial)
184
+ if clock() >= deadline:
185
+ return done(False, None)
tan/core/serial_url.py ADDED
@@ -0,0 +1,45 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Up-front validation of a `--port` that is a pyserial URL (tan-cli#1448).
3
+
4
+ `rfc2217://100.64.0.1:` (an empty port number) used to reach pyserial, which
5
+ failed deep inside with `'<=' not supported between instances of 'int' and
6
+ 'NoneType'`, reported as `monitor.capture-open-failed`. This names the problem
7
+ instead. A plain device path (no `://`) is not a URL and is never judged here.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from urllib.parse import urlsplit
12
+
13
+ #: pyserial's `serial.urlhandler` schemes (3.5). A scheme outside this set is
14
+ #: refused rather than handed to pyserial to fail obscurely.
15
+ KNOWN_SCHEMES = frozenset({"alt", "cp2110", "hwgrep", "loop", "rfc2217", "socket", "spy"})
16
+ #: The schemes that name a TCP endpoint and so need `host:port`.
17
+ NETWORK_SCHEMES = frozenset({"rfc2217", "socket"})
18
+
19
+
20
+ def port_url_problem(port: str) -> str | None:
21
+ """What is wrong with `port` as a serial URL, or `None` if it is fine or not a URL."""
22
+ if "://" not in port:
23
+ return None
24
+ scheme = port.split("://", 1)[0].lower()
25
+ if scheme not in KNOWN_SCHEMES:
26
+ return (
27
+ f"unknown serial URL scheme '{scheme}://' in '{port}' "
28
+ f"(known: {', '.join(sorted(s + '://' for s in KNOWN_SCHEMES))})"
29
+ )
30
+ if scheme not in NETWORK_SCHEMES:
31
+ return None
32
+ try:
33
+ parts = urlsplit(port)
34
+ host, number = parts.hostname, parts.port
35
+ except ValueError as err:
36
+ return f"'{port}' is not a valid {scheme}:// URL: {err}"
37
+ if not host:
38
+ return f"'{port}' has no host; use {scheme}://HOST:PORT"
39
+ if number is None:
40
+ netloc = parts.netloc
41
+ why = "the port number is empty" if netloc.endswith(":") else "the port number is missing"
42
+ return f"'{port}': {why}; use {scheme}://{host}:PORT (1-65535)"
43
+ if not 1 <= number <= 65535:
44
+ return f"'{port}': port {number} is outside 1-65535"
45
+ return None