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.
- openocd_python-2026.2.12/.gitignore +8 -0
- openocd_python-2026.2.12/LICENSE +21 -0
- openocd_python-2026.2.12/PKG-INFO +75 -0
- openocd_python-2026.2.12/README.md +46 -0
- openocd_python-2026.2.12/pyproject.toml +61 -0
- openocd_python-2026.2.12/src/openocd/__init__.py +64 -0
- openocd_python-2026.2.12/src/openocd/breakpoints.py +234 -0
- openocd_python-2026.2.12/src/openocd/cli.py +161 -0
- openocd_python-2026.2.12/src/openocd/connection/__init__.py +7 -0
- openocd_python-2026.2.12/src/openocd/connection/base.py +30 -0
- openocd_python-2026.2.12/src/openocd/connection/tcl_rpc.py +259 -0
- openocd_python-2026.2.12/src/openocd/connection/telnet.py +105 -0
- openocd_python-2026.2.12/src/openocd/errors.py +43 -0
- openocd_python-2026.2.12/src/openocd/events.py +112 -0
- openocd_python-2026.2.12/src/openocd/flash.py +381 -0
- openocd_python-2026.2.12/src/openocd/jtag/__init__.py +5 -0
- openocd_python-2026.2.12/src/openocd/jtag/boundary.py +52 -0
- openocd_python-2026.2.12/src/openocd/jtag/chain.py +218 -0
- openocd_python-2026.2.12/src/openocd/jtag/scan.py +58 -0
- openocd_python-2026.2.12/src/openocd/jtag/state.py +26 -0
- openocd_python-2026.2.12/src/openocd/memory.py +243 -0
- openocd_python-2026.2.12/src/openocd/process.py +148 -0
- openocd_python-2026.2.12/src/openocd/py.typed +0 -0
- openocd_python-2026.2.12/src/openocd/registers.py +186 -0
- openocd_python-2026.2.12/src/openocd/rtt.py +233 -0
- openocd_python-2026.2.12/src/openocd/session.py +330 -0
- openocd_python-2026.2.12/src/openocd/svd/__init__.py +5 -0
- openocd_python-2026.2.12/src/openocd/svd/decoder.py +54 -0
- openocd_python-2026.2.12/src/openocd/svd/parser.py +128 -0
- openocd_python-2026.2.12/src/openocd/svd/peripheral.py +186 -0
- openocd_python-2026.2.12/src/openocd/target.py +164 -0
- openocd_python-2026.2.12/src/openocd/transport.py +149 -0
- openocd_python-2026.2.12/src/openocd/types.py +185 -0
- openocd_python-2026.2.12/tests/__init__.py +0 -0
- openocd_python-2026.2.12/tests/conftest.py +37 -0
- openocd_python-2026.2.12/tests/mock_server.py +255 -0
- openocd_python-2026.2.12/tests/test_connection.py +113 -0
- openocd_python-2026.2.12/tests/test_error_paths.py +626 -0
- openocd_python-2026.2.12/tests/test_jtag.py +93 -0
- openocd_python-2026.2.12/tests/test_memory.py +93 -0
- openocd_python-2026.2.12/tests/test_registers.py +107 -0
- openocd_python-2026.2.12/tests/test_session.py +98 -0
- openocd_python-2026.2.12/tests/test_svd.py +249 -0
- openocd_python-2026.2.12/tests/test_target.py +74 -0
- openocd_python-2026.2.12/uv.lock +259 -0
|
@@ -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"]
|