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 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()