pub mod adc;
pub mod device;
pub mod encoding;
pub mod error;
pub mod gpio;
pub mod i2c;
pub mod onewire;
pub mod pwm;
pub mod spi;
pub mod uart;
use std::sync::Arc;
use std::time::Duration;
use pico_de_gallo_lib::{DeviceInfo, PicoDeGallo};
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::model::{CallToolResult, ContentBlock, ServerCapabilities, ServerInfo};
use rmcp::{ErrorData, ServerHandler, tool_handler};
use serde::Serialize;
use tokio::sync::{Mutex, OwnedMutexGuard};
pub(crate) fn ok_json<T: Serialize>(value: &T) -> Result<CallToolResult, ErrorData> {
Ok(CallToolResult::success(vec![ContentBlock::json(value)?]))
}
const NOT_FOUND: &str = "Failed to find matching nusb device";
pub(crate) struct Device {
inner: PicoDeGallo,
info: DeviceInfo,
_claim: OwnedMutexGuard<()>,
}
impl std::ops::Deref for Device {
type Target = PicoDeGallo;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Device {
pub(crate) fn info(&self) -> &DeviceInfo {
&self.info
}
}
#[derive(Clone)]
pub struct GalloMcp {
serial_number: Option<String>,
connection: Arc<Mutex<()>>,
pub(crate) tool_router: ToolRouter<Self>,
}
impl GalloMcp {
pub fn new(serial_number: Option<&str>) -> Self {
Self {
serial_number: serial_number.map(str::to_string),
connection: Arc::new(Mutex::new(())),
tool_router: Self::device_router()
+ Self::i2c_router()
+ Self::spi_router()
+ Self::uart_router()
+ Self::gpio_router()
+ Self::pwm_router()
+ Self::adc_router()
+ Self::onewire_router(),
}
}
#[cfg(test)]
pub(crate) fn router_for_test() -> ToolRouter<Self> {
Self::device_router()
+ Self::i2c_router()
+ Self::spi_router()
+ Self::uart_router()
+ Self::gpio_router()
+ Self::pwm_router()
+ Self::adc_router()
+ Self::onewire_router()
}
pub(crate) async fn connect(&self) -> Result<Device, ErrorData> {
const MAX_ATTEMPTS: u32 = 5;
const BACKOFF: Duration = Duration::from_millis(100);
let claim = self.connection.clone().lock_owned().await;
let mut attempt: u32 = 1;
let inner = loop {
let result = match self.serial_number.as_deref() {
Some(sn) => PicoDeGallo::try_new_with_serial_number(sn),
None => PicoDeGallo::try_new(),
};
match result {
Ok(dev) => break dev,
Err(e) if e.contains(NOT_FOUND) => {
return Err(ErrorData::internal_error(
"no device attached: connect a Pico de Gallo and retry".to_string(),
None,
));
}
Err(e) if attempt >= MAX_ATTEMPTS => {
return Err(ErrorData::internal_error(
format!("failed to open device after {attempt} attempts: {e}"),
None,
));
}
Err(_) => {
attempt += 1;
tokio::time::sleep(BACKOFF).await;
}
}
};
let info = inner.validate().await.map_err(error::map_validate_err)?;
let _ = inner.system_reset_subscriptions().await;
Ok(Device {
inner,
info,
_claim: claim,
})
}
}
#[tool_handler(router = self.tool_router)]
impl ServerHandler for GalloMcp {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build()).with_instructions(
"Bridge to a Pico de Gallo USB device (I2C/SPI/UART/GPIO/PWM/ADC/1-Wire). \
Bytes are hex strings like \"0x48,0x00\". Read tools are safe; tools that \
write or actuate pins are marked destructive and may require approval.",
)
}
}
#[cfg(test)]
mod tests {
#[test]
fn not_found_substring_matches_postcard_error() {
let postcard_err = "Failed to find matching nusb device!";
assert!(postcard_err.contains(crate::NOT_FOUND));
}
}