1use agentzero_core::{Tool, ToolContext, ToolResult};
2use anyhow::{anyhow, Context};
3use async_trait::async_trait;
4use serde::Deserialize;
5use serde_json::json;
6
7#[derive(Debug, Deserialize)]
10struct BoardInfoInput {
11 #[serde(default)]
12 board: Option<String>,
13}
14
15#[derive(Debug, Default, Clone, Copy)]
21pub struct HardwareBoardInfoTool;
22
23#[async_trait]
24impl Tool for HardwareBoardInfoTool {
25 fn name(&self) -> &'static str {
26 "hardware_board_info"
27 }
28
29 fn description(&self) -> &'static str {
30 "List discovered hardware boards or get detailed info for a specific board."
31 }
32
33 fn input_schema(&self) -> Option<serde_json::Value> {
34 Some(json!({
35 "type": "object",
36 "properties": {
37 "board": { "type": "string", "description": "Optional board ID to get detailed info for. Omit to list all boards." }
38 },
39 "additionalProperties": false
40 }))
41 }
42
43 async fn execute(&self, input: &str, _ctx: &ToolContext) -> anyhow::Result<ToolResult> {
44 let req: BoardInfoInput = serde_json::from_str(input)
45 .context("hardware_board_info expects JSON: {\"board\"?}")?;
46
47 match req.board {
48 Some(id) => {
49 if id.trim().is_empty() {
50 return Err(anyhow!("board id must not be empty"));
51 }
52 let board = crate::hardware::board_info(&id)?;
53 let output = json!({
54 "id": board.id,
55 "display_name": board.display_name,
56 "architecture": board.architecture,
57 "memory_kb": board.memory_kb,
58 })
59 .to_string();
60 Ok(ToolResult { output })
61 }
62 None => {
63 let boards = crate::hardware::discover_boards();
64 let entries: Vec<serde_json::Value> = boards
65 .iter()
66 .map(|b| {
67 json!({
68 "id": b.id,
69 "display_name": b.display_name,
70 "architecture": b.architecture,
71 "memory_kb": b.memory_kb,
72 })
73 })
74 .collect();
75 Ok(ToolResult {
76 output: serde_json::to_string_pretty(&entries)
77 .unwrap_or_else(|_| "[]".to_string()),
78 })
79 }
80 }
81 }
82}
83
84#[derive(Debug, Deserialize)]
87struct MemoryMapInput {
88 board: String,
89}
90
91#[derive(Debug, Default, Clone, Copy)]
95pub struct HardwareMemoryMapTool;
96
97#[async_trait]
98impl Tool for HardwareMemoryMapTool {
99 fn name(&self) -> &'static str {
100 "hardware_memory_map"
101 }
102
103 fn description(&self) -> &'static str {
104 "Get the flash and RAM memory map layout for a hardware board."
105 }
106
107 fn input_schema(&self) -> Option<serde_json::Value> {
108 Some(json!({
109 "type": "object",
110 "properties": {
111 "board": { "type": "string", "description": "The board ID to get the memory map for" }
112 },
113 "required": ["board"],
114 "additionalProperties": false
115 }))
116 }
117
118 async fn execute(&self, input: &str, _ctx: &ToolContext) -> anyhow::Result<ToolResult> {
119 let req: MemoryMapInput =
120 serde_json::from_str(input).context("hardware_memory_map expects JSON: {\"board\"}")?;
121
122 if req.board.trim().is_empty() {
123 return Err(anyhow!("board must not be empty"));
124 }
125
126 crate::hardware::board_info(&req.board)?;
128
129 let map = memory_map_for(&req.board);
130 Ok(ToolResult {
131 output: serde_json::to_string_pretty(&map).unwrap_or_else(|_| "{}".to_string()),
132 })
133 }
134}
135
136fn memory_map_for(board_id: &str) -> serde_json::Value {
137 match board_id {
138 "sim-stm32" => json!({
139 "board": "sim-stm32",
140 "regions": [
141 {"name": "flash", "start": "0x08000000", "end": "0x0803FFFF", "size_kb": 256, "access": "rx"},
142 {"name": "sram", "start": "0x20000000", "end": "0x2000FFFF", "size_kb": 64, "access": "rwx"},
143 {"name": "peripherals", "start": "0x40000000", "end": "0x5FFFFFFF", "size_kb": null, "access": "rw"},
144 ]
145 }),
146 "sim-rpi" => json!({
147 "board": "sim-rpi",
148 "regions": [
149 {"name": "sdram", "start": "0x00000000", "end": "0x3FFFFFFF", "size_kb": 1048576, "access": "rwx"},
150 {"name": "peripherals", "start": "0xFE000000", "end": "0xFEFFFFFF", "size_kb": null, "access": "rw"},
151 {"name": "gpu_memory", "start": "0xC0000000", "end": "0xFFFFFFFF", "size_kb": null, "access": "rw"},
152 ]
153 }),
154 _ => json!({
155 "board": board_id,
156 "regions": [],
157 "note": "no memory map available for this board"
158 }),
159 }
160}
161
162#[derive(Debug, Deserialize)]
165struct MemoryReadInput {
166 board: String,
167 address: String,
168 #[serde(default = "default_read_length")]
169 length: usize,
170}
171
172fn default_read_length() -> usize {
173 64
174}
175
176#[derive(Debug, Default, Clone, Copy)]
181pub struct HardwareMemoryReadTool;
182
183#[async_trait]
184impl Tool for HardwareMemoryReadTool {
185 fn name(&self) -> &'static str {
186 "hardware_memory_read"
187 }
188
189 fn description(&self) -> &'static str {
190 "Read memory from a hardware board at a given address."
191 }
192
193 fn input_schema(&self) -> Option<serde_json::Value> {
194 Some(json!({
195 "type": "object",
196 "properties": {
197 "board": { "type": "string", "description": "The board ID to read memory from" },
198 "address": { "type": "string", "description": "Hex address to read (e.g. 0x20000000)" },
199 "length": { "type": "integer", "description": "Number of bytes to read (1-256, default 64)" }
200 },
201 "required": ["board", "address"],
202 "additionalProperties": false
203 }))
204 }
205
206 async fn execute(&self, input: &str, _ctx: &ToolContext) -> anyhow::Result<ToolResult> {
207 let req: MemoryReadInput = serde_json::from_str(input)
208 .context("hardware_memory_read expects JSON: {\"board\", \"address\", \"length\"?}")?;
209
210 if req.board.trim().is_empty() {
211 return Err(anyhow!("board must not be empty"));
212 }
213 if req.address.trim().is_empty() {
214 return Err(anyhow!("address must not be empty"));
215 }
216
217 crate::hardware::board_info(&req.board)?;
219
220 let addr_str = req
222 .address
223 .trim_start_matches("0x")
224 .trim_start_matches("0X");
225 let address = u64::from_str_radix(addr_str, 16)
226 .map_err(|_| anyhow!("invalid hex address: {}", req.address))?;
227
228 let length = req.length.clamp(1, 256);
229
230 let data: Vec<u8> = (0..length)
232 .map(|i| ((address.wrapping_add(i as u64)) & 0xFF) as u8)
233 .collect();
234
235 let hex_dump = format_hex_dump(address, &data);
236
237 let output = json!({
238 "board": req.board,
239 "address": format!("0x{:08X}", address),
240 "length": length,
241 "mode": "simulated",
242 "hex_dump": hex_dump,
243 })
244 .to_string();
245
246 Ok(ToolResult { output })
247 }
248}
249
250fn format_hex_dump(base_addr: u64, data: &[u8]) -> String {
251 let mut lines = Vec::new();
252 for (i, chunk) in data.chunks(16).enumerate() {
253 let addr = base_addr + (i * 16) as u64;
254 let hex: Vec<String> = chunk.iter().map(|b| format!("{b:02X}")).collect();
255 let ascii: String = chunk
256 .iter()
257 .map(|&b| {
258 if b.is_ascii_graphic() || b == b' ' {
259 b as char
260 } else {
261 '.'
262 }
263 })
264 .collect();
265 lines.push(format!("{addr:08X} {:<48} |{ascii}|", hex.join(" ")));
266 }
267 lines.join("\n")
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use agentzero_core::ToolContext;
274
275 fn test_ctx() -> ToolContext {
276 ToolContext::new("/tmp".to_string())
277 }
278
279 #[tokio::test]
282 async fn board_info_list_all() {
283 let tool = HardwareBoardInfoTool;
284 let result = tool
285 .execute(r#"{}"#, &test_ctx())
286 .await
287 .expect("list should succeed");
288 assert!(result.output.contains("sim-stm32"));
289 assert!(result.output.contains("sim-rpi"));
290 }
291
292 #[tokio::test]
293 async fn board_info_specific_board() {
294 let tool = HardwareBoardInfoTool;
295 let result = tool
296 .execute(r#"{"board": "sim-stm32"}"#, &test_ctx())
297 .await
298 .expect("should succeed");
299 let v: serde_json::Value = serde_json::from_str(&result.output).unwrap();
300 assert_eq!(v["id"], "sim-stm32");
301 assert_eq!(v["architecture"], "arm-cortex-m");
302 }
303
304 #[tokio::test]
305 async fn board_info_unknown_board_fails() {
306 let tool = HardwareBoardInfoTool;
307 let err = tool
308 .execute(r#"{"board": "nonexistent"}"#, &test_ctx())
309 .await
310 .expect_err("unknown board should fail");
311 assert!(err.to_string().contains("unknown hardware board"));
312 }
313
314 #[tokio::test]
317 async fn memory_map_stm32() {
318 let tool = HardwareMemoryMapTool;
319 let result = tool
320 .execute(r#"{"board": "sim-stm32"}"#, &test_ctx())
321 .await
322 .expect("should succeed");
323 let v: serde_json::Value = serde_json::from_str(&result.output).unwrap();
324 assert_eq!(v["board"], "sim-stm32");
325 let regions = v["regions"].as_array().unwrap();
326 assert!(!regions.is_empty());
327 assert!(regions.iter().any(|r| r["name"] == "flash"));
328 assert!(regions.iter().any(|r| r["name"] == "sram"));
329 }
330
331 #[tokio::test]
332 async fn memory_map_unknown_board_fails() {
333 let tool = HardwareMemoryMapTool;
334 let err = tool
335 .execute(r#"{"board": "no-such-board"}"#, &test_ctx())
336 .await
337 .expect_err("unknown board should fail");
338 assert!(err.to_string().contains("unknown hardware board"));
339 }
340
341 #[tokio::test]
342 async fn memory_map_empty_board_fails() {
343 let tool = HardwareMemoryMapTool;
344 let err = tool
345 .execute(r#"{"board": ""}"#, &test_ctx())
346 .await
347 .expect_err("empty board should fail");
348 assert!(err.to_string().contains("board must not be empty"));
349 }
350
351 #[tokio::test]
354 async fn memory_read_success() {
355 let tool = HardwareMemoryReadTool;
356 let result = tool
357 .execute(
358 r#"{"board": "sim-stm32", "address": "0x20000000", "length": 32}"#,
359 &test_ctx(),
360 )
361 .await
362 .expect("should succeed");
363 let v: serde_json::Value = serde_json::from_str(&result.output).unwrap();
364 assert_eq!(v["board"], "sim-stm32");
365 assert_eq!(v["mode"], "simulated");
366 assert_eq!(v["length"], 32);
367 assert!(v["hex_dump"].as_str().unwrap().contains("20000000"));
368 }
369
370 #[tokio::test]
371 async fn memory_read_invalid_hex_fails() {
372 let tool = HardwareMemoryReadTool;
373 let err = tool
374 .execute(
375 r#"{"board": "sim-stm32", "address": "not_hex"}"#,
376 &test_ctx(),
377 )
378 .await
379 .expect_err("invalid hex should fail");
380 assert!(err.to_string().contains("invalid hex address"));
381 }
382
383 #[tokio::test]
384 async fn memory_read_unknown_board_fails() {
385 let tool = HardwareMemoryReadTool;
386 let err = tool
387 .execute(r#"{"board": "fake", "address": "0x00"}"#, &test_ctx())
388 .await
389 .expect_err("unknown board should fail");
390 assert!(err.to_string().contains("unknown hardware board"));
391 }
392
393 #[tokio::test]
394 async fn memory_read_clamps_length() {
395 let tool = HardwareMemoryReadTool;
396 let result = tool
397 .execute(
398 r#"{"board": "sim-stm32", "address": "0x08000000", "length": 9999}"#,
399 &test_ctx(),
400 )
401 .await
402 .expect("should clamp, not fail");
403 let v: serde_json::Value = serde_json::from_str(&result.output).unwrap();
404 assert_eq!(v["length"], 256);
405 }
406}