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 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
36pub 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#[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 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 #[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 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 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 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#[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}