zeroclaw_tools/
hardware_memory_map.rs1use async_trait::async_trait;
8use serde_json::json;
9use zeroclaw_api::tool::{Tool, ToolResult};
10
11const MEMORY_MAPS: &[(&str, &str)] = &[
13 (
14 "nucleo-f401re",
15 "Flash: 0x0800_0000 - 0x0807_FFFF (512 KB)\nRAM: 0x2000_0000 - 0x2001_FFFF (128 KB)\nSTM32F401RET6, ARM Cortex-M4",
16 ),
17 (
18 "nucleo-f411re",
19 "Flash: 0x0800_0000 - 0x0807_FFFF (512 KB)\nRAM: 0x2000_0000 - 0x2001_FFFF (128 KB)\nSTM32F411RET6, ARM Cortex-M4",
20 ),
21 (
22 "arduino-uno",
23 "Flash: 0x0000 - 0x3FFF (16 KB, ATmega328P)\nSRAM: 0x0100 - 0x08FF (2 KB)\nEEPROM: 0x0000 - 0x03FF (1 KB)",
24 ),
25 (
26 "arduino-mega",
27 "Flash: 0x0000 - 0x3FFFF (256 KB, ATmega2560)\nSRAM: 0x0200 - 0x21FF (8 KB)\nEEPROM: 0x0000 - 0x0FFF (4 KB)",
28 ),
29 (
30 "esp32",
31 "Flash: 0x3F40_0000 - 0x3F7F_FFFF (4 MB typical)\nIRAM: 0x4000_0000 - 0x4005_FFFF\nDRAM: 0x3FFB_0000 - 0x3FFF_FFFF",
32 ),
33];
34
35pub struct HardwareMemoryMapTool {
37 boards: Vec<String>,
38}
39
40impl HardwareMemoryMapTool {
41 pub fn new(boards: Vec<String>) -> Self {
42 Self { boards }
43 }
44
45 fn static_map_for_board(&self, board: &str) -> Option<&'static str> {
46 MEMORY_MAPS
47 .iter()
48 .find(|(b, _)| *b == board)
49 .map(|(_, m)| *m)
50 }
51}
52
53#[async_trait]
54impl Tool for HardwareMemoryMapTool {
55 fn name(&self) -> &str {
56 "hardware_memory_map"
57 }
58
59 fn description(&self) -> &str {
60 "Return the memory map (flash and RAM address ranges) for connected hardware. Use when: user asks for 'upper and lower memory addresses', 'memory map', 'address space', or 'readable addresses'. Returns flash/RAM ranges from datasheets."
61 }
62
63 fn parameters_schema(&self) -> serde_json::Value {
64 json!({
65 "type": "object",
66 "properties": {
67 "board": {
68 "type": "string",
69 "description": "Optional board name (e.g. nucleo-f401re, arduino-uno). If omitted, returns map for first configured board."
70 }
71 }
72 })
73 }
74
75 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
76 let board = args
77 .get("board")
78 .and_then(|v| v.as_str())
79 .map(String::from)
80 .or_else(|| self.boards.first().cloned());
81
82 let board = board.as_deref().unwrap_or("unknown");
83
84 if self.boards.is_empty() {
85 return Ok(ToolResult {
86 success: false,
87 output: String::new(),
88 error: Some(
89 "No peripherals configured. Add boards to config.toml [peripherals.boards]."
90 .into(),
91 ),
92 });
93 }
94
95 let mut output = String::new();
96
97 #[cfg(feature = "probe")]
98 let probe_ok = {
99 if board == "nucleo-f401re" || board == "nucleo-f411re" {
100 let chip = if board == "nucleo-f411re" {
101 "STM32F411RETx"
102 } else {
103 "STM32F401RETx"
104 };
105 match probe_rs_memory_map(chip) {
106 Ok(probe_msg) => {
107 output.push_str(&format!("**{}** (via probe-rs):\n{}\n", board, probe_msg));
108 true
109 }
110 Err(e) => {
111 output.push_str(&format!("Probe-rs failed: {}. ", e));
112 false
113 }
114 }
115 } else {
116 false
117 }
118 };
119
120 #[cfg(not(feature = "probe"))]
121 let probe_ok = false;
122
123 if !probe_ok {
124 if let Some(map) = self.static_map_for_board(board) {
125 use std::fmt::Write;
126 let _ = write!(output, "**{board}** (from datasheet):\n{map}");
127 } else {
128 use std::fmt::Write;
129 let known: Vec<&str> = MEMORY_MAPS.iter().map(|(b, _)| *b).collect();
130 let _ = write!(
131 output,
132 "No memory map for board '{board}'. Known boards: {}",
133 known.join(", ")
134 );
135 }
136 }
137
138 Ok(ToolResult {
139 success: true,
140 output,
141 error: None,
142 })
143 }
144}
145
146#[cfg(feature = "probe")]
147fn probe_rs_memory_map(chip: &str) -> anyhow::Result<String> {
148 use probe_rs::config::MemoryRegion;
149 use probe_rs::{Session, SessionConfig};
150
151 let session = Session::auto_attach(chip, SessionConfig::default()).map_err(|e| {
152 ::zeroclaw_log::record!(
153 ERROR,
154 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
155 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
156 .with_attrs(::serde_json::json!({
157 "chip": chip,
158 "error": format!("{}", e),
159 })),
160 "hardware_memory_map: probe-rs attach failed"
161 );
162 anyhow::Error::msg(format!("probe-rs attach failed: {}", e))
163 })?;
164
165 let target = session.target();
166 let mut out = String::new();
167
168 for region in target.memory_map.iter() {
169 match region {
170 MemoryRegion::Ram(ram) => {
171 let start = ram.range.start;
172 let end = ram.range.end;
173 let size_kb = (end - start) / 1024;
174 out.push_str(&format!(
175 "RAM: 0x{:08X} - 0x{:08X} ({} KB)\n",
176 start, end, size_kb
177 ));
178 }
179 MemoryRegion::Nvm(flash) => {
180 let start = flash.range.start;
181 let end = flash.range.end;
182 let size_kb = (end - start) / 1024;
183 out.push_str(&format!(
184 "Flash: 0x{:08X} - 0x{:08X} ({} KB)\n",
185 start, end, size_kb
186 ));
187 }
188 _ => {}
189 }
190 }
191
192 if out.is_empty() {
193 out = "Could not read memory regions from probe.".to_string();
194 }
195
196 Ok(out)
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn static_map_nucleo() {
205 let tool = HardwareMemoryMapTool::new(vec!["nucleo-f401re".into()]);
206 assert!(tool.static_map_for_board("nucleo-f401re").is_some());
207 assert!(
208 tool.static_map_for_board("nucleo-f401re")
209 .unwrap()
210 .contains("Flash")
211 );
212 }
213
214 #[test]
215 fn static_map_arduino() {
216 let tool = HardwareMemoryMapTool::new(vec!["arduino-uno".into()]);
217 assert!(tool.static_map_for_board("arduino-uno").is_some());
218 }
219}