openocd-python 2026.2.12__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 (45) hide show
  1. openocd_python-2026.2.12/.gitignore +8 -0
  2. openocd_python-2026.2.12/LICENSE +21 -0
  3. openocd_python-2026.2.12/PKG-INFO +75 -0
  4. openocd_python-2026.2.12/README.md +46 -0
  5. openocd_python-2026.2.12/pyproject.toml +61 -0
  6. openocd_python-2026.2.12/src/openocd/__init__.py +64 -0
  7. openocd_python-2026.2.12/src/openocd/breakpoints.py +234 -0
  8. openocd_python-2026.2.12/src/openocd/cli.py +161 -0
  9. openocd_python-2026.2.12/src/openocd/connection/__init__.py +7 -0
  10. openocd_python-2026.2.12/src/openocd/connection/base.py +30 -0
  11. openocd_python-2026.2.12/src/openocd/connection/tcl_rpc.py +259 -0
  12. openocd_python-2026.2.12/src/openocd/connection/telnet.py +105 -0
  13. openocd_python-2026.2.12/src/openocd/errors.py +43 -0
  14. openocd_python-2026.2.12/src/openocd/events.py +112 -0
  15. openocd_python-2026.2.12/src/openocd/flash.py +381 -0
  16. openocd_python-2026.2.12/src/openocd/jtag/__init__.py +5 -0
  17. openocd_python-2026.2.12/src/openocd/jtag/boundary.py +52 -0
  18. openocd_python-2026.2.12/src/openocd/jtag/chain.py +218 -0
  19. openocd_python-2026.2.12/src/openocd/jtag/scan.py +58 -0
  20. openocd_python-2026.2.12/src/openocd/jtag/state.py +26 -0
  21. openocd_python-2026.2.12/src/openocd/memory.py +243 -0
  22. openocd_python-2026.2.12/src/openocd/process.py +148 -0
  23. openocd_python-2026.2.12/src/openocd/py.typed +0 -0
  24. openocd_python-2026.2.12/src/openocd/registers.py +186 -0
  25. openocd_python-2026.2.12/src/openocd/rtt.py +233 -0
  26. openocd_python-2026.2.12/src/openocd/session.py +330 -0
  27. openocd_python-2026.2.12/src/openocd/svd/__init__.py +5 -0
  28. openocd_python-2026.2.12/src/openocd/svd/decoder.py +54 -0
  29. openocd_python-2026.2.12/src/openocd/svd/parser.py +128 -0
  30. openocd_python-2026.2.12/src/openocd/svd/peripheral.py +186 -0
  31. openocd_python-2026.2.12/src/openocd/target.py +164 -0
  32. openocd_python-2026.2.12/src/openocd/transport.py +149 -0
  33. openocd_python-2026.2.12/src/openocd/types.py +185 -0
  34. openocd_python-2026.2.12/tests/__init__.py +0 -0
  35. openocd_python-2026.2.12/tests/conftest.py +37 -0
  36. openocd_python-2026.2.12/tests/mock_server.py +255 -0
  37. openocd_python-2026.2.12/tests/test_connection.py +113 -0
  38. openocd_python-2026.2.12/tests/test_error_paths.py +626 -0
  39. openocd_python-2026.2.12/tests/test_jtag.py +93 -0
  40. openocd_python-2026.2.12/tests/test_memory.py +93 -0
  41. openocd_python-2026.2.12/tests/test_registers.py +107 -0
  42. openocd_python-2026.2.12/tests/test_session.py +98 -0
  43. openocd_python-2026.2.12/tests/test_svd.py +249 -0
  44. openocd_python-2026.2.12/tests/test_target.py +74 -0
  45. openocd_python-2026.2.12/uv.lock +259 -0
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ryan Malloy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: openocd-python
3
+ Version: 2026.2.12
4
+ Summary: Typed, async-first Python bindings for the full OpenOCD command surface
5
+ Project-URL: Homepage, https://git.supported.systems/ryan/openocd-python
6
+ Project-URL: Issues, https://git.supported.systems/ryan/openocd-python/issues
7
+ Author-email: Ryan Malloy <ryan@supported.systems>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: debugging,embedded,hardware,jtag,openocd,swd
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Debuggers
19
+ Classifier: Topic :: Software Development :: Embedded Systems
20
+ Classifier: Topic :: System :: Hardware
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: cmsis-svd>=0.4
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.8; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # openocd-python
31
+
32
+ Typed, async-first Python bindings for the full OpenOCD command surface.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install openocd-python
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ from openocd import Session
44
+
45
+ # Connect to a running OpenOCD instance
46
+ async with Session.connect() as ocd:
47
+ state = await ocd.target.halt()
48
+ pc = await ocd.registers.pc()
49
+ mem = await ocd.memory.read_u32(0x08000000, 4)
50
+ print(f"PC: {pc:#x}")
51
+
52
+ # Or spawn OpenOCD and connect
53
+ async with Session.start("interface/cmsis-dap.cfg -f target/stm32f1x.cfg") as ocd:
54
+ await ocd.target.halt()
55
+ regs = await ocd.registers.read_all()
56
+
57
+ # Synchronous API available too
58
+ with Session.start_sync("interface/cmsis-dap.cfg") as ocd:
59
+ ocd.target.halt()
60
+ print(f"PC: {ocd.registers.pc():#x}")
61
+ ```
62
+
63
+ ## Features
64
+
65
+ - **Async-first** with sync wrappers for every method
66
+ - **Typed returns** — dataclasses, not raw strings
67
+ - **Full OpenOCD surface**: target control, memory, registers, flash, JTAG, breakpoints, RTT
68
+ - **SVD decoding** — read a peripheral register and get named bitfields
69
+ - **Process management** — spawn and manage OpenOCD subprocesses
70
+ - **Dual transport** — TCL RPC (primary) and telnet (fallback)
71
+
72
+ ## Requirements
73
+
74
+ - Python 3.11+
75
+ - OpenOCD installed and on PATH (or pass `openocd_bin=`)
@@ -0,0 +1,46 @@
1
+ # openocd-python
2
+
3
+ Typed, async-first Python bindings for the full OpenOCD command surface.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install openocd-python
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from openocd import Session
15
+
16
+ # Connect to a running OpenOCD instance
17
+ async with Session.connect() as ocd:
18
+ state = await ocd.target.halt()
19
+ pc = await ocd.registers.pc()
20
+ mem = await ocd.memory.read_u32(0x08000000, 4)
21
+ print(f"PC: {pc:#x}")
22
+
23
+ # Or spawn OpenOCD and connect
24
+ async with Session.start("interface/cmsis-dap.cfg -f target/stm32f1x.cfg") as ocd:
25
+ await ocd.target.halt()
26
+ regs = await ocd.registers.read_all()
27
+
28
+ # Synchronous API available too
29
+ with Session.start_sync("interface/cmsis-dap.cfg") as ocd:
30
+ ocd.target.halt()
31
+ print(f"PC: {ocd.registers.pc():#x}")
32
+ ```
33
+
34
+ ## Features
35
+
36
+ - **Async-first** with sync wrappers for every method
37
+ - **Typed returns** — dataclasses, not raw strings
38
+ - **Full OpenOCD surface**: target control, memory, registers, flash, JTAG, breakpoints, RTT
39
+ - **SVD decoding** — read a peripheral register and get named bitfields
40
+ - **Process management** — spawn and manage OpenOCD subprocesses
41
+ - **Dual transport** — TCL RPC (primary) and telnet (fallback)
42
+
43
+ ## Requirements
44
+
45
+ - Python 3.11+
46
+ - OpenOCD installed and on PATH (or pass `openocd_bin=`)
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "openocd-python"
7
+ version = "2026.02.12"
8
+ description = "Typed, async-first Python bindings for the full OpenOCD command surface"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [
13
+ {name = "Ryan Malloy", email = "ryan@supported.systems"},
14
+ ]
15
+ keywords = ["openocd", "jtag", "swd", "embedded", "debugging", "hardware"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Debuggers",
25
+ "Topic :: Software Development :: Embedded Systems",
26
+ "Topic :: System :: Hardware",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = [
30
+ "cmsis-svd>=0.4",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "pytest>=8.0",
36
+ "pytest-asyncio>=0.24",
37
+ "ruff>=0.8",
38
+ ]
39
+
40
+ [project.scripts]
41
+ openocd-python = "openocd.cli:main"
42
+
43
+ [project.urls]
44
+ Homepage = "https://git.supported.systems/ryan/openocd-python"
45
+ Issues = "https://git.supported.systems/ryan/openocd-python/issues"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/openocd"]
49
+
50
+ [tool.ruff]
51
+ target-version = "py311"
52
+ line-length = 100
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "I", "UP", "B", "SIM"]
56
+
57
+ [tool.pytest.ini_options]
58
+ asyncio_mode = "auto"
59
+ markers = [
60
+ "hardware: requires physical DAP-Link hardware (deselect with '-m not hardware')",
61
+ ]
@@ -0,0 +1,64 @@
1
+ """openocd-python — typed, async-first Python bindings for OpenOCD."""
2
+
3
+ from openocd.errors import (
4
+ ConnectionError,
5
+ FlashError,
6
+ JTAGError,
7
+ OpenOCDError,
8
+ ProcessError,
9
+ SVDError,
10
+ TargetError,
11
+ TargetNotHaltedError,
12
+ TimeoutError,
13
+ )
14
+ from openocd.session import Session, SyncSession
15
+ from openocd.types import (
16
+ BitField,
17
+ Breakpoint,
18
+ DecodedRegister,
19
+ FlashBank,
20
+ FlashSector,
21
+ JTAGState,
22
+ MemoryRegion,
23
+ Register,
24
+ RTTChannel,
25
+ TAPInfo,
26
+ TargetState,
27
+ Watchpoint,
28
+ )
29
+
30
+ __all__ = [
31
+ # Session
32
+ "Session",
33
+ "SyncSession",
34
+ # Types
35
+ "BitField",
36
+ "Breakpoint",
37
+ "DecodedRegister",
38
+ "FlashBank",
39
+ "FlashSector",
40
+ "JTAGState",
41
+ "MemoryRegion",
42
+ "RTTChannel",
43
+ "Register",
44
+ "TAPInfo",
45
+ "TargetState",
46
+ "Watchpoint",
47
+ # Errors
48
+ "ConnectionError",
49
+ "FlashError",
50
+ "JTAGError",
51
+ "OpenOCDError",
52
+ "ProcessError",
53
+ "SVDError",
54
+ "TargetError",
55
+ "TargetNotHaltedError",
56
+ "TimeoutError",
57
+ ]
58
+
59
+ try:
60
+ from importlib.metadata import version
61
+
62
+ __version__ = version("openocd-python")
63
+ except Exception:
64
+ __version__ = "0.0.0"
@@ -0,0 +1,234 @@
1
+ """Breakpoint and watchpoint management.
2
+
3
+ Wraps OpenOCD's ``bp``, ``rbp``, ``wp``, and ``rwp`` commands for
4
+ setting, removing, and listing hardware/software breakpoints and
5
+ data watchpoints.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import logging
12
+ import re
13
+ from typing import Literal
14
+
15
+ from openocd.connection.base import Connection
16
+ from openocd.errors import OpenOCDError
17
+ from openocd.types import Breakpoint, Watchpoint
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+
22
+ class BreakpointError(OpenOCDError):
23
+ """A breakpoint or watchpoint operation failed."""
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Parsers
28
+ # ---------------------------------------------------------------------------
29
+
30
+ # Breakpoint(IVA): 0x08001234, 0x2, 1 (hw=1) or 0 (sw)
31
+ _BP_RE = re.compile(
32
+ r"Breakpoint\([^)]*\):\s*(?P<addr>0x[0-9a-fA-F]+),\s*"
33
+ r"(?P<len>0x[0-9a-fA-F]+),\s*(?P<hw>\d+)"
34
+ )
35
+
36
+ # Watchpoint output varies across OpenOCD versions. Common formats:
37
+ # address: 0x20000000, len: 0x4, r/w/a: 2 (access), value: ...
38
+ # Watchpoint(DWT): 0x20000000, 0x4, 2
39
+ _WP_RE = re.compile(
40
+ r"(?:address:\s*(?P<addr1>0x[0-9a-fA-F]+).*?len:\s*(?P<len1>0x[0-9a-fA-F]+).*?r/w/a:\s*(?P<rwa1>\d+))"
41
+ r"|"
42
+ r"(?:Watchpoint\([^)]*\):\s*(?P<addr2>0x[0-9a-fA-F]+),\s*(?P<len2>0x[0-9a-fA-F]+),\s*(?P<rwa2>\d+))"
43
+ )
44
+
45
+ # OpenOCD watchpoint access type encoding
46
+ _WP_ACCESS_MAP = {0: "r", 1: "w", 2: "rw"}
47
+ _WP_ACCESS_CMD = {"r": "r", "w": "w", "rw": "a"}
48
+
49
+
50
+ def _check_error(response: str, context: str) -> None:
51
+ """Raise BreakpointError if the response indicates failure."""
52
+ if "error" in response.lower():
53
+ raise BreakpointError(f"{context}: {response.strip()}")
54
+
55
+
56
+ def _parse_breakpoint_list(text: str) -> list[Breakpoint]:
57
+ """Parse the output of ``bp`` (no arguments) into Breakpoint objects."""
58
+ breakpoints: list[Breakpoint] = []
59
+ for idx, m in enumerate(_BP_RE.finditer(text)):
60
+ hw_flag = int(m.group("hw"))
61
+ breakpoints.append(
62
+ Breakpoint(
63
+ number=idx,
64
+ type="hw" if hw_flag else "sw",
65
+ address=int(m.group("addr"), 16),
66
+ length=int(m.group("len"), 16),
67
+ enabled=True,
68
+ )
69
+ )
70
+ return breakpoints
71
+
72
+
73
+ def _parse_watchpoint_list(text: str) -> list[Watchpoint]:
74
+ """Parse watchpoint listing output."""
75
+ watchpoints: list[Watchpoint] = []
76
+ for idx, m in enumerate(_WP_RE.finditer(text)):
77
+ # Match could come from either alternative in the regex
78
+ if m.group("addr1") is not None:
79
+ addr = int(m.group("addr1"), 16)
80
+ length = int(m.group("len1"), 16)
81
+ rwa = int(m.group("rwa1"))
82
+ else:
83
+ addr = int(m.group("addr2"), 16)
84
+ length = int(m.group("len2"), 16)
85
+ rwa = int(m.group("rwa2"))
86
+
87
+ watchpoints.append(
88
+ Watchpoint(
89
+ number=idx,
90
+ address=addr,
91
+ length=length,
92
+ access=_WP_ACCESS_MAP.get(rwa, "rw"),
93
+ )
94
+ )
95
+ return watchpoints
96
+
97
+
98
+ class BreakpointManager:
99
+ """Manage breakpoints and watchpoints via OpenOCD.
100
+
101
+ Breakpoints can be either software (patching the instruction) or
102
+ hardware (using on-chip comparators). Watchpoints trigger on data
103
+ access to a given address range.
104
+ """
105
+
106
+ def __init__(self, conn: Connection) -> None:
107
+ self._conn = conn
108
+
109
+ # ------------------------------------------------------------------
110
+ # Breakpoints
111
+ # ------------------------------------------------------------------
112
+
113
+ async def add(self, address: int, length: int = 2, hw: bool = False) -> None:
114
+ """Set a breakpoint at the given address.
115
+
116
+ Args:
117
+ address: Instruction address for the breakpoint.
118
+ length: Breakpoint length in bytes (2 for Thumb, 4 for ARM).
119
+ hw: Request a hardware breakpoint. If False, OpenOCD uses a
120
+ software breakpoint when possible.
121
+ """
122
+ cmd = f"bp 0x{address:08X} {length}"
123
+ if hw:
124
+ cmd += " hw"
125
+ resp = await self._conn.send(cmd)
126
+ _check_error(resp, f"bp 0x{address:08X}")
127
+ log.info("Breakpoint set at 0x%08X (len=%d, hw=%s)", address, length, hw)
128
+
129
+ async def remove(self, address: int) -> None:
130
+ """Remove a breakpoint at the given address.
131
+
132
+ Args:
133
+ address: Address of the breakpoint to remove.
134
+ """
135
+ cmd = f"rbp 0x{address:08X}"
136
+ resp = await self._conn.send(cmd)
137
+ _check_error(resp, f"rbp 0x{address:08X}")
138
+ log.info("Breakpoint removed at 0x%08X", address)
139
+
140
+ async def list(self) -> list[Breakpoint]:
141
+ """List all active breakpoints.
142
+
143
+ Returns:
144
+ A list of Breakpoint objects describing each active breakpoint.
145
+ """
146
+ resp = await self._conn.send("bp")
147
+ # An empty response or no matches means no breakpoints set
148
+ if not resp.strip():
149
+ return []
150
+ return _parse_breakpoint_list(resp)
151
+
152
+ # ------------------------------------------------------------------
153
+ # Watchpoints
154
+ # ------------------------------------------------------------------
155
+
156
+ async def add_watchpoint(
157
+ self,
158
+ address: int,
159
+ length: int,
160
+ access: Literal["r", "w", "rw"] = "rw",
161
+ ) -> None:
162
+ """Set a data watchpoint.
163
+
164
+ Args:
165
+ address: Memory address to watch.
166
+ length: Number of bytes to watch (must be power of 2 on most targets).
167
+ access: Access type -- ``"r"`` for read, ``"w"`` for write,
168
+ ``"rw"`` for read/write (access).
169
+ """
170
+ access_flag = _WP_ACCESS_CMD.get(access, "a")
171
+ cmd = f"wp 0x{address:08X} {length} {access_flag}"
172
+ resp = await self._conn.send(cmd)
173
+ _check_error(resp, f"wp 0x{address:08X}")
174
+ log.info("Watchpoint set at 0x%08X (len=%d, access=%s)", address, length, access)
175
+
176
+ async def remove_watchpoint(self, address: int) -> None:
177
+ """Remove a watchpoint at the given address.
178
+
179
+ Args:
180
+ address: Address of the watchpoint to remove.
181
+ """
182
+ cmd = f"rwp 0x{address:08X}"
183
+ resp = await self._conn.send(cmd)
184
+ _check_error(resp, f"rwp 0x{address:08X}")
185
+ log.info("Watchpoint removed at 0x%08X", address)
186
+
187
+ async def list_watchpoints(self) -> list[Watchpoint]:
188
+ """List all active watchpoints.
189
+
190
+ Returns:
191
+ A list of Watchpoint objects describing each active watchpoint.
192
+ """
193
+ # OpenOCD doesn't have a dedicated "list watchpoints" command
194
+ # but 'wp' with no arguments on some builds returns the list.
195
+ # The more reliable approach is using the TCL command.
196
+ resp = await self._conn.send("wp")
197
+ if not resp.strip():
198
+ return []
199
+ return _parse_watchpoint_list(resp)
200
+
201
+
202
+ # ======================================================================
203
+ # Sync wrapper
204
+ # ======================================================================
205
+
206
+ class SyncBreakpointManager:
207
+ """Synchronous wrapper around BreakpointManager."""
208
+
209
+ def __init__(self, bp_manager: BreakpointManager, loop: asyncio.AbstractEventLoop) -> None:
210
+ self._bp = bp_manager
211
+ self._loop = loop
212
+
213
+ def add(self, address: int, length: int = 2, hw: bool = False) -> None:
214
+ self._loop.run_until_complete(self._bp.add(address, length=length, hw=hw))
215
+
216
+ def remove(self, address: int) -> None:
217
+ self._loop.run_until_complete(self._bp.remove(address))
218
+
219
+ def list(self) -> list[Breakpoint]:
220
+ return self._loop.run_until_complete(self._bp.list())
221
+
222
+ def add_watchpoint(
223
+ self,
224
+ address: int,
225
+ length: int,
226
+ access: Literal["r", "w", "rw"] = "rw",
227
+ ) -> None:
228
+ self._loop.run_until_complete(self._bp.add_watchpoint(address, length, access=access))
229
+
230
+ def remove_watchpoint(self, address: int) -> None:
231
+ self._loop.run_until_complete(self._bp.remove_watchpoint(address))
232
+
233
+ def list_watchpoints(self) -> list[Watchpoint]:
234
+ return self._loop.run_until_complete(self._bp.list_watchpoints())
@@ -0,0 +1,161 @@
1
+ """CLI entry point for openocd-python.
2
+
3
+ Provides quick diagnostics and a REPL for interactive use:
4
+
5
+ $ openocd-python --help
6
+ $ openocd-python info # probe detection + target info
7
+ $ openocd-python repl # interactive command REPL
8
+ $ openocd-python read 0x08000000 16 # quick memory read
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import asyncio
15
+ import sys
16
+
17
+
18
+ def main() -> None:
19
+ try:
20
+ from importlib.metadata import version
21
+
22
+ pkg_version = version("openocd-python")
23
+ except Exception:
24
+ pkg_version = "dev"
25
+
26
+ parser = argparse.ArgumentParser(
27
+ prog="openocd-python",
28
+ description=f"OpenOCD Python bindings v{pkg_version}",
29
+ )
30
+ parser.add_argument(
31
+ "--version", action="version", version=f"openocd-python {pkg_version}"
32
+ )
33
+ parser.add_argument(
34
+ "--host", default="localhost", help="OpenOCD host (default: localhost)"
35
+ )
36
+ parser.add_argument(
37
+ "--port", type=int, default=6666, help="OpenOCD TCL RPC port (default: 6666)"
38
+ )
39
+
40
+ sub = parser.add_subparsers(dest="command")
41
+
42
+ sub.add_parser("info", help="Show target and adapter information")
43
+
44
+ repl_parser = sub.add_parser("repl", help="Interactive OpenOCD command REPL")
45
+ repl_parser.add_argument(
46
+ "--timeout", type=float, default=10.0, help="Command timeout in seconds"
47
+ )
48
+
49
+ read_parser = sub.add_parser("read", help="Read memory and display as hexdump")
50
+ read_parser.add_argument("address", help="Start address (hex, e.g. 0x08000000)")
51
+ read_parser.add_argument(
52
+ "size", type=int, nargs="?", default=64, help="Bytes to read (default: 64)"
53
+ )
54
+
55
+ sub.add_parser("scan", help="Scan the JTAG chain")
56
+
57
+ args = parser.parse_args()
58
+
59
+ if args.command is None:
60
+ parser.print_help()
61
+ sys.exit(0)
62
+
63
+ asyncio.run(_dispatch(args))
64
+
65
+
66
+ async def _dispatch(args: argparse.Namespace) -> None:
67
+ from openocd.session import Session
68
+
69
+ async with Session.connect(host=args.host, port=args.port) as ocd:
70
+ if args.command == "info":
71
+ await _cmd_info(ocd)
72
+ elif args.command == "repl":
73
+ await _cmd_repl(ocd, timeout=args.timeout)
74
+ elif args.command == "read":
75
+ await _cmd_read(ocd, args.address, args.size)
76
+ elif args.command == "scan":
77
+ await _cmd_scan(ocd)
78
+
79
+
80
+ async def _cmd_info(ocd) -> None:
81
+ """Display target state and adapter information."""
82
+ from openocd.errors import OpenOCDError
83
+
84
+ print("=== OpenOCD Target Info ===\n")
85
+
86
+ try:
87
+ state = await ocd.target.state()
88
+ print(f" Target: {state.name}")
89
+ print(f" State: {state.state}")
90
+ if state.current_pc is not None:
91
+ print(f" PC: 0x{state.current_pc:08X}")
92
+ except OpenOCDError as e:
93
+ print(f" Target: (error: {e})")
94
+
95
+ print()
96
+
97
+ try:
98
+ transport_name = await ocd.transport.select()
99
+ print(f" Transport: {transport_name}")
100
+ except OpenOCDError:
101
+ pass
102
+
103
+ try:
104
+ adapter = await ocd.transport.adapter_info()
105
+ print(f" Adapter: {adapter}")
106
+ except OpenOCDError:
107
+ pass
108
+
109
+ try:
110
+ speed = await ocd.transport.adapter_speed()
111
+ print(f" Speed: {speed} kHz")
112
+ except OpenOCDError:
113
+ pass
114
+
115
+
116
+ async def _cmd_repl(ocd, timeout: float = 10.0) -> None:
117
+ """Interactive command REPL."""
118
+ print("OpenOCD REPL (type 'quit' or Ctrl-D to exit)\n")
119
+ while True:
120
+ try:
121
+ cmd = input("ocd> ")
122
+ except (EOFError, KeyboardInterrupt):
123
+ print()
124
+ break
125
+ if cmd.strip().lower() in ("quit", "exit", "q"):
126
+ break
127
+ if not cmd.strip():
128
+ continue
129
+ try:
130
+ result = await ocd.command(cmd)
131
+ if result.strip():
132
+ print(result)
133
+ except Exception as e:
134
+ print(f"Error: {e}")
135
+
136
+
137
+ async def _cmd_read(ocd, address_str: str, size: int) -> None:
138
+ """Read memory and display as hexdump."""
139
+ addr = int(address_str, 0)
140
+ dump = await ocd.memory.hexdump(addr, size)
141
+ print(dump)
142
+
143
+
144
+ async def _cmd_scan(ocd) -> None:
145
+ """Scan the JTAG chain."""
146
+ taps = await ocd.jtag.scan_chain()
147
+ if not taps:
148
+ print("No TAPs found on the JTAG chain.")
149
+ return
150
+
151
+ print(f"{'TAP Name':<25s} {'IDCODE':>10s} IR Enabled")
152
+ print("-" * 50)
153
+ for tap in taps:
154
+ print(
155
+ f"{tap.name:<25s} 0x{tap.idcode:08X} {tap.ir_length:>2d} "
156
+ f"{'yes' if tap.enabled else 'no'}"
157
+ )
158
+
159
+
160
+ if __name__ == "__main__":
161
+ main()
@@ -0,0 +1,7 @@
1
+ """Connection backends for communicating with OpenOCD."""
2
+
3
+ from openocd.connection.base import Connection
4
+ from openocd.connection.tcl_rpc import TclRpcConnection
5
+ from openocd.connection.telnet import TelnetConnection
6
+
7
+ __all__ = ["Connection", "TclRpcConnection", "TelnetConnection"]