zeroclaw_tools/
hardware_memory_read.rs1use async_trait::async_trait;
7use serde_json::json;
8use zeroclaw_api::tool::{Tool, ToolResult};
9
10const NUCLEO_RAM_BASE: u64 = 0x2000_0000;
12
13pub struct HardwareMemoryReadTool {
15 boards: Vec<String>,
16}
17
18impl HardwareMemoryReadTool {
19 pub fn new(boards: Vec<String>) -> Self {
20 Self { boards }
21 }
22
23 fn chip_for_board(board: &str) -> Option<&'static str> {
24 match board {
25 "nucleo-f401re" => Some("STM32F401RETx"),
26 "nucleo-f411re" => Some("STM32F411RETx"),
27 _ => None,
28 }
29 }
30}
31
32#[async_trait]
33impl Tool for HardwareMemoryReadTool {
34 fn name(&self) -> &str {
35 "hardware_memory_read"
36 }
37
38 fn description(&self) -> &str {
39 "Read actual memory/register values from Nucleo via USB. Use when: user asks to 'read register values', 'read memory at address', 'dump memory', 'lower memory 0-126', or 'give address and value'. Returns hex dump. Requires Nucleo connected via USB and probe feature. Params: address (hex, e.g. 0x20000000 for RAM start), length (bytes, default 128)."
40 }
41
42 fn parameters_schema(&self) -> serde_json::Value {
43 json!({
44 "type": "object",
45 "properties": {
46 "address": {
47 "type": "string",
48 "description": "Memory address in hex (e.g. 0x20000000 for RAM start). Default: 0x20000000 (RAM base)."
49 },
50 "length": {
51 "type": "integer",
52 "description": "Number of bytes to read (default 128, max 256)."
53 },
54 "board": {
55 "type": "string",
56 "description": "Board name (nucleo-f401re). Optional if only one configured."
57 }
58 }
59 })
60 }
61
62 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
63 if self.boards.is_empty() {
64 return Ok(ToolResult {
65 success: false,
66 output: String::new(),
67 error: Some(
68 "No peripherals configured. Add nucleo-f401re to config.toml [peripherals.boards]."
69 .into(),
70 ),
71 });
72 }
73
74 let board = args
75 .get("board")
76 .and_then(|v| v.as_str())
77 .map(String::from)
78 .or_else(|| self.boards.first().cloned())
79 .unwrap_or_else(|| "nucleo-f401re".into());
80
81 let chip = Self::chip_for_board(&board);
82 if chip.is_none() {
83 return Ok(ToolResult {
84 success: false,
85 output: String::new(),
86 error: Some(format!(
87 "Memory read only supports nucleo-f401re, nucleo-f411re. Got: {}",
88 board
89 )),
90 });
91 }
92
93 let address_str = args
94 .get("address")
95 .and_then(|v| v.as_str())
96 .unwrap_or("0x20000000");
97 let _address = parse_hex_address(address_str).unwrap_or(NUCLEO_RAM_BASE);
98
99 let requested_length = args.get("length").and_then(|v| v.as_u64()).unwrap_or(128);
100 let _length = usize::try_from(requested_length)
101 .unwrap_or(256)
102 .clamp(1, 256);
103
104 #[cfg(feature = "probe")]
105 {
106 match probe_read_memory(chip.unwrap(), _address, _length) {
107 Ok(output) => {
108 return Ok(ToolResult {
109 success: true,
110 output,
111 error: None,
112 });
113 }
114 Err(e) => {
115 return Ok(ToolResult {
116 success: false,
117 output: String::new(),
118 error: Some(format!(
119 "probe-rs read failed: {}. Ensure Nucleo is connected via USB and built with --features probe.",
120 e
121 )),
122 });
123 }
124 }
125 }
126
127 #[cfg(not(feature = "probe"))]
128 {
129 Ok(ToolResult {
130 success: false,
131 output: String::new(),
132 error: Some(
133 "Memory read requires probe feature. Build with: cargo build --features hardware,probe"
134 .into(),
135 ),
136 })
137 }
138 }
139}
140
141fn parse_hex_address(s: &str) -> Option<u64> {
142 let s = s.trim().trim_start_matches("0x").trim_start_matches("0X");
143 u64::from_str_radix(s, 16).ok()
144}
145
146#[cfg(feature = "probe")]
147fn probe_read_memory(chip: &str, address: u64, length: usize) -> anyhow::Result<String> {
148 use probe_rs::MemoryInterface;
149 use probe_rs::Session;
150 use probe_rs::SessionConfig;
151
152 let mut session = Session::auto_attach(chip, SessionConfig::default()).map_err(|e| {
153 ::zeroclaw_log::record!(
154 ERROR,
155 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
156 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
157 .with_attrs(::serde_json::json!({
158 "chip": chip,
159 "error": format!("{}", e),
160 })),
161 "hardware_memory_read: probe-rs auto_attach failed"
162 );
163 anyhow::Error::msg(format!("{}", e))
164 })?;
165
166 let mut core = session.core(0)?;
167 let mut buf = vec![0u8; length];
168 core.read_8(address, &mut buf).map_err(|e| {
169 ::zeroclaw_log::record!(
170 ERROR,
171 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
172 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
173 .with_attrs(::serde_json::json!({
174 "chip": chip,
175 "address": address,
176 "length": length,
177 "error": format!("{}", e),
178 })),
179 "hardware_memory_read: probe-rs read_8 failed"
180 );
181 anyhow::Error::msg(format!("{}", e))
182 })?;
183
184 let mut out = format!("Memory read from 0x{:08X} ({} bytes):\n\n", address, length);
186 const COLS: usize = 16;
187 for (i, chunk) in buf.chunks(COLS).enumerate() {
188 let addr = address + (i * COLS) as u64;
189 let hex: String = chunk
190 .iter()
191 .map(|b| format!("{:02X}", b))
192 .collect::<Vec<_>>()
193 .join(" ");
194 let ascii: String = chunk
195 .iter()
196 .map(|&b| {
197 if b.is_ascii_graphic() || b == b' ' {
198 b as char
199 } else {
200 '.'
201 }
202 })
203 .collect();
204 out.push_str(&format!("0x{:08X} {:48} {}\n", addr, hex, ascii));
205 }
206 Ok(out)
207}