pytactl 2.0.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.
- pytactl/__init__.py +43 -0
- pytactl/cli.py +220 -0
- pytactl/debugboard.py +1054 -0
- pytactl/installconfigs.py +138 -0
- pytactl/service.py +152 -0
- pytactl/shell.py +93 -0
- pytactl/tac_configs/TAC_FTDI_13.tcnf +392 -0
- pytactl-2.0.0.dist-info/METADATA +265 -0
- pytactl-2.0.0.dist-info/RECORD +13 -0
- pytactl-2.0.0.dist-info/WHEEL +5 -0
- pytactl-2.0.0.dist-info/entry_points.txt +2 -0
- pytactl-2.0.0.dist-info/licenses/LICENSE +26 -0
- pytactl-2.0.0.dist-info/top_level.txt +1 -0
pytactl/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026 Qualcomm Technologies, Inc. and/or its subsidiaries.
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import platformdirs
|
|
7
|
+
|
|
8
|
+
__version__ = "2.0.0"
|
|
9
|
+
|
|
10
|
+
# Configs bundled with the package. Only the synthesized FTDI Alpaca-Lite
|
|
11
|
+
# default (TAC_FTDI_13.tcnf) ships here; the full config set (every .tcnf plus
|
|
12
|
+
# devicelist.json) is fetched with the "installconfigs" subcommand.
|
|
13
|
+
PACKAGE_TAC_CONFIG_PATH = os.path.join(os.path.dirname(__file__), "tac_configs")
|
|
14
|
+
|
|
15
|
+
# Where "installconfigs" writes the full config set by default.
|
|
16
|
+
INSTALLED_TAC_CONFIG_PATH = platformdirs.user_data_dir("pytactl")
|
|
17
|
+
|
|
18
|
+
# Default config repository fetched by "installconfigs".
|
|
19
|
+
DEFAULT_CONFIG_REPOSITORY = (
|
|
20
|
+
"https://github.com/qualcomm/qcom-test-automation-controller/"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Filename of the default FTDI Alpaca-Lite config used as a fallback. The upstream
|
|
24
|
+
# repository has no .tcnf for it (it is generated by FTDIPlatformConfiguration.cpp),
|
|
25
|
+
# so "installconfigs" copies the bundled TAC_FTDI_13.tcnf under this name.
|
|
26
|
+
DEFAULT_CONFIG_FILENAME = "default.tcnf"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def default_tac_config_path():
|
|
30
|
+
"""Resolve the default TAC config directory.
|
|
31
|
+
|
|
32
|
+
Prefer the installed config set (populated by "installconfigs") when it
|
|
33
|
+
exists and is non-empty; otherwise fall back to the configs bundled with
|
|
34
|
+
the package.
|
|
35
|
+
"""
|
|
36
|
+
installed = INSTALLED_TAC_CONFIG_PATH
|
|
37
|
+
if os.path.isdir(installed) and os.listdir(installed):
|
|
38
|
+
return installed
|
|
39
|
+
return PACKAGE_TAC_CONFIG_PATH
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Backwards-compatible module-level default; resolved at import time.
|
|
43
|
+
DEFAULT_TAC_CONFIG_PATH = default_tac_config_path()
|
pytactl/cli.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2025-2026 Qualcomm Technologies, Inc. and/or its subsidiaries.
|
|
4
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from argparse import ArgumentParser
|
|
9
|
+
|
|
10
|
+
from . import DEFAULT_CONFIG_REPOSITORY, __version__, default_tac_config_path
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _setup_logging(level):
|
|
16
|
+
logger.setLevel(level)
|
|
17
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
18
|
+
handler.setLevel(level)
|
|
19
|
+
formatter = logging.Formatter(
|
|
20
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
21
|
+
)
|
|
22
|
+
handler.setFormatter(formatter)
|
|
23
|
+
logger.addHandler(handler)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_parser():
|
|
27
|
+
parser = ArgumentParser(
|
|
28
|
+
prog="pytactl",
|
|
29
|
+
description="Test Automation Controller (TAC/Alpaca) for Qualcomm debug boards.",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Options shared by every subcommand.
|
|
36
|
+
base = ArgumentParser(add_help=False)
|
|
37
|
+
base.add_argument("--log-level", default="DEBUG", help="Log level (default: DEBUG)")
|
|
38
|
+
|
|
39
|
+
# Options shared by the board-driving subcommands.
|
|
40
|
+
common = ArgumentParser(add_help=False)
|
|
41
|
+
common.add_argument(
|
|
42
|
+
"--serial",
|
|
43
|
+
nargs="+",
|
|
44
|
+
help="Debug board serial number(s)",
|
|
45
|
+
)
|
|
46
|
+
common.add_argument(
|
|
47
|
+
"--tac-config-path",
|
|
48
|
+
default=default_tac_config_path(),
|
|
49
|
+
help="Path to directory with TAC configs (devicelist.json + .tcnf "
|
|
50
|
+
"files). Required for FTDI/PSOC boards; Bughopper boards need no configs. "
|
|
51
|
+
"Defaults to the configs installed by 'installconfigs', else those "
|
|
52
|
+
"bundled with the package.",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
subparsers = parser.add_subparsers(dest="mode", required=True, metavar="COMMAND")
|
|
56
|
+
|
|
57
|
+
subparsers.add_parser(
|
|
58
|
+
"list",
|
|
59
|
+
parents=[base],
|
|
60
|
+
help="List connected debug boards and their serial numbers",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
shell = subparsers.add_parser(
|
|
64
|
+
"shell", parents=[base, common], help="Run the interactive shell"
|
|
65
|
+
)
|
|
66
|
+
shell.add_argument(
|
|
67
|
+
"--config-file-path",
|
|
68
|
+
help="Path to a single config file; use for debugging the config file syntax.",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
oneshot = subparsers.add_parser(
|
|
72
|
+
"oneshot", parents=[base, common], help="Run a single command and exit"
|
|
73
|
+
)
|
|
74
|
+
oneshot.add_argument("command", help="Command to run, e.g. bootToEDL")
|
|
75
|
+
oneshot.add_argument(
|
|
76
|
+
"value",
|
|
77
|
+
nargs="?",
|
|
78
|
+
help="Optional integer value for pin commands, e.g. 1",
|
|
79
|
+
)
|
|
80
|
+
oneshot.add_argument(
|
|
81
|
+
"--config-file-path",
|
|
82
|
+
help="Path to a single config file; use for debugging the config file syntax.",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
installconfigs = subparsers.add_parser(
|
|
86
|
+
"installconfigs",
|
|
87
|
+
parents=[base],
|
|
88
|
+
help="Download TAC config files (.tcnf + devicelist.json) from the "
|
|
89
|
+
"config repository",
|
|
90
|
+
)
|
|
91
|
+
installconfigs.add_argument(
|
|
92
|
+
"--config-repository",
|
|
93
|
+
default=DEFAULT_CONFIG_REPOSITORY,
|
|
94
|
+
help="Config repository URL to fetch configs from (default: %(default)s)",
|
|
95
|
+
)
|
|
96
|
+
installconfigs.add_argument(
|
|
97
|
+
"--local-path",
|
|
98
|
+
help="Directory to install configs into (default: the platformdirs "
|
|
99
|
+
"user data directory for pytactl)",
|
|
100
|
+
)
|
|
101
|
+
installconfigs.add_argument(
|
|
102
|
+
"--ref",
|
|
103
|
+
default="HEAD",
|
|
104
|
+
help="Git ref (branch, tag, or commit) to fetch from (default: %(default)s)",
|
|
105
|
+
)
|
|
106
|
+
installconfigs.add_argument(
|
|
107
|
+
"--repository-path",
|
|
108
|
+
default="configurations",
|
|
109
|
+
help="Path within the repository holding the configs (default: %(default)s)",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
service = subparsers.add_parser(
|
|
113
|
+
"service", parents=[base, common], help="Run the REST API service"
|
|
114
|
+
)
|
|
115
|
+
service.add_argument(
|
|
116
|
+
"--hostname",
|
|
117
|
+
default="0.0.0.0",
|
|
118
|
+
help="Host name the server attaches to (default: 0.0.0.0)",
|
|
119
|
+
)
|
|
120
|
+
service.add_argument(
|
|
121
|
+
"--port",
|
|
122
|
+
default=5000,
|
|
123
|
+
type=int,
|
|
124
|
+
help="Port on the host to attach to (default: 5000)",
|
|
125
|
+
)
|
|
126
|
+
return parser
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _list_boards():
|
|
130
|
+
from .debugboard import Board
|
|
131
|
+
|
|
132
|
+
boards = Board.list_boards()
|
|
133
|
+
if not boards:
|
|
134
|
+
print("No connected debug boards found.")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
print("Connected debug boards:")
|
|
138
|
+
for board in boards:
|
|
139
|
+
serial = board["serial"] or "<no serial reported>"
|
|
140
|
+
vid_pid = f"{board['vid']:04x}:{board['pid']:04x}"
|
|
141
|
+
print(f" {board['type']:<14} vid:pid={vid_pid} serial={serial}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _single_board_serial(boards):
|
|
145
|
+
if not boards:
|
|
146
|
+
return None, "no connected debug boards found"
|
|
147
|
+
|
|
148
|
+
if len(boards) > 1:
|
|
149
|
+
serials = ", ".join(
|
|
150
|
+
board.get("serial") or "<no serial reported>" for board in boards
|
|
151
|
+
)
|
|
152
|
+
return None, f"multiple connected debug boards found: {serials}"
|
|
153
|
+
|
|
154
|
+
serial = boards[0].get("serial")
|
|
155
|
+
if not serial:
|
|
156
|
+
return None, "the connected debug board did not report a serial number"
|
|
157
|
+
|
|
158
|
+
return serial, None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _resolve_serial(args, parser, command):
|
|
162
|
+
if args.serial:
|
|
163
|
+
return args.serial[0]
|
|
164
|
+
|
|
165
|
+
if args.config_file_path:
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
from .debugboard import Board
|
|
169
|
+
|
|
170
|
+
serial, error = _single_board_serial(Board.list_boards())
|
|
171
|
+
if serial:
|
|
172
|
+
logger.info("Using only connected debug board serial %s", serial)
|
|
173
|
+
return serial
|
|
174
|
+
|
|
175
|
+
parser.error(f"{command} requires --serial or --config-file-path ({error})")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def main(argv=None):
|
|
179
|
+
parser = build_parser()
|
|
180
|
+
args = parser.parse_args(argv)
|
|
181
|
+
|
|
182
|
+
_setup_logging(args.log_level)
|
|
183
|
+
|
|
184
|
+
if args.mode == "list":
|
|
185
|
+
_list_boards()
|
|
186
|
+
elif args.mode == "installconfigs":
|
|
187
|
+
from .installconfigs import install_configs
|
|
188
|
+
|
|
189
|
+
install_configs(
|
|
190
|
+
args.config_repository,
|
|
191
|
+
args.local_path,
|
|
192
|
+
args.ref,
|
|
193
|
+
args.repository_path,
|
|
194
|
+
)
|
|
195
|
+
elif args.mode == "service":
|
|
196
|
+
if not args.serial:
|
|
197
|
+
parser.error("service requires --serial")
|
|
198
|
+
from .service import run_service
|
|
199
|
+
|
|
200
|
+
run_service(args.serial, args.tac_config_path, args.hostname, args.port)
|
|
201
|
+
elif args.mode == "oneshot":
|
|
202
|
+
from .shell import run_oneshot
|
|
203
|
+
|
|
204
|
+
serial = _resolve_serial(args, parser, "oneshot")
|
|
205
|
+
run_oneshot(
|
|
206
|
+
args.command,
|
|
207
|
+
serial,
|
|
208
|
+
args.config_file_path,
|
|
209
|
+
args.tac_config_path,
|
|
210
|
+
args.value,
|
|
211
|
+
)
|
|
212
|
+
else: # shell
|
|
213
|
+
from .shell import run_shell
|
|
214
|
+
|
|
215
|
+
serial = _resolve_serial(args, parser, "shell")
|
|
216
|
+
run_shell(serial, args.config_file_path, args.tac_config_path)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if __name__ == "__main__":
|
|
220
|
+
main()
|