> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lagerdata.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Debug

> Embedded debug operations for J-Link probes

Control embedded debug operations including device connection, firmware flashing, reset, and memory access using J-Link debug probes.

## Import

```python theme={null}
from lager import Net, NetType
```

## Methods

The Net-based API provides methods for embedded debugging operations.

| Method                                                                                                 | Description                                                           |
| ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
| `connect(speed, transport, *, force, ignore_if_connected, script, openocd_config, jlink_script, halt)` | Connect to target device (optional per-connect debug-script override) |
| `disconnect()`                                                                                         | Disconnect from target                                                |
| `reset(halt)`                                                                                          | Reset the device                                                      |
| `halt()`                                                                                               | Halt the target where it is, without a reset (OpenOCD only)           |
| `flash(firmware_path)`                                                                                 | Flash firmware to device                                              |
| `erase()`                                                                                              | Perform full chip erase                                               |
| `read_memory(address, length)`                                                                         | Read memory from device                                               |
| `status()`                                                                                             | Get connection status                                                 |
| `rtt(channel, search_addr, search_size, chunk_size)`                                                   | Create RTT session for bidirectional communication (raw bytes)        |
| `rtt_defmt(elf, channel)`                                                                              | RTT session decoded through `defmt-print` (yields log lines)          |
| `session(...)`                                                                                         | Scoped session: connect on entry, guaranteed teardown on exit         |

## Method Reference

### `Net.get(name, type=NetType.Debug)`

Get a debug net by name.

```python theme={null}
from lager import Net, NetType

dbg = Net.get('DUT', type=NetType.Debug)
```

**Parameters:**

| Parameter | Type      | Description             |
| --------- | --------- | ----------------------- |
| `name`    | `str`     | Name of the debug net   |
| `type`    | `NetType` | Must be `NetType.Debug` |

**Returns:** Debug Net instance

**Note:** The debug net must be configured with the target device name stored in the `channel` field (e.g., 'NRF52840\_XXAA', 'R7FA0E107').

### `connect(speed=None, transport=None, *, force=False, ignore_if_connected=False, script=None, openocd_config=None, jlink_script=None, halt=False)`

Connect to the target device (start the gdbserver for this probe). The backend
(J-Link or OpenOCD) is chosen automatically from the probe.

```python theme={null}
# Connect with default settings (4000 kHz, SWD)
dbg.connect()

# Connect with custom speed
dbg.connect(speed='adaptive')

# Connect with JTAG
dbg.connect(transport='JTAG')

# Connect with a per-connect J-Link script override (box path or base64 blob)
dbg.connect(script='/home/lagerdata/probes/my_target.JLinkScript')
```

**Parameters:**

| Parameter             | Type   | Default  | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| --------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `speed`               | `str`  | `'4000'` | Interface speed in kHz (e.g., '4000') or 'adaptive'                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `transport`           | `str`  | `'SWD'`  | Transport protocol ('SWD' or 'JTAG')                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `script`              | `str`  | `None`   | A per-connect debug-script override, on **either** backend: a path on the box **or** a base64-encoded blob. Copied to this net's path for whichever format it turns out to be, so subsequent `flash()` / `reset()` / `read_memory()` calls pick it up immediately. The format is classified by extension then content; a script for the *other* backend, or a blob that cannot be classified, raises `ValueError` rather than running the target under an attach sequence nobody asked for. |
| `force`               | `bool` | `False`  | Stop any gdbserver already running for this probe and start fresh.                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `ignore_if_connected` | `bool` | `False`  | If a gdbserver is already running for this probe, return its status without touching it.                                                                                                                                                                                                                                                                                                                                                                                                    |
| `openocd_config`      | `str`  | `None`   | The unambiguous OpenOCD form of `script`, skipping format detection. Wins over `script` when both are given. Must be a **complete** cfg, not a fragment — see the warning below.                                                                                                                                                                                                                                                                                                            |
| `jlink_script`        | `str`  | `None`   | The unambiguous J-Link form of `script`, skipping format detection. Wins over `script` when both are given. Use this for a base64 blob: a blob has no filename to classify, and the two commonest J-Link forms (`void InitTarget(void)` and `JLINK_ExecCommand`) are not among the content markers, so such a blob raises rather than routing on a guess.                                                                                                                                   |
| `halt`                | `bool` | `False`  | **OpenOCD only.** Run `reset halt` once the daemon is up. Note this is a *reset* followed by a halt — see [`halt()`](#halt) for stopping the core where it is.                                                                                                                                                                                                                                                                                                                              |

**Returns:** `dict` - Status dictionary with connection information

<Note>
  A script override takes effect only when the gdbserver relaunches. If a server is
  already up, pass `force=True` to restart it with the new script.
  `ignore_if_connected=True` returns early and does not relaunch, but it still
  repoints the file for later operations. Invalid input means a missing path that
  is not valid base64, or an empty string. The net ignores such input silently, and
  the script that it materialized earlier stays in effect.

  The override is scoped to **this net and this session**. Lager writes it to a
  per-net path rather than to the box-wide config that the net record and the HTTP
  debug service share. `disconnect()` clears the override. Two debug nets that
  connect with different scripts therefore no longer clobber one another.
</Note>

<Warning>
  An `openocd_config` override must be a **complete** cfg, not a fragment. Lager
  applies it after the interface and target configs. The launch line still carries
  lager's own `-c 'ftdi channel N'` whenever the net has a probe channel. OpenOCD
  does not recognize that command unless a cfg first selects the ftdi adapter
  driver. A cfg holding only, say, `adapter speed 1000` dies at startup with
  `invalid command name "ftdi"`.
</Warning>

### `disconnect()`

Disconnect from the target device.

```python theme={null}
dbg.disconnect()
```

**Returns:** `dict` - Status dictionary

### `reset(halt=False)`

Reset the device.

```python theme={null}
# Reset and continue execution
output = dbg.reset(halt=False)
print(output)

# Reset and halt for debugging
output = dbg.reset(halt=True)
print(output)
```

**Parameters:**

| Parameter | Type   | Default | Description          |
| --------- | ------ | ------- | -------------------- |
| `halt`    | `bool` | `False` | Halt CPU after reset |

**Returns:** `str` - Combined output from reset operation

**Self-heal (both backends):** Right after a `flash()` there is a short window
where the debug server is not reachable yet. On J-Link the cause is that the
restarted GDB server's PID is not observable yet. On OpenOCD the cause is a
transient daemon or RPC fault. A bare call raises inside that window. `reset()`
retries with bounded backoff on both the J-Link and OpenOCD backends, and it
restarts a server only when one is genuinely down. It never tears down a server
that is already up, so an attached RTT session stays intact. Callers no longer
need their own retry wrappers.

**DA1469x exception:** On DA1469x, `flash()` deliberately leaves the server down.
The flash ends in a software reset rather than a server restart, and the
documented flow is an explicit, halt-aware reconnect. So the self-heal still
retries on DA1469x, but it never auto-starts a server. A server that is genuinely
down still surfaces the original error, exactly as before. The self-heal never
brings a server up unhalted, because an unhalted server can return garbage
QSPI-XIP reads.

### `halt()`

Halt the target where it is, without a reset. **OpenOCD only.**

```python theme={null}
# Program, then stop the core on the image just written
dbg.flash('build/firmware.elf')
dbg.connect(force=True)
dbg.halt()
```

**Returns:** `str` - Combined output from the halt operation

This is **not** the same as `reset(halt=True)`. That runs OpenOCD's `reset halt`,
which pulses nRESET and re-enters through the reset vector. `halt()` issues a bare
`halt` instead, so the core stops where it is, and nRESET is never touched.

The distinction matters on parts that execute in place out of QSPI. After you
program such a part, `reset(halt=True)` re-runs the bootloader rather than
stopping on the image you just wrote. An unhalted re-attach risks garbage XIP
reads. Halting in place is how you attach after a program without disturbing
the image.

<Note>
  J-Link has no standalone halt-in-place primitive, because `reset_device` and
  `gdb_reset` both reset first. On that backend `halt()` raises, and the error
  names the halt-first `.JLinkScript` as the supported route.
</Note>

### `flash(firmware_path, flash_address=None)`

Flash firmware to the device.

```python theme={null}
# Flash a hex file
output = dbg.flash('/path/to/firmware.hex')
print(output)

# Flash a binary file -- a .bin carries no address, so pass the target's flash base
output = dbg.flash('/path/to/firmware.bin', 0x08000000)
print(output)

# Flash an ELF file
output = dbg.flash('/path/to/firmware.elf')
print(output)
```

**Parameters:**

| Parameter       | Type  | Description                                                                                        |
| --------------- | ----- | -------------------------------------------------------------------------------------------------- |
| `firmware_path` | `str` | Path to firmware file (.hex, .bin, or .elf)                                                        |
| `flash_address` | `int` | Load address for a `.bin`. Required for `.bin`, ignored for `.hex` / `.elf`, which carry their own |

**Returns:** `str` - Combined output from flash operation

**Note:** A `.bin` has no embedded address, so `flash()` raises without
`flash_address` rather than defaulting to `0x0`. Pass the target's flash base:
STM32 `0x08000000`, nRF52 `0x00000000`, DA1469x QSPI `0x16000000`.

**DA1469x on OpenOCD:** mainline OpenOCD has no flash driver for the DA1469x's
external QSPI. On that family, `flash()` and `erase()` drive the RAM-resident
flash\_loader instead, the same path `lager debug <net> flash` takes. Pass the
absolute XIP address (`0x16000000`), exactly as on J-Link. The loader artefacts
live on the box under
`/home/www-data/customer-binaries/openocd/flash-loaders/da1469x/`. A missing
loader raises rather than falling back to OpenOCD's `program`, which cannot
reach QSPI.

### `erase()`

Perform full chip erase. This erases ALL flash memory including protection settings.

On a DA1469x behind an OpenOCD probe this is the flash\_loader's range erase of
the first 1 MiB of QSPI, matching the J-Link path.

```python theme={null}
# Full chip erase
output = dbg.erase()
print(output)
```

**Returns:** `str` - Combined output from erase operation

### `read_memory(address, length)`

Read memory from the target device.

```python theme={null}
# Read 256 bytes starting at address 0x20000000
data = dbg.read_memory(0x20000000, 256)
print(f"Read {len(data)} bytes")
print(data.hex())
```

**Parameters:**

| Parameter | Type  | Description             |
| --------- | ----- | ----------------------- |
| `address` | `int` | Starting memory address |
| `length`  | `int` | Number of bytes to read |

**Returns:** `bytes` - Memory data

**Self-heal:** like `reset()`, `read_memory()` retries with bounded backoff across
the brief post-`flash()` settling window on both backends. It reconnects only when
no server is up, and it never disturbs a live session. `erase()` behaves the same
way. The same DA1469x exception applies. No server is auto-started, so a
post-flash DA1469x read raises clearly rather than returning unhalted-XIP garbage.

### `status()`

Get the current connection status.

```python theme={null}
status = dbg.status()
print(f"GDB server running: {status.get('running', False)}")
```

**Returns:** `dict` - `running` (bool), `pid` and `backend`.

This reports whether a gdbserver process is up for the probe. It is not a
statement about the target: a server can outlive the part it was attached to.
The CLI's `lager debug <net> status` reports both states separately.

### `session(speed=None, transport=None, connect=True, ignore_if_connected=True, disconnect_on_exit=True)`

Scoped debug session. It connects on entry and guarantees teardown on exit. The
safe flash → attach-RTT → reset ordering is therefore encoded once, rather than
rediscovered in every script. The `with` target is the net itself, so the full
surface (`flash`, `rtt_defmt`, `reset`, `read_memory`, …) is available inside the
block.

```python theme={null}
with dbg.session() as s:
    s.flash('build/app.hex')                 # built-in stop->flash->restart handoff
    with s.rtt_defmt(elf='build/app.elf') as logs:
        s.reset(halt=False)                  # reader re-attaches across the reset blip
        for line in logs:
            if 'boot ok' in line:
                break
# GDB server is torn down here (disconnect_on_exit=True)
```

**Parameters:**

| Parameter             | Type            | Default | Description                                                                               |
| --------------------- | --------------- | ------- | ----------------------------------------------------------------------------------------- |
| `speed`               | `str` or `None` | `None`  | Forwarded to `connect()`                                                                  |
| `transport`           | `str` or `None` | `None`  | Forwarded to `connect()`                                                                  |
| `connect`             | `bool`          | `True`  | Connect on entry. Set `False` to attach to a server you manage yourself                   |
| `ignore_if_connected` | `bool`          | `True`  | Reuse a running server instead of raising (regression-safe: never restarts a live server) |
| `disconnect_on_exit`  | `bool`          | `True`  | Stop the GDB server on exit. Set `False` to leave it running for later commands           |

**Returns:** a context manager yielding the debug net.

**Why it pairs with RTT:** the in-process RTT reader is reconnect-aware (see
below). A `flash()` or `reset()` inside the session can bounce the GDB server.
Such a bounce does not kill a log stream that you opened in the same block.

### `rtt(channel=0, search_addr=None, search_size=None, chunk_size=None)`

Create an RTT (Real-Time Transfer) session for bidirectional communication with the target device.

```python theme={null}
# Open RTT session on default channel (0)
with dbg.rtt() as rtt:
    # Read debug output
    data = rtt.read_some(timeout=1.0)
    if data:
        print(data.decode('utf-8'))

    # Send commands to device
    rtt.write(b'test_command\n')

# Use different RTT channel
with dbg.rtt(channel=1) as rtt:
    data = rtt.read_some(timeout=2.0)

# Specify RAM search region for RTT control block
with dbg.rtt(search_addr=0x20000000, search_size=0x10000) as rtt:
    data = rtt.read_some(timeout=1.0)
```

**Parameters:**

| Parameter     | Type            | Default | Description                                    |
| ------------- | --------------- | ------- | ---------------------------------------------- |
| `channel`     | `int`           | `0`     | RTT channel number (typically 0-15)            |
| `search_addr` | `int` or `None` | `None`  | RAM start address for RTT control block search |
| `search_size` | `int` or `None` | `None`  | Size of RAM region to search in bytes          |
| `chunk_size`  | `int` or `None` | `None`  | Size of each read chunk in bytes               |

**Returns:** RTT context manager with methods:

* `read_some(timeout)` - Read available data with timeout (returns bytes or None)
* `write(data)` - Write data to target (accepts bytes or str)

**Note:** Debug connection must be active before using RTT. Call `connect()` first.

**Reconnect-aware (both backends):** the RTT reader re-attaches by itself when the
GDB server or daemon restarts under it.

* **J-Link.** A `flash()` briefly frees the probe's USB and restarts the GDB
  server on the *same* ports, which drops the RTT socket. A `reset()` does the
  same through its Commander grab. The reader re-attaches to the same RTT telnet
  port instead of going silent. A long-lived `read_some()` or `rtt_defmt()` loop
  therefore keeps producing across a flash.

* **OpenOCD.** The daemon stays up across an ordinary flash, so the socket rarely
  drops. If it does drop, from a daemon force-restart or an rtt-server bounce, the
  reader re-runs `rtt setup` and `rtt server start` and re-attaches.

Reconnection is bounded on both backends, and the default is 30 s. The reader
re-attaches only once the server or daemon is back up, and it never *starts* one.
A flash that deliberately leaves the server down, such as on DA1469x, therefore
cannot make the reader spin forever. The reader also cannot disturb a DA1469x that
you left down on purpose. Pass `reconnect=False` for the legacy one-shot behavior.

## Examples

### Flash Firmware and Reset

```python theme={null}
from lager import Net, NetType

# Get debug net
dbg = Net.get('DUT', type=NetType.Debug)

# Connect to target
status = dbg.connect()
print(f"Connected: {status}")

# Flash firmware
output = dbg.flash('/etc/lager/firmware/app.hex')
print(output)

# Reset and run
output = dbg.reset(halt=False)
print(output)

# Disconnect
dbg.disconnect()
```

### Chip Erase Before Programming

```python theme={null}
from lager import Net, NetType

# Get debug net
dbg = Net.get('DUT', type=NetType.Debug)

# Connect to target
status = dbg.connect()
print(f"Connected: {status}")

# Erase entire chip first (ensures clean state)
print("Erasing chip...")
output = dbg.erase()
print(output)

# Flash new firmware
output = dbg.flash('/etc/lager/firmware/app.hex')
print(output)

# Disconnect
dbg.disconnect()
```

### Read Memory

```python theme={null}
from lager import Net, NetType

dbg = Net.get('DUT', type=NetType.Debug)

# Connect to target
dbg.connect()

# Read 256 bytes from RAM
data = dbg.read_memory(0x20000000, 256)
print(f"Read {len(data)} bytes")
print(data.hex())

# Disconnect
dbg.disconnect()
```

## CLI Commands (Recommended)

For most use cases, the CLI provides a simpler interface:

```bash theme={null}
# Start GDB server (connect to target)
lager debug <net> gdbserver --box <box-name>

# Flash firmware
lager debug <net> flash --hex firmware.hex --box <box-name>

# Reset device
lager debug <net> reset --box <box-name>

# Erase flash
lager debug <net> erase --box <box-name>

# Read memory
lager debug <net> memrd 0x20000000 256 --box <box-name>

# Disconnect
lager debug <net> disconnect --box <box-name>

# Check status
lager debug <net> status --box <box-name>
```

See the [CLI Debug Reference](/source/reference/cli/debug) for full CLI documentation.

## RTT Streaming

SEGGER Real-Time Transfer (RTT) enables high-speed bidirectional communication with embedded devices during debugging (faster than UART, no timing impact).

```python theme={null}
from lager import Net, NetType

# Connect debug probe first
debug = Net.get('debug1', type=NetType.Debug)
debug.connect()

# Open RTT session for reading debug output
with debug.rtt() as rtt:
    # Read debug output from MCU
    data = rtt.read_some(timeout=1.0)
    if data:
        print(data.decode('utf-8'))

    # Can also write commands to MCU
    rtt.write(b'start_test\n')
```

**RTT Methods:**

| Method               | Description                                              |
| -------------------- | -------------------------------------------------------- |
| `read_some(timeout)` | Read available data with timeout (returns bytes or None) |
| `write(data)`        | Write data to RTT (accepts bytes or str)                 |

<Warning>
  `rtt().read_some()` returns **raw, still-encoded** bytes. Firmware that logs with [defmt](https://defmt.ferrous-systems.com/) (the de-facto standard for embedded Rust) emits a compressed binary format — calling `.decode('utf-8')` on it yields garbage. For defmt firmware, use `rtt_defmt()` below or the CLI pipe, both of which decode through `defmt-print`.
</Warning>

### Decoding defmt logs with `rtt_defmt()`

`rtt_defmt(elf, channel=0)` opens an RTT session and pipes it through `defmt-print` (preinstalled on the Lager Box), yielding **decoded log lines** instead of raw bytes. The `elf` must be the exact firmware flashed on the target — defmt needs its symbol metadata to decode.

```python theme={null}
from lager import Net, NetType
import time

dbg = Net.get('debug1', type=NetType.Debug)
dbg.connect(ignore_if_connected=True)  # reuse a running gdbserver if one is up
dbg.flash('build/app.elf')   # skip if already flashed; same ELF you decode against
dbg.reset()                  # restart to capture boot logs

# Capture a bounded ~10s window of decoded logs
with dbg.rtt_defmt(elf='build/app.elf', channel=0) as logs:
    deadline = time.time() + 10
    while time.time() < deadline:
        line = logs.read_line(timeout=1.0)   # decoded str, or None
        if line:
            print(line)
            assert 'panic' not in line.lower(), f"firmware panicked: {line}"
```

`rtt_defmt()` returns a context manager exposing:

| Method                          | Description                                                       |
| ------------------------------- | ----------------------------------------------------------------- |
| `read_line(timeout=None)`       | Next decoded log line as `str`, or `None` on timeout / stream end |
| iteration (`for line in logs:`) | Yield decoded lines until the stream ends                         |
| `write(data)`                   | Send bytes or `str` to the target's RTT down-channel              |

Like the CLI pipe, the RTT stream never ends on its own. Bound your read loop with
a time budget or a line count, then exit the `with` block.

#### Driving the firmware while decoding its logs

`write()` makes a decoding session bi-directional, so a script can send a command
and assert on the decoded response. Decoding is one-way — `defmt-print` only sees
the up-channel — so writes bypass it and go straight to the target. This is the
only way to do both at once. The RTT telnet port accepts a single client, so you
cannot open a raw `rtt()` alongside a `rtt_defmt()`.

```python theme={null}
with dbg.rtt_defmt(elf='build/app.elf') as logs:
    logs.write(b'self_test\n')            # command the firmware
    deadline = time.time() + 5
    while time.time() < deadline:
        line = logs.read_line(timeout=1.0)
        if line and 'self_test: pass' in line:
            break
    else:
        raise AssertionError('firmware never reported a passing self-test')
```

<Warning>
  This requires the firmware to declare an RTT **down** buffer on the channel you
  opened. `defmt-rtt` alone only sets up the up buffer. With no down buffer, the
  target silently discards whatever you write. That looks like a host-side failure,
  and it is not one.
</Warning>

**Parameters:**

| Parameter         | Type            | Default  | Description                                                                                                      |
| ----------------- | --------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `elf`             | `str`           | required | Path to the firmware ELF flashed on the DUT (relative paths resolve against the script's working dir on the box) |
| `channel`         | `int`           | `0`      | RTT channel number                                                                                               |
| `defmt_print_bin` | `str` or `None` | `None`   | Override the `defmt-print` binary (path or name on PATH)                                                         |
| `read_timeout`    | `float`         | `0.5`    | Poll interval (seconds) for the internal RTT read loop                                                           |

**CLI Alternative:** For interactive tailing, pipe the CLI directly: `lager debug <net> gdbserver --box <box> --rtt 2>/dev/null | defmt-print -e build/app.elf`. See the [CLI Debug Reference](/source/reference/cli/debug#decoding-defmt-logs). Use `rtt_defmt()` when you need to assert on log content inside a test script; use the pipe when you just want to watch logs.

## Supported Devices

J-Link supports a wide range of ARM Cortex-M and other microcontrollers. Common device names:

| Manufacturer | Device Name        | Description              |
| ------------ | ------------------ | ------------------------ |
| Nordic       | `NRF52840_XXAA`    | nRF52840                 |
| Nordic       | `NRF52833_XXAA`    | nRF52833                 |
| Nordic       | `NRF5340_XXAA_APP` | nRF5340 Application Core |
| Renesas      | `R7FA0E107`        | RA0E1 Series             |
| Renesas      | `R7FA2L1`          | RA2L1 Series             |
| STMicro      | `STM32F103C8`      | STM32F1 Series           |
| STMicro      | `STM32F407VG`      | STM32F4 Series           |
| STMicro      | `STM32L476RG`      | STM32L4 Series           |

For a complete list, see [SEGGER's supported devices](https://www.segger.com/supported-devices/jlink/).

## Supported Hardware

| Debug Probe | Features                              |
| ----------- | ------------------------------------- |
| J-Link      | JTAG/SWD debugging, flash programming |
| CMSIS-DAP   | SWD debugging (via pyOCD backend)     |
| ST-Link     | SWD debugging (via pyOCD backend)     |

## Notes

* Debug nets must be configured with the target device name in the `channel` field
* The CLI (`lager debug`) is recommended for most use cases
* Python Net API is intended for advanced automation scripts running on the Lager Box
* Always call `disconnect()` when finished to release the debug probe
* Use `erase()` to perform a full chip erase and clear protection settings
* RTT requires an active debug connection (see RTT Streaming section above)
