Skip to main content

zeroclaw_hardware/peripherals/
mod.rs

1//! Hardware peripherals — STM32, RPi GPIO, etc.
2//!
3//! Peripherals extend the agent with physical capabilities. See
4//! `docs/hardware-peripherals-design.md` for the full design.
5
6pub mod traits;
7
8#[cfg(feature = "hardware")]
9pub mod serial;
10
11#[cfg(feature = "hardware")]
12pub mod arduino_flash;
13#[cfg(feature = "hardware")]
14pub mod arduino_upload;
15#[cfg(feature = "hardware")]
16pub mod capabilities_tool;
17#[cfg(feature = "hardware")]
18pub mod nucleo_flash;
19#[cfg(feature = "hardware")]
20pub mod smartroom;
21#[cfg(feature = "hardware")]
22pub mod uno_q_bridge;
23#[cfg(feature = "hardware")]
24pub mod uno_q_setup;
25
26#[cfg(all(feature = "peripheral-rpi", target_os = "linux"))]
27pub mod rpi;
28
29#[cfg(any(feature = "hardware", feature = "peripheral-rpi"))]
30pub use traits::Peripheral;
31
32use anyhow::Result;
33use zeroclaw_api::tool::Tool;
34use zeroclaw_config::schema::{PeripheralBoardConfig, PeripheralsConfig};
35#[cfg(feature = "hardware")]
36use zeroclaw_tools::hardware_memory_map::HardwareMemoryMapTool;
37
38/// List configured boards from config (no connection yet).
39pub fn list_configured_boards(config: &PeripheralsConfig) -> Vec<&PeripheralBoardConfig> {
40    if !config.enabled {
41        return Vec::new();
42    }
43    config.boards.iter().collect()
44}
45
46/// Create and connect peripherals from config, returning their tools.
47/// Returns empty vec if peripherals disabled or hardware feature off.
48#[cfg(feature = "hardware")]
49pub async fn create_peripheral_tools(config: &PeripheralsConfig) -> Result<Vec<Box<dyn Tool>>> {
50    if !config.enabled || config.boards.is_empty() {
51        return Ok(Vec::new());
52    }
53
54    let mut tools: Vec<Box<dyn Tool>> = Vec::new();
55    let mut serial_transports: Vec<(String, std::sync::Arc<serial::SerialTransport>)> = Vec::new();
56
57    for board in &config.boards {
58        // Arduino Uno Q: Bridge transport (socket to local Bridge app)
59        if board.transport == "bridge" && (board.board == "arduino-uno-q" || board.board == "uno-q")
60        {
61            tools.push(Box::new(uno_q_bridge::UnoQGpioReadTool));
62            tools.push(Box::new(uno_q_bridge::UnoQGpioWriteTool));
63            ::zeroclaw_log::record!(
64                INFO,
65                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
66                    .with_attrs(::serde_json::json!({"board": board.board})),
67                "Uno Q Bridge GPIO tools added"
68            );
69            continue;
70        }
71
72        // Native transport: RPi GPIO (Linux only)
73        #[cfg(all(feature = "peripheral-rpi", target_os = "linux"))]
74        if board.transport == "native"
75            && (board.board == "rpi-gpio" || board.board == "raspberry-pi")
76        {
77            match rpi::RpiGpioPeripheral::connect_from_config(board).await {
78                Ok(peripheral) => {
79                    tools.extend(peripheral.tools());
80                    ::zeroclaw_log::record!(
81                        INFO,
82                        ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
83                            .with_attrs(::serde_json::json!({"board": board.board})),
84                        "RPi GPIO peripheral connected"
85                    );
86                }
87                Err(e) => {
88                    ::zeroclaw_log::record!(
89                        WARN,
90                        ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
91                            .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
92                        &format!("Failed to connect RPi GPIO {}: {}", board.board, e)
93                    );
94                }
95            }
96            continue;
97        }
98
99        // Serial transport (STM32, ESP32, Arduino, etc.)
100        if board.transport != "serial" {
101            continue;
102        }
103        if board.path.is_none() {
104            ::zeroclaw_log::record!(
105                WARN,
106                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
107                    .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
108                &format!("Skipping serial board {}: no path", board.board)
109            );
110            continue;
111        }
112
113        match serial::SerialPeripheral::connect(board).await {
114            Ok(peripheral) => {
115                let mut p = peripheral;
116                if p.connect().await.is_err() {
117                    ::zeroclaw_log::record!(
118                        WARN,
119                        ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
120                            .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
121                        &format!("Peripheral {} connect warning (continuing)", p.name())
122                    );
123                }
124                serial_transports.push((board.board.clone(), p.transport()));
125                tools.extend(p.tools());
126                if board.board == "arduino-uno"
127                    && let Some(ref path) = board.path
128                {
129                    tools.push(Box::new(arduino_upload::ArduinoUploadTool::new(
130                        path.clone(),
131                    )));
132                    ::zeroclaw_log::record!(
133                        INFO,
134                        ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
135                        &format!("Arduino upload tool added (port: {})", path)
136                    );
137                }
138
139                // Smart-room named device tools (ESP32 / ESP32 simulator).
140                // Lets the model use device names instead of guessing pin numbers.
141                if board.board == "esp32" || board.board == "esp32-sim" {
142                    let transport = p.transport();
143                    tools.push(Box::new(smartroom::SetDeviceTool {
144                        transport: transport.clone(),
145                    }));
146                    tools.push(Box::new(smartroom::ReadDeviceTool { transport }));
147                    ::zeroclaw_log::record!(
148                        INFO,
149                        ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
150                            .with_attrs(::serde_json::json!({"board": board.board})),
151                        "Smart-room device tools added (set_device, read_device)"
152                    );
153                }
154
155                ::zeroclaw_log::record!(
156                    INFO,
157                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
158                        .with_attrs(::serde_json::json!({"board": board.board})),
159                    "Serial peripheral connected"
160                );
161            }
162            Err(e) => {
163                ::zeroclaw_log::record!(
164                    WARN,
165                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
166                        .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
167                    &format!("Failed to connect {}: {}", board.board, e)
168                );
169            }
170        }
171    }
172
173    // Phase B: Add hardware tools when any boards configured
174    if !tools.is_empty() {
175        let board_names: Vec<String> = config.boards.iter().map(|b| b.board.clone()).collect();
176        tools.push(Box::new(HardwareMemoryMapTool::new(board_names.clone())));
177        tools.push(Box::new(
178            zeroclaw_tools::hardware_board_info::HardwareBoardInfoTool::new(board_names.clone()),
179        ));
180        tools.push(Box::new(
181            zeroclaw_tools::hardware_memory_read::HardwareMemoryReadTool::new(board_names),
182        ));
183    }
184
185    // Phase C: Add hardware_capabilities tool when any serial boards
186    if !serial_transports.is_empty() {
187        tools.push(Box::new(capabilities_tool::HardwareCapabilitiesTool::new(
188            serial_transports,
189        )));
190    }
191
192    Ok(tools)
193}
194
195#[cfg(not(feature = "hardware"))]
196#[allow(clippy::unused_async)]
197pub async fn create_peripheral_tools(_config: &PeripheralsConfig) -> Result<Vec<Box<dyn Tool>>> {
198    Ok(Vec::new())
199}
200
201/// Create probe-rs / static board info tools (hardware_board_info, hardware_memory_map,
202/// hardware_memory_read). These use USB/probe-rs or static datasheet data — they never
203/// open a serial port, so they are safe to register regardless of the `hardware` feature.
204#[cfg(feature = "hardware")]
205pub fn create_board_info_tools(config: &PeripheralsConfig) -> Vec<Box<dyn Tool>> {
206    if !config.enabled || config.boards.is_empty() {
207        return Vec::new();
208    }
209    let board_names: Vec<String> = config.boards.iter().map(|b| b.board.clone()).collect();
210    vec![
211        Box::new(
212            zeroclaw_tools::hardware_memory_map::HardwareMemoryMapTool::new(board_names.clone()),
213        ),
214        Box::new(
215            zeroclaw_tools::hardware_board_info::HardwareBoardInfoTool::new(board_names.clone()),
216        ),
217        Box::new(zeroclaw_tools::hardware_memory_read::HardwareMemoryReadTool::new(board_names)),
218    ]
219}
220
221#[cfg(not(feature = "hardware"))]
222pub fn create_board_info_tools(_config: &PeripheralsConfig) -> Vec<Box<dyn Tool>> {
223    Vec::new()
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use zeroclaw_config::schema::{PeripheralBoardConfig, PeripheralsConfig};
230
231    #[test]
232    fn list_configured_boards_when_disabled_returns_empty() {
233        let config = PeripheralsConfig {
234            enabled: false,
235            boards: vec![PeripheralBoardConfig {
236                board: "nucleo-f401re".into(),
237                transport: "serial".into(),
238                path: Some("/dev/ttyACM0".into()),
239                baud: 115_200,
240            }],
241            datasheet_dir: None,
242        };
243        let result = list_configured_boards(&config);
244        assert!(
245            result.is_empty(),
246            "disabled peripherals should return no boards"
247        );
248    }
249
250    #[test]
251    fn list_configured_boards_when_enabled_with_boards() {
252        let config = PeripheralsConfig {
253            enabled: true,
254            boards: vec![
255                PeripheralBoardConfig {
256                    board: "nucleo-f401re".into(),
257                    transport: "serial".into(),
258                    path: Some("/dev/ttyACM0".into()),
259                    baud: 115_200,
260                },
261                PeripheralBoardConfig {
262                    board: "rpi-gpio".into(),
263                    transport: "native".into(),
264                    path: None,
265                    baud: 115_200,
266                },
267            ],
268            datasheet_dir: None,
269        };
270        let result = list_configured_boards(&config);
271        assert_eq!(result.len(), 2);
272        assert_eq!(result[0].board, "nucleo-f401re");
273        assert_eq!(result[1].board, "rpi-gpio");
274    }
275
276    #[test]
277    fn list_configured_boards_when_enabled_but_no_boards() {
278        let config = PeripheralsConfig {
279            enabled: true,
280            boards: vec![],
281            datasheet_dir: None,
282        };
283        let result = list_configured_boards(&config);
284        assert!(
285            result.is_empty(),
286            "enabled with no boards should return empty"
287        );
288    }
289
290    #[tokio::test]
291    async fn create_peripheral_tools_returns_empty_when_disabled() {
292        let config = PeripheralsConfig {
293            enabled: false,
294            boards: vec![],
295            datasheet_dir: None,
296        };
297        let tools = create_peripheral_tools(&config).await.unwrap();
298        assert!(
299            tools.is_empty(),
300            "disabled peripherals should produce no tools"
301        );
302    }
303}