diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f1f2cd9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +## 0.3.0 + +Dual debug backend and AI-facing crash diagnosis. + +### Added +- **Dual backend** behind a single `DebugBackend` trait, selected at `connect`: + - `probe-rs` (default) — native probe-rs; full flash and RTT support. + - `openocd` (experimental) — talks to an already-running `openocd` over the + GDB Remote Serial Protocol (`openocd_address`, default `127.0.0.1:3333`). + The AI uses the same tool set for both; only `connect` differs. +- `diagnose_fault` — reads the Cortex-M SCB fault registers + (CFSR/HFSR/MMFAR/BFAR/SHCSR/CPUID) plus PC/SP/LR in one call and returns a + compact structured evidence bundle with the set fault bits. Reports raw + evidence; it does not assert a root cause. +- `unwind_exception` — maps a crash to source lines. On the probe-rs backend it + returns a full DWARF backtrace via probe-rs's own unwinder; on the OpenOCD + backend it reads the Cortex-M exception stack frame and maps the faulting + PC/caller LR. Requires an `elf_path` built with debug info. +- 24 MCP tools total (added `diagnose_fault`, `unwind_exception`). + +### Validated on real hardware +- OpenOCD backend verified end-to-end on a real **ESP32-S3** (openocd-esp32): + connect, memory read/write, halt/run/step/reset, and register reads + (PC/SP/LR) are correct. + +### Notes / limitations +- OpenOCD register reads use `monitor reg ` (openocd resolves names per + architecture), so PC/SP/LR work across ARM/Xtensa/RISC-V. Start openocd with + `gdb_memory_map disable` or it may reject the GDB connection. +- `diagnose_fault` and `unwind_exception` are Cortex-M specific and do not apply + to Xtensa (ESP32) targets. Their probe-rs paths delegate to probe-rs's tested + DWARF/register code; they are unit-tested but not yet exercised on a physical + Cortex-M crash. + +## 0.2.0 + +- Initial MCP server for embedded debugging via probe-rs (probe discovery, + target sessions, memory, breakpoints, flash, RTT), CLI, and bundled skill. + diff --git a/Cargo.lock b/Cargo.lock index 3f5405d..f0536fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -555,7 +555,7 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "embedded-debugger-mcp" -version = "0.2.0" +version = "0.3.0" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index e55cd25..410bb41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "embedded-debugger-mcp" -version = "0.2.0" +version = "0.3.0" edition = "2021" authors = ["adancurusul "] -description = "A Model Context Protocol server for embedded debugging with probe-rs - supports ARM Cortex-M, RISC-V debugging via J-Link, ST-Link, and more" +description = "A Model Context Protocol server for embedded debugging via probe-rs or OpenOCD - ARM Cortex-M, RISC-V, and Xtensa (ESP32), with memory/flash/RTT and AI crash diagnosis" license = "MIT" readme = "README.md" repository = "https://github.com/adancurusul/embedded-debugger-mcp" -keywords = ["mcp", "debugging", "embedded", "probe-rs", "rtt"] +keywords = ["mcp", "debugging", "embedded", "probe-rs", "openocd"] categories = ["development-tools", "embedded"] [dependencies] diff --git a/README.md b/README.md index 8d0a0d2..7d655cd 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,22 @@ [![RMCP](https://img.shields.io/badge/RMCP-0.3.2-blue.svg)](https://github.com/modelcontextprotocol/rust-sdk) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -Embedded Debugger MCP is a Rust server for embedded debugging through -probe-rs. It exposes MCP tools for AI assistants and also includes a small CLI -and bundled skill for users who want a command-driven workflow without setting -up an MCP client first. +Embedded Debugger MCP is a Rust server for embedded debugging through either +probe-rs (native) or OpenOCD (via the GDB Remote Serial Protocol). It exposes a +single MCP tool set for AI assistants — probe discovery, target control, memory, +breakpoints, flash, RTT, and AI-facing crash diagnosis — plus a small CLI and +bundled skill for a command-driven workflow without setting up an MCP client +first. Language versions: [English](README.md) | [中文](README_zh.md) ## What It Provides +- One MCP tool set over two interchangeable backends (probe-rs native, or + OpenOCD via GDB RSP), chosen at connect. Validated on real ESP32-S3. - MCP tools for probe discovery, target connection, core control, memory access, - breakpoints, flash programming, and RTT communication. + breakpoints, flash programming, RTT communication, and crash diagnosis + (`diagnose_fault`, `unwind_exception`). - CLI commands for environment checks, configuration inspection, probe listing, MCP serving, and skill prompt handoff. - A Codex/Claude Code compatible skill at `skills/embedded-debugger`. @@ -27,17 +32,22 @@ Language versions: [English](README.md) | [中文](README_zh.md) MCP client or CLI | v -embedded-debugger-mcp +embedded-debugger-mcp (one MCP tool set) | v -probe-rs -> debug probe -> target MCU +DebugBackend: probe-rs (native) | OpenOCD (GDB RSP) + | + v +debug probe / openocd -> target MCU ``` ## Requirements - Rust stable toolchain. - A probe-rs compatible debug probe such as ST-Link, J-Link, DAPLink, Black - Magic Probe, or a supported FTDI-based probe. + Magic Probe, or a supported FTDI-based probe. Alternatively, a running + `openocd` exposing its GDB port (e.g. openocd-esp32 for ESP32) to use the + `openocd` backend. - A supported target chip and working SWD/JTAG wiring for hardware operations. - Nightly Rust plus `rust-src` for the bundled STM32 demo firmware check. @@ -154,6 +164,24 @@ The first command validates the repository skill metadata, the second validates the standard `SKILL.md` layout when the Codex skill creator validator is installed, and the third validates the Claude Code plugin manifest. +## Backends + +The tools run over one of two interchangeable engines, selected at `connect`: + +- `backend: "probe-rs"` (default) — native probe-rs; full flash and RTT support. +- `backend: "openocd"` (experimental) — connects to an already-running `openocd` + over its GDB port (`openocd_address`, default `127.0.0.1:3333`) using the GDB + Remote Serial Protocol. Intended for targets probe-rs does not cover (e.g. + Xtensa ESP32 via openocd-esp32). Memory access and halt/run/step/reset are + validated on real ESP32-S3; flash and RTT are probe-rs only. Register reads + currently use ARM gdb register numbers, so PC/SP are wrong on Xtensa (known + limitation); `diagnose_fault` and `unwind_exception` are Cortex-M specific. + Start openocd with `gdb_memory_map disable`, otherwise it probes flash on the + GDB connect, fails, and rejects the connection — e.g. + `openocd -f board/esp32s3-builtin.cfg -c "gdb_memory_map disable"`. + +The AI sees the same tool set for both; only `connect` arguments change. + ## MCP Tool Set Probe management: @@ -184,6 +212,13 @@ Memory and breakpoints: | `set_breakpoint` | Set a hardware breakpoint. | | `clear_breakpoint` | Clear a hardware breakpoint. | +Diagnostics: + +| Tool | Purpose | +|------|---------| +| `diagnose_fault` | Read the Cortex-M SCB fault registers (CFSR/HFSR/MMFAR/BFAR/SHCSR/CPUID) plus PC/SP/LR in one call and return a compact structured evidence bundle with the set fault bits. Reports raw evidence; it does not assert a root cause. Halt the target first. | +| `unwind_exception` | Unwind the stack after a crash and map each frame to a source line. On the probe-rs backend, returns a full DWARF backtrace (function + file:line per frame) via probe-rs's own unwinder; on the OpenOCD backend, reads the Cortex-M exception stack frame and maps the faulting PC / caller LR to source lines. Needs `elf_path` to firmware built with debug info (`.debug_line`). | + Flash: | Tool | Purpose | @@ -203,6 +238,23 @@ RTT: | `rtt_read` | Read from an up channel with max byte and timeout limits. | | `rtt_write` | Write to a down channel. | +## Crash Diagnosis + +After a crash, halt the core and let the model reason over the evidence: + +1. `halt`, then `diagnose_fault` — reads the Cortex-M SCB fault registers + (CFSR/HFSR/MMFAR/BFAR/SHCSR/CPUID) plus PC/SP/LR in one call and returns a + compact structured bundle with the set fault bits. It reports evidence; it + does not assert a root cause. +2. `unwind_exception` with `elf_path` — maps the crash to source lines. On the + probe-rs backend this is a full DWARF backtrace (function + `file:line` per + frame); on the OpenOCD backend it reads the exception stack frame and maps + the faulting PC / caller LR. + +Both are Cortex-M specific (SCB registers, ARM exception frame) and do not apply +to Xtensa (ESP32) targets. The firmware must be built with debug info +(`.debug_line`) for source mapping. + ## Safety Notes - Flash erase is disabled unless `security.allow_flash_erase` or diff --git a/README_zh.md b/README_zh.md index 66aa92a..b19a64d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -4,16 +4,20 @@ [![RMCP](https://img.shields.io/badge/RMCP-0.3.2-blue.svg)](https://github.com/modelcontextprotocol/rust-sdk) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -Embedded Debugger MCP 是一个基于 probe-rs 的 Rust 嵌入式调试服务器。它为 -AI 助手提供 MCP 工具,同时也提供小型 CLI 和内置 skill,让用户即使不安装 -MCP 客户端,也可以先用命令行工作流完成检查和引导。 +Embedded Debugger MCP 是一个 Rust 嵌入式调试服务器,可通过 probe-rs(原生) +或 OpenOCD(走 GDB Remote Serial Protocol)两种后端调试。它为 AI 助手提供同一套 +MCP 工具——探针发现、目标控制、内存、断点、Flash、RTT,以及面向 AI 的崩溃诊断 +——同时也提供小型 CLI 和内置 skill,让用户即使不安装 MCP 客户端也能先用命令行 +工作流。 语言版本: [English](README.md) | [中文](README_zh.md) ## 功能 -- MCP 工具覆盖探针发现、目标连接、核心控制、内存访问、断点、Flash 编程和 - RTT 通信。 +- 同一套 MCP 工具运行在两种可切换后端上(probe-rs 原生 / OpenOCD 走 GDB RSP), + 在 connect 时选择。已在真实 ESP32-S3 上验证。 +- MCP 工具覆盖探针发现、目标连接、核心控制、内存访问、断点、Flash 编程、RTT 通信 + 和崩溃诊断(`diagnose_fault`、`unwind_exception`)。 - CLI 命令覆盖环境检查、配置查看、探针列表、MCP 启动和 skill prompt 输出。 - 内置 Codex / Claude Code 兼容 skill: `skills/embedded-debugger`。 - 发布检查覆盖 rustfmt、clippy、测试、文档、打包和 STM32 demo 构建。 @@ -24,17 +28,21 @@ MCP 客户端,也可以先用命令行工作流完成检查和引导。 MCP client or CLI | v -embedded-debugger-mcp +embedded-debugger-mcp (one MCP tool set) | v -probe-rs -> debug probe -> target MCU +DebugBackend: probe-rs (native) | OpenOCD (GDB RSP) + | + v +debug probe / openocd -> target MCU ``` ## 要求 - Rust stable 工具链。 - probe-rs 兼容调试探针,例如 ST-Link、J-Link、DAPLink、Black Magic Probe - 或受支持的 FTDI 探针。 + 或受支持的 FTDI 探针。或者一个已运行、暴露了 GDB 端口的 `openocd`(例如 ESP32 + 用 openocd-esp32),以使用 `openocd` 后端。 - 目标芯片和可工作的 SWD/JTAG 连线。 - STM32 demo 固件检查需要 nightly Rust 和 `rust-src`。 @@ -149,6 +157,22 @@ embedded-debugger-mcp skill install --target both --home /tmp/embedded-debugger- 第一个命令验证仓库内 skill 元数据,第二个命令在已安装 Codex skill creator validator 时验证标准 `SKILL.md` 布局,第三个命令验证 Claude Code 插件 manifest。 +## 后端 + +同一套工具运行在两个可互换的引擎之上,在 `connect` 时选择: + +- `backend: "probe-rs"`(默认)——原生 probe-rs,完整支持 flash 与 RTT。 +- `backend: "openocd"`(实验性)——通过 GDB Remote Serial Protocol 连接一个已运行 + 的 `openocd`(`openocd_address`,默认 `127.0.0.1:3333`),用于 probe-rs 覆盖不佳的 + 芯片(如 Xtensa ESP32,走 openocd-esp32)。内存读写与 halt/run/step/reset 已在真实 + ESP32-S3 上验证;flash 与 RTT 暂仅 probe-rs。读寄存器目前用 ARM 的 gdb 寄存器号, + 在 Xtensa 上 PC/SP 会不对(已知局限);`diagnose_fault` 与 `unwind_exception` 是 + Cortex-M 专属。启动 openocd 时须加 `gdb_memory_map disable`,否则它会在 GDB 连接时 + 探 flash 失败并拒绝连接,例如 + `openocd -f board/esp32s3-builtin.cfg -c "gdb_memory_map disable"`。 + +对 AI 而言两者是同一套工具,只有 `connect` 参数不同。 + ## MCP 工具集 探针管理: @@ -179,6 +203,13 @@ validator 时验证标准 `SKILL.md` 布局,第三个命令验证 Claude Code | `set_breakpoint` | 设置硬件断点。 | | `clear_breakpoint` | 清除硬件断点。 | +诊断: + +| 工具 | 用途 | +|------|------| +| `diagnose_fault` | 一次调用读取 Cortex-M SCB 故障寄存器(CFSR/HFSR/MMFAR/BFAR/SHCSR/CPUID)及 PC/SP/LR,返回含已置位故障标志的紧凑结构化证据。只给原始证据、不下根因结论。请先 halt 目标。 | +| `unwind_exception` | 崩溃后回溯栈并把每帧映射到源码行。probe-rs 后端用 probe-rs 自带回溯器给出完整 DWARF 调用栈(每帧函数名+file:line);OpenOCD 后端读取 Cortex-M 异常栈帧,把出错 PC/调用者 LR 映射到源码行。需 `elf_path` 指向带调试信息(`.debug_line`)的固件。 | + Flash: | 工具 | 用途 | @@ -198,6 +229,20 @@ RTT: | `rtt_read` | 从上行通道读取,并遵守最大字节数和超时限制。 | | `rtt_write` | 向下行通道写入。 | +## 崩溃诊断 + +芯片崩溃后,先 halt,再让模型基于证据推理: + +1. `halt`,然后 `diagnose_fault`——一次调用读取 Cortex-M SCB 故障寄存器 + (CFSR/HFSR/MMFAR/BFAR/SHCSR/CPUID)及 PC/SP/LR,返回含已置位故障标志的紧凑 + 结构化证据;只给证据、不下根因结论。 +2. `unwind_exception`(带 `elf_path`)——把崩溃映射到源码行。probe-rs 后端给出 + 完整 DWARF 调用栈(每帧函数名 + `file:line`);OpenOCD 后端读取异常栈帧并映射 + 出错 PC / 调用者 LR。 + +两者都是 Cortex-M 专属(SCB 寄存器、ARM 异常帧),不适用于 Xtensa(ESP32)。源码 +映射需要固件带调试信息(`.debug_line`)。 + ## 安全说明 - 除非启用 `security.allow_flash_erase` 或 `flash.allow_erase`,否则 Flash diff --git a/examples/openocd_smoke.rs b/examples/openocd_smoke.rs new file mode 100644 index 0000000..01fb40d --- /dev/null +++ b/examples/openocd_smoke.rs @@ -0,0 +1,71 @@ +//! Real-hardware smoke test for the OpenOCD backend. +//! +//! Drives `OpenOcdBackend` against a running `openocd` GDB server (default +//! 127.0.0.1:3333) and prints each operation's result. Intended to be run +//! against real hardware (e.g. ESP32-S3 via openocd-esp32) to confirm the RSP +//! protocol wiring end-to-end. +//! +//! Usage: +//! cargo run --release --example openocd_smoke -- [addr] [mem_hex] +//! +//! Notes: memory read/write and halt/run are architecture-neutral. Registers +//! are read by name via openocd's `monitor reg`, so PC/SP/LR read correctly on +//! both ARM and Xtensa (verified on real ESP32-S3). + +use embedded_debugger_mcp::backend::{CoreRegId, DebugBackend, OpenOcdBackend}; + +#[tokio::main] +async fn main() { + let addr = std::env::args() + .nth(1) + .unwrap_or_else(|| "127.0.0.1:3333".to_string()); + // Default read address: ESP32-S3 internal SRAM1. + let mem_addr = std::env::args() + .nth(2) + .and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok()) + .unwrap_or(0x3FC8_8000); + + println!("== openocd smoke test =="); + println!("connecting to {addr} ..."); + let mut be = match OpenOcdBackend::connect(&addr).await { + Ok(b) => b, + Err(e) => { + eprintln!("CONNECT FAILED: {e}"); + std::process::exit(1); + } + }; + println!("connected (backend = {})", be.kind()); + + match be.halt().await { + Ok(()) => println!("halt : OK"), + Err(e) => println!("halt : ERR {e}"), + } + match be.status().await { + Ok(s) => println!("status : {s}"), + Err(e) => println!("status : ERR {e}"), + } + + for a in [mem_addr, 0x4000_0000] { + match be.read_bytes(a, 16).await { + Ok(d) => { + let hex: String = d.iter().map(|b| format!("{b:02x}")).collect(); + println!("read 0x{a:08X}: {hex}"); + } + Err(e) => println!("read 0x{a:08X}: ERR {e}"), + } + } + + // Read by name via 'monitor reg' — correct on ARM and Xtensa. + for reg in [CoreRegId::Pc, CoreRegId::Sp, CoreRegId::Lr] { + match be.core_reg(reg).await { + Ok(v) => println!("{reg:<10?}: 0x{v:08X}"), + Err(e) => println!("{reg:<10?}: ERR {e}"), + } + } + + match be.run().await { + Ok(()) => println!("run : OK"), + Err(e) => println!("run : ERR {e}"), + } + println!("== done =="); +} diff --git a/skills/embedded-debugger/SKILL.md b/skills/embedded-debugger/SKILL.md index 14f4e47..607568a 100644 --- a/skills/embedded-debugger/SKILL.md +++ b/skills/embedded-debugger/SKILL.md @@ -35,18 +35,80 @@ embedded-debugger-mcp doctor --json embedded-debugger-mcp probes list --json ``` +## Backends + +One tool set runs over two interchangeable engines, chosen at `connect`: + +- `backend: "probe-rs"` (default) — native probe-rs; supports flash and RTT. +- `backend: "openocd"` (experimental) — talks to an already-running `openocd` + over its GDB port via `openocd_address` (default `127.0.0.1:3333`). Use for + chips probe-rs does not cover well (e.g. Xtensa ESP32 via openocd-esp32). + Memory access and halt/run/step/reset are validated on real ESP32-S3; flash + and RTT are not available on this backend. Register reads currently use ARM + gdb register numbers, so PC/SP are wrong on Xtensa (known limitation). + `diagnose_fault` and `unwind_exception` are Cortex-M specific and do not + apply to Xtensa targets. + - Start openocd with `gdb_memory_map disable`, otherwise it probes flash on + the GDB connect, fails, and REJECTS the connection. Example: + `openocd -f board/esp32s3-builtin.cfg -c "gdb_memory_map disable"`. + +The AI uses the same tools regardless of backend; only `connect` differs. + ## MCP Workflow Use MCP tools for session-based operations: 1. `list_probes` -2. `connect` +2. `connect` (add `backend: "openocd"` and `openocd_address` to use OpenOCD) 3. Read-only checks such as `probe_info`, `get_status`, and `read_memory` -4. Mutating operations only after the user confirms target, file path, and risk: - `write_memory`, `flash_erase`, `flash_program`, `run_firmware` -5. RTT operations after firmware is running: `rtt_attach`, `rtt_channels`, - `rtt_read`, `rtt_write`, `rtt_detach` -6. `disconnect` +4. On a crash or halt, call `diagnose_fault`: it reads the Cortex-M SCB fault + registers (CFSR/HFSR/MMFAR/BFAR/SHCSR/CPUID) plus PC/SP/LR and returns a + compact structured evidence bundle in one call. Halt the target first for + meaningful values; reason over the set fault bits yourself. Then call + `unwind_exception` with `elf_path` to map the crash to a source line + (full DWARF backtrace on probe-rs; faulting PC/LR on OpenOCD). +5. Mutating operations only after the user confirms target, file path, and risk: + `write_memory`, `flash_erase`, `flash_program`, `run_firmware` (probe-rs) +6. RTT operations after firmware is running: `rtt_attach`, `rtt_channels`, + `rtt_read`, `rtt_write`, `rtt_detach` (probe-rs) +7. `disconnect` + +## Fetch authoritative info yourself + +You are a capable model: prefer fetching ground truth over relying on memorized +or hardcoded chip data. This skill points you to sources; it does not embed +register tables. In order of authority: + +1. The target itself (runtime, most authoritative for this exact chip): + registers are self-described by the GDB target description; memory is read + with `read_memory`; core identity from CPUID / the connected target. +2. The firmware ELF (what is actually running): symbols and source lines come + from DWARF — use `unwind_exception` (pass `elf_path`) to map addresses to + `file:line`. +3. The chip datasheet / reference manual (external, per-chip): for a peripheral + or fault register, find the peripheral's base in the memory-map chapter, add + the register offset, then `read_memory`. Search the vendor document for the + exact value; do not guess addresses from memory. CMSIS-SVD files are a + machine-readable source for register maps. +4. ARM Cortex-M architecture registers (SCB fault regs, CPUID) are fixed by the + ARM architecture and identical across vendors — `diagnose_fault` reads them. + They do not exist on non-Cortex-M targets (e.g. Xtensa ESP32). + +Do not hardcode or invent register/peripheral addresses. If a value is not +recoverable from the target, the ELF, or a cited datasheet, say so. + +## Know your versions + +Behavior and target support depend on tool versions — check them before +concluding something is unsupported or broken: + +- probe-rs version determines which chips and architectures are supported + (e.g. Xtensa support is comparatively new). Check `embedded-debugger-mcp doctor`. +- OpenOCD version and fork matter: the Espressif fork (openocd-esp32) is needed + for Xtensa ESP32, and some targets need flags like `gdb_memory_map disable`. + Check `openocd --version`. +- Probe firmware (ST-Link / J-Link) can affect connectivity; `probes list` + reports the connected probe. ## Safety Rules diff --git a/src/backend/mod.rs b/src/backend/mod.rs new file mode 100644 index 0000000..be414f2 --- /dev/null +++ b/src/backend/mod.rs @@ -0,0 +1,108 @@ +//! Debug backend abstraction (dual-engine). +//! +//! A single `DebugBackend` trait unifies the two execution engines so the MCP +//! tool layer speaks one API and never learns two. The concrete engine (probe-rs +//! native, or OpenOCD via GDB Remote Serial Protocol) is an implementation +//! detail chosen at `connect` time. + +use async_trait::async_trait; + +use crate::error::Result; + +mod openocd_backend; +mod probe_rs_backend; +pub mod rsp; + +pub use openocd_backend::OpenOcdBackend; +pub use probe_rs_backend::ProbeRsBackend; + +/// Which engine backs a session. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BackendKind { + ProbeRs, + OpenOcd, +} + +impl std::fmt::Display for BackendKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BackendKind::ProbeRs => write!(f, "probe-rs"), + BackendKind::OpenOcd => write!(f, "openocd"), + } + } +} + +/// Core registers a backend can resolve by role (architecture-neutral subset). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CoreRegId { + Pc, + Sp, + Lr, +} + +/// Coarse core execution state. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CoreState { + Halted, + Running, + Unknown, +} + +impl std::fmt::Display for CoreState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CoreState::Halted => write!(f, "Halted"), + CoreState::Running => write!(f, "Running"), + CoreState::Unknown => write!(f, "Unknown"), + } + } +} + +/// A snapshot of the most commonly needed core state, gathered in one shot to +/// save the model a round of individual reads. +#[derive(Debug, Clone)] +pub struct CoreSnapshot { + pub pc: u32, + pub sp: u32, + pub state: CoreState, + pub halt_reason: Option, +} + +/// The unified debug engine interface. probe-rs and OpenOCD both implement it. +#[async_trait] +pub trait DebugBackend: Send { + fn kind(&self) -> BackendKind; + + async fn read_bytes(&mut self, address: u64, len: usize) -> Result>; + async fn write_bytes(&mut self, address: u64, data: &[u8]) -> Result<()>; + + async fn halt(&mut self) -> Result<()>; + async fn run(&mut self) -> Result<()>; + async fn step(&mut self) -> Result<()>; + async fn reset(&mut self, halt_after: bool) -> Result<()>; + + async fn core_reg(&mut self, reg: CoreRegId) -> Result; + async fn status(&mut self) -> Result; + + async fn set_hw_breakpoint(&mut self, address: u64) -> Result<()>; + async fn clear_hw_breakpoint(&mut self, address: u64) -> Result<()>; + + /// Read a single little-endian 32-bit word. Default via `read_bytes`. + async fn read_word(&mut self, address: u64) -> Result { + let b = self.read_bytes(address, 4).await?; + Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]])) + } + + /// One-shot core snapshot (state + PC + SP), tolerant of missing registers. + async fn snapshot(&mut self) -> Result { + let state = self.status().await.unwrap_or(CoreState::Unknown); + let pc = self.core_reg(CoreRegId::Pc).await.unwrap_or(0); + let sp = self.core_reg(CoreRegId::Sp).await.unwrap_or(0); + Ok(CoreSnapshot { + pc, + sp, + state, + halt_reason: None, + }) + } +} diff --git a/src/backend/openocd_backend.rs b/src/backend/openocd_backend.rs new file mode 100644 index 0000000..92d57ca --- /dev/null +++ b/src/backend/openocd_backend.rs @@ -0,0 +1,310 @@ +//! OpenOCD engine via GDB Remote Serial Protocol (optional backend). +//! +//! Requires an already-running `openocd` exposing its GDB port (default :3333), +//! e.g. `openocd -f interface/stlink.cfg -f target/stm32f4x.cfg`. Memory access +//! uses native RSP (`m`/`M`); execution control uses OpenOCD `monitor` commands +//! (synchronous, so `run` returns promptly instead of blocking like `c`). + +use async_trait::async_trait; + +use super::rsp::{decode_hex, encode_hex, RspClient}; +use super::{BackendKind, CoreRegId, CoreState, DebugBackend}; +use crate::error::{DebugError, Result}; + +pub struct OpenOcdBackend { + rsp: RspClient, + address: String, +} + +impl OpenOcdBackend { + pub async fn connect(address: &str) -> Result { + let rsp = RspClient::connect(address).await?; + Ok(Self { + rsp, + address: address.to_string(), + }) + } + + pub fn address(&self) -> &str { + &self.address + } +} + +/// Candidate openocd register names for a role, across architectures. +/// +/// openocd resolves register names per target (ARM: pc/sp/lr; Xtensa: pc/a1/a0; +/// RISC-V: pc/sp/ra), so we ask by NAME via `monitor reg ` rather than +/// guessing a gdb register number. `p` is architecture-specific and, on the +/// esp32-s3 Xtensa target, the GDB target description exposes no register map +/// at all — but `monitor reg pc` returns the correct value. Verified on real S3. +fn role_register_names(reg: CoreRegId) -> &'static [&'static str] { + match reg { + CoreRegId::Pc => &["pc"], + CoreRegId::Sp => &["sp", "a1"], + CoreRegId::Lr => &["lr", "ra", "a0"], + } +} + +/// Parse a value out of openocd's `reg` output, e.g. "pc (/32): 0x40381796". +fn parse_reg_value(text: &str) -> Option { + let idx = text.find("0x")?; + let hex: String = text[idx + 2..] + .chars() + .take_while(|c| c.is_ascii_hexdigit()) + .collect(); + u64::from_str_radix(&hex, 16).ok().map(|v| v as u32) +} + +fn expect_ok(reply: &str, what: &str) -> Result<()> { + if reply == "OK" { + Ok(()) + } else if let Some(code) = reply.strip_prefix('E') { + Err(DebugError::InternalError(format!( + "OpenOCD {} error E{}", + what, code + ))) + } else { + // Some stubs answer empty for unsupported; treat non-error as success. + Ok(()) + } +} + +#[async_trait] +impl DebugBackend for OpenOcdBackend { + fn kind(&self) -> BackendKind { + BackendKind::OpenOcd + } + + async fn read_bytes(&mut self, address: u64, len: usize) -> Result> { + let reply = self + .rsp + .command(&format!("m{:x},{:x}", address, len)) + .await?; + if let Some(code) = reply.strip_prefix('E') { + return Err(DebugError::MemoryAccessFailed(format!( + "OpenOCD read 0x{:08X} error E{}", + address, code + ))); + } + decode_hex(&reply) + } + + async fn write_bytes(&mut self, address: u64, data: &[u8]) -> Result<()> { + let reply = self + .rsp + .command(&format!( + "M{:x},{:x}:{}", + address, + data.len(), + encode_hex(data) + )) + .await?; + expect_ok(&reply, "write") + } + + async fn halt(&mut self) -> Result<()> { + self.rsp.monitor("halt").await.map(|_| ()) + } + + async fn run(&mut self) -> Result<()> { + self.rsp.monitor("resume").await.map(|_| ()) + } + + async fn step(&mut self) -> Result<()> { + self.rsp.monitor("step").await.map(|_| ()) + } + + async fn reset(&mut self, halt_after: bool) -> Result<()> { + let cmd = if halt_after { + "reset halt" + } else { + "reset run" + }; + self.rsp.monitor(cmd).await.map(|_| ()) + } + + async fn core_reg(&mut self, reg: CoreRegId) -> Result { + // Ask openocd for the register by name; it resolves names per target + // architecture, so this works without hardcoding gdb register numbers. + // Output looks like "pc (/32): 0x40381796". + for name in role_register_names(reg) { + if let Ok(out) = self.rsp.monitor(&format!("reg {}", name)).await { + if let Some(value) = parse_reg_value(&out) { + return Ok(value); + } + } + } + Err(DebugError::InternalError(format!( + "could not read register {:?} via 'monitor reg' (tried {:?})", + reg, + role_register_names(reg) + ))) + } + + async fn status(&mut self) -> Result { + let reply = self.rsp.command("?").await?; + Ok(if reply.starts_with('T') || reply.starts_with('S') { + CoreState::Halted + } else { + CoreState::Unknown + }) + } + + async fn set_hw_breakpoint(&mut self, address: u64) -> Result<()> { + let reply = self.rsp.command(&format!("Z1,{:x},2", address)).await?; + expect_ok(&reply, "set breakpoint") + } + + async fn clear_hw_breakpoint(&mut self, address: u64) -> Result<()> { + let reply = self.rsp.command(&format!("z1,{:x},2", address)).await?; + expect_ok(&reply, "clear breakpoint") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::backend::{CoreRegId, CoreState, DebugBackend}; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::{TcpListener, TcpStream}; + + fn checksum(bytes: &[u8]) -> u8 { + bytes.iter().fold(0u8, |a, b| a.wrapping_add(*b)) + } + + fn hex_encode(b: &[u8]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect() + } + + fn hex_decode(s: &str) -> Vec { + (0..s.len()) + .step_by(2) + .filter_map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()) + .collect() + } + + /// Read one RSP packet payload from a socket ($payload#cc). None on EOF. + async fn read_packet(sock: &mut TcpStream) -> Option { + let mut b = [0u8; 1]; + loop { + if sock.read_exact(&mut b).await.is_err() { + return None; + } + if b[0] == b'$' { + break; + } + } + let mut data = Vec::new(); + loop { + sock.read_exact(&mut b).await.ok()?; + if b[0] == b'#' { + break; + } + data.push(b[0]); + } + let mut cks = [0u8; 2]; + sock.read_exact(&mut cks).await.ok()?; + String::from_utf8(data).ok() + } + + async fn send_packet(sock: &mut TcpStream, payload: &str) { + let framed = format!("${}#{:02x}", payload, checksum(payload.as_bytes())); + sock.write_all(framed.as_bytes()).await.unwrap(); + sock.flush().await.unwrap(); + } + + /// Minimal GDB-RSP server that answers the subset our client uses. + async fn mock_rsp_server(listener: TcpListener) { + let (mut sock, _) = listener.accept().await.unwrap(); + loop { + let pkt = match read_packet(&mut sock).await { + Some(p) => p, + None => break, + }; + // Ack the client's packet. + sock.write_all(b"+").await.unwrap(); + + // Build the response packet(s). "OK" acks writes, monitor commands, + // and breakpoints. `monitor reg pc` replies (like real openocd) with + // the register line as a single hex-encoded packet (no O prefix, no + // trailing OK). + let responses: Vec = if pkt.starts_with('m') { + vec!["deadbeef".to_string()] // 4 bytes for a read + } else if pkt == "?" { + vec!["T05".to_string()] // halted + } else if let Some(hexcmd) = pkt.strip_prefix("qRcmd,") { + let cmd = String::from_utf8(hex_decode(hexcmd)).unwrap_or_default(); + if cmd == "reg pc" { + let line = "pc (/32): 0x08000100\n"; + vec![hex_encode(line.as_bytes())] + } else { + vec!["OK".to_string()] + } + } else if pkt.starts_with('M') || pkt.starts_with('Z') || pkt.starts_with('z') { + vec!["OK".to_string()] + } else { + vec![String::new()] + }; + + for r in &responses { + send_packet(&mut sock, r).await; + let mut ack = [0u8; 1]; + if sock.read_exact(&mut ack).await.is_err() { + return; + } + } + } + } + + #[tokio::test] + async fn openocd_backend_drives_rsp_end_to_end() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + let server = tokio::spawn(mock_rsp_server(listener)); + + let mut backend = OpenOcdBackend::connect(&addr).await.expect("connect"); + assert_eq!(backend.kind(), BackendKind::OpenOcd); + + // Memory read: "m..,4" -> "deadbeef". + let data = backend.read_bytes(0x2000_0000, 4).await.expect("read"); + assert_eq!(data, vec![0xde, 0xad, 0xbe, 0xef]); + + // Memory write: "M..:..." -> OK. + backend + .write_bytes(0x2000_0000, &[0x01, 0x02]) + .await + .expect("write"); + + // Register read: "pf" (r15/PC) -> little-endian 0x08000100. + let pc = backend.core_reg(CoreRegId::Pc).await.expect("reg"); + assert_eq!(pc, 0x0800_0100); + + // Status: "?" -> T05 => Halted. + assert_eq!(backend.status().await.unwrap(), CoreState::Halted); + + // Control via monitor (qRcmd) -> OK. + backend.halt().await.expect("halt"); + backend.run().await.expect("run"); + + // Breakpoints: Z1/z1 -> OK. + backend + .set_hw_breakpoint(0x0800_0100) + .await + .expect("set bp"); + backend + .clear_hw_breakpoint(0x0800_0100) + .await + .expect("clear bp"); + + drop(backend); + let _ = server.await; + } + + #[test] + fn parses_openocd_reg_output() { + // Real formats observed from openocd-esp32 on a live ESP32-S3. + assert_eq!(parse_reg_value("pc (/32): 0x40381796\n"), Some(0x4038_1796)); + assert_eq!(parse_reg_value("a1 (/32): 0x3fcc67d0"), Some(0x3fcc_67d0)); + assert_eq!(parse_reg_value("no hex here"), None); + } +} diff --git a/src/backend/probe_rs_backend.rs b/src/backend/probe_rs_backend.rs new file mode 100644 index 0000000..ead9db3 --- /dev/null +++ b/src/backend/probe_rs_backend.rs @@ -0,0 +1,126 @@ +//! probe-rs native engine (default backend). + +use std::sync::Arc; + +use async_trait::async_trait; +use probe_rs::{CoreStatus, MemoryInterface, RegisterValue, Session}; +use tokio::sync::Mutex; + +use super::{BackendKind, CoreRegId, CoreState, DebugBackend}; +use crate::error::{DebugError, Result}; + +/// Wraps a shared probe-rs `Session`. The same `Arc` is also held by the +/// session record so probe-rs-specific tools (flash, RTT) can keep using it. +pub struct ProbeRsBackend { + session: Arc>, +} + +impl ProbeRsBackend { + pub fn new(session: Arc>) -> Self { + Self { session } + } +} + +#[async_trait] +impl DebugBackend for ProbeRsBackend { + fn kind(&self) -> BackendKind { + BackendKind::ProbeRs + } + + async fn read_bytes(&mut self, address: u64, len: usize) -> Result> { + let mut session = self.session.lock().await; + let mut core = session.core(0)?; + let mut buf = vec![0u8; len]; + core.read(address, &mut buf) + .map_err(|e| DebugError::MemoryAccessFailed(e.to_string()))?; + Ok(buf) + } + + async fn write_bytes(&mut self, address: u64, data: &[u8]) -> Result<()> { + let mut session = self.session.lock().await; + let mut core = session.core(0)?; + core.write(address, data) + .map_err(|e| DebugError::MemoryAccessFailed(e.to_string()))?; + Ok(()) + } + + async fn halt(&mut self) -> Result<()> { + let mut session = self.session.lock().await; + let mut core = session.core(0)?; + core.halt(std::time::Duration::from_millis(1000)) + .map_err(|e| DebugError::InternalError(format!("halt failed: {}", e)))?; + Ok(()) + } + + async fn run(&mut self) -> Result<()> { + let mut session = self.session.lock().await; + let mut core = session.core(0)?; + core.run() + .map_err(|e| DebugError::InternalError(format!("run failed: {}", e)))?; + Ok(()) + } + + async fn step(&mut self) -> Result<()> { + let mut session = self.session.lock().await; + let mut core = session.core(0)?; + core.step() + .map_err(|e| DebugError::InternalError(format!("step failed: {}", e)))?; + Ok(()) + } + + async fn reset(&mut self, halt_after: bool) -> Result<()> { + let mut session = self.session.lock().await; + let mut core = session.core(0)?; + if halt_after { + core.reset_and_halt(std::time::Duration::from_millis(1000)) + .map_err(|e| DebugError::InternalError(format!("reset_and_halt failed: {}", e)))?; + } else { + core.reset() + .map_err(|e| DebugError::InternalError(format!("reset failed: {}", e)))?; + } + Ok(()) + } + + async fn core_reg(&mut self, reg: CoreRegId) -> Result { + let mut session = self.session.lock().await; + let mut core = session.core(0)?; + let id = match reg { + CoreRegId::Pc => core.program_counter(), + CoreRegId::Sp => core.stack_pointer(), + CoreRegId::Lr => core.return_address(), + }; + let value: RegisterValue = core + .read_core_reg(id) + .map_err(|e| DebugError::InternalError(format!("read core reg failed: {}", e)))?; + Ok(value.try_into().unwrap_or(0u32)) + } + + async fn status(&mut self) -> Result { + let mut session = self.session.lock().await; + let mut core = session.core(0)?; + let status = core + .status() + .map_err(|e| DebugError::InternalError(format!("status failed: {}", e)))?; + Ok(match status { + CoreStatus::Halted(_) => CoreState::Halted, + CoreStatus::Running => CoreState::Running, + _ => CoreState::Unknown, + }) + } + + async fn set_hw_breakpoint(&mut self, address: u64) -> Result<()> { + let mut session = self.session.lock().await; + let mut core = session.core(0)?; + core.set_hw_breakpoint(address) + .map_err(|e| DebugError::InternalError(format!("set breakpoint failed: {}", e)))?; + Ok(()) + } + + async fn clear_hw_breakpoint(&mut self, address: u64) -> Result<()> { + let mut session = self.session.lock().await; + let mut core = session.core(0)?; + core.clear_hw_breakpoint(address) + .map_err(|e| DebugError::InternalError(format!("clear breakpoint failed: {}", e)))?; + Ok(()) + } +} diff --git a/src/backend/rsp.rs b/src/backend/rsp.rs new file mode 100644 index 0000000..6869e1e --- /dev/null +++ b/src/backend/rsp.rs @@ -0,0 +1,197 @@ +//! Minimal GDB Remote Serial Protocol (RSP) client over an async TCP socket. +//! +//! Only the subset needed to drive an OpenOCD (or any gdbserver) target is +//! implemented: packet framing + checksum, memory read/write, register read, +//! `monitor` (qRcmd) passthrough, breakpoints, and target interrupt. +//! +//! Using RSP (rather than shelling out `openocd -c "TCL"`) means the model +//! makes one structured tool call instead of typing commands and parsing text. + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; + +use crate::error::{DebugError, Result}; + +/// RSP checksum: modulo-256 sum of the payload bytes. +pub(crate) fn checksum(bytes: &[u8]) -> u8 { + bytes.iter().fold(0u8, |acc, b| acc.wrapping_add(*b)) +} + +/// Encode raw bytes as lowercase hex. +pub(crate) fn encode_hex(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for b in bytes { + s.push_str(&format!("{:02x}", b)); + } + s +} + +/// Decode a lowercase/uppercase hex string into bytes. +pub(crate) fn decode_hex(s: &str) -> Result> { + let s = s.trim(); + if !s.len().is_multiple_of(2) { + return Err(DebugError::InternalError(format!( + "RSP hex payload has odd length: {:?}", + s + ))); + } + (0..s.len()) + .step_by(2) + .map(|i| { + u8::from_str_radix(&s[i..i + 2], 16) + .map_err(|e| DebugError::InternalError(format!("RSP invalid hex {:?}: {}", s, e))) + }) + .collect() +} + +pub struct RspClient { + stream: TcpStream, +} + +impl RspClient { + pub async fn connect(addr: &str) -> Result { + let stream = TcpStream::connect(addr).await.map_err(|e| { + DebugError::ConnectionFailed(format!("OpenOCD RSP connect to {}: {}", addr, e)) + })?; + Ok(Self { stream }) + } + + /// Send a framed packet `$payload#cc` and wait for the `+` acknowledgement. + pub async fn send_packet(&mut self, payload: &str) -> Result<()> { + let framed = format!("${}#{:02x}", payload, checksum(payload.as_bytes())); + self.stream.write_all(framed.as_bytes()).await?; + self.stream.flush().await?; + self.read_ack().await + } + + async fn read_ack(&mut self) -> Result<()> { + let mut b = [0u8; 1]; + loop { + self.stream.read_exact(&mut b).await?; + match b[0] { + b'+' => return Ok(()), + b'-' => { + return Err(DebugError::InternalError( + "RSP peer sent NACK ('-')".to_string(), + )) + } + _ => continue, + } + } + } + + /// Receive one framed packet, returning its unescaped payload. Sends `+`. + pub async fn recv_packet(&mut self) -> Result { + let mut byte = [0u8; 1]; + // Skip until start-of-packet. + loop { + self.stream.read_exact(&mut byte).await?; + if byte[0] == b'$' { + break; + } + } + let mut data: Vec = Vec::new(); + loop { + self.stream.read_exact(&mut byte).await?; + if byte[0] == b'#' { + break; + } + if byte[0] == b'}' { + // RSP escape: next byte XOR 0x20. + self.stream.read_exact(&mut byte).await?; + data.push(byte[0] ^ 0x20); + } else { + data.push(byte[0]); + } + } + let mut cks = [0u8; 2]; + self.stream.read_exact(&mut cks).await?; + self.stream.write_all(b"+").await?; + self.stream.flush().await?; + String::from_utf8(data) + .map_err(|e| DebugError::InternalError(format!("RSP non-utf8 payload: {}", e))) + } + + /// Send a packet and return the single reply payload. + pub async fn command(&mut self, payload: &str) -> Result { + self.send_packet(payload).await?; + self.recv_packet().await + } + + /// Interrupt (halt) a running target: raw 0x03, then drain the stop reply. + pub async fn interrupt(&mut self) -> Result<()> { + self.stream.write_all(&[0x03]).await?; + self.stream.flush().await?; + let _ = self.recv_packet().await?; + Ok(()) + } + + /// Run an OpenOCD monitor command via `qRcmd`, collecting any `O`-output. + pub async fn monitor(&mut self, cmd: &str) -> Result { + self.send_packet(&format!("qRcmd,{}", encode_hex(cmd.as_bytes()))) + .await?; + let mut out = String::new(); + loop { + let pkt = self.recv_packet().await?; + if pkt == "OK" || pkt.is_empty() { + break; + } + if let Some(rest) = pkt.strip_prefix('O') { + if let Ok(bytes) = decode_hex(rest) { + out.push_str(&String::from_utf8_lossy(&bytes)); + } + continue; + } + if pkt.starts_with('E') { + return Err(DebugError::InternalError(format!( + "OpenOCD monitor '{}' error: {}", + cmd, pkt + ))); + } + // openocd returns qRcmd command output as a single hex-encoded + // packet (no 'O' prefix and no trailing OK). Decode it if it is hex. + if let Ok(bytes) = decode_hex(&pkt) { + out.push_str(&String::from_utf8_lossy(&bytes)); + } else { + out.push_str(&pkt); + } + break; + } + Ok(out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn checksum_matches_gdb_spec() { + // '$m0,4#' example: checksum of "m0,4" + assert_eq!( + checksum(b"m0,4"), + b'm'.wrapping_add(b'0') + .wrapping_add(b',') + .wrapping_add(b'4') + ); + } + + #[test] + fn hex_roundtrip() { + let bytes = [0x00u8, 0x01, 0x08, 0xff, 0x7f]; + assert_eq!(decode_hex(&encode_hex(&bytes)).unwrap(), bytes); + } + + #[test] + fn decode_hex_rejects_odd_length() { + assert!(decode_hex("abc").is_err()); + } + + #[test] + fn decode_little_endian_register() { + // 'p' reply for PC=0x08000100 is target-endian (LE) => "00010008" + let raw = decode_hex("00010008").unwrap(); + let pc = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]); + assert_eq!(pc, 0x0800_0100); + } +} diff --git a/src/lib.rs b/src/lib.rs index 13c336a..6211cb9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ //! Provides AI assistants with debugging and flash programming tools for //! embedded systems including ARM Cortex-M, RISC-V, J-Link, DAPLink, ST-Link, and other debug probes. +pub mod backend; pub mod config; pub mod debugger; pub mod error; diff --git a/src/tools/debugger_tools.rs b/src/tools/debugger_tools.rs index 31d5683..c58fd54 100644 --- a/src/tools/debugger_tools.rs +++ b/src/tools/debugger_tools.rs @@ -2,6 +2,7 @@ //! //! The tool router is split by domain while keeping one exported handler type for RMCP. +mod diagnostics; mod flash; mod formatting; mod guards; diff --git a/src/tools/debugger_tools/diagnostics.rs b/src/tools/debugger_tools/diagnostics.rs new file mode 100644 index 0000000..7d58440 --- /dev/null +++ b/src/tools/debugger_tools/diagnostics.rs @@ -0,0 +1,397 @@ +//! AI-facing crash-diagnosis tools. +//! +//! Design for flagship models: aggregate the scattered reads into ONE call and +//! return a compact, structured evidence bundle (raw register values + which +//! bits are set). We do NOT assert a root cause or embed a teaching decision +//! tree — the model reasons over the evidence. This saves context (one call +//! instead of ~6 `read_memory`) without over-teaching. + +use rmcp::{handler::server::tool::Parameters, model::*, tool, tool_router, ErrorData as McpError}; +use std::future::Future; +use tracing::{debug, info}; + +use super::session::EmbeddedDebuggerToolHandler; +use crate::backend::{CoreRegId, DebugBackend}; +use crate::tools::types::*; +use probe_rs::debug::{DebugInfo, DebugRegisters}; +use probe_rs::exception_handler_for_core; + +// ARMv7-M System Control Block fault registers (identical on all Cortex-M). +// Source: ARMv7-M Architecture Reference Manual, System Control Block. +const CPUID: u64 = 0xE000_ED00; +const ICSR: u64 = 0xE000_ED04; +const SHCSR: u64 = 0xE000_ED24; +const CFSR: u64 = 0xE000_ED28; +const HFSR: u64 = 0xE000_ED2C; +const MMFAR: u64 = 0xE000_ED34; +const BFAR: u64 = 0xE000_ED38; + +// Actionable CFSR bits (bit index -> name). Compact evidence, not a tutorial. +const CFSR_BITS: &[(u32, &str)] = &[ + // MemManage fault (MFSR, byte 0) + (0, "IACCVIOL"), + (1, "DACCVIOL"), + (3, "MUNSTKERR"), + (4, "MSTKERR"), + (7, "MMARVALID"), + // BusFault (BFSR, byte 1) + (8, "IBUSERR"), + (9, "PRECISERR"), + (10, "IMPRECISERR"), + (11, "UNSTKERR"), + (12, "STKERR"), + (15, "BFARVALID"), + // UsageFault (UFSR, bytes 2-3) + (16, "UNDEFINSTR"), + (17, "INVSTATE"), + (18, "INVPC"), + (19, "NOCP"), + (24, "UNALIGNED"), + (25, "DIVBYZERO"), +]; + +const HFSR_BITS: &[(u32, &str)] = &[(1, "VECTTBL"), (30, "FORCED"), (31, "DEBUGEVT")]; + +fn set_flags(value: u32, table: &[(u32, &str)]) -> Vec { + table + .iter() + .filter(|(bit, _)| value & (1 << bit) != 0) + .map(|(_, name)| name.to_string()) + .collect() +} + +async fn read_word_opt(backend: &mut Box, addr: u64) -> Option { + backend.read_word(addr).await.ok() +} + +fn hex_or_null(v: Option) -> serde_json::Value { + match v { + Some(x) => serde_json::Value::String(format!("0x{:08X}", x)), + None => serde_json::Value::Null, + } +} + +/// Decoded Cortex-M EXC_RETURN (the LR value while halted in an exception +/// handler). Determines which stack holds the pushed frame and its size. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +struct ExcReturn { + in_exception: bool, + uses_psp: bool, + extended: bool, +} + +fn decode_exc_return(lr: u32) -> ExcReturn { + // EXC_RETURN values are 0xFFFFFFxx. Bit 2 (SPSEL): 1 => PSP. Bit 4: 0 => + // extended (FPU) frame. + let in_exception = (lr & 0xFFFF_FF00) == 0xFFFF_FF00; + ExcReturn { + in_exception, + uses_psp: in_exception && (lr & 0x4) != 0, + extended: in_exception && (lr & 0x10) == 0, + } +} + +/// Cortex-M exception stack frame offsets (basic and extended share these for +/// the core registers; FP registers, if any, follow xPSR). +const STACKED_LR_OFFSET: u64 = 0x14; +const STACKED_PC_OFFSET: u64 = 0x18; + +/// Build a frame JSON object from an address and its resolved source location. +fn frame_json(index: usize, addr: u32, role: &str, di: &DebugInfo) -> serde_json::Value { + let sl = di.get_source_location(addr as u64); + let (file, line) = match &sl { + Some(s) => (Some(s.path.to_path().display().to_string()), s.line), + None => (None, None), + }; + serde_json::json!({ + "index": index, + "role": role, + "pc": format!("0x{:08X}", addr), + "file": file, + "line": line, + }) +} + +#[tool_router(router = diagnostics_tool_router, vis = "pub")] +impl EmbeddedDebuggerToolHandler { + #[tool( + description = "Aggregate crash evidence in one call: read the Cortex-M SCB fault registers \ + (CFSR/HFSR/MMFAR/BFAR/SHCSR/CPUID/ICSR) plus PC/SP/LR and return a compact structured \ + bundle with the set fault bits. Returns raw evidence for you to reason over; it does not \ + assert a root cause. Halt the target first for meaningful values." + )] + async fn diagnose_fault( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!("Diagnosing fault for session: {}", args.session_id); + let session_arc = self.get_session(&args.session_id).await?; + + let mut backend = session_arc.backend.lock().await; + let backend_kind = backend.kind().to_string(); + + let cfsr = read_word_opt(&mut backend, CFSR).await; + let hfsr = read_word_opt(&mut backend, HFSR).await; + let mmfar = read_word_opt(&mut backend, MMFAR).await; + let bfar = read_word_opt(&mut backend, BFAR).await; + let shcsr = read_word_opt(&mut backend, SHCSR).await; + let cpuid = read_word_opt(&mut backend, CPUID).await; + let icsr = read_word_opt(&mut backend, ICSR).await; + + let pc = backend.core_reg(CoreRegId::Pc).await.ok(); + let sp = backend.core_reg(CoreRegId::Sp).await.ok(); + let lr = backend.core_reg(CoreRegId::Lr).await.ok(); + drop(backend); + + let cfsr_flags = cfsr.map(|v| set_flags(v, CFSR_BITS)).unwrap_or_default(); + let hfsr_flags = hfsr.map(|v| set_flags(v, HFSR_BITS)).unwrap_or_default(); + + // Fault addresses are only meaningful when the matching *ARVALID bit is set. + let mmfar_valid = cfsr.map(|v| v & (1 << 7) != 0).unwrap_or(false); + let bfar_valid = cfsr.map(|v| v & (1 << 15) != 0).unwrap_or(false); + let mmfar_value = if mmfar_valid { + hex_or_null(mmfar) + } else { + serde_json::Value::Null + }; + let bfar_value = if bfar_valid { + hex_or_null(bfar) + } else { + serde_json::Value::Null + }; + + let report = serde_json::json!({ + "session": args.session_id, + "backend": backend_kind, + "target": session_arc.target_chip, + "core": { + "pc": hex_or_null(pc), + "sp": hex_or_null(sp), + "lr": hex_or_null(lr), + }, + "scb_raw": { + "cfsr": hex_or_null(cfsr), + "hfsr": hex_or_null(hfsr), + "shcsr": hex_or_null(shcsr), + "cpuid": hex_or_null(cpuid), + "icsr": hex_or_null(icsr), + "mmfar": hex_or_null(mmfar), + "bfar": hex_or_null(bfar), + }, + "cfsr_flags": cfsr_flags, + "hfsr_flags": hfsr_flags, + "fault_address": { + "mmfar": mmfar_value, + "bfar": bfar_value, + }, + "note": "Raw Cortex-M fault evidence. No root cause asserted; reason from the set bits and fault address. Values are meaningful only when the core is halted in the fault context." + }); + + let text = serde_json::to_string_pretty(&report) + .unwrap_or_else(|e| format!("{{\"error\":\"serialize failed: {}\"}}", e)); + + info!("Diagnose completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(text)])) + } + + #[tool( + description = "Unwind the stack after a crash and map each frame to a source line. On the \ + probe-rs backend this returns a full DWARF backtrace (function + file:line per frame). On \ + the OpenOCD backend it reads the Cortex-M exception stack frame and maps the faulting PC \ + and caller LR to source lines. Requires elf_path pointing at firmware built with debug \ + info (.debug_line). Halt the target in the fault context first." + )] + async fn unwind_exception( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!("Unwinding stack for session: {}", args.session_id); + let session_arc = self.get_session(&args.session_id).await?; + let elf = args.elf_path.clone(); + + // probe-rs backend: full DWARF backtrace via probe-rs's own unwinder. + // Everything after acquiring the lock is synchronous so the !Send + // DebugInfo never crosses an await point. + if let Some(prs) = session_arc.probe_rs_session.clone() { + let frames = { + let mut session = prs.lock().await; + let mut core = session.core(0).map_err(|e| { + McpError::internal_error(format!("Failed to get core: {}", e), None) + })?; + let di = DebugInfo::from_file(&elf).map_err(|e| { + McpError::internal_error( + format!("Failed to load debug info from '{}': {}", elf, e), + None, + ) + })?; + let registers = DebugRegisters::from_core(&mut core); + let handler = exception_handler_for_core(core.core_type()); + let instruction_set = core.instruction_set().ok(); + let stack = di + .unwind(&mut core, registers, handler.as_ref(), instruction_set) + .map_err(|e| { + McpError::internal_error(format!("Stack unwind failed: {}", e), None) + })?; + stack + .iter() + .enumerate() + .map(|(i, f)| { + let pc: u32 = f.pc.try_into().unwrap_or(0); + let (file, line) = match &f.source_location { + Some(s) => (Some(s.path.to_path().display().to_string()), s.line), + None => (None, None), + }; + serde_json::json!({ + "index": i, + "pc": format!("0x{:08X}", pc), + "function": f.function_name.clone(), + "file": file, + "line": line, + }) + }) + .collect::>() + }; + + let report = serde_json::json!({ + "session": args.session_id, + "backend": "probe-rs", + "elf": elf, + "method": "probe-rs DWARF unwind (full backtrace, innermost first)", + "frames": frames, + "note": "Each frame is mapped to source via DWARF. Needs firmware built with debug info; halt in the fault context for a meaningful trace." + }); + let text = serde_json::to_string_pretty(&report) + .unwrap_or_else(|e| format!("{{\"error\":\"serialize failed: {}\"}}", e)); + info!( + "Unwind (probe-rs) completed for session: {}", + args.session_id + ); + return Ok(CallToolResult::success(vec![Content::text(text)])); + } + + // OpenOCD (no probe-rs Core): Level-1 exception-frame read + mapping. + let (cur_pc, cur_sp, lr, backend_kind) = { + let mut backend = session_arc.backend.lock().await; + let kind = backend.kind().to_string(); + let pc = backend.core_reg(CoreRegId::Pc).await.ok(); + let sp = backend.core_reg(CoreRegId::Sp).await.ok(); + let lr = backend.core_reg(CoreRegId::Lr).await.ok(); + (pc, sp, lr, kind) + }; + + let exc = lr.map(decode_exc_return).unwrap_or_default(); + let mut stacked_pc = None; + let mut stacked_lr = None; + let note: &str; + if exc.in_exception && !exc.uses_psp { + if let Some(sp) = cur_sp { + let mut backend = session_arc.backend.lock().await; + stacked_pc = backend.read_word(sp as u64 + STACKED_PC_OFFSET).await.ok(); + stacked_lr = backend.read_word(sp as u64 + STACKED_LR_OFFSET).await.ok(); + note = "Cortex-M exception frame on MSP (bare-metal). Frame role 'faulting-pc' is the crashing instruction."; + } else { + note = "In an exception but SP is unavailable; showing current PC only."; + } + } else if exc.in_exception && exc.uses_psp { + note = "Faulting frame is on PSP (RTOS/threaded); the OpenOCD backend cannot read PSP here. Use the probe-rs backend for a full backtrace."; + } else { + note = "Core is not in an exception context (LR is not EXC_RETURN); showing current PC only."; + } + + // Synchronous DWARF mapping (DebugInfo is !Send; no await in this block). + let frames = { + let di = DebugInfo::from_file(&elf).map_err(|e| { + McpError::internal_error( + format!("Failed to load debug info from '{}': {}", elf, e), + None, + ) + })?; + let mut v = Vec::new(); + let mut idx = 0; + if let Some(p) = stacked_pc { + v.push(frame_json(idx, p, "faulting-pc", &di)); + idx += 1; + } + if let Some(l) = stacked_lr { + v.push(frame_json(idx, l, "caller-lr", &di)); + idx += 1; + } + if let Some(p) = cur_pc { + v.push(frame_json(idx, p, "current-pc", &di)); + } + v + }; + + let report = serde_json::json!({ + "session": args.session_id, + "backend": backend_kind, + "elf": elf, + "method": "cortex-m exception frame (Level 1)", + "in_exception": exc.in_exception, + "frames": frames, + "note": note, + }); + let text = serde_json::to_string_pretty(&report) + .unwrap_or_else(|e| format!("{{\"error\":\"serialize failed: {}\"}}", e)); + info!( + "Unwind (level-1) completed for session: {}", + args.session_id + ); + Ok(CallToolResult::success(vec![Content::text(text)])) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decodes_divbyzero_and_valid_bits() { + // DIVBYZERO (bit 25) + MMARVALID (bit 7) + let cfsr = (1 << 25) | (1 << 7); + let flags = set_flags(cfsr, CFSR_BITS); + assert!(flags.contains(&"DIVBYZERO".to_string())); + assert!(flags.contains(&"MMARVALID".to_string())); + assert!(!flags.contains(&"UNALIGNED".to_string())); + } + + #[test] + fn decodes_hfsr_forced() { + let flags = set_flags(1 << 30, HFSR_BITS); + assert_eq!(flags, vec!["FORCED".to_string()]); + } + + #[test] + fn exc_return_thread_msp_basic() { + // 0xFFFFFFF9: return to Thread mode, MSP, basic frame. + let e = decode_exc_return(0xFFFF_FFF9); + assert!(e.in_exception); + assert!(!e.uses_psp); + assert!(!e.extended); + } + + #[test] + fn exc_return_thread_psp() { + // 0xFFFFFFFD: return to Thread mode, PSP. + let e = decode_exc_return(0xFFFF_FFFD); + assert!(e.in_exception); + assert!(e.uses_psp); + } + + #[test] + fn exc_return_extended_fpu_frame() { + // 0xFFFFFFE1: handler mode, MSP, extended (FPU) frame (bit4 == 0). + let e = decode_exc_return(0xFFFF_FFE1); + assert!(e.in_exception); + assert!(!e.uses_psp); + assert!(e.extended); + } + + #[test] + fn normal_lr_is_not_exception() { + // A normal return address is not EXC_RETURN. + let e = decode_exc_return(0x0800_1234); + assert!(!e.in_exception); + assert_eq!(e, ExcReturn::default()); + } +} diff --git a/src/tools/debugger_tools/flash.rs b/src/tools/debugger_tools/flash.rs index 3155224..7e929f3 100644 --- a/src/tools/debugger_tools/flash.rs +++ b/src/tools/debugger_tools/flash.rs @@ -61,7 +61,8 @@ impl EmbeddedDebuggerToolHandler { // Perform erase operation { - let mut session = session_arc.session.lock().await; + let probe_session = session_arc.probe_session()?; + let mut session = probe_session.lock().await; match crate::flash::FlashManager::erase_flash(&mut session, erase_type).await { Ok(result) => { let message = format!( @@ -140,7 +141,8 @@ impl EmbeddedDebuggerToolHandler { // Perform programming operation { - let mut session = session_arc.session.lock().await; + let probe_session = session_arc.probe_session()?; + let mut session = probe_session.lock().await; let verify = args.verify || self.config.flash.verify_after_program; match crate::flash::FlashManager::program_file( &mut session, @@ -291,7 +293,8 @@ impl EmbeddedDebuggerToolHandler { // Perform verification { - let mut session = session_arc.session.lock().await; + let probe_session = session_arc.probe_session()?; + let mut session = probe_session.lock().await; match crate::flash::FlashManager::verify_flash(&mut session, expected_data, address) .await { @@ -388,7 +391,8 @@ impl EmbeddedDebuggerToolHandler { // Step 1: Erase flash status_messages.push("Step 1/5: Erasing flash memory...".to_string()); { - let mut session = session_arc.session.lock().await; + let probe_session = session_arc.probe_session()?; + let mut session = probe_session.lock().await; match crate::flash::FlashManager::erase_flash( &mut session, crate::flash::EraseType::All, @@ -423,7 +427,8 @@ impl EmbeddedDebuggerToolHandler { }; { - let mut session = session_arc.session.lock().await; + let probe_session = session_arc.probe_session()?; + let mut session = probe_session.lock().await; match crate::flash::FlashManager::program_file( &mut session, &file_path, @@ -451,7 +456,8 @@ impl EmbeddedDebuggerToolHandler { if args.reset_after_flash { status_messages.push("Step 3/5: Resetting target...".to_string()); { - let mut session = session_arc.session.lock().await; + let probe_session = session_arc.probe_session()?; + let mut session = probe_session.lock().await; let mut core = match session.core(0) { Ok(core) => core, Err(e) => { @@ -520,14 +526,14 @@ impl EmbeddedDebuggerToolHandler { attempt ); rtt_manager - .attach_with_elf(session_arc.session.clone(), &file_path) + .attach_with_elf(session_arc.probe_session()?, &file_path) .await } 3..=5 => { // Attempts 3-5: standard attach, let probe-rs auto-scan memory debug!("RTT attempt {}: Using standard memory map scan", attempt); rtt_manager - .attach(session_arc.session.clone(), None, None) + .attach(session_arc.probe_session()?, None, None) .await } 6..=7 => { @@ -542,7 +548,7 @@ impl EmbeddedDebuggerToolHandler { (0x20008000, 0x2000A000), // SRAM2: 8KB ]; rtt_manager - .attach(session_arc.session.clone(), None, Some(stm32g4_ranges)) + .attach(session_arc.probe_session()?, None, Some(stm32g4_ranges)) .await } _ => { @@ -553,7 +559,7 @@ impl EmbeddedDebuggerToolHandler { attempt, cb_addr ); rtt_manager - .attach(session_arc.session.clone(), Some(cb_addr), None) + .attach(session_arc.probe_session()?, Some(cb_addr), None) .await } }; diff --git a/src/tools/debugger_tools/management.rs b/src/tools/debugger_tools/management.rs index 1a693f0..bab245b 100644 --- a/src/tools/debugger_tools/management.rs +++ b/src/tools/debugger_tools/management.rs @@ -4,9 +4,11 @@ use std::sync::Arc; use tracing::{debug, error, info}; use super::session::{DebugSession, EmbeddedDebuggerToolHandler}; +use crate::backend::{DebugBackend, OpenOcdBackend, ProbeRsBackend}; use crate::rtt::RttManager; use crate::tools::types::*; use probe_rs::{probe::list::Lister, Permissions}; +use tokio::sync::OwnedSemaphorePermit; #[tool_router(router = management_tool_router, vis = "pub")] impl EmbeddedDebuggerToolHandler { @@ -74,6 +76,11 @@ impl EmbeddedDebuggerToolHandler { ) })?; + // OpenOCD backend: talk to an already-running openocd over GDB RSP. + if args.backend.eq_ignore_ascii_case("openocd") { + return self.connect_openocd(&args, session_slot).await; + } + // Real probe-rs implementation let probes = Lister::new().list_all(); @@ -150,12 +157,17 @@ impl EmbeddedDebuggerToolHandler { let session_id = format!("session_{}", uuid::Uuid::new_v4()); + let shared_session = Arc::new(tokio::sync::Mutex::new(session)); + let backend: Box = + Box::new(ProbeRsBackend::new(shared_session.clone())); + let debug_session = DebugSession { session_id: session_id.clone(), probe_identifier: probe_info.identifier.clone(), target_chip: args.target_chip.clone(), created_at: chrono::Utc::now(), - session: Arc::new(tokio::sync::Mutex::new(session)), + backend: Arc::new(tokio::sync::Mutex::new(backend)), + probe_rs_session: Some(shared_session), rtt_manager: Arc::new(tokio::sync::Mutex::new( RttManager::new(), )), @@ -318,3 +330,62 @@ impl EmbeddedDebuggerToolHandler { Ok(CallToolResult::success(vec![Content::text(message)])) } } + +impl EmbeddedDebuggerToolHandler { + /// Establish a session backed by an already-running OpenOCD over GDB RSP. + async fn connect_openocd( + &self, + args: &ConnectArgs, + session_slot: OwnedSemaphorePermit, + ) -> Result { + let address = args.openocd_address.clone(); + info!("Connecting via OpenOCD GDB RSP at {}", address); + + let mut backend = OpenOcdBackend::connect(&address).await.map_err(|e| { + McpError::internal_error( + format!( + "Failed to connect to OpenOCD at {}: {}. Ensure openocd is running and \ + exposing its GDB port (e.g. openocd -f interface/stlink.cfg -f target/stm32f4x.cfg).", + address, e + ), + None, + ) + })?; + + if args.halt_after_connect { + let _ = backend.halt().await; + } + + let session_id = format!("session_{}", uuid::Uuid::new_v4()); + let boxed: Box = Box::new(backend); + let debug_session = DebugSession { + session_id: session_id.clone(), + probe_identifier: format!("openocd@{}", address), + target_chip: args.target_chip.clone(), + created_at: chrono::Utc::now(), + backend: Arc::new(tokio::sync::Mutex::new(boxed)), + probe_rs_session: None, + rtt_manager: Arc::new(tokio::sync::Mutex::new(RttManager::new())), + _session_slot: session_slot, + }; + + { + let mut sessions = self.sessions.write().await; + sessions.insert(session_id.clone(), Arc::new(debug_session)); + } + + let message = format!( + "Debug session established (OpenOCD backend).\n\n\ + Session ID: {}\n\ + OpenOCD: {}\n\ + Target: {}\n\ + Halted after connect: {}\n\n\ + Available: read_memory, write_memory, halt, run, step, reset, breakpoints, \ + diagnose_fault. Flash and RTT require the probe-rs backend.", + session_id, address, args.target_chip, args.halt_after_connect + ); + + info!("Created OpenOCD debug session: {}", session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } +} diff --git a/src/tools/debugger_tools/memory.rs b/src/tools/debugger_tools/memory.rs index 1a8262b..1f86dc0 100644 --- a/src/tools/debugger_tools/memory.rs +++ b/src/tools/debugger_tools/memory.rs @@ -5,7 +5,6 @@ use tracing::{debug, error, info}; use super::formatting::{format_memory_data, parse_address, parse_data}; use super::session::EmbeddedDebuggerToolHandler; use crate::tools::types::*; -use probe_rs::MemoryInterface; #[tool_router(router = memory_tool_router, vis = "pub")] impl EmbeddedDebuggerToolHandler { @@ -23,7 +22,6 @@ impl EmbeddedDebuggerToolHandler { args.session_id, args.address ); - // Parse address let address = match parse_address(&args.address) { Ok(addr) => addr, Err(e) => { @@ -38,51 +36,30 @@ impl EmbeddedDebuggerToolHandler { let session_arc = self.get_session(&args.session_id).await?; self.ensure_memory_read_allowed(&session_arc, address, args.size)?; - // Read memory - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - let mut data = vec![0u8; args.size]; - match core.read(address, &mut data) { - Ok(_) => { - debug!("Read {} bytes from address 0x{:08X}", data.len(), address); + let data = { + let mut backend = session_arc.backend.lock().await; + backend.read_bytes(address, args.size).await.map_err(|e| { + error!( + "Failed to read memory for session {}: {}", + args.session_id, e + ); + McpError::internal_error(format!("Failed to read memory: {}", e), None) + })? + }; - let formatted_data = format_memory_data(&data, &args.format, address); - let message = format!( - "Memory read completed successfully.\n\n\ - Session ID: {}\n\ - Address: 0x{:08X}\n\ - Size: {} bytes\n\ - Format: {}\n\n\ - Data:\n{}", - args.session_id, address, args.size, args.format, formatted_data - ); + let formatted_data = format_memory_data(&data, &args.format, address); + let message = format!( + "Memory read completed successfully.\n\n\ + Session ID: {}\n\ + Address: 0x{:08X}\n\ + Size: {} bytes\n\ + Format: {}\n\n\ + Data:\n{}", + args.session_id, address, args.size, args.format, formatted_data + ); - info!("Memory read completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to read memory for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to read memory: {}", e), - None, - )) - } - } - } + info!("Memory read completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) } #[tool(description = "Write memory to the target")] @@ -95,7 +72,6 @@ impl EmbeddedDebuggerToolHandler { args.session_id, args.address ); - // Parse address let address = match parse_address(&args.address) { Ok(addr) => addr, Err(e) => { @@ -107,7 +83,6 @@ impl EmbeddedDebuggerToolHandler { } }; - // Parse data based on format let data = match parse_data(&args.data, &args.format) { Ok(data) => data, Err(e) => { @@ -122,51 +97,33 @@ impl EmbeddedDebuggerToolHandler { let session_arc = self.get_session(&args.session_id).await?; self.ensure_memory_write_allowed(&session_arc, address, data.len())?; - // Write memory { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; + let mut backend = session_arc.backend.lock().await; + backend.write_bytes(address, &data).await.map_err(|e| { + error!( + "Failed to write memory for session {}: {}", + args.session_id, e + ); + McpError::internal_error(format!("Failed to write memory: {}", e), None) + })?; + } - match core.write(address, &data) { - Ok(_) => { - let message = format!( - "Memory write completed successfully.\n\n\ - Session ID: {}\n\ - Address: 0x{:08X}\n\ - Data: {}\n\ - Format: {}\n\ - Bytes written: {}", - args.session_id, - address, - args.data, - args.format, - data.len() - ); + let message = format!( + "Memory write completed successfully.\n\n\ + Session ID: {}\n\ + Address: 0x{:08X}\n\ + Data: {}\n\ + Format: {}\n\ + Bytes written: {}", + args.session_id, + address, + args.data, + args.format, + data.len() + ); - info!("Memory write completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to write memory for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to write memory: {}", e), - None, - )) - } - } - } + info!("Memory write completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) } // ============================================================================= @@ -193,7 +150,6 @@ impl EmbeddedDebuggerToolHandler { )); } - // Parse address let address = match parse_address(&args.address) { Ok(addr) => addr, Err(e) => { @@ -207,49 +163,31 @@ impl EmbeddedDebuggerToolHandler { let session_arc = self.get_session(&args.session_id).await?; - // Set breakpoint { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; + let mut backend = session_arc.backend.lock().await; + backend.set_hw_breakpoint(address).await.map_err(|e| { + error!( + "Failed to set breakpoint for session {}: {}", + args.session_id, e + ); + McpError::internal_error(format!("Failed to set breakpoint: {}", e), None) + })?; + } - match core.set_hw_breakpoint(address) { - Ok(_) => { - let message = format!( - "Breakpoint set successfully.\n\n\ - Session ID: {}\n\ - Address: 0x{:08X}\n\ - Type: Hardware breakpoint\n\n\ - The target will halt when execution reaches this address.", - args.session_id, address - ); + let message = format!( + "Breakpoint set successfully.\n\n\ + Session ID: {}\n\ + Address: 0x{:08X}\n\ + Type: Hardware breakpoint\n\n\ + The target will halt when execution reaches this address.", + args.session_id, address + ); - info!( - "Breakpoint set for session: {} at 0x{:08X}", - args.session_id, address - ); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to set breakpoint for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to set breakpoint: {}", e), - None, - )) - } - } - } + info!( + "Breakpoint set for session: {} at 0x{:08X}", + args.session_id, address + ); + Ok(CallToolResult::success(vec![Content::text(message)])) } #[tool(description = "Clear a breakpoint at the specified address")] @@ -262,7 +200,6 @@ impl EmbeddedDebuggerToolHandler { args.session_id, args.address ); - // Parse address let address = match parse_address(&args.address) { Ok(addr) => addr, Err(e) => { @@ -274,58 +211,31 @@ impl EmbeddedDebuggerToolHandler { } }; - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; + let session_arc = self.get_session(&args.session_id).await?; - // Clear breakpoint { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; + let mut backend = session_arc.backend.lock().await; + backend.clear_hw_breakpoint(address).await.map_err(|e| { + error!( + "Failed to clear breakpoint for session {}: {}", + args.session_id, e + ); + McpError::internal_error(format!("Failed to clear breakpoint: {}", e), None) + })?; + } - match core.clear_hw_breakpoint(address) { - Ok(_) => { - let message = format!( - "Breakpoint cleared successfully.\n\n\ - Session ID: {}\n\ - Address: 0x{:08X}\n\n\ - The breakpoint has been removed.", - args.session_id, address - ); + let message = format!( + "Breakpoint cleared successfully.\n\n\ + Session ID: {}\n\ + Address: 0x{:08X}\n\n\ + The breakpoint has been removed.", + args.session_id, address + ); - info!( - "Breakpoint cleared for session: {} at 0x{:08X}", - args.session_id, address - ); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to clear breakpoint for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to clear breakpoint: {}", e), - None, - )) - } - } - } + info!( + "Breakpoint cleared for session: {} at 0x{:08X}", + args.session_id, address + ); + Ok(CallToolResult::success(vec![Content::text(message)])) } } diff --git a/src/tools/debugger_tools/rtt.rs b/src/tools/debugger_tools/rtt.rs index 1cb7d5b..a6c57c1 100644 --- a/src/tools/debugger_tools/rtt.rs +++ b/src/tools/debugger_tools/rtt.rs @@ -68,7 +68,7 @@ impl EmbeddedDebuggerToolHandler { let mut rtt_manager = session_arc.rtt_manager.lock().await; match rtt_manager .attach( - session_arc.session.clone(), + session_arc.probe_session()?, control_block_address, memory_ranges, ) diff --git a/src/tools/debugger_tools/server.rs b/src/tools/debugger_tools/server.rs index 15164dc..048c927 100644 --- a/src/tools/debugger_tools/server.rs +++ b/src/tools/debugger_tools/server.rs @@ -13,7 +13,7 @@ impl ServerHandler for EmbeddedDebuggerToolHandler { protocol_version: ProtocolVersion::V_2024_11_05, capabilities: ServerCapabilities::builder().enable_tools().build(), server_info: Implementation::from_build_env(), - instructions: Some("Embedded debugging and flash programming MCP server for ARM Cortex-M, RISC-V, and other probe-rs-supported targets. Exposes 22 tools for probe detection, target sessions, memory operations, breakpoints, RTT communication, and flash programming: list_probes, connect, disconnect, probe_info, halt, run, reset, step, get_status, read_memory, write_memory, set_breakpoint, clear_breakpoint, rtt_attach, rtt_detach, rtt_read, rtt_write, rtt_channels, flash_erase, flash_program, flash_verify, run_firmware.".to_string()), + instructions: Some("Embedded debugging and flash programming MCP server for ARM Cortex-M, RISC-V, and other targets. A single tool set runs over two interchangeable backends chosen at connect: probe-rs (default, native, RTT/flash) or OpenOCD (backend=\"openocd\", via GDB RSP, for chips probe-rs does not cover). Exposes 24 tools: list_probes, connect, disconnect, probe_info, halt, run, reset, step, get_status, read_memory, write_memory, set_breakpoint, clear_breakpoint, diagnose_fault, unwind_exception, rtt_attach, rtt_detach, rtt_read, rtt_write, rtt_channels, flash_erase, flash_program, flash_verify, run_firmware.".to_string()), } } @@ -22,7 +22,7 @@ impl ServerHandler for EmbeddedDebuggerToolHandler { _request: InitializeRequestParam, _context: RequestContext, ) -> Result { - info!("Embedded Debugger MCP server initialized with 22 tools (18 debug + 4 flash)"); + info!("Embedded Debugger MCP server initialized with 24 tools (dual backend: probe-rs + OpenOCD)"); Ok(self.get_info()) } } diff --git a/src/tools/debugger_tools/session.rs b/src/tools/debugger_tools/session.rs index 7d3ba51..9dd9ea8 100644 --- a/src/tools/debugger_tools/session.rs +++ b/src/tools/debugger_tools/session.rs @@ -3,22 +3,44 @@ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::{OwnedSemaphorePermit, RwLock, Semaphore}; +use crate::backend::DebugBackend; use crate::config::Config; use crate::rtt::RttManager; use probe_rs::Session; -/// Debug session information +/// Debug session information. +/// +/// `backend` is the unified execution engine (probe-rs or OpenOCD) that the +/// core/memory/control tools drive. `probe_rs_session` is only present for +/// probe-rs sessions and is retained so probe-rs-specific tools (flash, RTT) +/// can keep using the native `Session` directly. pub struct DebugSession { pub session_id: String, pub probe_identifier: String, pub target_chip: String, pub created_at: chrono::DateTime, - pub session: Arc>, + pub backend: Arc>>, + pub probe_rs_session: Option>>, pub rtt_manager: Arc>, pub(super) _session_slot: OwnedSemaphorePermit, } -/// Embedded debugger tool handler with debug, RTT, and flash tools +impl DebugSession { + /// Return the probe-rs `Session` for probe-rs-only operations, or a clear + /// error when the active session uses the OpenOCD backend. + pub(super) fn probe_session(&self) -> Result>, McpError> { + self.probe_rs_session.clone().ok_or_else(|| { + McpError::internal_error( + "This operation requires the probe-rs backend; the active session uses OpenOCD. \ + Reconnect with backend=\"probe-rs\" for flash/RTT operations." + .to_string(), + None, + ) + }) + } +} + +/// Embedded debugger tool handler with debug, RTT, and flash tools. #[derive(Clone)] pub struct EmbeddedDebuggerToolHandler { #[allow(dead_code)] @@ -37,6 +59,7 @@ impl EmbeddedDebuggerToolHandler { tool_router: Self::management_tool_router() + Self::target_control_tool_router() + Self::memory_tool_router() + + Self::diagnostics_tool_router() + Self::rtt_tool_router() + Self::flash_tool_router(), sessions: Arc::new(RwLock::new(HashMap::new())), @@ -82,7 +105,7 @@ mod tests { .map(|tool| tool.name.as_ref()) .collect::>(); - assert_eq!(tools.len(), 22); + assert_eq!(tools.len(), 24); for expected in [ "list_probes", "connect", @@ -97,6 +120,8 @@ mod tests { "write_memory", "set_breakpoint", "clear_breakpoint", + "diagnose_fault", + "unwind_exception", "rtt_attach", "rtt_detach", "rtt_read", diff --git a/src/tools/debugger_tools/target_control.rs b/src/tools/debugger_tools/target_control.rs index 03a36ac..e81c43f 100644 --- a/src/tools/debugger_tools/target_control.rs +++ b/src/tools/debugger_tools/target_control.rs @@ -1,15 +1,15 @@ use rmcp::{handler::server::tool::Parameters, model::*, tool, tool_router, ErrorData as McpError}; use std::future::Future; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, info}; use super::session::EmbeddedDebuggerToolHandler; +use crate::backend::CoreState; use crate::tools::types::*; -use probe_rs::{CoreStatus, RegisterValue}; #[tool_router(router = target_control_tool_router, vis = "pub")] impl EmbeddedDebuggerToolHandler { // ============================================================================= - // Target Control Tools (5 tools) + // Target Control Tools (5 tools) — routed through the unified DebugBackend // ============================================================================= #[tool(description = "Halt the target CPU execution")] @@ -18,120 +18,57 @@ impl EmbeddedDebuggerToolHandler { Parameters(args): Parameters, ) -> Result { debug!("Halting target for session: {}", args.session_id); - let session_arc = self.get_session(&args.session_id).await?; - // Halt the target - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - match core.halt(std::time::Duration::from_millis(1000)) { - Ok(_) => { - // Get status after halt - match core.status() { - Ok(_status) => { - let pc = core - .read_core_reg(core.program_counter()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - let sp = core - .read_core_reg(core.stack_pointer()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - - let message = format!( - "Target halted successfully!\n\n\ - Session ID: {}\n\ - PC: 0x{:08X}\n\ - SP: 0x{:08X}\n\ - State: Halted\n", - args.session_id, pc, sp - ); - - info!("Halt completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - warn!("Failed to get status after halt: {}", e); - let message = format!( - "Target halted successfully!\n\n\ - Session ID: {}\n\ - State: Halted\n", - args.session_id - ); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - } - } - Err(e) => { - error!( - "Failed to halt target for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to halt target: {}", e), - None, - )) - } - } - } + let mut backend = session_arc.backend.lock().await; + backend.halt().await.map_err(|e| { + error!( + "Failed to halt target for session {}: {}", + args.session_id, e + ); + McpError::internal_error(format!("Failed to halt target: {}", e), None) + })?; + let snap = backend.snapshot().await.ok(); + drop(backend); + + let (pc, sp) = snap.map(|s| (s.pc, s.sp)).unwrap_or((0, 0)); + let message = format!( + "Target halted successfully!\n\n\ + Session ID: {}\n\ + PC: 0x{:08X}\n\ + SP: 0x{:08X}\n\ + State: Halted\n", + args.session_id, pc, sp + ); + info!("Halt completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) } #[tool(description = "Resume target CPU execution")] async fn run(&self, Parameters(args): Parameters) -> Result { debug!("Running target for session: {}", args.session_id); - let session_arc = self.get_session(&args.session_id).await?; - // Resume the target { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - match core.run() { - Ok(_) => { - let message = format!( - "Target resumed execution successfully!\n\n\ - Session ID: {}\n\ - Status: Running\n\n\ - The target is now executing code. Use 'halt' to stop execution.", - args.session_id - ); - - info!("Run completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to run target for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to run target: {}", e), - None, - )) - } - } + let mut backend = session_arc.backend.lock().await; + backend.run().await.map_err(|e| { + error!( + "Failed to run target for session {}: {}", + args.session_id, e + ); + McpError::internal_error(format!("Failed to run target: {}", e), None) + })?; } + + let message = format!( + "Target resumed execution successfully!\n\n\ + Session ID: {}\n\ + Status: Running\n\n\ + The target is now executing code. Use 'halt' to stop execution.", + args.session_id + ); + info!("Run completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) } #[tool(description = "Reset the target CPU")] @@ -144,7 +81,7 @@ impl EmbeddedDebuggerToolHandler { if args.reset_type != "hardware" { return Err(McpError::internal_error( format!( - "Unsupported reset_type '{}'. probe-rs core reset is exposed as 'hardware' by this server.", + "Unsupported reset_type '{}'. Core reset is exposed as 'hardware' by this server.", args.reset_type ), None, @@ -153,75 +90,39 @@ impl EmbeddedDebuggerToolHandler { let session_arc = self.get_session(&args.session_id).await?; - // Reset the target - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - let reset_result = if args.halt_after_reset { - core.reset_and_halt(std::time::Duration::from_millis( - self.config.debugger.connection_timeout_ms, - )) - .map(|_| ()) + let mut backend = session_arc.backend.lock().await; + backend.reset(args.halt_after_reset).await.map_err(|e| { + error!( + "Failed to reset target for session {}: {}", + args.session_id, e + ); + McpError::internal_error(format!("Failed to reset target: {}", e), None) + })?; + let snap = backend.snapshot().await.ok(); + drop(backend); + + let (pc, sp) = snap.map(|s| (s.pc, s.sp)).unwrap_or((0, 0)); + let message = format!( + "Target reset completed successfully.\n\n\ + Session ID: {}\n\ + Reset type: {}\n\ + Halted after reset: {}\n\ + PC: 0x{:08X}\n\ + SP: 0x{:08X}\n\ + State: {}\n", + args.session_id, + args.reset_type, + args.halt_after_reset, + pc, + sp, + if args.halt_after_reset { + "Halted" } else { - core.reset() - }; - - match reset_result { - Ok(_) => { - let pc = core - .read_core_reg(core.program_counter()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - let sp = core - .read_core_reg(core.stack_pointer()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - - let message = format!( - "Target reset completed successfully.\n\n\ - Session ID: {}\n\ - Reset type: {}\n\ - Halted after reset: {}\n\ - PC: 0x{:08X}\n\ - SP: 0x{:08X}\n\ - State: {}\n", - args.session_id, - args.reset_type, - args.halt_after_reset, - pc, - sp, - if args.halt_after_reset { - "Halted" - } else { - "Running" - } - ); - - info!("Reset completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to reset target for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to reset target: {}", e), - None, - )) - } + "Running" } - } + ); + info!("Reset completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) } #[tool(description = "Execute a single instruction step")] @@ -230,58 +131,30 @@ impl EmbeddedDebuggerToolHandler { Parameters(args): Parameters, ) -> Result { debug!("Single stepping target for session: {}", args.session_id); - let session_arc = self.get_session(&args.session_id).await?; - // Single step the target - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - match core.step() { - Ok(_) => { - let pc = core - .read_core_reg(core.program_counter()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - let sp = core - .read_core_reg(core.stack_pointer()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - - let message = format!( - "Single step completed successfully!\n\n\ - Session ID: {}\n\ - PC: 0x{:08X}\n\ - SP: 0x{:08X}\n\ - State: Halted\n", - args.session_id, pc, sp - ); - - info!("Step completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to step target for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to step target: {}", e), - None, - )) - } - } - } + let mut backend = session_arc.backend.lock().await; + backend.step().await.map_err(|e| { + error!( + "Failed to step target for session {}: {}", + args.session_id, e + ); + McpError::internal_error(format!("Failed to step target: {}", e), None) + })?; + let snap = backend.snapshot().await.ok(); + drop(backend); + + let (pc, sp) = snap.map(|s| (s.pc, s.sp)).unwrap_or((0, 0)); + let message = format!( + "Single step completed successfully!\n\n\ + Session ID: {}\n\ + PC: 0x{:08X}\n\ + SP: 0x{:08X}\n\ + State: Halted\n", + args.session_id, pc, sp + ); + info!("Step completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) } #[tool(description = "Get current status of the target CPU and debug session")] @@ -290,77 +163,42 @@ impl EmbeddedDebuggerToolHandler { Parameters(args): Parameters, ) -> Result { debug!("Getting status for session: {}", args.session_id); - let session_arc = self.get_session(&args.session_id).await?; - // Get target status - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - match core.status() { - Ok(status) => { - let pc = core - .read_core_reg(core.program_counter()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - let sp = core - .read_core_reg(core.stack_pointer()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - - let is_halted = matches!(status, CoreStatus::Halted(_)); - let halt_reason = match status { - CoreStatus::Halted(reason) => format!("{:?}", reason), - CoreStatus::Running => "N/A".to_string(), - _ => "Unknown".to_string(), - }; - - let message = format!( - "Debug Session Status\n\n\ - Core Information:\n\ - - PC: 0x{:08X}\n\ - - SP: 0x{:08X}\n\ - - State: {}\n\ - - Halt reason: {}\n\n\ - Session Information:\n\ - - ID: {}\n\ - - Connected: true\n\ - - Target: {}\n\ - - Probe: {}\n\ - - Duration: {:.1} minutes\n", - pc, - sp, - if is_halted { "Halted" } else { "Running" }, - halt_reason, - args.session_id, - session_arc.target_chip, - session_arc.probe_identifier, - (chrono::Utc::now() - session_arc.created_at).num_seconds() as f64 / 60.0 - ); - - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to get core status for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to get core status: {}", e), - None, - )) - } - } - } + let snap = { + let mut backend = session_arc.backend.lock().await; + backend.snapshot().await.map_err(|e| { + error!( + "Failed to get status for session {}: {}", + args.session_id, e + ); + McpError::internal_error(format!("Failed to get core status: {}", e), None) + })? + }; + + let is_halted = snap.state == CoreState::Halted; + let message = format!( + "Debug Session Status\n\n\ + Core Information:\n\ + - PC: 0x{:08X}\n\ + - SP: 0x{:08X}\n\ + - State: {}\n\ + - Halt reason: {}\n\n\ + Session Information:\n\ + - ID: {}\n\ + - Connected: true\n\ + - Target: {}\n\ + - Probe: {}\n\ + - Duration: {:.1} minutes\n", + snap.pc, + snap.sp, + if is_halted { "Halted" } else { "Running" }, + snap.halt_reason.as_deref().unwrap_or("N/A"), + args.session_id, + session_arc.target_chip, + session_arc.probe_identifier, + (chrono::Utc::now() - session_arc.created_at).num_seconds() as f64 / 60.0 + ); + Ok(CallToolResult::success(vec![Content::text(message)])) } } diff --git a/src/tools/types.rs b/src/tools/types.rs index ce35e62..2093002 100644 --- a/src/tools/types.rs +++ b/src/tools/types.rs @@ -27,11 +27,24 @@ pub struct ConnectArgs { /// Whether to halt after connecting #[serde(default = "default_true")] pub halt_after_connect: bool, + /// Debug engine: "probe-rs" (default, native) or "openocd" (GDB RSP). + #[serde(default = "default_backend")] + pub backend: String, + /// OpenOCD GDB remote address when backend="openocd" (default "127.0.0.1:3333"). + /// Requires a running `openocd` exposing its GDB port. + #[serde(default = "default_openocd_address")] + pub openocd_address: String, } fn default_speed_khz() -> u32 { 4000 } +fn default_backend() -> String { + "probe-rs".to_string() +} +fn default_openocd_address() -> String { + "127.0.0.1:3333".to_string() +} fn default_true() -> bool { true } @@ -92,6 +105,25 @@ pub struct GetStatusArgs { pub session_id: String, } +// ============================================================================= +// Diagnostics Types +// ============================================================================= + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct DiagnoseFaultArgs { + /// Session ID + pub session_id: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct UnwindExceptionArgs { + /// Session ID + pub session_id: String, + /// Path to the firmware ELF built with debug info (.debug_line), used to + /// map addresses to source file:line. + pub elf_path: String, +} + // ============================================================================= // Memory Operation Types // =============================================================================