use std::time::Duration;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::CallToolResult;
use rmcp::{ErrorData, tool, tool_router};
use crate::encoding::validate_timeout_ms;
use crate::error::{invalid_arg, map_pdg_err};
use crate::{GalloMcp, ok_json};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct GpioGetParams {
pub pin: u8,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct GpioPutParams {
pub pin: u8,
pub high: bool,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct GpioSetConfigParams {
pub pin: u8,
pub direction: String,
#[serde(default = "default_pull")]
pub pull: String,
}
fn default_pull() -> String {
"none".to_string()
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct GpioWaitParams {
pub pin: u8,
pub timeout_ms: u32,
}
#[tool_router(router = gpio_router, vis = "pub(crate)")]
impl GalloMcp {
#[tool(
description = "Read a GPIO pin level",
annotations(read_only_hint = true)
)]
async fn gpio_get(
&self,
Parameters(p): Parameters<GpioGetParams>,
) -> Result<CallToolResult, ErrorData> {
let dev = self.connect().await?;
let state = dev.gpio_get(p.pin).await.map_err(map_pdg_err)?;
ok_json(&serde_json::json!({ "high": matches!(state, pico_de_gallo_lib::GpioState::High) }))
}
#[tool(
description = "Set a GPIO pin level",
annotations(destructive_hint = true, read_only_hint = false)
)]
async fn gpio_put(
&self,
Parameters(p): Parameters<GpioPutParams>,
) -> Result<CallToolResult, ErrorData> {
use pico_de_gallo_lib::GpioState;
let s = if p.high {
GpioState::High
} else {
GpioState::Low
};
let dev = self.connect().await?;
dev.gpio_put(p.pin, s).await.map_err(map_pdg_err)?;
ok_json(&"ok")
}
#[tool(
description = "Configure a GPIO pin direction and pull",
annotations(destructive_hint = true, read_only_hint = false)
)]
async fn gpio_set_config(
&self,
Parameters(p): Parameters<GpioSetConfigParams>,
) -> Result<CallToolResult, ErrorData> {
use pico_de_gallo_lib::{GpioDirection, GpioPull};
let dir = match p.direction.to_lowercase().as_str() {
"input" => GpioDirection::Input,
"output" => GpioDirection::Output,
o => return Err(invalid_arg(format!("unknown direction '{o}'"))),
};
let pull = match p.pull.to_lowercase().as_str() {
"none" => GpioPull::None,
"up" => GpioPull::Up,
"down" => GpioPull::Down,
o => return Err(invalid_arg(format!("unknown pull '{o}'"))),
};
let dev = self.connect().await?;
dev.gpio_set_config(p.pin, dir, pull)
.await
.map_err(map_pdg_err)?;
ok_json(&"ok")
}
#[tool(
description = "Wait for a rising edge with a timeout",
annotations(read_only_hint = true)
)]
async fn gpio_wait_for_rising_edge_with_timeout(
&self,
Parameters(p): Parameters<GpioWaitParams>,
) -> Result<CallToolResult, ErrorData> {
let ms = validate_timeout_ms(p.timeout_ms).map_err(invalid_arg)?;
let dev = self.connect().await?;
dev.gpio_wait_for_rising_edge_with_timeout(p.pin, Duration::from_millis(u64::from(ms)))
.await
.map_err(map_pdg_err)?;
ok_json(&"edge")
}
#[tool(
description = "Wait for a falling edge with a timeout",
annotations(read_only_hint = true)
)]
async fn gpio_wait_for_falling_edge_with_timeout(
&self,
Parameters(p): Parameters<GpioWaitParams>,
) -> Result<CallToolResult, ErrorData> {
let ms = validate_timeout_ms(p.timeout_ms).map_err(invalid_arg)?;
let dev = self.connect().await?;
dev.gpio_wait_for_falling_edge_with_timeout(p.pin, Duration::from_millis(u64::from(ms)))
.await
.map_err(map_pdg_err)?;
ok_json(&"edge")
}
#[tool(
description = "Wait for any edge with a timeout",
annotations(read_only_hint = true)
)]
async fn gpio_wait_for_any_edge_with_timeout(
&self,
Parameters(p): Parameters<GpioWaitParams>,
) -> Result<CallToolResult, ErrorData> {
let ms = validate_timeout_ms(p.timeout_ms).map_err(invalid_arg)?;
let dev = self.connect().await?;
dev.gpio_wait_for_any_edge_with_timeout(p.pin, Duration::from_millis(u64::from(ms)))
.await
.map_err(map_pdg_err)?;
ok_json(&"edge")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wait_params_deserialize() {
let p: GpioWaitParams = serde_json::from_str(r#"{"pin":5,"timeout_ms":1000}"#).unwrap();
assert_eq!(p.pin, 5);
assert_eq!(p.timeout_ms, 1000);
}
#[test]
fn gpio_tools_registered() {
let names: Vec<String> = crate::GalloMcp::router_for_test()
.list_all()
.iter()
.map(|t| t.name.to_string())
.collect();
for e in [
"gpio_get",
"gpio_put",
"gpio_set_config",
"gpio_wait_for_rising_edge_with_timeout",
"gpio_wait_for_falling_edge_with_timeout",
"gpio_wait_for_any_edge_with_timeout",
] {
assert!(names.contains(&e.to_string()), "missing {e}");
}
}
}