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/svd/parser.py ADDED
@@ -0,0 +1,128 @@
1
+ """SVD file loading and peripheral/register lookup.
2
+
3
+ Wraps cmsis_svd to parse CMSIS-SVD XML files and provide indexed access
4
+ to peripherals and their registers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from cmsis_svd import SVDParser
14
+
15
+ from openocd.errors import SVDError
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ class SVDParserWrapper:
21
+ """Load and cache parsed SVD device data."""
22
+
23
+ def __init__(self) -> None:
24
+ self._device: Any = None
25
+ self._peripherals: dict[str, Any] = {}
26
+
27
+ @property
28
+ def loaded(self) -> bool:
29
+ """Whether an SVD file has been parsed."""
30
+ return self._device is not None
31
+
32
+ def load(self, svd_path: Path) -> None:
33
+ """Parse an SVD file and index peripherals/registers.
34
+
35
+ Args:
36
+ svd_path: Path to the .svd file on disk.
37
+
38
+ Raises:
39
+ SVDError: If the file cannot be found or parsed.
40
+ """
41
+ path = Path(svd_path)
42
+ if not path.exists():
43
+ raise SVDError(f"SVD file not found: {path}")
44
+
45
+ try:
46
+ parser = SVDParser.for_xml_file(str(path))
47
+ self._device = parser.get_device()
48
+ except Exception as exc:
49
+ raise SVDError(f"Failed to parse SVD file {path}: {exc}") from exc
50
+
51
+ self._peripherals = {p.name: p for p in self._device.get_peripherals()}
52
+ log.info(
53
+ "Loaded SVD for %s — %d peripherals",
54
+ getattr(self._device, "name", "unknown"),
55
+ len(self._peripherals),
56
+ )
57
+
58
+ def _require_loaded(self) -> None:
59
+ if not self.loaded:
60
+ raise SVDError("No SVD file loaded — call load() first")
61
+
62
+ def get_peripheral(self, name: str) -> Any:
63
+ """Look up a peripheral by name.
64
+
65
+ Args:
66
+ name: Peripheral name (case-sensitive, e.g. "GPIOA", "USART1").
67
+
68
+ Returns:
69
+ The cmsis_svd peripheral object.
70
+
71
+ Raises:
72
+ SVDError: If no SVD is loaded or the peripheral is not found.
73
+ """
74
+ self._require_loaded()
75
+ periph = self._peripherals.get(name)
76
+ if periph is None:
77
+ raise SVDError(
78
+ f"Peripheral '{name}' not found. "
79
+ f"Available: {', '.join(sorted(self._peripherals))}"
80
+ )
81
+ return periph
82
+
83
+ def get_register(self, peripheral: str, register: str) -> Any:
84
+ """Look up a register within a peripheral.
85
+
86
+ Args:
87
+ peripheral: Peripheral name.
88
+ register: Register name (e.g. "CR1", "SR").
89
+
90
+ Returns:
91
+ The cmsis_svd register object.
92
+
93
+ Raises:
94
+ SVDError: If the peripheral or register is not found.
95
+ """
96
+ periph = self.get_peripheral(peripheral)
97
+ registers = periph.registers or []
98
+ for reg in registers:
99
+ if reg.name == register:
100
+ return reg
101
+
102
+ available = [r.name for r in registers]
103
+ raise SVDError(
104
+ f"Register '{register}' not found in {peripheral}. "
105
+ f"Available: {', '.join(sorted(available))}"
106
+ )
107
+
108
+ def list_peripherals(self) -> list[str]:
109
+ """Return sorted names of all peripherals in the SVD.
110
+
111
+ Raises:
112
+ SVDError: If no SVD is loaded.
113
+ """
114
+ self._require_loaded()
115
+ return sorted(self._peripherals.keys())
116
+
117
+ def list_registers(self, peripheral: str) -> list[str]:
118
+ """Return sorted register names for a peripheral.
119
+
120
+ Args:
121
+ peripheral: Peripheral name.
122
+
123
+ Raises:
124
+ SVDError: If the peripheral is not found.
125
+ """
126
+ periph = self.get_peripheral(peripheral)
127
+ registers = periph.registers or []
128
+ return sorted(r.name for r in registers)
@@ -0,0 +1,186 @@
1
+ """SVDManager — combines SVD parsing, register decoding, and hardware reads.
2
+
3
+ This is the primary interface for SVD-based register inspection. It ties
4
+ the SVD parser, bitfield decoder, and the Memory subsystem together so
5
+ callers can do things like:
6
+
7
+ decoded = await svd.read_register("GPIOA", "ODR")
8
+ print(decoded)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import logging
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING
17
+
18
+ from openocd.svd.decoder import decode_register
19
+ from openocd.svd.parser import SVDParserWrapper
20
+ from openocd.types import DecodedRegister
21
+
22
+ if TYPE_CHECKING:
23
+ from openocd.connection.base import Connection
24
+ from openocd.memory import Memory
25
+
26
+ log = logging.getLogger(__name__)
27
+
28
+
29
+ class SVDManager:
30
+ """High-level SVD register access: parse, read, decode."""
31
+
32
+ def __init__(self, conn: Connection, memory: Memory) -> None:
33
+ self._conn = conn
34
+ self._memory = memory
35
+ self._parser = SVDParserWrapper()
36
+
37
+ @property
38
+ def loaded(self) -> bool:
39
+ """Whether an SVD file has been loaded."""
40
+ return self._parser.loaded
41
+
42
+ async def load(self, svd_path: Path) -> None:
43
+ """Parse an SVD file and make its peripherals available.
44
+
45
+ This is a synchronous file parse wrapped in the async interface
46
+ for consistency with the rest of the API.
47
+
48
+ Args:
49
+ svd_path: Path to the .svd XML file.
50
+
51
+ Raises:
52
+ SVDError: If the file is missing or unparseable.
53
+ """
54
+ await asyncio.to_thread(self._parser.load, svd_path)
55
+
56
+ def list_peripherals(self) -> list[str]:
57
+ """Return sorted peripheral names from the loaded SVD.
58
+
59
+ Raises:
60
+ SVDError: If no SVD is loaded.
61
+ """
62
+ return self._parser.list_peripherals()
63
+
64
+ def list_registers(self, peripheral: str) -> list[str]:
65
+ """Return sorted register names for a peripheral.
66
+
67
+ Args:
68
+ peripheral: Peripheral name (e.g. "GPIOA").
69
+
70
+ Raises:
71
+ SVDError: If no SVD is loaded or peripheral not found.
72
+ """
73
+ return self._parser.list_registers(peripheral)
74
+
75
+ async def read_register(self, peripheral: str, register: str) -> DecodedRegister:
76
+ """Read a register from hardware and decode it using SVD metadata.
77
+
78
+ This is the primary method: it computes the register's memory-mapped
79
+ address from the SVD, reads 32 bits from the target, and returns
80
+ a fully decoded result with named bitfields.
81
+
82
+ Args:
83
+ peripheral: Peripheral name (e.g. "GPIOA").
84
+ register: Register name (e.g. "ODR").
85
+
86
+ Returns:
87
+ DecodedRegister with address, raw value, and decoded fields.
88
+
89
+ Raises:
90
+ SVDError: If peripheral/register not found.
91
+ TargetError: If the memory read fails.
92
+ """
93
+ periph_obj = self._parser.get_peripheral(peripheral)
94
+ reg_obj = self._parser.get_register(peripheral, register)
95
+ address = periph_obj.base_address + reg_obj.address_offset
96
+
97
+ values = await self._memory.read_u32(address)
98
+ raw = values[0]
99
+ return decode_register(periph_obj, reg_obj, raw)
100
+
101
+ async def read_peripheral(self, peripheral: str) -> dict[str, DecodedRegister]:
102
+ """Read and decode every register in a peripheral.
103
+
104
+ Args:
105
+ peripheral: Peripheral name.
106
+
107
+ Returns:
108
+ Dict mapping register name to its DecodedRegister.
109
+
110
+ Raises:
111
+ SVDError: If peripheral not found.
112
+ TargetError: If any memory read fails.
113
+ """
114
+ periph_obj = self._parser.get_peripheral(peripheral)
115
+ registers = periph_obj.registers or []
116
+ result: dict[str, DecodedRegister] = {}
117
+
118
+ for reg_obj in registers:
119
+ address = periph_obj.base_address + reg_obj.address_offset
120
+ try:
121
+ values = await self._memory.read_u32(address)
122
+ raw = values[0]
123
+ result[reg_obj.name] = decode_register(periph_obj, reg_obj, raw)
124
+ except Exception as exc:
125
+ log.warning(
126
+ "Failed to read %s.%s @ 0x%08X: %s",
127
+ peripheral,
128
+ reg_obj.name,
129
+ address,
130
+ exc,
131
+ )
132
+ # Skip unreadable registers (write-only, reserved, etc.)
133
+
134
+ return result
135
+
136
+ def decode(self, peripheral: str, register: str, value: int) -> DecodedRegister:
137
+ """Decode a raw value without reading hardware.
138
+
139
+ Useful when you already have the register value (from a log,
140
+ a previous read, or a known reset value).
141
+
142
+ Args:
143
+ peripheral: Peripheral name.
144
+ register: Register name.
145
+ value: Raw 32-bit register value.
146
+
147
+ Returns:
148
+ DecodedRegister with the decoded bitfields.
149
+ """
150
+ periph_obj = self._parser.get_peripheral(peripheral)
151
+ reg_obj = self._parser.get_register(peripheral, register)
152
+ return decode_register(periph_obj, reg_obj, value)
153
+
154
+
155
+ class SyncSVDManager:
156
+ """Synchronous wrapper around SVDManager."""
157
+
158
+ def __init__(self, manager: SVDManager, loop: asyncio.AbstractEventLoop) -> None:
159
+ self._manager = manager
160
+ self._loop = loop
161
+
162
+ @property
163
+ def loaded(self) -> bool:
164
+ return self._manager.loaded
165
+
166
+ def load(self, svd_path: Path) -> None:
167
+ self._loop.run_until_complete(self._manager.load(svd_path))
168
+
169
+ def list_peripherals(self) -> list[str]:
170
+ return self._manager.list_peripherals()
171
+
172
+ def list_registers(self, peripheral: str) -> list[str]:
173
+ return self._manager.list_registers(peripheral)
174
+
175
+ def read_register(self, peripheral: str, register: str) -> DecodedRegister:
176
+ return self._loop.run_until_complete(
177
+ self._manager.read_register(peripheral, register)
178
+ )
179
+
180
+ def read_peripheral(self, peripheral: str) -> dict[str, DecodedRegister]:
181
+ return self._loop.run_until_complete(
182
+ self._manager.read_peripheral(peripheral)
183
+ )
184
+
185
+ def decode(self, peripheral: str, register: str, value: int) -> DecodedRegister:
186
+ return self._manager.decode(peripheral, register, value)
openocd/target.py ADDED
@@ -0,0 +1,164 @@
1
+ """Target state control — halt, resume, step, reset, and state queries.
2
+
3
+ Wraps the OpenOCD target commands: halt, resume, step, reset,
4
+ wait_halt, and targets (for state inspection).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ import re
12
+ from typing import Literal
13
+
14
+ from openocd.connection.tcl_rpc import TclRpcConnection
15
+ from openocd.errors import TargetError, TimeoutError
16
+ from openocd.types import TargetState
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+ # Matches a target row from "targets" output, e.g.:
21
+ # " 0* stm32f1x.cpu cortex_m little stm32f1x.cpu halted"
22
+ _TARGET_ROW_RE = re.compile(
23
+ r"^\s*\d+\*?\s+" # index, optional current marker
24
+ r"(\S+)\s+" # target name
25
+ r"\S+\s+" # type
26
+ r"\S+\s+" # endian
27
+ r"\S+\s+" # tap name
28
+ r"(\S+)" # state
29
+ )
30
+
31
+
32
+ class Target:
33
+ """Target execution control — halt, resume, step, reset."""
34
+
35
+ def __init__(self, conn: TclRpcConnection) -> None:
36
+ self._conn = conn
37
+
38
+ async def halt(self) -> TargetState:
39
+ """Halt the target and return the resulting state."""
40
+ resp = await self._conn.send("halt")
41
+ if "error" in resp.lower() and "already" not in resp.lower():
42
+ raise TargetError(f"halt failed: {resp}")
43
+ return await self._parse_state()
44
+
45
+ async def resume(self, address: int | None = None) -> None:
46
+ """Resume execution, optionally from a specific address."""
47
+ cmd = "resume"
48
+ if address is not None:
49
+ cmd = f"resume 0x{address:x}"
50
+ resp = await self._conn.send(cmd)
51
+ if "error" in resp.lower():
52
+ raise TargetError(f"resume failed: {resp}")
53
+
54
+ async def step(self, address: int | None = None) -> TargetState:
55
+ """Single-step and return the resulting state."""
56
+ cmd = "step"
57
+ if address is not None:
58
+ cmd = f"step 0x{address:x}"
59
+ resp = await self._conn.send(cmd)
60
+ if "error" in resp.lower():
61
+ raise TargetError(f"step failed: {resp}")
62
+ return await self._parse_state()
63
+
64
+ async def reset(self, mode: Literal["run", "halt", "init"] = "halt") -> None:
65
+ """Reset the target.
66
+
67
+ Args:
68
+ mode: Reset mode — "run" resumes after reset, "halt" stops at
69
+ the reset vector, "init" runs init scripts after reset.
70
+ """
71
+ resp = await self._conn.send(f"reset {mode}")
72
+ if "error" in resp.lower():
73
+ raise TargetError(f"reset failed: {resp}")
74
+
75
+ async def wait_halt(self, timeout_ms: int = 5000) -> TargetState:
76
+ """Block until the target halts or the timeout expires.
77
+
78
+ Args:
79
+ timeout_ms: Maximum wait time in milliseconds.
80
+
81
+ Raises:
82
+ TimeoutError: Target did not halt within the deadline.
83
+ """
84
+ resp = await self._conn.send(f"wait_halt {timeout_ms}")
85
+ if "timed out" in resp.lower() or "time out" in resp.lower():
86
+ raise TimeoutError(f"Target did not halt within {timeout_ms}ms")
87
+ if "error" in resp.lower():
88
+ raise TargetError(f"wait_halt failed: {resp}")
89
+ return await self._parse_state()
90
+
91
+ async def state(self) -> TargetState:
92
+ """Query and return the current target state."""
93
+ return await self._parse_state()
94
+
95
+ async def _parse_state(self) -> TargetState:
96
+ """Parse the ``targets`` command output into a TargetState.
97
+
98
+ The output looks like::
99
+
100
+ TargetName Type Endian TapName State
101
+ -- ------------------ ---------- ------ ------------------ ------------
102
+ 0* stm32f1x.cpu cortex_m little stm32f1x.cpu halted
103
+
104
+ If the target is halted, also reads the program counter via ``reg pc``.
105
+ """
106
+ resp = await self._conn.send("targets")
107
+
108
+ name = "unknown"
109
+ raw_state = "unknown"
110
+
111
+ for line in resp.splitlines():
112
+ m = _TARGET_ROW_RE.match(line)
113
+ if m:
114
+ name = m.group(1)
115
+ raw_state = m.group(2).lower()
116
+ break
117
+
118
+ # Normalize to our known state literals
119
+ if raw_state not in ("running", "halted", "reset", "debug-running"):
120
+ raw_state = "unknown"
121
+
122
+ pc: int | None = None
123
+ if raw_state == "halted":
124
+ try:
125
+ pc = await self._read_pc()
126
+ except Exception:
127
+ log.debug("Could not read PC while halted", exc_info=True)
128
+
129
+ return TargetState(name=name, state=raw_state, current_pc=pc)
130
+
131
+ async def _read_pc(self) -> int:
132
+ """Read the program counter from the halted target."""
133
+ resp = await self._conn.send("reg pc")
134
+ # Output: "pc (/32): 0x08001234"
135
+ m = re.search(r":\s*(0x[0-9a-fA-F]+)", resp)
136
+ if not m:
137
+ raise TargetError(f"Cannot parse PC from: {resp}")
138
+ return int(m.group(1), 16)
139
+
140
+
141
+ class SyncTarget:
142
+ """Synchronous wrapper around Target."""
143
+
144
+ def __init__(self, target: Target, loop: asyncio.AbstractEventLoop) -> None:
145
+ self._target = target
146
+ self._loop = loop
147
+
148
+ def halt(self) -> TargetState:
149
+ return self._loop.run_until_complete(self._target.halt())
150
+
151
+ def resume(self, address: int | None = None) -> None:
152
+ self._loop.run_until_complete(self._target.resume(address))
153
+
154
+ def step(self, address: int | None = None) -> TargetState:
155
+ return self._loop.run_until_complete(self._target.step(address))
156
+
157
+ def reset(self, mode: Literal["run", "halt", "init"] = "halt") -> None:
158
+ self._loop.run_until_complete(self._target.reset(mode))
159
+
160
+ def wait_halt(self, timeout_ms: int = 5000) -> TargetState:
161
+ return self._loop.run_until_complete(self._target.wait_halt(timeout_ms))
162
+
163
+ def state(self) -> TargetState:
164
+ return self._loop.run_until_complete(self._target.state())
openocd/transport.py ADDED
@@ -0,0 +1,149 @@
1
+ """Transport selection and debug adapter configuration.
2
+
3
+ OpenOCD supports multiple debug transports (JTAG, SWD, SWIM, etc.)
4
+ and various adapter interfaces (CMSIS-DAP, ST-Link, J-Link, etc.).
5
+ This module provides access to transport and adapter state.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import TYPE_CHECKING
12
+
13
+ from openocd.errors import OpenOCDError
14
+
15
+ if TYPE_CHECKING:
16
+ from openocd.connection.base import Connection
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ class Transport:
22
+ """Query and configure the debug transport and adapter.
23
+
24
+ Usage::
25
+
26
+ transport = Transport(conn)
27
+ current = await transport.select() # e.g. "swd"
28
+ available = await transport.list() # e.g. ["jtag", "swd"]
29
+ speed = await transport.adapter_speed() # current kHz
30
+ await transport.adapter_speed(4000) # set to 4 MHz
31
+ """
32
+
33
+ def __init__(self, conn: Connection) -> None:
34
+ self._conn = conn
35
+
36
+ async def select(self) -> str:
37
+ """Get the currently selected transport.
38
+
39
+ Returns:
40
+ Transport name string (e.g. "jtag", "swd", "swim").
41
+
42
+ Raises:
43
+ OpenOCDError: If the command fails.
44
+ """
45
+ response = await self._conn.send("transport select")
46
+ response = response.strip()
47
+ if not response:
48
+ raise OpenOCDError("Empty response from 'transport select'")
49
+ return response
50
+
51
+ async def list(self) -> list[str]:
52
+ """List transports available for the current adapter.
53
+
54
+ Returns:
55
+ List of transport name strings.
56
+
57
+ Raises:
58
+ OpenOCDError: If the command fails.
59
+ """
60
+ response = await self._conn.send("transport list")
61
+ response = response.strip()
62
+ if not response:
63
+ raise OpenOCDError("Empty response from 'transport list'")
64
+
65
+ # OpenOCD may return a Tcl list like "jtag swd" or one per line
66
+ transports: list[str] = []
67
+ for line in response.splitlines():
68
+ for token in line.split():
69
+ cleaned = token.strip("{}")
70
+ if cleaned:
71
+ transports.append(cleaned)
72
+ return transports
73
+
74
+ async def adapter_info(self) -> str:
75
+ """Get adapter/interface information.
76
+
77
+ Tries ``adapter name`` first (newer OpenOCD), falls back to
78
+ ``adapter info`` for older versions.
79
+
80
+ Returns:
81
+ Adapter description string.
82
+ """
83
+ # "adapter name" is the preferred command in OpenOCD >= 0.12
84
+ response = await self._conn.send("adapter name")
85
+ response = response.strip()
86
+
87
+ if not response or "invalid" in response.lower() or "error" in response.lower():
88
+ response = await self._conn.send("adapter info")
89
+ response = response.strip()
90
+
91
+ if not response:
92
+ raise OpenOCDError("Could not determine adapter info")
93
+ return response
94
+
95
+ async def adapter_speed(self, khz: int | None = None) -> int:
96
+ """Get or set the adapter clock speed.
97
+
98
+ Args:
99
+ khz: If provided, set the adapter speed to this value in kHz.
100
+ If None, just query the current speed.
101
+
102
+ Returns:
103
+ The current (or newly set) adapter speed in kHz.
104
+
105
+ Raises:
106
+ OpenOCDError: If the command fails or response is not parseable.
107
+ """
108
+ cmd = f"adapter speed {khz}" if khz is not None else "adapter speed"
109
+
110
+ response = await self._conn.send(cmd)
111
+ response = response.strip()
112
+
113
+ # OpenOCD response is typically just a number, or
114
+ # "adapter speed: 4000 kHz" depending on the interface
115
+ speed = _parse_speed(response)
116
+ if speed is None:
117
+ raise OpenOCDError(f"Cannot parse adapter speed from: {response!r}")
118
+
119
+ if khz is not None:
120
+ log.info("Adapter speed set to %d kHz", speed)
121
+ return speed
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # Helpers
126
+ # ---------------------------------------------------------------------------
127
+
128
+ def _parse_speed(response: str) -> int | None:
129
+ """Extract a numeric kHz value from an adapter speed response.
130
+
131
+ Handles formats like:
132
+ "4000"
133
+ "adapter speed: 4000 kHz"
134
+ "4000 kHz"
135
+ """
136
+ # Try the whole thing as a plain integer
137
+ try:
138
+ return int(response)
139
+ except ValueError:
140
+ pass
141
+
142
+ # Pull out the first integer-looking token
143
+ for token in response.replace(":", " ").split():
144
+ try:
145
+ return int(token)
146
+ except ValueError:
147
+ continue
148
+
149
+ return None