use crate::base::{McpError, ToolDescriptor};
use serde_json::{json, Value};
use std::io::{Read, Write};
use crate::library::exec_with_cleanup;
fn no_args() -> Value {
json!({"type": "object", "properties": {}, "additionalProperties": false})
}
pub fn core_pause_descriptor() -> ToolDescriptor {
ToolDescriptor::with_timeout(
"core_pause",
"Pause Core2 on the Jumperless V5, stopping LED rendering, overlay animations, \
and logo swirls. Use before bulk LED/overlay operations to prevent flicker. \
CRITICAL: do NOT call this before jumperless.slot.save — slot_save already \
pauses Core2 internally; double-pause will deadlock or crash. \
WARNING: pausing for >100ms disrupts waveform generation (wavegen stops) \
and may drop logic analyzer samples. Always follow with core_resume promptly.",
no_args(),
1_000,
)
}
pub fn core_resume_descriptor() -> ToolDescriptor {
ToolDescriptor::with_timeout(
"core_resume",
"Resume Core2 on the Jumperless V5 after a pause_core2 call. \
LED rendering, overlay animations, and service scheduling resume immediately. \
Extended pauses (>100ms) may cause LED flicker artifacts as Core2 re-renders \
from scratch on resume.",
no_args(),
1_000,
)
}
pub fn descriptors() -> Vec<ToolDescriptor> {
vec![core_pause_descriptor(), core_resume_descriptor()]
}
pub fn handle_core_pause<P: Read + Write + ?Sized>(port: &mut P) -> Result<Value, McpError> {
let code = "pause_core2(True)";
exec_with_cleanup(port, code, "core_pause")?;
tracing::info!("core_pause: Core2 paused — resume promptly to avoid waveform disruption");
Ok(json!({
"paused": true,
"note": "pause_core2 is a stateless volatile bool — no read-back. If another path calls pause_core2(False), this pause is voided without notice."
}))
}
pub fn handle_core_resume<P: Read + Write + ?Sized>(port: &mut P) -> Result<Value, McpError> {
let code = "pause_core2(False)";
exec_with_cleanup(port, code, "core_resume")?;
tracing::info!("core_resume: Core2 resumed");
Ok(json!({
"resumed": true,
"note": "pause_core2 is a stateless volatile bool — no read-back. Core2 is assumed running but state cannot be confirmed."
}))
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::VecDeque;
use std::io::{self, Read, Write};
struct MockPort {
read_data: VecDeque<u8>,
pub write_data: Vec<u8>,
}
impl MockPort {
fn with_responses(responses: &[&[u8]]) -> Self {
let mut buf = Vec::new();
for r in responses {
buf.extend_from_slice(r);
}
MockPort {
read_data: VecDeque::from(buf),
write_data: Vec::new(),
}
}
fn ok_frame() -> Vec<u8> {
b"OK\x04\x04>".to_vec()
}
fn error_frame(msg: &str) -> Vec<u8> {
let mut v = b"OK\x04".to_vec();
v.extend_from_slice(msg.as_bytes());
v.push(b'\n');
v.push(b'\x04');
v.push(b'>');
v
}
}
impl Read for MockPort {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = buf.len().min(self.read_data.len());
if n == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"MockPort: no more scripted bytes",
));
}
for (dst, src) in buf[..n].iter_mut().zip(self.read_data.drain(..n)) {
*dst = src;
}
Ok(n)
}
}
impl Write for MockPort {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.write_data.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn all_descriptors_have_correct_names() {
let descs = descriptors();
let names: Vec<&str> = descs.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"core_pause"));
assert!(names.contains(&"core_resume"));
assert_eq!(descs.len(), 2);
}
#[test]
fn all_descriptors_have_object_schema_with_additional_properties_false() {
for d in descriptors() {
assert!(
matches!(d.input_schema, Value::Object(_)),
"descriptor '{}' must have object input_schema",
d.name
);
assert_eq!(
d.input_schema.get("additionalProperties"),
Some(&Value::Bool(false)),
"descriptor '{}' must have additionalProperties=false",
d.name
);
}
}
#[test]
fn core_pause_description_warns_against_double_pause() {
let d = core_pause_descriptor();
assert!(
d.description.contains("slot.save") || d.description.contains("double-pause"),
"core_pause must warn against double-pause with slot.save"
);
}
#[test]
fn core_pause_description_warns_wavegen() {
let d = core_pause_descriptor();
assert!(
d.description.contains("wavegen") || d.description.contains("waveform"),
"core_pause must warn about wavegen disruption"
);
}
#[test]
fn core_pause_happy_path_returns_paused_true_with_note() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let result = handle_core_pause(&mut port).unwrap();
assert_eq!(result["paused"], true);
assert!(
result.get("note").and_then(|v| v.as_str()).is_some(),
"core_pause response must include 'note' about stateless volatile bool"
);
assert!(
result["note"].as_str().unwrap().contains("stateless"),
"note must mention stateless nature"
);
}
#[test]
fn core_pause_device_error_sends_ctrl_c() {
let err = MockPort::error_frame("NameError: pause_core2");
let mut port = MockPort::with_responses(&[&err]);
let result = handle_core_pause(&mut port);
assert!(result.is_err());
assert!(port.write_data.contains(&0x03));
}
#[test]
fn core_resume_happy_path_returns_resumed_true_with_note() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let result = handle_core_resume(&mut port).unwrap();
assert_eq!(result["resumed"], true);
assert!(
result.get("note").and_then(|v| v.as_str()).is_some(),
"core_resume response must include 'note' about stateless volatile bool"
);
assert!(
result["note"].as_str().unwrap().contains("stateless"),
"note must mention stateless nature"
);
}
#[test]
fn core_resume_device_error_sends_ctrl_c() {
let err = MockPort::error_frame("NameError: pause_core2");
let mut port = MockPort::with_responses(&[&err]);
let result = handle_core_resume(&mut port);
assert!(result.is_err());
assert!(port.write_data.contains(&0x03));
}
}