construct/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 crate::config::{Config, PeripheralBoardConfig, PeripheralsConfig};
31#[cfg(feature = "hardware")]
32use crate::tools::HardwareMemoryMapTool;
33use crate::tools::Tool;
34use anyhow::Result;
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#[allow(clippy::module_name_repetitions)]
46pub async fn handle_command(cmd: crate::PeripheralCommands, config: &Config) -> Result<()> {
47 match cmd {
48 crate::PeripheralCommands::List => {
49 let boards = list_configured_boards(&config.peripherals);
50 if boards.is_empty() {
51 println!("No peripherals configured.");
52 println!();
53 println!("Add one with: construct peripheral add <board> <path>");
54 println!(" Example: construct peripheral add nucleo-f401re /dev/ttyACM0");
55 println!();
56 println!("Or add to config.toml:");
57 println!(" [peripherals]");
58 println!(" enabled = true");
59 println!();
60 println!(" [[peripherals.boards]]");
61 println!(" board = \"nucleo-f401re\"");
62 println!(" transport = \"serial\"");
63 println!(" path = \"/dev/ttyACM0\"");
64 } else {
65 println!("Configured peripherals:");
66 for b in boards {
67 let path = b.path.as_deref().unwrap_or("(native)");
68 println!(" {} {} {}", b.board, b.transport, path);
69 }
70 }
71 }
72 crate::PeripheralCommands::Add { board, path } => {
73 let transport = if path == "native" { "native" } else { "serial" };
74 let path_opt = if path == "native" {
75 None
76 } else {
77 Some(path.clone())
78 };
79
80 let mut cfg = Box::pin(crate::config::Config::load_or_init()).await?;
81 cfg.peripherals.enabled = true;
82
83 if cfg
84 .peripherals
85 .boards
86 .iter()
87 .any(|b| b.board == board && b.path.as_deref() == path_opt.as_deref())
88 {
89 println!("Board {} at {:?} already configured.", board, path_opt);
90 return Ok(());
91 }
92
93 cfg.peripherals.boards.push(PeripheralBoardConfig {
94 board: board.clone(),
95 transport: transport.to_string(),
96 path: path_opt,
97 baud: 115_200,
98 });
99 cfg.save().await?;
100 println!("Added {} at {}. Restart daemon to apply.", board, path);
101 }
102 #[cfg(feature = "hardware")]
103 crate::PeripheralCommands::Flash { port } => {
104 let port_str = arduino_flash::resolve_port(config, port.as_deref())
105 .or_else(|| port.clone())
106 .ok_or_else(|| anyhow::anyhow!(
107 "No port specified. Use --port /dev/cu.usbmodem* or add arduino-uno to config.toml"
108 ))?;
109 arduino_flash::flash_arduino_firmware(&port_str)?;
110 }
111 #[cfg(not(feature = "hardware"))]
112 crate::PeripheralCommands::Flash { .. } => {
113 println!("Arduino flash requires the 'hardware' feature.");
114 println!("Build with: cargo build --features hardware");
115 }
116 #[cfg(feature = "hardware")]
117 crate::PeripheralCommands::SetupUnoQ { host } => {
118 uno_q_setup::setup_uno_q_bridge(host.as_deref())?;
119 }
120 #[cfg(not(feature = "hardware"))]
121 crate::PeripheralCommands::SetupUnoQ { .. } => {
122 println!("Uno Q setup requires the 'hardware' feature.");
123 println!("Build with: cargo build --features hardware");
124 }
125 #[cfg(feature = "hardware")]
126 crate::PeripheralCommands::FlashNucleo => {
127 nucleo_flash::flash_nucleo_firmware()?;
128 }
129 #[cfg(not(feature = "hardware"))]
130 crate::PeripheralCommands::FlashNucleo => {
131 println!("Nucleo flash requires the 'hardware' feature.");
132 println!("Build with: cargo build --features hardware");
133 }
134 }
135 Ok(())
136}
137
138#[cfg(feature = "hardware")]
141pub async fn create_peripheral_tools(config: &PeripheralsConfig) -> Result<Vec<Box<dyn Tool>>> {
142 if !config.enabled || config.boards.is_empty() {
143 return Ok(Vec::new());
144 }
145
146 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
147 let mut serial_transports: Vec<(String, std::sync::Arc<serial::SerialTransport>)> = Vec::new();
148
149 for board in &config.boards {
150 if board.transport == "bridge" && (board.board == "arduino-uno-q" || board.board == "uno-q")
152 {
153 tools.push(Box::new(uno_q_bridge::UnoQGpioReadTool));
154 tools.push(Box::new(uno_q_bridge::UnoQGpioWriteTool));
155 tracing::info!(board = %board.board, "Uno Q Bridge GPIO tools added");
156 continue;
157 }
158
159 #[cfg(all(feature = "peripheral-rpi", target_os = "linux"))]
161 if board.transport == "native"
162 && (board.board == "rpi-gpio" || board.board == "raspberry-pi")
163 {
164 match rpi::RpiGpioPeripheral::connect_from_config(board).await {
165 Ok(peripheral) => {
166 tools.extend(peripheral.tools());
167 tracing::info!(board = %board.board, "RPi GPIO peripheral connected");
168 }
169 Err(e) => {
170 tracing::warn!("Failed to connect RPi GPIO {}: {}", board.board, e);
171 }
172 }
173 continue;
174 }
175
176 if board.transport != "serial" {
178 continue;
179 }
180 if board.path.is_none() {
181 tracing::warn!("Skipping serial board {}: no path", board.board);
182 continue;
183 }
184
185 match serial::SerialPeripheral::connect(board).await {
186 Ok(peripheral) => {
187 let mut p = peripheral;
188 if p.connect().await.is_err() {
189 tracing::warn!("Peripheral {} connect warning (continuing)", p.name());
190 }
191 serial_transports.push((board.board.clone(), p.transport()));
192 tools.extend(p.tools());
193 if board.board == "arduino-uno" {
194 if let Some(ref path) = board.path {
195 tools.push(Box::new(arduino_upload::ArduinoUploadTool::new(
196 path.clone(),
197 )));
198 tracing::info!("Arduino upload tool added (port: {})", path);
199 }
200 }
201 tracing::info!(board = %board.board, "Serial peripheral connected");
202 }
203 Err(e) => {
204 tracing::warn!("Failed to connect {}: {}", board.board, e);
205 }
206 }
207 }
208
209 if !tools.is_empty() {
211 let board_names: Vec<String> = config.boards.iter().map(|b| b.board.clone()).collect();
212 tools.push(Box::new(HardwareMemoryMapTool::new(board_names.clone())));
213 tools.push(Box::new(crate::tools::HardwareBoardInfoTool::new(
214 board_names.clone(),
215 )));
216 tools.push(Box::new(crate::tools::HardwareMemoryReadTool::new(
217 board_names,
218 )));
219 }
220
221 if !serial_transports.is_empty() {
223 tools.push(Box::new(capabilities_tool::HardwareCapabilitiesTool::new(
224 serial_transports,
225 )));
226 }
227
228 Ok(tools)
229}
230
231#[cfg(not(feature = "hardware"))]
232#[allow(clippy::unused_async)]
233pub async fn create_peripheral_tools(_config: &PeripheralsConfig) -> Result<Vec<Box<dyn Tool>>> {
234 Ok(Vec::new())
235}
236
237#[cfg(feature = "hardware")]
241pub fn create_board_info_tools(config: &PeripheralsConfig) -> Vec<Box<dyn Tool>> {
242 if !config.enabled || config.boards.is_empty() {
243 return Vec::new();
244 }
245 let board_names: Vec<String> = config.boards.iter().map(|b| b.board.clone()).collect();
246 vec![
247 Box::new(crate::tools::HardwareMemoryMapTool::new(
248 board_names.clone(),
249 )),
250 Box::new(crate::tools::HardwareBoardInfoTool::new(
251 board_names.clone(),
252 )),
253 Box::new(crate::tools::HardwareMemoryReadTool::new(board_names)),
254 ]
255}
256
257#[cfg(not(feature = "hardware"))]
258pub fn create_board_info_tools(_config: &PeripheralsConfig) -> Vec<Box<dyn Tool>> {
259 Vec::new()
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::config::{PeripheralBoardConfig, PeripheralsConfig};
266
267 #[test]
268 fn list_configured_boards_when_disabled_returns_empty() {
269 let config = PeripheralsConfig {
270 enabled: false,
271 boards: vec![PeripheralBoardConfig {
272 board: "nucleo-f401re".into(),
273 transport: "serial".into(),
274 path: Some("/dev/ttyACM0".into()),
275 baud: 115_200,
276 }],
277 datasheet_dir: None,
278 };
279 let result = list_configured_boards(&config);
280 assert!(
281 result.is_empty(),
282 "disabled peripherals should return no boards"
283 );
284 }
285
286 #[test]
287 fn list_configured_boards_when_enabled_with_boards() {
288 let config = PeripheralsConfig {
289 enabled: true,
290 boards: vec![
291 PeripheralBoardConfig {
292 board: "nucleo-f401re".into(),
293 transport: "serial".into(),
294 path: Some("/dev/ttyACM0".into()),
295 baud: 115_200,
296 },
297 PeripheralBoardConfig {
298 board: "rpi-gpio".into(),
299 transport: "native".into(),
300 path: None,
301 baud: 115_200,
302 },
303 ],
304 datasheet_dir: None,
305 };
306 let result = list_configured_boards(&config);
307 assert_eq!(result.len(), 2);
308 assert_eq!(result[0].board, "nucleo-f401re");
309 assert_eq!(result[1].board, "rpi-gpio");
310 }
311
312 #[test]
313 fn list_configured_boards_when_enabled_but_no_boards() {
314 let config = PeripheralsConfig {
315 enabled: true,
316 boards: vec![],
317 datasheet_dir: None,
318 };
319 let result = list_configured_boards(&config);
320 assert!(
321 result.is_empty(),
322 "enabled with no boards should return empty"
323 );
324 }
325
326 #[tokio::test]
327 async fn create_peripheral_tools_returns_empty_when_disabled() {
328 let config = PeripheralsConfig {
329 enabled: false,
330 boards: vec![],
331 datasheet_dir: None,
332 };
333 let tools = create_peripheral_tools(&config).await.unwrap();
334 assert!(
335 tools.is_empty(),
336 "disabled peripherals should produce no tools"
337 );
338 }
339}