1use super::device::DeviceRegistry;
20use super::protocol::ZcCommand;
21use async_trait::async_trait;
22use serde_json::json;
23use std::sync::Arc;
24use tokio::sync::RwLock;
25use zeroclaw_api::attribution::ToolKind;
26use zeroclaw_api::tool::{Tool, ToolResult};
27use zeroclaw_api::tool_attribution;
28
29tool_attribution!(GpioWriteTool, ToolKind::Plugin);
30tool_attribution!(GpioReadTool, ToolKind::Plugin);
31
32pub struct GpioWriteTool {
40 registry: Arc<RwLock<DeviceRegistry>>,
41}
42
43impl GpioWriteTool {
44 pub fn new(registry: Arc<RwLock<DeviceRegistry>>) -> Self {
45 Self { registry }
46 }
47}
48
49#[async_trait]
50impl Tool for GpioWriteTool {
51 fn name(&self) -> &str {
52 "gpio_write"
53 }
54
55 fn description(&self) -> &str {
56 "Set a GPIO pin HIGH (1) or LOW (0) on a connected hardware device"
57 }
58
59 fn parameters_schema(&self) -> serde_json::Value {
60 json!({
61 "type": "object",
62 "properties": {
63 "device": {
64 "type": "string",
65 "description": "Device alias e.g. pico0, arduino0"
66 },
67 "pin": {
68 "type": "integer",
69 "description": "GPIO pin number"
70 },
71 "value": {
72 "type": "integer",
73 "enum": [0, 1],
74 "description": "1 = HIGH (on), 0 = LOW (off)"
75 }
76 },
77 "required": ["pin", "value"]
78 })
79 }
80
81 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
82 let pin = match args.get("pin").and_then(|v| v.as_u64()) {
83 Some(p) => p,
84 None => {
85 return Ok(ToolResult {
86 success: false,
87 output: String::new(),
88 error: Some("missing required parameter: pin".to_string()),
89 });
90 }
91 };
92 let value = match args.get("value").and_then(|v| v.as_u64()) {
93 Some(v) => v,
94 None => {
95 return Ok(ToolResult {
96 success: false,
97 output: String::new(),
98 error: Some("missing required parameter: value".to_string()),
99 });
100 }
101 };
102
103 if value > 1 {
104 return Ok(ToolResult {
105 success: false,
106 output: String::new(),
107 error: Some("value must be 0 or 1".to_string()),
108 });
109 }
110
111 let (device_alias, ctx) = {
114 let registry = self.registry.read().await;
115 match registry.resolve_gpio_device(&args) {
116 Ok(resolved) => resolved,
117 Err(msg) => {
118 return Ok(ToolResult {
119 success: false,
120 output: String::new(),
121 error: Some(msg),
122 });
123 }
124 }
125 };
127
128 let cmd = ZcCommand::new("gpio_write", json!({ "pin": pin, "value": value }));
129
130 match ctx.transport.send(&cmd).await {
131 Ok(resp) if resp.ok => {
132 let state = resp
133 .data
134 .get("state")
135 .and_then(|v| v.as_str())
136 .unwrap_or(if value == 1 { "HIGH" } else { "LOW" });
137 Ok(ToolResult {
138 success: true,
139 output: format!("GPIO {} set {} on {}", pin, state, device_alias),
140 error: None,
141 })
142 }
143 Ok(resp) => Ok(ToolResult {
144 success: false,
145 output: String::new(),
146 error: Some(
147 resp.error
148 .unwrap_or_else(|| "device returned ok:false".to_string()),
149 ),
150 }),
151 Err(e) => Ok(ToolResult {
152 success: false,
153 output: String::new(),
154 error: Some(format!("transport error: {}", e)),
155 }),
156 }
157 }
158}
159
160pub struct GpioReadTool {
167 registry: Arc<RwLock<DeviceRegistry>>,
168}
169
170impl GpioReadTool {
171 pub fn new(registry: Arc<RwLock<DeviceRegistry>>) -> Self {
172 Self { registry }
173 }
174}
175
176#[async_trait]
177impl Tool for GpioReadTool {
178 fn name(&self) -> &str {
179 "gpio_read"
180 }
181
182 fn description(&self) -> &str {
183 "Read the current HIGH/LOW state of a GPIO pin on a connected device"
184 }
185
186 fn parameters_schema(&self) -> serde_json::Value {
187 json!({
188 "type": "object",
189 "properties": {
190 "device": {
191 "type": "string",
192 "description": "Device alias e.g. pico0, arduino0"
193 },
194 "pin": {
195 "type": "integer",
196 "description": "GPIO pin number to read"
197 }
198 },
199 "required": ["pin"]
200 })
201 }
202
203 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
204 let pin = match args.get("pin").and_then(|v| v.as_u64()) {
205 Some(p) => p,
206 None => {
207 return Ok(ToolResult {
208 success: false,
209 output: String::new(),
210 error: Some("missing required parameter: pin".to_string()),
211 });
212 }
213 };
214
215 let (device_alias, ctx) = {
218 let registry = self.registry.read().await;
219 match registry.resolve_gpio_device(&args) {
220 Ok(resolved) => resolved,
221 Err(msg) => {
222 return Ok(ToolResult {
223 success: false,
224 output: String::new(),
225 error: Some(msg),
226 });
227 }
228 }
229 };
231
232 let cmd = ZcCommand::new("gpio_read", json!({ "pin": pin }));
233
234 match ctx.transport.send(&cmd).await {
235 Ok(resp) if resp.ok => {
236 let value = resp.data.get("value").and_then(|v| v.as_u64()).unwrap_or(0);
237 let state = resp
238 .data
239 .get("state")
240 .and_then(|v| v.as_str())
241 .unwrap_or(if value == 1 { "HIGH" } else { "LOW" });
242 Ok(ToolResult {
243 success: true,
244 output: format!("GPIO {} is {} ({}) on {}", pin, state, value, device_alias),
245 error: None,
246 })
247 }
248 Ok(resp) => Ok(ToolResult {
249 success: false,
250 output: String::new(),
251 error: Some(
252 resp.error
253 .unwrap_or_else(|| "device returned ok:false".to_string()),
254 ),
255 }),
256 Err(e) => Ok(ToolResult {
257 success: false,
258 output: String::new(),
259 error: Some(format!("transport error: {}", e)),
260 }),
261 }
262 }
263}
264
265pub fn gpio_tools(registry: Arc<RwLock<DeviceRegistry>>) -> Vec<Box<dyn Tool>> {
272 vec![
273 Box::new(GpioWriteTool::new(registry.clone())),
274 Box::new(GpioReadTool::new(registry)),
275 ]
276}
277
278#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::{
284 device::{DeviceCapabilities, DeviceRegistry},
285 protocol::ZcResponse,
286 transport::{Transport, TransportError, TransportKind},
287 };
288 use std::sync::atomic::{AtomicBool, Ordering};
289
290 struct MockTransport {
292 response: tokio::sync::Mutex<ZcResponse>,
293 connected: AtomicBool,
294 last_cmd: tokio::sync::Mutex<Option<ZcCommand>>,
295 }
296
297 impl MockTransport {
298 fn new(response: ZcResponse) -> Self {
299 Self {
300 response: tokio::sync::Mutex::new(response),
301 connected: AtomicBool::new(true),
302 last_cmd: tokio::sync::Mutex::new(None),
303 }
304 }
305
306 fn disconnected() -> Self {
307 let t = Self::new(ZcResponse::error("mock: disconnected"));
308 t.connected.store(false, Ordering::SeqCst);
309 t
310 }
311
312 async fn last_command(&self) -> Option<ZcCommand> {
313 self.last_cmd.lock().await.clone()
314 }
315 }
316
317 #[async_trait]
318 impl Transport for MockTransport {
319 async fn send(&self, cmd: &ZcCommand) -> Result<ZcResponse, TransportError> {
320 if !self.connected.load(Ordering::SeqCst) {
321 return Err(TransportError::Disconnected);
322 }
323 *self.last_cmd.lock().await = Some(cmd.clone());
324 Ok(self.response.lock().await.clone())
325 }
326
327 fn kind(&self) -> TransportKind {
328 TransportKind::Serial
329 }
330
331 fn is_connected(&self) -> bool {
332 self.connected.load(Ordering::SeqCst)
333 }
334 }
335
336 fn registry_with_mock(transport: Arc<MockTransport>) -> Arc<RwLock<DeviceRegistry>> {
338 let mut reg = DeviceRegistry::new();
339 let alias = reg.register(
340 "raspberry-pi-pico",
341 Some(0x2e8a),
342 Some(0x000a),
343 Some("/dev/ttyACM0".to_string()),
344 Some("ARM Cortex-M0+".to_string()),
345 );
346 reg.attach_transport(
347 &alias,
348 transport as Arc<dyn Transport>,
349 DeviceCapabilities {
350 gpio: true,
351 ..Default::default()
352 },
353 )
354 .expect("alias was just registered");
355 Arc::new(RwLock::new(reg))
356 }
357
358 #[tokio::test]
361 async fn gpio_write_success() {
362 let mock = Arc::new(MockTransport::new(ZcResponse::success(
363 json!({"pin": 25, "value": 1, "state": "HIGH"}),
364 )));
365 let reg = registry_with_mock(mock.clone());
366 let tool = GpioWriteTool::new(reg);
367
368 let result = tool
369 .execute(json!({"device": "pico0", "pin": 25, "value": 1}))
370 .await
371 .unwrap();
372
373 assert!(result.success);
374 assert_eq!(result.output, "GPIO 25 set HIGH on pico0");
375 assert!(result.error.is_none());
376
377 let cmd = mock.last_command().await.unwrap();
379 assert_eq!(cmd.cmd, "gpio_write");
380 assert_eq!(cmd.params["pin"], 25);
381 assert_eq!(cmd.params["value"], 1);
382 }
383
384 #[tokio::test]
385 async fn gpio_write_low() {
386 let mock = Arc::new(MockTransport::new(ZcResponse::success(
387 json!({"pin": 13, "value": 0, "state": "LOW"}),
388 )));
389 let reg = registry_with_mock(mock.clone());
390 let tool = GpioWriteTool::new(reg);
391
392 let result = tool
393 .execute(json!({"device": "pico0", "pin": 13, "value": 0}))
394 .await
395 .unwrap();
396
397 assert!(result.success);
398 assert_eq!(result.output, "GPIO 13 set LOW on pico0");
399 }
400
401 #[tokio::test]
402 async fn gpio_write_device_error() {
403 let mock = Arc::new(MockTransport::new(ZcResponse::error(
404 "pin 99 not available",
405 )));
406 let reg = registry_with_mock(mock);
407 let tool = GpioWriteTool::new(reg);
408
409 let result = tool
410 .execute(json!({"device": "pico0", "pin": 99, "value": 1}))
411 .await
412 .unwrap();
413
414 assert!(!result.success);
415 assert_eq!(result.error.as_deref(), Some("pin 99 not available"));
416 }
417
418 #[tokio::test]
419 async fn gpio_write_transport_disconnected() {
420 let mock = Arc::new(MockTransport::disconnected());
421 let reg = registry_with_mock(mock);
422 let tool = GpioWriteTool::new(reg);
423
424 let result = tool
425 .execute(json!({"device": "pico0", "pin": 25, "value": 1}))
426 .await
427 .unwrap();
428
429 assert!(!result.success);
430 assert!(result.error.as_deref().unwrap().contains("transport"));
431 }
432
433 #[tokio::test]
434 async fn gpio_write_unknown_device() {
435 let mock = Arc::new(MockTransport::new(ZcResponse::success(json!({}))));
436 let reg = registry_with_mock(mock);
437 let tool = GpioWriteTool::new(reg);
438
439 let result = tool
440 .execute(json!({"device": "nonexistent", "pin": 25, "value": 1}))
441 .await
442 .unwrap();
443
444 assert!(!result.success);
445 assert!(result.error.as_deref().unwrap().contains("not found"));
446 }
447
448 #[tokio::test]
449 async fn gpio_write_invalid_value() {
450 let mock = Arc::new(MockTransport::new(ZcResponse::success(json!({}))));
451 let reg = registry_with_mock(mock);
452 let tool = GpioWriteTool::new(reg);
453
454 let result = tool
455 .execute(json!({"device": "pico0", "pin": 25, "value": 5}))
456 .await
457 .unwrap();
458
459 assert!(!result.success);
460 assert_eq!(result.error.as_deref(), Some("value must be 0 or 1"));
461 }
462
463 #[tokio::test]
464 async fn gpio_write_missing_params() {
465 let mock = Arc::new(MockTransport::new(ZcResponse::success(json!({}))));
466 let reg = registry_with_mock(mock);
467 let tool = GpioWriteTool::new(reg);
468
469 let result = tool
471 .execute(json!({"device": "pico0", "value": 1}))
472 .await
473 .unwrap();
474 assert!(!result.success);
475 assert!(
476 result
477 .error
478 .as_deref()
479 .unwrap_or("")
480 .contains("missing required parameter: pin")
481 );
482
483 let empty_reg = Arc::new(RwLock::new(DeviceRegistry::new()));
485 let tool_no_reg = GpioWriteTool::new(empty_reg);
486 let result = tool_no_reg
487 .execute(json!({"pin": 25, "value": 1}))
488 .await
489 .unwrap();
490 assert!(!result.success);
491 assert!(result.error.as_deref().unwrap_or("").contains("no GPIO"));
492
493 let result = tool
495 .execute(json!({"device": "pico0", "pin": 25}))
496 .await
497 .unwrap();
498 assert!(!result.success);
499 assert!(
500 result
501 .error
502 .as_deref()
503 .unwrap_or("")
504 .contains("missing required parameter: value")
505 );
506 }
507
508 #[tokio::test]
511 async fn gpio_read_success() {
512 let mock = Arc::new(MockTransport::new(ZcResponse::success(
513 json!({"pin": 25, "value": 1, "state": "HIGH"}),
514 )));
515 let reg = registry_with_mock(mock.clone());
516 let tool = GpioReadTool::new(reg);
517
518 let result = tool
519 .execute(json!({"device": "pico0", "pin": 25}))
520 .await
521 .unwrap();
522
523 assert!(result.success);
524 assert_eq!(result.output, "GPIO 25 is HIGH (1) on pico0");
525 assert!(result.error.is_none());
526
527 let cmd = mock.last_command().await.unwrap();
528 assert_eq!(cmd.cmd, "gpio_read");
529 assert_eq!(cmd.params["pin"], 25);
530 }
531
532 #[tokio::test]
533 async fn gpio_read_low() {
534 let mock = Arc::new(MockTransport::new(ZcResponse::success(
535 json!({"pin": 13, "value": 0, "state": "LOW"}),
536 )));
537 let reg = registry_with_mock(mock);
538 let tool = GpioReadTool::new(reg);
539
540 let result = tool
541 .execute(json!({"device": "pico0", "pin": 13}))
542 .await
543 .unwrap();
544
545 assert!(result.success);
546 assert_eq!(result.output, "GPIO 13 is LOW (0) on pico0");
547 }
548
549 #[tokio::test]
550 async fn gpio_read_device_error() {
551 let mock = Arc::new(MockTransport::new(ZcResponse::error("pin not configured")));
552 let reg = registry_with_mock(mock);
553 let tool = GpioReadTool::new(reg);
554
555 let result = tool
556 .execute(json!({"device": "pico0", "pin": 99}))
557 .await
558 .unwrap();
559
560 assert!(!result.success);
561 assert_eq!(result.error.as_deref(), Some("pin not configured"));
562 }
563
564 #[tokio::test]
565 async fn gpio_read_transport_disconnected() {
566 let mock = Arc::new(MockTransport::disconnected());
567 let reg = registry_with_mock(mock);
568 let tool = GpioReadTool::new(reg);
569
570 let result = tool
571 .execute(json!({"device": "pico0", "pin": 25}))
572 .await
573 .unwrap();
574
575 assert!(!result.success);
576 assert!(result.error.as_deref().unwrap().contains("transport"));
577 }
578
579 #[tokio::test]
580 async fn gpio_read_missing_params() {
581 let mock = Arc::new(MockTransport::new(ZcResponse::success(json!({}))));
582 let reg = registry_with_mock(mock);
583 let tool = GpioReadTool::new(reg);
584
585 let result = tool.execute(json!({"device": "pico0"})).await.unwrap();
587 assert!(!result.success);
588 assert!(
589 result
590 .error
591 .as_deref()
592 .unwrap_or("")
593 .contains("missing required parameter: pin")
594 );
595
596 let empty_reg = Arc::new(RwLock::new(DeviceRegistry::new()));
598 let tool_no_reg = GpioReadTool::new(empty_reg);
599 let result = tool_no_reg.execute(json!({"pin": 25})).await.unwrap();
600 assert!(!result.success);
601 assert!(result.error.as_deref().unwrap_or("").contains("no GPIO"));
602 }
603
604 #[test]
607 fn gpio_tools_factory_returns_two() {
608 let reg = Arc::new(RwLock::new(DeviceRegistry::new()));
609 let tools = gpio_tools(reg);
610 assert_eq!(tools.len(), 2);
611 assert_eq!(tools[0].name(), "gpio_write");
612 assert_eq!(tools[1].name(), "gpio_read");
613 }
614
615 #[test]
616 fn gpio_write_spec_is_valid() {
617 let reg = Arc::new(RwLock::new(DeviceRegistry::new()));
618 let tool = GpioWriteTool::new(reg);
619 let spec = tool.spec();
620 assert_eq!(spec.name, "gpio_write");
621 assert!(spec.parameters["properties"]["device"].is_object());
622 assert!(spec.parameters["properties"]["pin"].is_object());
623 assert!(spec.parameters["properties"]["value"].is_object());
624 let required = spec.parameters["required"].as_array().unwrap();
625 assert_eq!(required.len(), 2, "required should be [pin, value]");
626 }
627
628 #[test]
629 fn gpio_read_spec_is_valid() {
630 let reg = Arc::new(RwLock::new(DeviceRegistry::new()));
631 let tool = GpioReadTool::new(reg);
632 let spec = tool.spec();
633 assert_eq!(spec.name, "gpio_read");
634 assert!(spec.parameters["properties"]["device"].is_object());
635 assert!(spec.parameters["properties"]["pin"].is_object());
636 let required = spec.parameters["required"].as_array().unwrap();
637 assert_eq!(required.len(), 1, "required should be [pin]");
638 }
639}