openocd-python 2026.2.12__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.
openocd/__init__.py ADDED
@@ -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"
openocd/breakpoints.py ADDED
@@ -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())
openocd/cli.py ADDED
@@ -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"]
@@ -0,0 +1,30 @@
1
+ """Abstract base class for OpenOCD connection backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from collections.abc import Callable
7
+
8
+
9
+ class Connection(ABC):
10
+ """Protocol-agnostic interface to an OpenOCD instance."""
11
+
12
+ @abstractmethod
13
+ async def connect(self, host: str, port: int) -> None:
14
+ """Open a connection to the given host and port."""
15
+
16
+ @abstractmethod
17
+ async def send(self, command: str) -> str:
18
+ """Send a command string and return the response."""
19
+
20
+ @abstractmethod
21
+ async def close(self) -> None:
22
+ """Close the connection."""
23
+
24
+ @abstractmethod
25
+ async def enable_notifications(self) -> None:
26
+ """Enable asynchronous event notifications from OpenOCD."""
27
+
28
+ @abstractmethod
29
+ def on_notification(self, callback: Callable[[str], None]) -> None:
30
+ """Register a callback for incoming notifications."""