PyOpenocdClient 0.1.2__py3-none-any.whl → 0.2.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.
@@ -6,6 +6,7 @@ from .errors import ( # noqa: F401
6
6
  OcdCommandFailedError,
7
7
  OcdCommandTimeoutError,
8
8
  OcdConnectionError,
9
+ OcdEmptyResponseError,
9
10
  OcdInvalidResponseError,
10
11
  )
11
12
  from .types import BpInfo, BpType, OcdCommandResult, WpInfo, WpType # noqa: F401
@@ -0,0 +1,99 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from .client import PyOpenocdClient
7
+ from .errors import OcdCommandTimeoutError, OcdConnectionError, OcdInvalidResponseError
8
+
9
+ _DEFAULT_HOST = "127.0.0.1"
10
+ _DEFAULT_PORT = 6666
11
+ _DEFAULT_TIMEOUT = 5.0
12
+
13
+ _EXIT_CODE_CONNECTION_ERROR = 91
14
+ _EXIT_CODE_INVALID_RESPONSE = 92
15
+ _EXIT_CODE_COMMAND_TIMEOUT = 93
16
+
17
+
18
+ def eprint(s: str) -> None:
19
+ print("error: " + s, file=sys.stderr)
20
+
21
+
22
+ def parse_args() -> argparse.Namespace:
23
+ desc = (
24
+ "Command-line utility that sends a single Tcl command to OpenOCD\n"
25
+ "through its Tcl-RPC interface."
26
+ )
27
+ epilog = (
28
+ "If the Tcl command completes successfully, this tool exits with code 0.\n"
29
+ "Connection errors or command execution errors cause it to exit with\n"
30
+ "a non-zero code.\n\n"
31
+ "All the output of the Tcl command is written to stdout.\n"
32
+ "Any error messages coming from this tool are written to stderr."
33
+ )
34
+ parser = argparse.ArgumentParser(
35
+ description=desc,
36
+ epilog=epilog,
37
+ formatter_class=argparse.RawDescriptionHelpFormatter,
38
+ )
39
+ parser.color = False
40
+
41
+ parser.add_argument(
42
+ "--host",
43
+ default=_DEFAULT_HOST,
44
+ help=(
45
+ "Hostname or address of the machine where OpenOCD is running "
46
+ f"(default: {_DEFAULT_HOST})"
47
+ ),
48
+ )
49
+ parser.add_argument(
50
+ "--port",
51
+ type=int,
52
+ default=_DEFAULT_PORT,
53
+ help=f"OpenOCD's Tcl-RPC server port number (default: {_DEFAULT_PORT})",
54
+ )
55
+ parser.add_argument(
56
+ "--timeout",
57
+ type=float,
58
+ default=_DEFAULT_TIMEOUT,
59
+ help=(
60
+ "Timeout for the command execution in seconds "
61
+ f"(default: {_DEFAULT_TIMEOUT:.1f})"
62
+ ),
63
+ )
64
+ parser.add_argument(
65
+ "command",
66
+ help="The Tcl command to send to OpenOCD",
67
+ )
68
+
69
+ return parser.parse_args()
70
+
71
+
72
+ def main() -> int:
73
+ args = parse_args()
74
+
75
+ try:
76
+ with PyOpenocdClient(args.host, args.port) as ocd:
77
+ ocd.set_default_timeout(args.timeout)
78
+ result = ocd.cmd(args.command, throw=False)
79
+ print(result.out)
80
+
81
+ if result.retcode != 0:
82
+ eprint(f"The command has failed (return code: {result.retcode}).")
83
+ return result.retcode
84
+
85
+ except OcdConnectionError as e:
86
+ eprint(str(e))
87
+ return _EXIT_CODE_CONNECTION_ERROR
88
+
89
+ except OcdInvalidResponseError as e:
90
+ eprint("OpenOCD responded unexpectedly: " + str(e))
91
+ return _EXIT_CODE_INVALID_RESPONSE
92
+
93
+ except OcdCommandTimeoutError as e:
94
+ eprint(f"Timeout: The command did not complete within {e.timeout:.1f} seconds.")
95
+ return _EXIT_CODE_COMMAND_TIMEOUT
96
+
97
+
98
+ if __name__ == "__main__": # pragma: no cover
99
+ sys.exit(main())
@@ -7,7 +7,12 @@ from typing import Any, List, Optional, Tuple, Type
7
7
 
8
8
  from .baseclient import _PyOpenocdBaseClient
9
9
  from .bp_parser import _BpParser
10
- from .errors import OcdCommandFailedError, OcdInvalidResponseError, _OcdParsingError
10
+ from .errors import (
11
+ OcdCommandFailedError,
12
+ OcdEmptyResponseError,
13
+ OcdInvalidResponseError,
14
+ _OcdParsingError,
15
+ )
11
16
  from .types import BpInfo, OcdCommandResult, WpInfo, WpType
12
17
  from .wp_parser import _WpParser
13
18
 
@@ -227,6 +232,14 @@ class PyOpenocdClient:
227
232
  and re.match(r"^<-?\d+,", s) is not None
228
233
  )
229
234
 
235
+ if len(raw_result) == 0:
236
+ msg = (
237
+ "Received empty response from OpenOCD. "
238
+ "(This is OK for 'shutdown' or 'exit' commands but unexpected "
239
+ "for any other command.) "
240
+ )
241
+ raise OcdEmptyResponseError(msg, raw_cmd, raw_result)
242
+
230
243
  if not is_expected_raw_result(raw_result):
231
244
  msg = (
232
245
  "Received unexpected response from OpenOCD. "
@@ -672,11 +685,37 @@ class PyOpenocdClient:
672
685
  """
673
686
  self.disconnect()
674
687
 
675
- def shutdown(self) -> None:
688
+ def _shutdown_supports_any_exit_code(self) -> bool:
689
+ return "shutdown ['error'|exit_code]" in self.cmd("help shutdown").out
690
+
691
+ def shutdown(self, exit_code: int = 0) -> None:
676
692
  """
677
- Shut down the OpenOCD process by sending the ``shutdown`` command to it.
678
- PyOpenocd client also gets immediately disconnected from OpenOCD.
693
+ Shut down (terminate) the OpenOCD process by sending the ``shutdown``
694
+ command to it. PyOpenocdClient also immediately disconnects from OpenOCD.
695
+
696
+ The optional argument ``exit_code`` allows to set the exit code
697
+ of the OpenOCD process. Default is 0 (success).
698
+
699
+ .. versionadded:: 0.2.0
700
+ Argument ``exit_code``.
679
701
  """
702
+ if not (0 <= exit_code <= 255):
703
+ raise ValueError("The exit_status must be in range 0..255.")
704
+
705
+ # For compatibility with older OpenOCD, prefer:
706
+ # - "shutdown" over "shutdown 0"
707
+ # - "shutdown error" over "shutdown 1"
708
+ if exit_code == 0:
709
+ cmd = "shutdown"
710
+ elif exit_code == 1:
711
+ cmd = "shutdown error"
712
+ else:
713
+ if not self._shutdown_supports_any_exit_code():
714
+ raise ValueError(
715
+ "This version of OpenOCD supports exit code 0 or 1 only"
716
+ )
717
+ cmd = f"shutdown {exit_code}"
718
+
680
719
  # Different OpenOCD versions respond to "shutdown" command differently:
681
720
  #
682
721
  # - OpenOCD 0.12.0 and older:
@@ -684,16 +723,19 @@ class PyOpenocdClient:
684
723
  # which can be obtained normally as for any other TCL command -
685
724
  # e.g. via the "catch" command.
686
725
  #
687
- # - OpenOCD 0.13.0-dev and newer (from to commit "93f16eed4"):
726
+ # - OpenOCD 0.13.0-dev and newer (starting from commit "93f16eed4"):
688
727
  # The "shutdown" command immediately ends the TCL processing and
689
728
  # an empty response is sent back to the TCL client.
690
729
 
691
- # For the above reasons, send the shutdown command via raw_cmd() and:
692
- # - don't wrap "shutdown" into any other TCL commands,
693
- # - don't expect any particular response.
694
- self.raw_cmd("shutdown")
695
-
696
- self.disconnect()
730
+ # Tolerate both the above scenarios:
731
+ try:
732
+ self.cmd(cmd)
733
+ except OcdCommandFailedError:
734
+ pass
735
+ except OcdEmptyResponseError:
736
+ pass
737
+ finally:
738
+ self.disconnect()
697
739
 
698
740
  def raw_cmd(self, raw_cmd: str, timeout: Optional[float] = None) -> str:
699
741
  """
@@ -1,5 +1,7 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
 
3
+ import warnings
4
+
3
5
  from .types import OcdCommandResult
4
6
 
5
7
 
@@ -87,9 +89,9 @@ class OcdInvalidResponseError(OcdBaseException):
87
89
  That is, PyOpenocdClient could not parse and/or interpret that command output.
88
90
  """
89
91
 
90
- def __init__(self, msg: str, raw_cmd: str, out: str):
92
+ def __init__(self, msg: str, raw_cmd: str, raw_out: str):
91
93
  self._raw_cmd = raw_cmd
92
- self._out = out
94
+ self._raw_out = raw_out
93
95
  super().__init__(msg)
94
96
 
95
97
  @property
@@ -99,12 +101,44 @@ class OcdInvalidResponseError(OcdBaseException):
99
101
  """
100
102
  return self._raw_cmd
101
103
 
104
+ @property
105
+ def raw_out(self) -> str:
106
+ """
107
+ The actual raw command's output that was unexpected
108
+ (that could not be understood or parsed).
109
+
110
+ .. versionadded:: 0.2.0
111
+ """
112
+ return self._raw_out
113
+
102
114
  @property
103
115
  def out(self) -> str:
104
116
  """
105
- The actual invalid response of the command.
117
+ Deprecated alias for ``raw_out``, kept just for compatibility.
118
+
119
+ .. deprecated:: 0.2.0
106
120
  """
107
- return self._out
121
+ warnings.warn(
122
+ "OcdInvalidResponseError.out is deprecated. "
123
+ "Please use OcdInvalidResponseError.raw_out instead.",
124
+ DeprecationWarning,
125
+ )
126
+ return self._raw_out
127
+
128
+
129
+ class OcdEmptyResponseError(OcdInvalidResponseError):
130
+ """
131
+ Exception which denotes that a TCL command produced an empty response.
132
+ It is a sub-class of :py:class:`py_openocd_client.OcdInvalidResponseError`.
133
+
134
+ Empty responses occur for commands ``exit`` and ``shutdown``, that is, commands
135
+ that immediately terminate the TCL session. For other commands, empty responses are
136
+ unexpected and would mean that OpenOCD mis-behaves.
137
+
138
+ .. versionadded:: 0.2.0
139
+ """
140
+
141
+ pass
108
142
 
109
143
 
110
144
  class OcdConnectionError(OcdBaseException):
File without changes
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyOpenocdClient
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Library for controlling OpenOCD from Python programs
5
5
  Author-email: Jan Matyas <info@janmatyas.net>
6
+ License-Expression: MIT
6
7
  Project-URL: Homepage, https://github.com/HonzaMat/PyOpenocdClient
7
8
  Project-URL: Issues, https://github.com/HonzaMat/PyOpenocdClient/issues
8
9
  Project-URL: Documentation, https://pyopenocdclient.readthedocs.io/en/latest/
9
10
  Classifier: Programming Language :: Python :: 3
10
- Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: OS Independent
12
- Requires-Python: >=3.7
12
+ Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Dynamic: license-file
@@ -24,11 +24,13 @@ Dynamic: license-file
24
24
  **PyOpenocdClient** is a Python library for controlling [OpenOCD](https://openocd.org)
25
25
  software tool.
26
26
 
27
- It allows to send TCL commands from Python programs to OpenOCD &mdash; for instance commands like halt execution of the program, view data in memory, place breakpoints, single-step, ...
27
+ It allows to send Tcl commands from Python programs to OpenOCD &mdash; for instance commands like halt execution of the program, view data in memory, place breakpoints, single-step, ...
28
+
29
+ In addition, a console utility is included with PyOpenocdClient that can be used to send Tcl commands to OpenOCD from non-Python software, such as shell scripts.
28
30
 
29
31
  Main features of PyOpenocdClient:
30
32
 
31
- * allow to send any TCL command to OpenOCD and obtain its result;
33
+ * you can send any Tcl command to OpenOCD and obtain its result;
32
34
 
33
35
  * shorcuts for quick use of most common OpenOCD commands are provided;
34
36
 
@@ -36,11 +38,11 @@ Main features of PyOpenocdClient:
36
38
 
37
39
  * the code is fully covered via unit tests;
38
40
 
39
- * automatic integration testing against multiple versions of OpenOCD;
41
+ * integration testing regularly runs against multiple versions of OpenOCD;
40
42
 
41
43
  * the code is multiplatform and portable &mdash; it does not have any dependencies except for the Python's standard library;
42
44
 
43
- * fully open-source under a permissive license (MIT license).
45
+ * the library is fully open-source under a permissive license (MIT license).
44
46
 
45
47
 
46
48
  ## Quick instructions
@@ -51,7 +53,7 @@ Install PyOpenocdClient package using Pip:
51
53
  $ python3 -m pip install PyOpenocdClient
52
54
  ```
53
55
 
54
- Basic usage:
56
+ Basic usage from Python:
55
57
 
56
58
  ```python
57
59
  from py_openocd_client import PyOpenocdClient
@@ -64,6 +66,18 @@ with PyOpenocdClient(host="localhost", port=6666) as ocd:
64
66
  # ...
65
67
  ```
66
68
 
69
+ Basic usage from a command-line or shell scripts:
70
+
71
+ ```bash
72
+
73
+ $ openocd_cmd --host 127.0.0.1 --port 6666 version
74
+ Open On-Chip Debugger 0.12.0+dev-02634-g390b9d731 (2026-08-29-14:15)
75
+
76
+ $ openocd_cmd --host 127.0.0.1 --port 6666 "reset halt ; reg pc"
77
+ pc (/32): 0x08001234
78
+
79
+ ```
80
+
67
81
  ## Documentation
68
82
 
69
83
  For full documentation, please visit: https://pyopenocdclient.readthedocs.io/en/latest/
@@ -0,0 +1,15 @@
1
+ py_openocd_client/__init__.py,sha256=1vwYXd_WD5F35hhS4lF8BkE_aPo6xpnIGaHlEjkUPkM,359
2
+ py_openocd_client/baseclient.py,sha256=BkwX79VqrZkZ_vGk4QnnD9PXGKn8erueMuwDpcAD7GA,9103
3
+ py_openocd_client/bp_parser.py,sha256=yQw8qrK6GmZhR5sIdzjD90zW0StQVUkgAerwqiB08pg,4013
4
+ py_openocd_client/cli.py,sha256=MVtIAIj-60ZPfDKJmOgQTg8YAYTOUtdLkS6CQQTKM74,2815
5
+ py_openocd_client/client.py,sha256=RlZCx6Cf3HdLpfIjBFvz8oNyz6GXYKc4g2LObedXhjw,27760
6
+ py_openocd_client/errors.py,sha256=7uEneGsOgwkwGDKteTPv79Y1REdb0ZU-sALzJK8wZe0,4712
7
+ py_openocd_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ py_openocd_client/types.py,sha256=gOoND2jpGqfAqKDE-LwowTL6eHlTd9GHNsAJQvl8C5o,2204
9
+ py_openocd_client/wp_parser.py,sha256=gcwewLfMus26vve7yPDZnW7kQKvnZpWFJ-CnxPw1Acc,1583
10
+ pyopenocdclient-0.2.0.dist-info/licenses/LICENSE,sha256=LQ4E7j3KmZ2D4Bx0cNd7_k39v0KJlF72VjOAl6k5Aas,1094
11
+ pyopenocdclient-0.2.0.dist-info/METADATA,sha256=2ZbRgwjIgQyhlEUsCI3Tm2U-pTi4GP25eRkf4rRoXB8,3245
12
+ pyopenocdclient-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
13
+ pyopenocdclient-0.2.0.dist-info/entry_points.txt,sha256=iViuS3xCpmJHGh4vtSWX7IlczcwCSC9gzxH_6BKqAwM,59
14
+ pyopenocdclient-0.2.0.dist-info/top_level.txt,sha256=YzDCf323Zc2HbUuT7wyJ2IHg8vlELGt9Ve0k_u3lhMk,18
15
+ pyopenocdclient-0.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ openocd_cmd = py_openocd_client.cli:main
@@ -1,12 +0,0 @@
1
- py_openocd_client/__init__.py,sha256=THbr8De7S91EatjLlLIDbkWNLoGAQf5j6kn69xOqGHg,332
2
- py_openocd_client/baseclient.py,sha256=BkwX79VqrZkZ_vGk4QnnD9PXGKn8erueMuwDpcAD7GA,9103
3
- py_openocd_client/bp_parser.py,sha256=yQw8qrK6GmZhR5sIdzjD90zW0StQVUkgAerwqiB08pg,4013
4
- py_openocd_client/client.py,sha256=BuqJfL_DF_vpvnS8m-BVugUx6GQbEy4qV7jKGk0ov64,26424
5
- py_openocd_client/errors.py,sha256=9K48w4rVgaOkCF4q3ORp3KxH31DVQL0JNXoGktgE-H0,3703
6
- py_openocd_client/types.py,sha256=gOoND2jpGqfAqKDE-LwowTL6eHlTd9GHNsAJQvl8C5o,2204
7
- py_openocd_client/wp_parser.py,sha256=gcwewLfMus26vve7yPDZnW7kQKvnZpWFJ-CnxPw1Acc,1583
8
- pyopenocdclient-0.1.2.dist-info/licenses/LICENSE,sha256=LQ4E7j3KmZ2D4Bx0cNd7_k39v0KJlF72VjOAl6k5Aas,1094
9
- pyopenocdclient-0.1.2.dist-info/METADATA,sha256=00dFEgphjGU15wTOueEC9g7RXsqf3rmxqxbuLC1VmQg,2804
10
- pyopenocdclient-0.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
- pyopenocdclient-0.1.2.dist-info/top_level.txt,sha256=YzDCf323Zc2HbUuT7wyJ2IHg8vlELGt9Ve0k_u3lhMk,18
12
- pyopenocdclient-0.1.2.dist-info/RECORD,,