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/rtt.py ADDED
@@ -0,0 +1,233 @@
1
+ """Real-Time Transfer (RTT) support via OpenOCD.
2
+
3
+ SEGGER RTT provides high-speed bidirectional communication between
4
+ a debug host and an embedded target using shared memory in RAM.
5
+ OpenOCD exposes RTT through its ``rtt`` command family.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import contextlib
12
+ import logging
13
+ from typing import TYPE_CHECKING
14
+
15
+ from openocd.errors import OpenOCDError
16
+ from openocd.types import RTTChannel
17
+
18
+ if TYPE_CHECKING:
19
+ from openocd.connection.base import Connection
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+
24
+ class RTTManager:
25
+ """Control and use SEGGER RTT channels via OpenOCD.
26
+
27
+ Typical flow::
28
+
29
+ rtt = RTTManager(conn)
30
+ await rtt.setup(address=0x20000000, size=0x1000)
31
+ await rtt.start()
32
+ channels = await rtt.channels()
33
+ data = await rtt.read(0)
34
+ await rtt.write(0, "hello\\n")
35
+ await rtt.stop()
36
+ """
37
+
38
+ def __init__(self, conn: Connection) -> None:
39
+ self._conn = conn
40
+
41
+ async def setup(
42
+ self,
43
+ address: int,
44
+ size: int,
45
+ id_string: str = "SEGGER RTT",
46
+ ) -> None:
47
+ """Configure RTT control block search parameters.
48
+
49
+ Args:
50
+ address: Start address of the RAM region to search.
51
+ size: Size of the search region in bytes.
52
+ id_string: RTT control block identifier (default "SEGGER RTT").
53
+
54
+ Raises:
55
+ OpenOCDError: If the setup command fails.
56
+ """
57
+ cmd = f'rtt setup 0x{address:X} 0x{size:X} "{id_string}"'
58
+ response = await self._conn.send(cmd)
59
+ _check_rtt_response(response, cmd)
60
+ log.info(
61
+ "RTT setup: search 0x%08X +0x%X id=%r",
62
+ address,
63
+ size,
64
+ id_string,
65
+ )
66
+
67
+ async def start(self) -> None:
68
+ """Start RTT — searches for the control block and activates channels.
69
+
70
+ Raises:
71
+ OpenOCDError: If the control block is not found or start fails.
72
+ """
73
+ response = await self._conn.send("rtt start")
74
+ _check_rtt_response(response, "rtt start")
75
+ log.info("RTT started")
76
+
77
+ async def stop(self) -> None:
78
+ """Stop RTT communication.
79
+
80
+ Raises:
81
+ OpenOCDError: If the stop command fails.
82
+ """
83
+ response = await self._conn.send("rtt stop")
84
+ _check_rtt_response(response, "rtt stop")
85
+ log.info("RTT stopped")
86
+
87
+ async def channels(self) -> list[RTTChannel]:
88
+ """List available RTT channels.
89
+
90
+ Returns:
91
+ List of RTTChannel descriptors (index, name, size, direction).
92
+
93
+ Raises:
94
+ OpenOCDError: If the channels command fails.
95
+ """
96
+ response = await self._conn.send("rtt channels")
97
+ _check_rtt_response(response, "rtt channels")
98
+ return _parse_channels(response)
99
+
100
+ async def read(self, channel: int) -> str:
101
+ """Read pending data from an RTT up-channel.
102
+
103
+ Args:
104
+ channel: Channel index (typically 0 for Terminal).
105
+
106
+ Returns:
107
+ The data read as a string (may be empty if nothing pending).
108
+
109
+ Raises:
110
+ OpenOCDError: If the read command fails.
111
+ """
112
+ cmd = f"rtt channelread {channel}"
113
+ response = await self._conn.send(cmd)
114
+ _check_rtt_response(response, cmd)
115
+ return response
116
+
117
+ async def write(self, channel: int, data: str) -> None:
118
+ """Write data to an RTT down-channel.
119
+
120
+ Args:
121
+ channel: Channel index (typically 0 for Terminal).
122
+ data: String data to send to the target.
123
+
124
+ Raises:
125
+ OpenOCDError: If the write command fails.
126
+ """
127
+ # Escape TCL special characters to prevent injection
128
+ escaped = (
129
+ data.replace("\\", "\\\\")
130
+ .replace('"', '\\"')
131
+ .replace("[", "\\[")
132
+ .replace("$", "\\$")
133
+ )
134
+ cmd = f'rtt channelwrite {channel} "{escaped}"'
135
+ response = await self._conn.send(cmd)
136
+ _check_rtt_response(response, cmd)
137
+
138
+
139
+ class SyncRTTManager:
140
+ """Synchronous wrapper around RTTManager."""
141
+
142
+ def __init__(self, manager: RTTManager, loop: asyncio.AbstractEventLoop) -> None:
143
+ self._manager = manager
144
+ self._loop = loop
145
+
146
+ def setup(
147
+ self,
148
+ address: int,
149
+ size: int,
150
+ id_string: str = "SEGGER RTT",
151
+ ) -> None:
152
+ self._loop.run_until_complete(
153
+ self._manager.setup(address, size, id_string)
154
+ )
155
+
156
+ def start(self) -> None:
157
+ self._loop.run_until_complete(self._manager.start())
158
+
159
+ def stop(self) -> None:
160
+ self._loop.run_until_complete(self._manager.stop())
161
+
162
+ def channels(self) -> list[RTTChannel]:
163
+ return self._loop.run_until_complete(self._manager.channels())
164
+
165
+ def read(self, channel: int) -> str:
166
+ return self._loop.run_until_complete(self._manager.read(channel))
167
+
168
+ def write(self, channel: int, data: str) -> None:
169
+ self._loop.run_until_complete(self._manager.write(channel, data))
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # Helpers
174
+ # ---------------------------------------------------------------------------
175
+
176
+ def _check_rtt_response(response: str, command: str) -> None:
177
+ """Raise on error responses from RTT commands."""
178
+ if response and "error" in response.lower():
179
+ raise OpenOCDError(f"RTT command failed ({command}): {response}")
180
+
181
+
182
+ def _parse_channels(response: str) -> list[RTTChannel]:
183
+ """Parse the output of ``rtt channels`` into RTTChannel objects.
184
+
185
+ OpenOCD typically outputs lines like::
186
+
187
+ Up-channels:
188
+ 0: Terminal 1024
189
+ Down-channels:
190
+ 0: Terminal 16
191
+
192
+ The exact format may vary by OpenOCD version; this parser is
193
+ intentionally lenient.
194
+ """
195
+ channels: list[RTTChannel] = []
196
+ direction = "up"
197
+
198
+ for line in response.splitlines():
199
+ stripped = line.strip()
200
+ lower = stripped.lower()
201
+
202
+ if "up-channel" in lower or lower.startswith("up"):
203
+ direction = "up"
204
+ continue
205
+ if "down-channel" in lower or lower.startswith("down"):
206
+ direction = "down"
207
+ continue
208
+
209
+ # Try to parse lines like "0: Terminal 1024"
210
+ if ":" in stripped and stripped[0].isdigit():
211
+ parts = stripped.split(":", 1)
212
+ try:
213
+ index = int(parts[0].strip())
214
+ except ValueError:
215
+ continue
216
+
217
+ rest = parts[1].strip().split()
218
+ name = rest[0] if rest else f"channel_{index}"
219
+ size = 0
220
+ if len(rest) >= 2:
221
+ with contextlib.suppress(ValueError):
222
+ size = int(rest[-1])
223
+
224
+ channels.append(
225
+ RTTChannel(
226
+ index=index,
227
+ name=name,
228
+ size=size,
229
+ direction=direction,
230
+ )
231
+ )
232
+
233
+ return channels
openocd/session.py ADDED
@@ -0,0 +1,330 @@
1
+ """Session — the main entry point for openocd-python.
2
+
3
+ Manages the connection lifecycle and provides access to all subsystems
4
+ (target, memory, registers, flash, JTAG, SVD, etc.).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING
14
+
15
+ from openocd.connection.tcl_rpc import TclRpcConnection
16
+ from openocd.process import OpenOCDProcess
17
+
18
+ if TYPE_CHECKING:
19
+ from openocd.breakpoints import BreakpointManager, SyncBreakpointManager
20
+ from openocd.flash import Flash, SyncFlash
21
+ from openocd.jtag import JTAGController, SyncJTAGController
22
+ from openocd.memory import Memory, SyncMemory
23
+ from openocd.registers import Registers, SyncRegisters
24
+ from openocd.rtt import RTTManager
25
+ from openocd.svd import SVDManager, SyncSVDManager
26
+ from openocd.target import SyncTarget, Target
27
+ from openocd.transport import Transport
28
+
29
+ log = logging.getLogger(__name__)
30
+
31
+
32
+ class Session:
33
+ """Main entry point. Manages connection and provides access to subsystems."""
34
+
35
+ def __init__(self, connection: TclRpcConnection, process: OpenOCDProcess | None = None) -> None:
36
+ self._conn = connection
37
+ self._process = process
38
+ self._target: Target | None = None
39
+ self._memory: Memory | None = None
40
+ self._registers: Registers | None = None
41
+ self._flash: Flash | None = None
42
+ self._jtag: JTAGController | None = None
43
+ self._breakpoints: BreakpointManager | None = None
44
+ self._rtt: RTTManager | None = None
45
+ self._svd: SVDManager | None = None
46
+ self._transport: Transport | None = None
47
+
48
+ # ------------------------------------------------------------------
49
+ # Factory methods
50
+ # ------------------------------------------------------------------
51
+
52
+ @classmethod
53
+ async def start(
54
+ cls,
55
+ config: str | Path,
56
+ *,
57
+ tcl_port: int = 6666,
58
+ openocd_bin: str | None = None,
59
+ timeout: float = 10.0,
60
+ extra_args: list[str] | None = None,
61
+ ) -> Session:
62
+ """Spawn an OpenOCD process and connect to it.
63
+
64
+ Args:
65
+ config: Config file path or ``-f``/``-c`` flags string.
66
+ tcl_port: TCL RPC port.
67
+ openocd_bin: Custom OpenOCD binary path.
68
+ timeout: Seconds to wait for OpenOCD readiness.
69
+ extra_args: Additional CLI arguments for OpenOCD.
70
+ """
71
+ proc = OpenOCDProcess()
72
+ await proc.start(
73
+ str(config), extra_args=extra_args, tcl_port=tcl_port, openocd_bin=openocd_bin
74
+ )
75
+ await proc.wait_ready(timeout=timeout)
76
+
77
+ try:
78
+ conn = TclRpcConnection(timeout=timeout)
79
+ await conn.connect("localhost", tcl_port)
80
+ except Exception:
81
+ await proc.stop()
82
+ raise
83
+
84
+ return cls(connection=conn, process=proc)
85
+
86
+ @classmethod
87
+ async def connect(
88
+ cls,
89
+ host: str = "localhost",
90
+ port: int = 6666,
91
+ timeout: float = 10.0,
92
+ ) -> Session:
93
+ """Connect to an already-running OpenOCD instance."""
94
+ conn = TclRpcConnection(timeout=timeout)
95
+ await conn.connect(host, port)
96
+ return cls(connection=conn)
97
+
98
+ # ------------------------------------------------------------------
99
+ # Sync factory wrappers
100
+ # ------------------------------------------------------------------
101
+
102
+ @classmethod
103
+ def start_sync(cls, config: str | Path, **kwargs) -> SyncSession:
104
+ """Synchronous version of start(). Returns a SyncSession."""
105
+ loop = _get_or_create_loop()
106
+ session = loop.run_until_complete(cls.start(config, **kwargs))
107
+ return SyncSession(session, loop)
108
+
109
+ @classmethod
110
+ def connect_sync(cls, host: str = "localhost", port: int = 6666, **kwargs) -> SyncSession:
111
+ """Synchronous version of connect(). Returns a SyncSession."""
112
+ loop = _get_or_create_loop()
113
+ session = loop.run_until_complete(cls.connect(host, port, **kwargs))
114
+ return SyncSession(session, loop)
115
+
116
+ # ------------------------------------------------------------------
117
+ # Context manager
118
+ # ------------------------------------------------------------------
119
+
120
+ async def __aenter__(self) -> Session:
121
+ return self
122
+
123
+ async def __aexit__(self, *exc) -> None:
124
+ await self.close()
125
+
126
+ async def close(self) -> None:
127
+ """Close the connection and stop the subprocess if we spawned it."""
128
+ await self._conn.close()
129
+ if self._process:
130
+ await self._process.stop()
131
+
132
+ # ------------------------------------------------------------------
133
+ # Raw command escape hatch
134
+ # ------------------------------------------------------------------
135
+
136
+ async def command(self, cmd: str) -> str:
137
+ """Send a raw OpenOCD command and return the response string."""
138
+ return await self._conn.send(cmd)
139
+
140
+ # ------------------------------------------------------------------
141
+ # Subsystem accessors (lazy-initialized)
142
+ # ------------------------------------------------------------------
143
+
144
+ @property
145
+ def target(self) -> Target:
146
+ if self._target is None:
147
+ from openocd.target import Target
148
+ self._target = Target(self._conn)
149
+ return self._target
150
+
151
+ @property
152
+ def memory(self) -> Memory:
153
+ if self._memory is None:
154
+ from openocd.memory import Memory
155
+ self._memory = Memory(self._conn)
156
+ return self._memory
157
+
158
+ @property
159
+ def registers(self) -> Registers:
160
+ if self._registers is None:
161
+ from openocd.registers import Registers
162
+ self._registers = Registers(self._conn)
163
+ return self._registers
164
+
165
+ @property
166
+ def flash(self) -> Flash:
167
+ if self._flash is None:
168
+ from openocd.flash import Flash
169
+ self._flash = Flash(self._conn)
170
+ return self._flash
171
+
172
+ @property
173
+ def jtag(self) -> JTAGController:
174
+ if self._jtag is None:
175
+ from openocd.jtag import JTAGController
176
+ self._jtag = JTAGController(self._conn)
177
+ return self._jtag
178
+
179
+ @property
180
+ def breakpoints(self) -> BreakpointManager:
181
+ if self._breakpoints is None:
182
+ from openocd.breakpoints import BreakpointManager
183
+ self._breakpoints = BreakpointManager(self._conn)
184
+ return self._breakpoints
185
+
186
+ @property
187
+ def rtt(self) -> RTTManager:
188
+ if self._rtt is None:
189
+ from openocd.rtt import RTTManager
190
+ self._rtt = RTTManager(self._conn)
191
+ return self._rtt
192
+
193
+ @property
194
+ def svd(self) -> SVDManager:
195
+ if self._svd is None:
196
+ from openocd.svd import SVDManager
197
+ self._svd = SVDManager(self._conn, self.memory)
198
+ return self._svd
199
+
200
+ @property
201
+ def transport(self) -> Transport:
202
+ if self._transport is None:
203
+ from openocd.transport import Transport
204
+ self._transport = Transport(self._conn)
205
+ return self._transport
206
+
207
+ # ------------------------------------------------------------------
208
+ # Event shortcuts
209
+ # ------------------------------------------------------------------
210
+
211
+ def on_halt(self, callback: Callable[[str], None]) -> None:
212
+ """Register a callback for target halt events."""
213
+ def _filter(msg: str) -> None:
214
+ if "halted" in msg.lower():
215
+ callback(msg)
216
+ self._conn.on_notification(_filter)
217
+
218
+ def on_reset(self, callback: Callable[[str], None]) -> None:
219
+ """Register a callback for target reset events."""
220
+ def _filter(msg: str) -> None:
221
+ if "reset" in msg.lower():
222
+ callback(msg)
223
+ self._conn.on_notification(_filter)
224
+
225
+
226
+ # ======================================================================
227
+ # Sync wrapper
228
+ # ======================================================================
229
+
230
+ class SyncSession:
231
+ """Wraps an async Session for synchronous use."""
232
+
233
+ def __init__(self, session: Session, loop: asyncio.AbstractEventLoop) -> None:
234
+ self._session = session
235
+ self._loop = loop
236
+ self._target: SyncTarget | None = None
237
+ self._memory: SyncMemory | None = None
238
+ self._registers: SyncRegisters | None = None
239
+ self._flash: SyncFlash | None = None
240
+ self._jtag: SyncJTAGController | None = None
241
+ self._breakpoints: SyncBreakpointManager | None = None
242
+ self._svd: SyncSVDManager | None = None
243
+
244
+ def __enter__(self) -> SyncSession:
245
+ return self
246
+
247
+ def __exit__(self, *exc) -> None:
248
+ self._loop.run_until_complete(self._session.close())
249
+
250
+ def command(self, cmd: str) -> str:
251
+ return self._loop.run_until_complete(self._session.command(cmd))
252
+
253
+ @property
254
+ def target(self) -> SyncTarget:
255
+ if self._target is None:
256
+ from openocd.target import SyncTarget
257
+ self._target = SyncTarget(self._session.target, self._loop)
258
+ return self._target
259
+
260
+ @property
261
+ def memory(self) -> SyncMemory:
262
+ if self._memory is None:
263
+ from openocd.memory import SyncMemory
264
+ self._memory = SyncMemory(self._session.memory, self._loop)
265
+ return self._memory
266
+
267
+ @property
268
+ def registers(self) -> SyncRegisters:
269
+ if self._registers is None:
270
+ from openocd.registers import SyncRegisters
271
+ self._registers = SyncRegisters(self._session.registers, self._loop)
272
+ return self._registers
273
+
274
+ @property
275
+ def flash(self) -> SyncFlash:
276
+ if self._flash is None:
277
+ from openocd.flash import SyncFlash
278
+ self._flash = SyncFlash(self._session.flash, self._loop)
279
+ return self._flash
280
+
281
+ @property
282
+ def jtag(self) -> SyncJTAGController:
283
+ if self._jtag is None:
284
+ from openocd.jtag import SyncJTAGController
285
+ self._jtag = SyncJTAGController(self._session.jtag, self._loop)
286
+ return self._jtag
287
+
288
+ @property
289
+ def breakpoints(self) -> SyncBreakpointManager:
290
+ if self._breakpoints is None:
291
+ from openocd.breakpoints import SyncBreakpointManager
292
+ self._breakpoints = SyncBreakpointManager(self._session.breakpoints, self._loop)
293
+ return self._breakpoints
294
+
295
+ @property
296
+ def svd(self) -> SyncSVDManager:
297
+ if self._svd is None:
298
+ from openocd.svd import SyncSVDManager
299
+ self._svd = SyncSVDManager(self._session.svd, self._loop)
300
+ return self._svd
301
+
302
+
303
+ # ======================================================================
304
+ # Helpers
305
+ # ======================================================================
306
+
307
+ def _get_or_create_loop() -> asyncio.AbstractEventLoop:
308
+ """Get or create an event loop for synchronous usage.
309
+
310
+ Raises RuntimeError if called from within an already-running async
311
+ context (where ``run_until_complete`` would deadlock).
312
+ """
313
+ try:
314
+ asyncio.get_running_loop()
315
+ except RuntimeError:
316
+ pass # No running loop — this is the expected path for sync usage
317
+ else:
318
+ raise RuntimeError(
319
+ "Cannot use sync API from an async context. "
320
+ "Use the async Session.start()/connect() instead."
321
+ )
322
+ try:
323
+ loop = asyncio.get_event_loop()
324
+ if loop.is_closed():
325
+ loop = asyncio.new_event_loop()
326
+ asyncio.set_event_loop(loop)
327
+ except RuntimeError:
328
+ loop = asyncio.new_event_loop()
329
+ asyncio.set_event_loop(loop)
330
+ return loop
@@ -0,0 +1,5 @@
1
+ """SVD (System View Description) integration for peripheral/register decoding."""
2
+
3
+ from openocd.svd.peripheral import SVDManager, SyncSVDManager
4
+
5
+ __all__ = ["SVDManager", "SyncSVDManager"]
openocd/svd/decoder.py ADDED
@@ -0,0 +1,54 @@
1
+ """Register value decoding using SVD metadata.
2
+
3
+ Takes a raw integer read from hardware and splits it into named bitfields
4
+ using the field definitions from a parsed SVD file.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from openocd.types import BitField, DecodedRegister
12
+
13
+
14
+ def decode_register(
15
+ peripheral_obj: Any,
16
+ register_obj: Any,
17
+ raw_value: int,
18
+ ) -> DecodedRegister:
19
+ """Decode a raw register value into named bitfields using SVD metadata.
20
+
21
+ Args:
22
+ peripheral_obj: cmsis_svd peripheral (used for base_address and name).
23
+ register_obj: cmsis_svd register (used for fields, address_offset, name).
24
+ raw_value: The 32-bit value read from hardware.
25
+
26
+ Returns:
27
+ A DecodedRegister with all fields extracted and annotated.
28
+ """
29
+ address = peripheral_obj.base_address + register_obj.address_offset
30
+ fields: list[BitField] = []
31
+
32
+ for svd_field in register_obj.fields or []:
33
+ mask = ((1 << svd_field.bit_width) - 1) << svd_field.bit_offset
34
+ value = (raw_value & mask) >> svd_field.bit_offset
35
+ fields.append(
36
+ BitField(
37
+ name=svd_field.name,
38
+ offset=svd_field.bit_offset,
39
+ width=svd_field.bit_width,
40
+ value=value,
41
+ description=svd_field.description or "",
42
+ )
43
+ )
44
+
45
+ # Sort fields by bit offset (low to high) for consistent display
46
+ fields.sort(key=lambda f: f.offset)
47
+
48
+ return DecodedRegister(
49
+ peripheral=peripheral_obj.name,
50
+ register=register_obj.name,
51
+ address=address,
52
+ raw_value=raw_value,
53
+ fields=fields,
54
+ )