PyOpenocdClient 0.1.1__tar.gz → 0.2.0__tar.gz

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 (19) hide show
  1. {pyopenocdclient-0.1.1/src/PyOpenocdClient.egg-info → pyopenocdclient-0.2.0}/PKG-INFO +22 -8
  2. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/README.md +19 -5
  3. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/pyproject.toml +14 -3
  4. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0/src/PyOpenocdClient.egg-info}/PKG-INFO +22 -8
  5. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/src/PyOpenocdClient.egg-info/SOURCES.txt +3 -0
  6. pyopenocdclient-0.2.0/src/PyOpenocdClient.egg-info/entry_points.txt +2 -0
  7. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/src/py_openocd_client/__init__.py +1 -0
  8. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/src/py_openocd_client/baseclient.py +57 -7
  9. pyopenocdclient-0.2.0/src/py_openocd_client/cli.py +99 -0
  10. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/src/py_openocd_client/client.py +72 -21
  11. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/src/py_openocd_client/errors.py +38 -4
  12. pyopenocdclient-0.2.0/src/py_openocd_client/py.typed +0 -0
  13. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/LICENSE +0 -0
  14. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/setup.cfg +0 -0
  15. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/src/PyOpenocdClient.egg-info/dependency_links.txt +0 -0
  16. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/src/PyOpenocdClient.egg-info/top_level.txt +0 -0
  17. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/src/py_openocd_client/bp_parser.py +0 -0
  18. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/src/py_openocd_client/types.py +0 -0
  19. {pyopenocdclient-0.1.1 → pyopenocdclient-0.2.0}/src/py_openocd_client/wp_parser.py +0 -0
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyOpenocdClient
3
- Version: 0.1.1
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/
@@ -8,11 +8,13 @@
8
8
  **PyOpenocdClient** is a Python library for controlling [OpenOCD](https://openocd.org)
9
9
  software tool.
10
10
 
11
- 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, ...
11
+ 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, ...
12
+
13
+ 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.
12
14
 
13
15
  Main features of PyOpenocdClient:
14
16
 
15
- * allow to send any TCL command to OpenOCD and obtain its result;
17
+ * you can send any Tcl command to OpenOCD and obtain its result;
16
18
 
17
19
  * shorcuts for quick use of most common OpenOCD commands are provided;
18
20
 
@@ -20,11 +22,11 @@ Main features of PyOpenocdClient:
20
22
 
21
23
  * the code is fully covered via unit tests;
22
24
 
23
- * automatic integration testing against multiple versions of OpenOCD;
25
+ * integration testing regularly runs against multiple versions of OpenOCD;
24
26
 
25
27
  * the code is multiplatform and portable &mdash; it does not have any dependencies except for the Python's standard library;
26
28
 
27
- * fully open-source under a permissive license (MIT license).
29
+ * the library is fully open-source under a permissive license (MIT license).
28
30
 
29
31
 
30
32
  ## Quick instructions
@@ -35,7 +37,7 @@ Install PyOpenocdClient package using Pip:
35
37
  $ python3 -m pip install PyOpenocdClient
36
38
  ```
37
39
 
38
- Basic usage:
40
+ Basic usage from Python:
39
41
 
40
42
  ```python
41
43
  from py_openocd_client import PyOpenocdClient
@@ -48,6 +50,18 @@ with PyOpenocdClient(host="localhost", port=6666) as ocd:
48
50
  # ...
49
51
  ```
50
52
 
53
+ Basic usage from a command-line or shell scripts:
54
+
55
+ ```bash
56
+
57
+ $ openocd_cmd --host 127.0.0.1 --port 6666 version
58
+ Open On-Chip Debugger 0.12.0+dev-02634-g390b9d731 (2026-08-29-14:15)
59
+
60
+ $ openocd_cmd --host 127.0.0.1 --port 6666 "reset halt ; reg pc"
61
+ pc (/32): 0x08001234
62
+
63
+ ```
64
+
51
65
  ## Documentation
52
66
 
53
67
  For full documentation, please visit: https://pyopenocdclient.readthedocs.io/en/latest/
@@ -1,19 +1,30 @@
1
+
2
+ [build-system]
3
+ requires = ["setuptools>=61"]
4
+ build-backend = "setuptools.build_meta"
5
+
1
6
  [project]
2
7
  name = "PyOpenocdClient"
3
- version = "0.1.1"
8
+ version = "0.2.0"
4
9
  authors = [
5
10
  { name="Jan Matyas", email="info@janmatyas.net" },
6
11
  ]
7
12
  description = "Library for controlling OpenOCD from Python programs"
8
13
  readme = "README.md"
9
- requires-python = ">=3.7"
14
+ requires-python = ">=3.10"
10
15
  classifiers = [
11
16
  "Programming Language :: Python :: 3",
12
- "License :: OSI Approved :: MIT License",
13
17
  "Operating System :: OS Independent",
14
18
  ]
19
+ license = "MIT"
15
20
 
16
21
  [project.urls]
17
22
  Homepage = "https://github.com/HonzaMat/PyOpenocdClient"
18
23
  Issues = "https://github.com/HonzaMat/PyOpenocdClient/issues"
19
24
  Documentation = "https://pyopenocdclient.readthedocs.io/en/latest/"
25
+
26
+ [project.scripts]
27
+ openocd_cmd = "py_openocd_client.cli:main"
28
+
29
+ [tool.setuptools.package-data]
30
+ py_openocd_client = ["py.typed"]
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyOpenocdClient
3
- Version: 0.1.1
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/
@@ -4,11 +4,14 @@ pyproject.toml
4
4
  src/PyOpenocdClient.egg-info/PKG-INFO
5
5
  src/PyOpenocdClient.egg-info/SOURCES.txt
6
6
  src/PyOpenocdClient.egg-info/dependency_links.txt
7
+ src/PyOpenocdClient.egg-info/entry_points.txt
7
8
  src/PyOpenocdClient.egg-info/top_level.txt
8
9
  src/py_openocd_client/__init__.py
9
10
  src/py_openocd_client/baseclient.py
10
11
  src/py_openocd_client/bp_parser.py
12
+ src/py_openocd_client/cli.py
11
13
  src/py_openocd_client/client.py
12
14
  src/py_openocd_client/errors.py
15
+ src/py_openocd_client/py.typed
13
16
  src/py_openocd_client/types.py
14
17
  src/py_openocd_client/wp_parser.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ openocd_cmd = py_openocd_client.cli:main
@@ -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
@@ -94,10 +94,35 @@ class _PyOpenocdBaseClient:
94
94
  raise ValueError("Timeout must be greater than zero")
95
95
  self._default_recv_timeout = timeout
96
96
 
97
- def _check_no_premature_recvd_bytes(self) -> None:
97
+ def _check_connection_before_command(self) -> None:
98
98
  assert self._socket is not None
99
99
  rd, _, _ = select.select([self._socket], [], [], 0) # Don't block, just poll
100
- if len(rd) > 0:
100
+
101
+ if len(rd) == 0:
102
+ # The socket is not ready for recv() now, which is the expected state.
103
+ # Success.
104
+ return
105
+
106
+ # The socket is ready for recv(). This is unexpected at this point
107
+ # and always means an error. Try receive from the socket to find out
108
+ # what happened:
109
+ try:
110
+ recvd_data = self._socket.recv(128)
111
+ except OSError as e:
112
+ # This is unlikely to happen: It would mean that the socket is ready
113
+ # for recv() but then recv() failed.
114
+ raise OcdConnectionError(
115
+ "Connection to OpenOCD broken for an unknown reason"
116
+ ) from e
117
+
118
+ if len(recvd_data) == 0:
119
+ # Empty received data means that the connection got closed by OpenOCD
120
+ # in the meanwhile.
121
+ raise OcdConnectionError("Connection closed by OpenOCD")
122
+ else:
123
+ # It looks like OpenOCD sent us some extra, unsolicited bytes (without us
124
+ # sending any command to OpenOCD). This is a violation of the communication
125
+ # protocol.
101
126
  raise OcdConnectionError(
102
127
  "Received unexpected bytes from OpenOCD before "
103
128
  "the command was even sent."
@@ -107,12 +132,27 @@ class _PyOpenocdBaseClient:
107
132
  assert self.is_connected()
108
133
  assert self._socket is not None
109
134
 
110
- # Safety:
111
- self._check_no_premature_recvd_bytes()
135
+ # Perform basic connection check before sending a command.
136
+ # Note that this is merely a safety/correctness check which itself
137
+ # does not guarantee that the subsequent send() and recv() calls
138
+ # will succeed.
139
+ self._check_connection_before_command()
112
140
 
113
141
  data = raw_cmd.encode(self.CHARSET) + self.COMMAND_DELIMITER
114
- self._socket.settimeout(self.SEND_TIMEOUT)
115
- self._socket.send(data)
142
+
143
+ try:
144
+ self._socket.settimeout(self.SEND_TIMEOUT)
145
+ except OSError as e:
146
+ raise OcdConnectionError(
147
+ "Could not send a command to OpenOCD, failed to set socket timeout"
148
+ ) from e
149
+
150
+ try:
151
+ self._socket.send(data)
152
+ except OSError as e:
153
+ raise OcdConnectionError(
154
+ "Could not send a command to OpenOCD, socket error occurred"
155
+ ) from e
116
156
 
117
157
  def _do_recv_response(self, raw_cmd: str, timeout: Optional[float] = None) -> str:
118
158
  assert self.is_connected()
@@ -123,7 +163,13 @@ class _PyOpenocdBaseClient:
123
163
  timeout if timeout is not None else self._default_recv_timeout
124
164
  )
125
165
 
126
- self._socket.settimeout(self.RECV_POLL_TIMEOUT)
166
+ try:
167
+ self._socket.settimeout(self.RECV_POLL_TIMEOUT)
168
+ except OSError as e:
169
+ raise OcdConnectionError(
170
+ "Could not receive a response from OpenOCD, "
171
+ "failed to set socket timeout"
172
+ ) from e
127
173
 
128
174
  time_start = time.time()
129
175
  while time.time() < (time_start + effective_timeout):
@@ -131,6 +177,10 @@ class _PyOpenocdBaseClient:
131
177
  d = self._socket.recv(self.RECV_BLOCK_SIZE)
132
178
  except socket.timeout:
133
179
  continue
180
+ except OSError as e:
181
+ raise OcdConnectionError(
182
+ "Could not receive a response from OpenOCD, socket error occurred"
183
+ ) from e
134
184
 
135
185
  if d == b"":
136
186
  raise OcdConnectionError("Connection closed by OpenOCD")
@@ -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
 
@@ -211,25 +216,42 @@ class PyOpenocdClient:
211
216
  raw_cmd = cmd
212
217
 
213
218
  raw_cmd = "set CMD_RETCODE [ catch { " + raw_cmd + " } CMD_OUTPUT ] ; "
214
- raw_cmd += 'return "$CMD_RETCODE $CMD_OUTPUT" ; '
219
+
220
+ # Older OpenOCD versions - prior to 93f16eed4(*) - incorrectly trimmed trailing
221
+ # whitespace from the string passed to the return command. Work around this bug
222
+ # by wrapping the string by non-whitespace characters.
223
+ # (*): https://review.openocd.org/c/openocd/+/9084
224
+ raw_cmd += 'return "<$CMD_RETCODE,$CMD_OUTPUT>" ; '
215
225
 
216
226
  raw_result = self.raw_cmd(raw_cmd, timeout=timeout)
217
227
 
218
- # Verify the raw output from OpenOCD the has the expected format. It can be:
219
- #
220
- # - Command return code (positive or negative decimal number) and that's it.
221
- #
222
- # - Or, command return code (positive or negative decimal number) followed by
223
- # a space character and optionally followed by the command's textual output.
224
- if re.match(r"^-?\d+($| )", raw_result) is None:
228
+ def is_expected_raw_result(s: str) -> bool:
229
+ return (
230
+ s.startswith("<")
231
+ and s.endswith(">")
232
+ and re.match(r"^<-?\d+,", s) is not None
233
+ )
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
+
243
+ if not is_expected_raw_result(raw_result):
225
244
  msg = (
226
245
  "Received unexpected response from OpenOCD. "
227
246
  "It looks like OpenOCD misbehaves. "
228
247
  )
229
248
  raise OcdInvalidResponseError(msg, raw_cmd, raw_result)
230
249
 
231
- raw_result_parts = raw_result.split(" ", maxsplit=1)
232
- assert len(raw_result_parts) in [1, 2]
250
+ # Remove leading "<" and trailing ">"
251
+ raw_result = raw_result[1:-1]
252
+ raw_result_parts = raw_result.split(",", maxsplit=1)
253
+ assert len(raw_result_parts) == 2
254
+
233
255
  retcode = int(raw_result_parts[0], 10)
234
256
  out = raw_result_parts[1] if len(raw_result_parts) == 2 else ""
235
257
 
@@ -663,11 +685,37 @@ class PyOpenocdClient:
663
685
  """
664
686
  self.disconnect()
665
687
 
666
- 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:
667
692
  """
668
- Shut down the OpenOCD process by sending the ``shutdown`` command to it.
669
- 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``.
670
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
+
671
719
  # Different OpenOCD versions respond to "shutdown" command differently:
672
720
  #
673
721
  # - OpenOCD 0.12.0 and older:
@@ -675,16 +723,19 @@ class PyOpenocdClient:
675
723
  # which can be obtained normally as for any other TCL command -
676
724
  # e.g. via the "catch" command.
677
725
  #
678
- # - OpenOCD 0.13.0-dev and newer (from to commit "93f16eed4"):
726
+ # - OpenOCD 0.13.0-dev and newer (starting from commit "93f16eed4"):
679
727
  # The "shutdown" command immediately ends the TCL processing and
680
728
  # an empty response is sent back to the TCL client.
681
729
 
682
- # For the above reasons, send the shutdown command via raw_cmd() and:
683
- # - don't wrap "shutdown" into any other TCL commands,
684
- # - don't expect any particular response.
685
- self.raw_cmd("shutdown")
686
-
687
- 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()
688
739
 
689
740
  def raw_cmd(self, raw_cmd: str, timeout: Optional[float] = None) -> str:
690
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
File without changes