zeroclaw_hardware/peripherals/
mod.rs1pub 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
38pub 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#[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 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 #[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 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 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 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 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#[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}