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
tan/core/jlink_probe.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""J-Link probe selection for `tan flash` (tan-cli#1312). Pure decision logic
|
|
3
|
+
plus one read-only sysfs enumerator.
|
|
4
|
+
|
|
5
|
+
WHY THIS EXISTS. J-Link Commander selects an emulator by SERIAL only
|
|
6
|
+
(`SelectEmuBySN`). On the Alp Lab bench every probe carries the SAME OEM-cloned
|
|
7
|
+
serial, so a serial cannot tell two boards apart, and a Flow D MRAM write
|
|
8
|
+
issued with a shared serial lands on whichever probe the J-Link DLL opens
|
|
9
|
+
first. What DOES identify a probe on Linux is its USB port path (`3-4.2`, the
|
|
10
|
+
sysfs device name), and that path is not something J-Link can be told.
|
|
11
|
+
|
|
12
|
+
THE DESIGN (the honest one), in three layers:
|
|
13
|
+
|
|
14
|
+
1. SELECTION (`resolve_probe_selection`, from a sysfs enumeration). A
|
|
15
|
+
`--probe-usb-path` is resolved to the serial sysfs reports for that port. A
|
|
16
|
+
serial (CLI or `flash_args.jlink_serial`) is matched against the visible
|
|
17
|
+
probes by its CANONICAL form (`canon_serial`: `603000869` and
|
|
18
|
+
`000603000869` are the same serial -- YAML turns the unquoted one into an
|
|
19
|
+
int). A serial no visible probe carries is refused; one that several visible
|
|
20
|
+
probes carry is refused unless a USB path names the target.
|
|
21
|
+
2. VERIFICATION at spawn time (`verify_emulator_list`). Selection alone cannot
|
|
22
|
+
make J-Link open the right probe when serials collide -- only isolation
|
|
23
|
+
OUTSIDE tan can (a wrapper masking every other probe's `/dev/bus/usb` node
|
|
24
|
+
AND sysfs directory in a private mount namespace). So immediately before
|
|
25
|
+
EACH `JLinkExe` spawn tan runs the same binary read-only (`ShowEmuList`) and
|
|
26
|
+
requires exactly one emulator whose canonical serial matches the selection
|
|
27
|
+
(and that the listing run itself succeeded: rc 0, no timeout). When the
|
|
28
|
+
serial is SHARED, one emulator in the list still proves nothing about WHICH
|
|
29
|
+
port it is -- a wrapper masking the wrong place passes that -- so the
|
|
30
|
+
wrapper must also print `TAN_PROBE_ISOLATED_USB_PATH=<its target port>`
|
|
31
|
+
(`HANDSHAKE_PREFIX`) and tan requires it to equal the selected path; the
|
|
32
|
+
envelope then says `wrapper-attested:<path>`, never "verified". A masking
|
|
33
|
+
wrapper on PATH passes; plain `JLinkExe` with cloned serials refuses.
|
|
34
|
+
`TAN_PROBE_USB_PATH=<resolved path>` is exported in that spawn's environment
|
|
35
|
+
so a wrapper can cross-check its own mask against it.
|
|
36
|
+
3. TOCTOU (`snapshot_drift`). The enumeration is repeated before each spawn
|
|
37
|
+
and the run refuses if the set of (path, canonical serial) changed since
|
|
38
|
+
selection.
|
|
39
|
+
|
|
40
|
+
Enumeration is strictly read-only (directory listing + reading two sysfs
|
|
41
|
+
attributes). It never opens a probe.
|
|
42
|
+
"""
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import os
|
|
46
|
+
import re
|
|
47
|
+
import sys
|
|
48
|
+
from dataclasses import dataclass
|
|
49
|
+
from typing import Any
|
|
50
|
+
|
|
51
|
+
#: SEGGER's USB vendor id as sysfs spells it.
|
|
52
|
+
SEGGER_VENDOR_ID = "1366"
|
|
53
|
+
|
|
54
|
+
SYSFS_USB_DEVICES = "/sys/bus/usb/devices"
|
|
55
|
+
|
|
56
|
+
#: A sysfs USB device name for a device on a hub port: `<bus>-<port>[.<port>...]`.
|
|
57
|
+
_USB_PATH_RE = re.compile(r"^[0-9]+-[0-9]+(\.[0-9]+)*$")
|
|
58
|
+
|
|
59
|
+
#: Exported into every `JLinkExe` spawn of a selected probe: the resolved USB
|
|
60
|
+
#: path, for a wrapper to cross-check its mask against.
|
|
61
|
+
USB_PATH_ENV = "TAN_PROBE_USB_PATH"
|
|
62
|
+
|
|
63
|
+
ISOLATION_VERIFIED = "verified-single-emulator"
|
|
64
|
+
|
|
65
|
+
#: A line a wrapper prints (from its actual target port) into `ShowEmuList`
|
|
66
|
+
#: output; the only way tan can bind a single listed emulator to a USB path.
|
|
67
|
+
HANDSHAKE_PREFIX = "TAN_PROBE_ISOLATED_USB_PATH="
|
|
68
|
+
_HANDSHAKE_RE = re.compile(r"^" + HANDSHAKE_PREFIX + r"(\S+)\s*$", re.MULTILINE)
|
|
69
|
+
|
|
70
|
+
CODE_AMBIGUOUS = "ambiguous"
|
|
71
|
+
CODE_NOT_FOUND = "not-found"
|
|
72
|
+
CODE_CONFLICT = "selector-conflict"
|
|
73
|
+
#: A spawn-time guard failure that is NOT an ambiguity: the check itself could
|
|
74
|
+
#: not be completed or found nothing (`flash.probe-verify-failed`).
|
|
75
|
+
CODE_VERIFY_FAILED = "verify-failed"
|
|
76
|
+
|
|
77
|
+
#: One `ShowEmuList` line: `J-Link[0]: Connection: USB, Serial number: 000603000869, ...`.
|
|
78
|
+
_EMU_SERIAL_RE = re.compile(r"Serial number:\s*([0-9A-Za-z]+)")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ProbeEnumerationError(Exception):
|
|
82
|
+
"""A device directory that is populated but cannot be read (unreadable
|
|
83
|
+
`idVendor`, or a SEGGER device with no readable `serial`). NOT the same as
|
|
84
|
+
an empty directory, which is what a mount-namespace mask leaves behind."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class JLinkProbe:
|
|
89
|
+
"""One visible J-Link: its USB port path and the serial sysfs reports."""
|
|
90
|
+
|
|
91
|
+
usb_path: str
|
|
92
|
+
serial: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class ProbeSelection:
|
|
97
|
+
"""The outcome of resolving which probe a run will use.
|
|
98
|
+
|
|
99
|
+
`refusal_code` is one of the `CODE_*` suffixes (the issue code is
|
|
100
|
+
`flash.probe-<suffix>`) and is set iff the run must refuse before any write.
|
|
101
|
+
`serial` is what Flow D must put in `SelectEmuBySN` (`None`: pin nothing).
|
|
102
|
+
`candidates` lists every visible probe sharing `serial` when more than one
|
|
103
|
+
does -- the case only spawn-time verification can settle.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
serial: str | None
|
|
107
|
+
usb_path: str | None
|
|
108
|
+
source: str
|
|
109
|
+
visible: int | None
|
|
110
|
+
candidates: tuple[str, ...] = ()
|
|
111
|
+
overrides_manifest_serial: str | None = None
|
|
112
|
+
refusal_code: str | None = None
|
|
113
|
+
refusal: str | None = None
|
|
114
|
+
|
|
115
|
+
def as_dict(self) -> dict[str, Any]:
|
|
116
|
+
out: dict[str, Any] = {
|
|
117
|
+
"source": self.source,
|
|
118
|
+
"serial": self.serial,
|
|
119
|
+
"usbPath": self.usb_path,
|
|
120
|
+
"visibleProbes": self.visible,
|
|
121
|
+
}
|
|
122
|
+
if self.overrides_manifest_serial is not None:
|
|
123
|
+
out["overridesFlashArgsSerial"] = self.overrides_manifest_serial
|
|
124
|
+
if self.candidates:
|
|
125
|
+
out["candidates"] = list(self.candidates)
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
def describe(self) -> str:
|
|
129
|
+
who = self.serial if self.serial is not None else "(none pinned)"
|
|
130
|
+
where = f" at USB path {self.usb_path}" if self.usb_path else ""
|
|
131
|
+
shared = ""
|
|
132
|
+
if len(self.candidates) > 1 and self.usb_path:
|
|
133
|
+
shared = (
|
|
134
|
+
f"; serial shared by {', '.join(self.candidates)} -- a real run needs the "
|
|
135
|
+
"J-Link on PATH to show exactly one emulator with it (masking wrapper)"
|
|
136
|
+
)
|
|
137
|
+
return f"probe selection: serial {who}{where} via {self.source}{shared}"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def canon_serial(text: str) -> str:
|
|
141
|
+
"""The comparison form of a J-Link serial: stripped, and when all digits,
|
|
142
|
+
without leading zeros (`000603000869` == `603000869`)."""
|
|
143
|
+
text = text.strip()
|
|
144
|
+
return str(int(text)) if text.isascii() and text.isdigit() else text
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def is_valid_usb_path(text: str) -> bool:
|
|
148
|
+
return bool(_USB_PATH_RE.match(text))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def enumerate_jlinks(root: str | None = None) -> list[JLinkProbe] | None:
|
|
152
|
+
"""Every SEGGER device visible in sysfs, sorted by path. READ-ONLY.
|
|
153
|
+
|
|
154
|
+
`None` means "this host cannot enumerate" (no sysfs USB tree: macOS,
|
|
155
|
+
Windows, a container without sysfs) -- distinct from `[]`, "enumerated and
|
|
156
|
+
found none". Raises `ProbeEnumerationError` for a populated device
|
|
157
|
+
directory it cannot read -- never read as "no probe there"."""
|
|
158
|
+
if root is None and not sys.platform.startswith("linux"):
|
|
159
|
+
return None # no sysfs USB tree to read: cannot enumerate
|
|
160
|
+
root = root or SYSFS_USB_DEVICES
|
|
161
|
+
try:
|
|
162
|
+
names = sorted(os.listdir(root))
|
|
163
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
164
|
+
return None
|
|
165
|
+
except OSError as err:
|
|
166
|
+
raise ProbeEnumerationError(f"{root}: {err.strerror or err}") from err
|
|
167
|
+
probes: list[JLinkProbe] = []
|
|
168
|
+
for name in names:
|
|
169
|
+
# Interfaces (`3-4.2:1.0`) and root hubs (`usb3`) are not devices on a
|
|
170
|
+
# port; only `<bus>-<port>` names are.
|
|
171
|
+
if ":" in name or not _USB_PATH_RE.match(name):
|
|
172
|
+
continue
|
|
173
|
+
probe = _read_device(os.path.join(root, name), name)
|
|
174
|
+
if probe is not None:
|
|
175
|
+
probes.append(probe)
|
|
176
|
+
return probes
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _read_device(base: str, name: str) -> JLinkProbe | None:
|
|
180
|
+
try:
|
|
181
|
+
populated = bool(os.listdir(base))
|
|
182
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
183
|
+
populated = False # unplugged between the two listings, or not a device dir
|
|
184
|
+
except OSError as err:
|
|
185
|
+
raise ProbeEnumerationError(f"{name}: {err.strerror or err}") from err
|
|
186
|
+
if not populated:
|
|
187
|
+
return None # an empty directory is what a namespace mask leaves
|
|
188
|
+
vendor = _read_attr(os.path.join(base, "idVendor"))
|
|
189
|
+
if vendor is None:
|
|
190
|
+
raise ProbeEnumerationError(f"{name}: idVendor is unreadable")
|
|
191
|
+
if vendor != SEGGER_VENDOR_ID:
|
|
192
|
+
return None
|
|
193
|
+
serial = _read_attr(os.path.join(base, "serial"))
|
|
194
|
+
if not serial:
|
|
195
|
+
raise ProbeEnumerationError(f"{name}: a SEGGER device whose serial is unreadable")
|
|
196
|
+
return JLinkProbe(usb_path=name, serial=serial)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _read_attr(path: str) -> str | None:
|
|
200
|
+
try:
|
|
201
|
+
with open(path, encoding="utf-8", errors="replace") as handle:
|
|
202
|
+
return handle.read().strip()
|
|
203
|
+
except OSError:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def probe_set(probes: list[JLinkProbe] | None) -> frozenset[tuple[str, str]] | None:
|
|
208
|
+
"""The identity of an enumeration for the TOCTOU comparison."""
|
|
209
|
+
if probes is None:
|
|
210
|
+
return None
|
|
211
|
+
return frozenset((p.usb_path, canon_serial(p.serial)) for p in probes)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def snapshot_drift(
|
|
215
|
+
snapshot: frozenset[tuple[str, str]] | None, now: list[JLinkProbe] | None
|
|
216
|
+
) -> str | None:
|
|
217
|
+
"""A refusal when the visible probes changed between selection and spawn."""
|
|
218
|
+
if snapshot is None:
|
|
219
|
+
return None
|
|
220
|
+
current = probe_set(now)
|
|
221
|
+
if current == snapshot:
|
|
222
|
+
return None
|
|
223
|
+
return (
|
|
224
|
+
"the visible J-Links changed between probe selection and the spawn "
|
|
225
|
+
f"(was {_fmt_set(snapshot)}, now {_fmt_set(current)}); refusing rather than "
|
|
226
|
+
"writing through a probe set that was not the one selected."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _fmt_set(items: frozenset[tuple[str, str]] | None) -> str:
|
|
231
|
+
if items is None:
|
|
232
|
+
return "unenumerable"
|
|
233
|
+
return ", ".join(f"{p} ({s})" for p, s in sorted(items)) or "none"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def parse_show_emu_list(text: str) -> list[str]:
|
|
237
|
+
"""The serials a `ShowEmuList` output names, in order."""
|
|
238
|
+
return _EMU_SERIAL_RE.findall(text)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def parse_handshakes(text: str) -> list[str]:
|
|
242
|
+
"""The distinct `TAN_PROBE_ISOLATED_USB_PATH=` values a listing carries."""
|
|
243
|
+
return sorted(set(_HANDSHAKE_RE.findall(text)))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def verify_listing(
|
|
247
|
+
serial: str | None,
|
|
248
|
+
usb_path: str | None,
|
|
249
|
+
candidates: tuple[str, ...],
|
|
250
|
+
listing: str,
|
|
251
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
252
|
+
"""Spawn-time verification of a `ShowEmuList` listing. Returns
|
|
253
|
+
`(code, message, isolation)`: `code is None` means proceed and `isolation`
|
|
254
|
+
is what the envelope reports; else `code` is `CODE_AMBIGUOUS` or
|
|
255
|
+
`CODE_VERIFY_FAILED`.
|
|
256
|
+
|
|
257
|
+
`serial=None` is the unpinned run on a host that cannot enumerate: exactly
|
|
258
|
+
one emulator in total is required."""
|
|
259
|
+
emus = parse_show_emu_list(listing)
|
|
260
|
+
want = canon_serial(serial) if serial is not None else None
|
|
261
|
+
matching = [e for e in emus if want is None or canon_serial(e) == want]
|
|
262
|
+
if not matching:
|
|
263
|
+
return (CODE_VERIFY_FAILED,
|
|
264
|
+
f"J-Link's own ShowEmuList shows no emulator"
|
|
265
|
+
f"{' with serial ' + serial if serial else ''} "
|
|
266
|
+
f"(saw: {', '.join(emus) or 'none'}); refusing to write.", None)
|
|
267
|
+
if len(matching) > 1:
|
|
268
|
+
return (CODE_AMBIGUOUS, _several_emulators(len(matching), serial), None)
|
|
269
|
+
return _bind_to_path(usb_path, candidates, parse_handshakes(listing))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _several_emulators(n: int, serial: str | None) -> str:
|
|
273
|
+
return (
|
|
274
|
+
f"J-Link's own ShowEmuList shows {n} emulators"
|
|
275
|
+
f"{' with serial ' + serial if serial else ''}, so SelectEmuBySN would open "
|
|
276
|
+
"whichever enumerates first -- possibly the wrong board. J-Link selects by "
|
|
277
|
+
"serial only; run it where only the target probe is visible (a wrapper masking "
|
|
278
|
+
"the other probes' /dev/bus/usb nodes AND sysfs directories in a private mount "
|
|
279
|
+
"namespace) or give the probes distinct serials. flash_args.expect_dpidr stays "
|
|
280
|
+
"mandatory on a shared-serial bench: it is the only check that the board "
|
|
281
|
+
"answering is the one intended."
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _bind_to_path(
|
|
286
|
+
usb_path: str | None, candidates: tuple[str, ...], handshakes: list[str]
|
|
287
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
288
|
+
if len(handshakes) > 1:
|
|
289
|
+
return (CODE_AMBIGUOUS,
|
|
290
|
+
f"ShowEmuList carries conflicting {HANDSHAKE_PREFIX} lines "
|
|
291
|
+
f"({', '.join(handshakes)}); refusing.", None)
|
|
292
|
+
if handshakes and usb_path is not None and handshakes[0] != usb_path:
|
|
293
|
+
return (CODE_AMBIGUOUS,
|
|
294
|
+
f"the J-Link wrapper is isolated to USB path {handshakes[0]} but "
|
|
295
|
+
f"{usb_path} was selected; refusing -- this is the wrong place.", None)
|
|
296
|
+
if handshakes:
|
|
297
|
+
return None, None, f"wrapper-attested:{handshakes[0]}"
|
|
298
|
+
if len(candidates) > 1:
|
|
299
|
+
return (CODE_AMBIGUOUS,
|
|
300
|
+
f"serial is shared by {', '.join(candidates)}: one emulator in "
|
|
301
|
+
"ShowEmuList cannot be bound to the selected USB path by itself (a "
|
|
302
|
+
"wrapper masking the WRONG place would look identical). The J-Link on "
|
|
303
|
+
f"PATH must print {HANDSHAKE_PREFIX}<its target port> and it must equal "
|
|
304
|
+
f"{usb_path}.", None)
|
|
305
|
+
return None, None, ISOLATION_VERIFIED
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _paths(probes: list[JLinkProbe]) -> tuple[str, ...]:
|
|
309
|
+
return tuple(p.usb_path for p in probes)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _refuse(
|
|
313
|
+
code: str,
|
|
314
|
+
message: str,
|
|
315
|
+
*,
|
|
316
|
+
serial: str | None,
|
|
317
|
+
usb_path: str | None,
|
|
318
|
+
source: str,
|
|
319
|
+
visible: int | None,
|
|
320
|
+
candidates: tuple[str, ...] = (),
|
|
321
|
+
) -> ProbeSelection:
|
|
322
|
+
return ProbeSelection(
|
|
323
|
+
serial=serial,
|
|
324
|
+
usb_path=usb_path,
|
|
325
|
+
source=source,
|
|
326
|
+
visible=visible,
|
|
327
|
+
candidates=candidates,
|
|
328
|
+
refusal_code=code,
|
|
329
|
+
refusal=message,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
_HINT = (
|
|
334
|
+
"Pass --probe-usb-path <bus-port> to name one probe. Note J-Link Commander selects "
|
|
335
|
+
"by serial only, so with shared serials the write is additionally refused unless "
|
|
336
|
+
"the J-Link on PATH is isolated to that one probe (a masking wrapper). "
|
|
337
|
+
"flash_args.expect_dpidr stays mandatory on a shared-serial bench."
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def resolve_probe_selection(
|
|
342
|
+
probes: list[JLinkProbe] | None,
|
|
343
|
+
*,
|
|
344
|
+
cli_serial: str | None,
|
|
345
|
+
cli_usb_path: str | None,
|
|
346
|
+
manifest_serial: str | None,
|
|
347
|
+
) -> ProbeSelection:
|
|
348
|
+
"""Decide which J-Link a Flow D run will use, or that it must refuse.
|
|
349
|
+
|
|
350
|
+
`probes` is `enumerate_jlinks()`'s answer (`None`: cannot enumerate).
|
|
351
|
+
Precedence: `--probe-usb-path`, then `--probe-serial`, then
|
|
352
|
+
`flash_args.jlink_serial`; the CLI selectors override the manifest for this
|
|
353
|
+
run. Pure."""
|
|
354
|
+
if cli_usb_path is not None:
|
|
355
|
+
return _by_usb_path(probes, cli_usb_path, cli_serial, manifest_serial)
|
|
356
|
+
serial = cli_serial if cli_serial is not None else manifest_serial
|
|
357
|
+
if serial is not None:
|
|
358
|
+
return _by_serial(probes, serial, cli_serial, manifest_serial)
|
|
359
|
+
return _unselected(probes)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _usb_path_refusal(
|
|
363
|
+
probes: list[JLinkProbe] | None, path: str, cli_serial: str | None
|
|
364
|
+
) -> ProbeSelection | None:
|
|
365
|
+
visible = None if probes is None else len(probes)
|
|
366
|
+
if probes is None:
|
|
367
|
+
return _refuse(
|
|
368
|
+
CODE_NOT_FOUND,
|
|
369
|
+
f"--probe-usb-path {path}: this host cannot enumerate USB devices (no "
|
|
370
|
+
"sysfs), so tan cannot verify which probe that is. Refusing rather than "
|
|
371
|
+
"writing through an unverified probe.",
|
|
372
|
+
serial=None, usb_path=path, source="cli-usb-path", visible=None,
|
|
373
|
+
)
|
|
374
|
+
hit = next((p for p in probes if p.usb_path == path), None)
|
|
375
|
+
if hit is None:
|
|
376
|
+
return _refuse(
|
|
377
|
+
CODE_NOT_FOUND,
|
|
378
|
+
f"--probe-usb-path {path}: no J-Link is visible at that USB path. "
|
|
379
|
+
f"Visible J-Links: {', '.join(_paths(probes)) or 'none'}.",
|
|
380
|
+
serial=None, usb_path=path, source="cli-usb-path",
|
|
381
|
+
visible=visible, candidates=_paths(probes),
|
|
382
|
+
)
|
|
383
|
+
if cli_serial is not None and canon_serial(cli_serial) != canon_serial(hit.serial):
|
|
384
|
+
return _refuse(
|
|
385
|
+
CODE_CONFLICT,
|
|
386
|
+
f"--probe-usb-path {path} is a probe with serial {hit.serial}, but "
|
|
387
|
+
f"--probe-serial {cli_serial} was also given. The two selectors name "
|
|
388
|
+
"different probes; refusing.",
|
|
389
|
+
serial=hit.serial, usb_path=path, source="cli-usb-path", visible=visible,
|
|
390
|
+
candidates=_paths(
|
|
391
|
+
[p for p in probes if canon_serial(p.serial) == canon_serial(hit.serial)]
|
|
392
|
+
),
|
|
393
|
+
)
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _by_usb_path(
|
|
398
|
+
probes: list[JLinkProbe] | None,
|
|
399
|
+
path: str,
|
|
400
|
+
cli_serial: str | None,
|
|
401
|
+
manifest_serial: str | None,
|
|
402
|
+
) -> ProbeSelection:
|
|
403
|
+
refusal = _usb_path_refusal(probes, path, cli_serial)
|
|
404
|
+
if refusal is not None:
|
|
405
|
+
return refusal
|
|
406
|
+
assert probes is not None # `_usb_path_refusal` refused the None case
|
|
407
|
+
hit = next(p for p in probes if p.usb_path == path)
|
|
408
|
+
sharing = [p for p in probes if canon_serial(p.serial) == canon_serial(hit.serial)]
|
|
409
|
+
overrides = (
|
|
410
|
+
manifest_serial
|
|
411
|
+
if manifest_serial is not None
|
|
412
|
+
and canon_serial(manifest_serial) != canon_serial(hit.serial)
|
|
413
|
+
else None
|
|
414
|
+
)
|
|
415
|
+
return ProbeSelection(
|
|
416
|
+
serial=hit.serial,
|
|
417
|
+
usb_path=path,
|
|
418
|
+
source="cli-usb-path",
|
|
419
|
+
visible=len(probes),
|
|
420
|
+
candidates=_paths(sharing) if len(sharing) > 1 else (),
|
|
421
|
+
overrides_manifest_serial=overrides,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _by_serial(
|
|
426
|
+
probes: list[JLinkProbe] | None,
|
|
427
|
+
serial: str,
|
|
428
|
+
cli_serial: str | None,
|
|
429
|
+
manifest_serial: str | None,
|
|
430
|
+
) -> ProbeSelection:
|
|
431
|
+
source = "cli-serial" if cli_serial is not None else "flash_args.jlink_serial"
|
|
432
|
+
overrides = (
|
|
433
|
+
manifest_serial
|
|
434
|
+
if cli_serial is not None
|
|
435
|
+
and manifest_serial is not None
|
|
436
|
+
and canon_serial(manifest_serial) != canon_serial(cli_serial)
|
|
437
|
+
else None
|
|
438
|
+
)
|
|
439
|
+
if probes is None:
|
|
440
|
+
# Unverifiable host: the spawn-time ShowEmuList check is the only guard.
|
|
441
|
+
return ProbeSelection(serial, None, source, None, overrides_manifest_serial=overrides)
|
|
442
|
+
matches = [p for p in probes if canon_serial(p.serial) == canon_serial(serial)]
|
|
443
|
+
if not matches:
|
|
444
|
+
return _refuse(
|
|
445
|
+
CODE_NOT_FOUND,
|
|
446
|
+
f"serial {serial} ({source}): no visible J-Link carries that serial. "
|
|
447
|
+
"Visible J-Links: "
|
|
448
|
+
+ (", ".join(f"{p.usb_path} ({p.serial})" for p in probes) or "none")
|
|
449
|
+
+ ".",
|
|
450
|
+
serial=serial, usb_path=None, source=source, visible=len(probes),
|
|
451
|
+
candidates=_paths(probes),
|
|
452
|
+
)
|
|
453
|
+
if len(matches) > 1:
|
|
454
|
+
return _refuse(
|
|
455
|
+
CODE_AMBIGUOUS,
|
|
456
|
+
f"serial {serial} ({source}) is shared by {len(matches)} visible probes "
|
|
457
|
+
f"({', '.join(_paths(matches))}), so J-Link would open whichever "
|
|
458
|
+
"enumerates first -- possibly the wrong board. " + _HINT,
|
|
459
|
+
serial=serial, usb_path=None, source=source, visible=len(probes),
|
|
460
|
+
candidates=_paths(matches),
|
|
461
|
+
)
|
|
462
|
+
return ProbeSelection(
|
|
463
|
+
matches[0].serial, matches[0].usb_path, source, len(probes),
|
|
464
|
+
overrides_manifest_serial=overrides,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _unselected(probes: list[JLinkProbe] | None) -> ProbeSelection:
|
|
469
|
+
if probes is None or not probes:
|
|
470
|
+
return ProbeSelection(None, None, "unpinned", None if probes is None else 0)
|
|
471
|
+
if len(probes) > 1:
|
|
472
|
+
return _refuse(
|
|
473
|
+
CODE_AMBIGUOUS,
|
|
474
|
+
f"{len(probes)} J-Links are visible ({', '.join(_paths(probes))}) and nothing "
|
|
475
|
+
"selects one: no --probe-usb-path, no --probe-serial, no "
|
|
476
|
+
"flash_args.jlink_serial. Refusing before any write. " + _HINT,
|
|
477
|
+
serial=None, usb_path=None, source="unpinned", visible=len(probes),
|
|
478
|
+
candidates=_paths(probes),
|
|
479
|
+
)
|
|
480
|
+
only = probes[0]
|
|
481
|
+
return ProbeSelection(only.serial, only.usb_path, "sole-visible", 1)
|