use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::CallToolResult;
use rmcp::{ErrorData, tool, tool_router};
use crate::encoding::{Bytes, parse_bytes};
use crate::error::{invalid_arg, map_pdg_err};
use crate::{GalloMcp, ok_json};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SpiReadParams {
pub count: u16,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SpiWriteParams {
pub data: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SpiTransferParams {
pub data: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SpiSetConfigParams {
pub frequency: u32,
#[serde(default)]
pub first_transition: bool,
#[serde(default)]
pub idle_low: bool,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
#[serde(tag = "op", rename_all = "lowercase")]
pub enum SpiBatchOpParam {
Read { count: u16 },
Write { data: String },
Transfer { data: String },
Delay { ns: u32 },
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SpiBatchParams {
pub cs: u8,
pub ops: Vec<SpiBatchOpParam>,
}
#[tool_router(router = spi_router, vis = "pub(crate)")]
impl GalloMcp {
#[tool(
description = "Read bytes from SPI",
annotations(read_only_hint = true)
)]
async fn spi_read(
&self,
Parameters(p): Parameters<SpiReadParams>,
) -> Result<CallToolResult, ErrorData> {
let dev = self.connect().await?;
let data = dev.spi_read(p.count).await.map_err(map_pdg_err)?;
ok_json(&Bytes::from_slice(&data))
}
#[tool(
description = "Write bytes to SPI",
annotations(destructive_hint = true, read_only_hint = false)
)]
async fn spi_write(
&self,
Parameters(p): Parameters<SpiWriteParams>,
) -> Result<CallToolResult, ErrorData> {
let bytes = parse_bytes(&p.data).map_err(invalid_arg)?;
let dev = self.connect().await?;
dev.spi_write(&bytes).await.map_err(map_pdg_err)?;
ok_json(&"ok")
}
#[tool(
description = "Full-duplex SPI transfer",
annotations(read_only_hint = true)
)]
async fn spi_transfer(
&self,
Parameters(p): Parameters<SpiTransferParams>,
) -> Result<CallToolResult, ErrorData> {
let bytes = parse_bytes(&p.data).map_err(invalid_arg)?;
let dev = self.connect().await?;
let data = dev.spi_transfer(&bytes).await.map_err(map_pdg_err)?;
ok_json(&Bytes::from_slice(&data))
}
#[tool(
description = "Flush the SPI TX buffer",
annotations(destructive_hint = true, read_only_hint = false)
)]
async fn spi_flush(&self) -> Result<CallToolResult, ErrorData> {
let dev = self.connect().await?;
dev.spi_flush().await.map_err(map_pdg_err)?;
ok_json(&"ok")
}
#[tool(
description = "Get the current SPI configuration",
annotations(read_only_hint = true)
)]
async fn spi_get_config(&self) -> Result<CallToolResult, ErrorData> {
let dev = self.connect().await?;
let c = dev.spi_get_config().await.map_err(map_pdg_err)?;
ok_json(&format!("{c:?}"))
}
#[tool(
description = "Set SPI frequency/phase/polarity",
annotations(destructive_hint = true, read_only_hint = false)
)]
async fn spi_set_config(
&self,
Parameters(p): Parameters<SpiSetConfigParams>,
) -> Result<CallToolResult, ErrorData> {
use pico_de_gallo_lib::{SpiPhase, SpiPolarity};
let phase = if p.first_transition {
SpiPhase::CaptureOnFirstTransition
} else {
SpiPhase::CaptureOnSecondTransition
};
let pol = if p.idle_low {
SpiPolarity::IdleLow
} else {
SpiPolarity::IdleHigh
};
let dev = self.connect().await?;
dev.spi_set_config(p.frequency, phase, pol)
.await
.map_err(map_pdg_err)?;
ok_json(&"ok")
}
#[tool(
description = "Execute a batch of SPI operations under chip-select",
annotations(destructive_hint = true, read_only_hint = false)
)]
async fn spi_batch(
&self,
Parameters(p): Parameters<SpiBatchParams>,
) -> Result<CallToolResult, ErrorData> {
use pico_de_gallo_lib::SpiBatchOp;
let mut bufs: Vec<Vec<u8>> = Vec::new();
for op in &p.ops {
match op {
SpiBatchOpParam::Write { data } | SpiBatchOpParam::Transfer { data } => {
bufs.push(parse_bytes(data).map_err(invalid_arg)?);
}
_ => {}
}
}
let mut ops: Vec<SpiBatchOp<'_>> = Vec::with_capacity(p.ops.len());
let mut b = 0usize;
for op in &p.ops {
match op {
SpiBatchOpParam::Read { count } => ops.push(SpiBatchOp::Read { len: *count }),
SpiBatchOpParam::Write { .. } => {
ops.push(SpiBatchOp::Write { data: &bufs[b] });
b += 1;
}
SpiBatchOpParam::Transfer { .. } => {
ops.push(SpiBatchOp::Transfer { data: &bufs[b] });
b += 1;
}
SpiBatchOpParam::Delay { ns } => ops.push(SpiBatchOp::DelayNs { ns: *ns }),
}
}
let dev = self.connect().await?;
let out = dev.spi_batch(p.cs, &ops).await.map_err(map_pdg_err)?;
ok_json(&Bytes::from_slice(&out))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transfer_params_deserialize() {
let p: SpiTransferParams = serde_json::from_str(r#"{"data":"0x01,0x02"}"#).unwrap();
assert_eq!(p.data, "0x01,0x02");
}
#[test]
fn batch_params_deserialize() {
let p: SpiBatchParams = serde_json::from_str(
r#"{"cs":0,"ops":[{"op":"write","data":"0x9F"},{"op":"read","count":3},{"op":"delay","ns":1000}]}"#,
)
.unwrap();
assert_eq!(p.cs, 0);
assert_eq!(p.ops.len(), 3);
}
#[test]
fn spi_tools_registered() {
let names: Vec<String> = crate::GalloMcp::router_for_test()
.list_all()
.iter()
.map(|t| t.name.to_string())
.collect();
for e in [
"spi_read",
"spi_write",
"spi_transfer",
"spi_flush",
"spi_get_config",
"spi_set_config",
"spi_batch",
] {
assert!(names.contains(&e.to_string()), "missing {e}");
}
}
}