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