Skip to main content
Control embedded debug operations including device connection, firmware flashing, reset, and memory access. A J-Link probe uses the J-Link backend. Other supported probes use the OpenOCD backend.

Import

Methods

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

Method Reference

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

Get a debug net by name.
Parameters: 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 to the target device (start the gdbserver for this probe). The backend (J-Link or OpenOCD) is chosen automatically from the probe.
Parameters: connect() classifies script before it applies openocd_config or jlink_script. If you give script and one of those two, the explicit parameter wins only when script is for the same backend. A script for the other backend raises ValueError first. Returns: dict - Status dictionary with connection information Raises: If a gdbserver already runs for the probe and you pass neither force nor ignore_if_connected, connect() raises. The error is RuntimeError on OpenOCD and JLinkAlreadyRunningError (from lager.debug) on J-Link.
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. On J-Link, it still repoints the script for later operations. On OpenOCD, the override cfg stays in effect for each relaunch, including a self-heal relaunch, until disconnect(). 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.
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".

disconnect()

Disconnect from the target device.
Returns: dict - Status dictionary

reset(halt=False)

Reset the device.
Parameters: 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.
On OpenOCD, flash() needs a running daemon. Without one, it raises RuntimeError and tells you to call connect() first. Returns: str - Combined output from the halt operation Like reset(), halt() retries across the short window after flash() when the server is not reachable yet. 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.
J-Link has no standalone halt-in-place primitive, because reset_device and gdb_reset both reset first. On that backend halt() raises NotImplementedError, and the error names the halt-first .JLinkScript as the supported route.

flash(firmware_path, flash_address=None)

Flash firmware to the device.
Parameters: Returns: str - Combined output from flash operation Raises: RuntimeError when the probe reports that the flash failed, on both backends. On J-Link, that is a flash that programmed nothing or a failed read-back compare, the same lines that fail lager debug <net> flash. The message holds the failing line and the output. 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. A missing loader raises rather than falling back to OpenOCD’s program, which cannot reach QSPI.
  • An address outside 0x16000000–0x17FFFFFF raises before the box touches the probe. The error says flash address 0x... is outside the DA1469x QSPI XIP window.
  • Flash a .bin. The loader writes the bytes of the file unchanged, so it does not decode a .hex or .elf file.
  • The loader needs flash_loader.elf and flash_loader.elf.bin in /home/www-data/customer-binaries/openocd/flash-loaders/da1469x/ in the container. On the box host, that directory is ~/third_party/customer-binaries/openocd/flash-loaders/da1469x/. LAGER_FLASH_LOADERS_DIR replaces the parent directory.
  • OpenOCD has no built-in target config for this family, so the net needs an openocd_config.
  • A dropped debug read does not stop the loader. The loader retries the read until the deadline of its step.
  • A failure raises with the last progress line of the loader. If the erase stage already ran, the message says that the board can be blank. Flash it again.

erase(start=None, length=None)

Erase the flash of the target. With no arguments, on most targets this is a full chip erase, which erases ALL flash memory including protection settings. On a DA1469x, erase() erases only the first 1 MiB of the external QSPI, from 0x16000000. This applies on both backends. The OpenOCD backend uses the flash_loader. The J-Link backend uses a range erase, and a LAGER_ERASE_RANGE line in the JLinkScript of the net sets a different range. start and length set the range instead, on either backend and any target. Pass both or neither. start is an absolute address, and on a DA1469x the XIP address, exactly as flash() takes it. On a DA1469x the range must lie inside 0x16000000–0x17FFFFFF, and it replaces both the 1 MiB default and the LAGER_ERASE_RANGE line. On any other target the range replaces the full chip erase. A range the target cannot erase raises ValueError before the box touches the probe. When a range applies, the first line of the output names it, for example Erasing 0x16000000-0x161FFFFF (2 MiB).
Parameters:
  • start (int, optional): First address to erase, given together with length
  • length (int, optional): Number of bytes to erase from start
Returns: str - Combined output from erase operation Raises: RuntimeError when J-Link erased nothing: it did not attach, or it did not confirm the erase. The message holds the failing line and the output. The self-heal does not run the erase again. On OpenOCD, a failed erase raises as before.

read_memory(address, length)

Read memory from the target device.
Parameters: 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.
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.
Parameters: 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.
Parameters: 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 limit 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. rtt() and rtt_defmt() take no reconnect argument, so reconnection is always on.

Examples

Flash Firmware and Reset

Chip Erase Before Programming

Read Memory

For most use cases, the CLI provides a simpler interface:
See the CLI Debug Reference 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).
RTT Methods:
rtt().read_some() returns raw, still-encoded bytes. Firmware that logs with defmt (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.

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.
rtt_defmt() returns a context manager exposing: 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().
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.
Parameters: 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. 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: For a complete list, see SEGGER’s supported devices.

Supported Hardware

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() for a full chip erase that also clears protection settings (on DA1469x, a 1 MiB QSPI range). erase(start, length) erases one range.
  • RTT requires an active debug connection (see RTT Streaming section above)