Skip to main content

agentzero_tools/
hardware_tools.rs

1use agentzero_core::{Tool, ToolContext, ToolResult};
2use anyhow::{anyhow, Context};
3use async_trait::async_trait;
4use serde::Deserialize;
5use serde_json::json;
6
7// --- hardware_board_info ---
8
9#[derive(Debug, Deserialize)]
10struct BoardInfoInput {
11    #[serde(default)]
12    board: Option<String>,
13}
14
15/// Query connected board information.
16///
17/// Operations:
18/// - With no `board`: list all discovered boards
19/// - With `board`: get detailed info for a specific board ID
20#[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// --- hardware_memory_map ---
85
86#[derive(Debug, Deserialize)]
87struct MemoryMapInput {
88    board: String,
89}
90
91/// Read hardware memory map layout for a board.
92///
93/// Returns flash and RAM address ranges based on known datasheets.
94#[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        // Validate the board exists
127        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// --- hardware_memory_read ---
163
164#[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/// Read hardware memory at a given address.
177///
178/// In simulation mode, returns representative data for the requested region.
179/// With real hardware (future), reads via debug probe.
180#[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        // Validate board exists
218        crate::hardware::board_info(&req.board)?;
219
220        // Parse hex address
221        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        // Simulated read: produce deterministic data based on address
231        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    // --- board info tests ---
280
281    #[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    // --- memory map tests ---
315
316    #[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    // --- memory read tests ---
352
353    #[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}