use crate::base::{McpError, ToolDescriptor};
use serde_json::{json, Value};
use std::io::{Read, Write};
use crate::library::exec_with_cleanup;
const WAVEGEN_STRING_CHANNELS: &[&str] = &["DAC0", "DAC1", "TOP_RAIL", "BOTTOM_RAIL"];
fn encode_wavegen_channel(v: &Value) -> Result<String, McpError> {
match v {
Value::Number(n) => {
let i = n.as_i64().ok_or_else(|| {
McpError::Protocol("wavegen channel must be an integer 0 or 1".into())
})?;
if !(0..=1).contains(&i) {
return Err(McpError::Protocol(format!(
"wavegen channel integer must be 0 or 1; got {i}"
)));
}
Ok(i.to_string())
}
Value::String(s) => {
if s.len() > 16 {
return Err(McpError::Protocol(format!(
"wavegen channel string too long (max 16 chars): '{s}'"
)));
}
if !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err(McpError::Protocol(format!(
"wavegen channel string contains invalid characters: '{s}'"
)));
}
if WAVEGEN_STRING_CHANNELS.contains(&s.as_str()) {
Ok(s.clone())
} else {
Err(McpError::Protocol(format!(
"unknown wavegen channel '{s}'; valid strings: DAC0, DAC1, TOP_RAIL, BOTTOM_RAIL"
)))
}
}
other => Err(McpError::Protocol(format!(
"wavegen channel must be int 0/1 or string alias (DAC0/DAC1/TOP_RAIL/BOTTOM_RAIL); got {other}"
))),
}
}
#[allow(dead_code)]
fn validate_wavegen_int_channel(v: &Value) -> Result<i64, McpError> {
let i = v
.as_i64()
.ok_or_else(|| McpError::Protocol("channel must be an integer 0 or 1".into()))?;
if !(0..=1).contains(&i) {
return Err(McpError::Protocol(format!(
"channel must be 0 or 1; got {i}"
)));
}
Ok(i)
}
fn validate_wave_type(v: &Value) -> Result<i64, McpError> {
let i = v
.as_i64()
.ok_or_else(|| McpError::Protocol("wave type must be an integer".into()))?;
if !(0..=3).contains(&i) {
return Err(McpError::Protocol(format!(
"wave type must be 0-3 (0=SINE, 1=TRIANGLE, 2=SAWTOOTH, 3=SQUARE); got {i}"
)));
}
Ok(i)
}
fn validate_freq(v: &Value) -> Result<f64, McpError> {
let f = v
.as_f64()
.ok_or_else(|| McpError::Protocol("hz must be a number".into()))?;
if !f.is_finite() {
return Err(McpError::Protocol(format!(
"hz must be finite (not NaN or infinity); got {f}"
)));
}
if !(0.0001..=10_000.0).contains(&f) {
return Err(McpError::Protocol(format!(
"hz {f} out of range; must be 0.0001 to 10000 Hz (firmware-supported range)"
)));
}
Ok(f)
}
fn validate_amplitude(v: &Value) -> Result<f64, McpError> {
let f = v
.as_f64()
.ok_or_else(|| McpError::Protocol("vpp must be a number".into()))?;
if !f.is_finite() {
return Err(McpError::Protocol(format!(
"vpp must be finite (not NaN or infinity); got {f}"
)));
}
if !(0.0..=16.0).contains(&f) {
return Err(McpError::Protocol(format!(
"vpp {f} out of range; must be 0.0 to 16.0 Vpp"
)));
}
Ok(f)
}
fn validate_offset(v: &Value) -> Result<f64, McpError> {
let f = v
.as_f64()
.ok_or_else(|| McpError::Protocol("volts must be a number".into()))?;
if !f.is_finite() {
return Err(McpError::Protocol(format!(
"offset must be finite (not NaN or infinity); got {f}"
)));
}
if !(-8.0..=8.0).contains(&f) {
return Err(McpError::Protocol(format!(
"offset {f} V out of range; must be -8.0 to +8.0 V"
)));
}
Ok(f)
}
pub fn wavegen_set_output_descriptor() -> ToolDescriptor {
ToolDescriptor::with_timeout(
"wavegen_set_output",
"Select the output channel for the Jumperless V5 waveform generator. \
channel: integer 0 (DAC0) or 1 (DAC1, default), OR string alias \
\"DAC0\" / \"DAC1\" / \"TOP_RAIL\" / \"BOTTOM_RAIL\". \
Call before wavegen_start. Returns {\"set\": true, \"channel\": <value>}.",
json!({
"type": "object",
"properties": {
"channel": {
"oneOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 1,
"description": "Wavegen output channel as integer (0=DAC0, 1=DAC1)"
},
{
"type": "string",
"enum": ["DAC0", "DAC1", "TOP_RAIL", "BOTTOM_RAIL"],
"description": "Wavegen output channel as named constant"
}
]
}
},
"required": ["channel"],
"additionalProperties": false
}),
1_500,
)
}
pub fn wavegen_set_wave_descriptor() -> ToolDescriptor {
ToolDescriptor::with_timeout(
"wavegen_set_wave",
"Set the waveform shape for the Jumperless V5 waveform generator. \
type: integer 0=SINE, 1=TRIANGLE, 2=SAWTOOTH, 3=SQUARE \
(firmware §09.5 ordering — NOT the same as the LLM spec doc). \
Can be changed live while the generator is running. \
Returns {\"set\": true, \"type\": int}.",
json!({
"type": "object",
"properties": {
"type": {
"type": "integer",
"enum": [0, 1, 2, 3],
"description": "Waveform shape: 0=SINE, 1=TRIANGLE, 2=SAWTOOTH, 3=SQUARE (firmware §09.5 ordering)."
}
},
"required": ["type"],
"additionalProperties": false
}),
1_500,
)
}
pub fn wavegen_set_freq_descriptor() -> ToolDescriptor {
ToolDescriptor::with_timeout(
"wavegen_set_freq",
"Set the output frequency for the Jumperless V5 waveform generator. \
hz: float, must be > 0. Firmware supports 0.0001 to 10000 Hz. \
Can be changed live while the generator is running. \
Returns {\"set\": true, \"hz\": float}.",
json!({
"type": "object",
"properties": {
"hz": {
"type": "number",
"minimum": 0.0001,
"maximum": 10000.0,
"description": "Frequency in Hz. Firmware range: 0.0001-10000 Hz."
}
},
"required": ["hz"],
"additionalProperties": false
}),
1_500,
)
}
pub fn wavegen_set_amplitude_descriptor() -> ToolDescriptor {
ToolDescriptor::with_timeout(
"wavegen_set_amplitude",
"Set the peak-to-peak output amplitude for the Jumperless V5 waveform generator. \
vpp: float, range 0.0 to 16.0 V (V5 amplifies DAC to ±8 V; max Vpp is 16 V). \
NOTE: 16 Vpp will saturate any external circuit not rated for ±8 V. \
Can be changed live while the generator is running. \
Returns {\"set\": true, \"vpp\": float}.",
json!({
"type": "object",
"properties": {
"vpp": {
"type": "number",
"minimum": 0.0,
"maximum": 16.0,
"description": "Peak-to-peak voltage in Volts. Range: 0.0 to 16.0 Vpp."
}
},
"required": ["vpp"],
"additionalProperties": false
}),
1_500,
)
}
pub fn wavegen_set_offset_descriptor() -> ToolDescriptor {
ToolDescriptor::with_timeout(
"wavegen_set_offset",
"Set the DC offset for the Jumperless V5 waveform generator output. \
volts: float, range -8.0 to +8.0 V. Example: 1.65 centers a 3.3 Vpp signal \
in the 0–3.3 V range. Can be changed live while the generator is running. \
Returns {\"set\": true, \"volts\": float}.",
json!({
"type": "object",
"properties": {
"volts": {
"type": "number",
"minimum": -8.0,
"maximum": 8.0,
"description": "DC offset in Volts. Range: -8.0 to +8.0 V."
}
},
"required": ["volts"],
"additionalProperties": false
}),
1_500,
)
}
pub fn wavegen_start_descriptor() -> ToolDescriptor {
ToolDescriptor::with_timeout(
"wavegen_start",
"Start the Jumperless V5 waveform generator on the previously configured channel. \
The firmware `wavegen_start()` call takes NO channel argument — the active output \
channel is whatever was set by the most recent `wavegen_set_output(channel)` call. \
Configure waveform shape, frequency, amplitude, and offset before calling this. \
NOTE: wavegen runs on Core2 and blocks LED/routing updates until wavegen_stop. \
Returns {\"running\": true}.",
json!({
"type": "object",
"properties": {},
"additionalProperties": false
}),
1_500,
)
}
pub fn wavegen_stop_descriptor() -> ToolDescriptor {
ToolDescriptor::with_timeout(
"wavegen_stop",
"Stop the Jumperless V5 waveform generator immediately. \
No arguments required. After stopping, LED updates and routing changes \
resume on Core2. Returns {\"running\": false}.",
json!({
"type": "object",
"properties": {},
"additionalProperties": false
}),
1_500,
)
}
pub fn descriptors() -> Vec<ToolDescriptor> {
vec![
wavegen_set_output_descriptor(),
wavegen_set_wave_descriptor(),
wavegen_set_freq_descriptor(),
wavegen_set_amplitude_descriptor(),
wavegen_set_offset_descriptor(),
wavegen_start_descriptor(),
wavegen_stop_descriptor(),
]
}
pub fn handle_wavegen_set_output<P: Read + Write + ?Sized>(
port: &mut P,
args: &Value,
) -> Result<Value, McpError> {
let channel_val = args
.get("channel")
.ok_or_else(|| McpError::Protocol("missing required arg: channel".into()))?;
let channel_expr = encode_wavegen_channel(channel_val)?;
let code = format!("wavegen_set_output({channel_expr})");
exec_with_cleanup(port, &code, "wavegen_set_output")?;
Ok(json!({ "set": true, "channel": channel_val }))
}
pub fn handle_wavegen_set_wave<P: Read + Write + ?Sized>(
port: &mut P,
args: &Value,
) -> Result<Value, McpError> {
let type_val = args
.get("type")
.ok_or_else(|| McpError::Protocol("missing required arg: type".into()))?;
let wave_type = validate_wave_type(type_val)?;
let code = format!("wavegen_set_wave({wave_type})");
exec_with_cleanup(port, &code, "wavegen_set_wave")?;
Ok(json!({ "set": true, "type": wave_type }))
}
pub fn handle_wavegen_set_freq<P: Read + Write + ?Sized>(
port: &mut P,
args: &Value,
) -> Result<Value, McpError> {
let hz_val = args
.get("hz")
.ok_or_else(|| McpError::Protocol("missing required arg: hz".into()))?;
let hz = validate_freq(hz_val)?;
let code = format!("wavegen_set_freq({hz})");
exec_with_cleanup(port, &code, "wavegen_set_freq")?;
Ok(json!({ "set": true, "hz": hz }))
}
pub fn handle_wavegen_set_amplitude<P: Read + Write + ?Sized>(
port: &mut P,
args: &Value,
) -> Result<Value, McpError> {
let vpp_val = args
.get("vpp")
.ok_or_else(|| McpError::Protocol("missing required arg: vpp".into()))?;
let vpp = validate_amplitude(vpp_val)?;
let code = format!("wavegen_set_amplitude({vpp})");
exec_with_cleanup(port, &code, "wavegen_set_amplitude")?;
Ok(json!({ "set": true, "vpp": vpp }))
}
pub fn handle_wavegen_set_offset<P: Read + Write + ?Sized>(
port: &mut P,
args: &Value,
) -> Result<Value, McpError> {
let volts_val = args
.get("volts")
.ok_or_else(|| McpError::Protocol("missing required arg: volts".into()))?;
let volts = validate_offset(volts_val)?;
let code = format!("wavegen_set_offset({volts})");
exec_with_cleanup(port, &code, "wavegen_set_offset")?;
Ok(json!({ "set": true, "volts": volts }))
}
pub fn handle_wavegen_start<P: Read + Write + ?Sized>(
port: &mut P,
_args: &Value,
) -> Result<Value, McpError> {
let code = "wavegen_start()";
exec_with_cleanup(port, code, "wavegen_start")?;
Ok(json!({ "running": true }))
}
pub fn handle_wavegen_stop<P: Read + Write + ?Sized>(port: &mut P) -> Result<Value, McpError> {
let code = "wavegen_stop()";
exec_with_cleanup(port, code, "wavegen_stop")?;
Ok(json!({ "running": false }))
}
#[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()
}
}
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(&"wavegen_set_output"),
"missing wavegen_set_output"
);
assert!(
names.contains(&"wavegen_set_wave"),
"missing wavegen_set_wave"
);
assert!(
names.contains(&"wavegen_set_freq"),
"missing wavegen_set_freq"
);
assert!(
names.contains(&"wavegen_set_amplitude"),
"missing wavegen_set_amplitude"
);
assert!(
names.contains(&"wavegen_set_offset"),
"missing wavegen_set_offset"
);
assert!(names.contains(&"wavegen_start"), "missing wavegen_start");
assert!(names.contains(&"wavegen_stop"), "missing wavegen_stop");
assert_eq!(descs.len(), 7);
}
#[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 set_output_int_channel_0_happy() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let args = json!({"channel": 0});
let result = handle_wavegen_set_output(&mut port, &args).unwrap();
assert_eq!(result["set"], true);
assert_eq!(result["channel"], 0);
let sent = String::from_utf8_lossy(&port.write_data);
assert!(
sent.contains("wavegen_set_output(0)"),
"expected wavegen_set_output(0) in command; got: {sent}"
);
}
#[test]
fn set_output_string_channel_dac1_happy() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let args = json!({"channel": "DAC1"});
let result = handle_wavegen_set_output(&mut port, &args).unwrap();
assert_eq!(result["set"], true);
let sent = String::from_utf8_lossy(&port.write_data);
assert!(
sent.contains("wavegen_set_output(DAC1)"),
"expected bare DAC1 identifier; got: {sent}"
);
}
#[test]
fn set_wave_type_0_sine_happy() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let args = json!({"type": 0});
let result = handle_wavegen_set_wave(&mut port, &args).unwrap();
assert_eq!(result["set"], true);
assert_eq!(result["type"], 0);
let sent = String::from_utf8_lossy(&port.write_data);
assert!(sent.contains("wavegen_set_wave(0)"), "got: {sent}");
}
#[test]
fn set_wave_type_3_square_happy() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let args = json!({"type": 3});
let result = handle_wavegen_set_wave(&mut port, &args).unwrap();
assert_eq!(result["set"], true);
assert_eq!(result["type"], 3);
}
#[test]
fn set_freq_100hz_happy() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let args = json!({"hz": 100.0});
let result = handle_wavegen_set_freq(&mut port, &args).unwrap();
assert_eq!(result["set"], true);
assert!((result["hz"].as_f64().unwrap() - 100.0).abs() < 1e-9);
let sent = String::from_utf8_lossy(&port.write_data);
assert!(sent.contains("wavegen_set_freq("), "got: {sent}");
}
#[test]
fn set_amplitude_3v3_happy() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let args = json!({"vpp": 3.3});
let result = handle_wavegen_set_amplitude(&mut port, &args).unwrap();
assert_eq!(result["set"], true);
assert!((result["vpp"].as_f64().unwrap() - 3.3).abs() < 1e-9);
}
#[test]
fn set_amplitude_16v_boundary_happy() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let args = json!({"vpp": 16.0});
let result = handle_wavegen_set_amplitude(&mut port, &args).unwrap();
assert_eq!(result["set"], true);
assert!((result["vpp"].as_f64().unwrap() - 16.0).abs() < 1e-9);
}
#[test]
fn set_offset_1v65_happy() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let args = json!({"volts": 1.65});
let result = handle_wavegen_set_offset(&mut port, &args).unwrap();
assert_eq!(result["set"], true);
assert!((result["volts"].as_f64().unwrap() - 1.65).abs() < 1e-9);
}
#[test]
fn wavegen_start_happy() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let result = handle_wavegen_start(&mut port, &json!({})).unwrap();
assert_eq!(result["running"], true);
assert!(
result.get("channel").is_none(),
"response must NOT echo a channel field — the firmware call doesn't \
confirm channel, so we don't lie about it; response: {result}"
);
let sent = String::from_utf8_lossy(&port.write_data);
assert!(
sent.contains("wavegen_start()"),
"firmware call must be no-arg wavegen_start(); got: {sent}"
);
}
#[test]
fn wavegen_start_ignores_legacy_channel_arg() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let result = handle_wavegen_start(&mut port, &json!({"channel": 0})).unwrap();
assert_eq!(result["running"], true);
assert!(result.get("channel").is_none());
}
#[test]
fn wavegen_stop_happy() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let result = handle_wavegen_stop(&mut port).unwrap();
assert_eq!(result["running"], false);
let sent = String::from_utf8_lossy(&port.write_data);
assert!(sent.contains("wavegen_stop()"), "got: {sent}");
}
#[test]
fn set_output_invalid_channel_2_rejected_no_device_bytes() {
let mut port = MockPort::with_responses(&[]);
let args = json!({"channel": 2});
let result = handle_wavegen_set_output(&mut port, &args);
assert!(result.is_err(), "channel 2 must be rejected");
assert!(
port.write_data.is_empty(),
"no bytes must be sent for invalid channel; write_data: {:?}",
port.write_data
);
match result.unwrap_err() {
McpError::Protocol(msg) => {
assert!(
msg.contains("0 or 1") || msg.contains("0-1") || msg.contains("channel"),
"error must describe valid range; got: {msg}"
);
}
other => panic!("expected McpError::Protocol, got: {other:?}"),
}
}
#[test]
fn set_wave_type_4_rejected_no_device_bytes() {
let mut port = MockPort::with_responses(&[]);
let args = json!({"type": 4});
let result = handle_wavegen_set_wave(&mut port, &args);
assert!(result.is_err(), "wave type 4 must be rejected");
assert!(
port.write_data.is_empty(),
"no bytes sent for invalid wave type"
);
match result.unwrap_err() {
McpError::Protocol(msg) => {
assert!(
msg.contains("0-3") || msg.contains("type"),
"error must describe valid range; got: {msg}"
);
}
other => panic!("expected McpError::Protocol, got: {other:?}"),
}
}
#[test]
fn set_wave_type_neg1_rejected_no_device_bytes() {
let mut port = MockPort::with_responses(&[]);
let args = json!({"type": -1});
let result = handle_wavegen_set_wave(&mut port, &args);
assert!(result.is_err(), "wave type -1 must be rejected");
assert!(
port.write_data.is_empty(),
"no bytes sent for invalid wave type"
);
}
#[test]
fn set_freq_zero_rejected_no_device_bytes() {
let mut port = MockPort::with_responses(&[]);
let args = json!({"hz": 0.0});
let result = handle_wavegen_set_freq(&mut port, &args);
assert!(result.is_err(), "hz=0 must be rejected");
assert!(
port.write_data.is_empty(),
"no bytes sent for invalid frequency"
);
match result.unwrap_err() {
McpError::Protocol(msg) => {
assert!(
msg.contains("> 0") || msg.contains("hz") || msg.contains("0"),
"error must describe > 0 constraint; got: {msg}"
);
}
other => panic!("expected McpError::Protocol, got: {other:?}"),
}
}
#[test]
fn set_freq_negative_rejected_no_device_bytes() {
let mut port = MockPort::with_responses(&[]);
let args = json!({"hz": -1.0});
let result = handle_wavegen_set_freq(&mut port, &args);
assert!(result.is_err(), "negative hz must be rejected");
assert!(
port.write_data.is_empty(),
"no bytes sent for invalid frequency"
);
}
#[test]
fn set_amplitude_16_point_1_rejected_no_device_bytes() {
let mut port = MockPort::with_responses(&[]);
let args = json!({"vpp": 16.1});
let result = handle_wavegen_set_amplitude(&mut port, &args);
assert!(result.is_err(), "vpp=16.1 must be rejected (max 16.0)");
assert!(
port.write_data.is_empty(),
"no bytes sent for out-of-range vpp"
);
match result.unwrap_err() {
McpError::Protocol(msg) => {
assert!(
msg.contains("16.0") || msg.contains("out of range") || msg.contains("vpp"),
"error must describe 16.0 Vpp limit; got: {msg}"
);
}
other => panic!("expected McpError::Protocol, got: {other:?}"),
}
}
#[test]
fn set_amplitude_negative_rejected_no_device_bytes() {
let mut port = MockPort::with_responses(&[]);
let args = json!({"vpp": -0.1});
let result = handle_wavegen_set_amplitude(&mut port, &args);
assert!(result.is_err(), "negative vpp must be rejected");
assert!(port.write_data.is_empty(), "no bytes sent for negative vpp");
}
#[test]
fn set_offset_plus_8_point_1_rejected_no_device_bytes() {
let mut port = MockPort::with_responses(&[]);
let args = json!({"volts": 8.1});
let result = handle_wavegen_set_offset(&mut port, &args);
assert!(result.is_err(), "offset 8.1 V must be rejected (max +8.0)");
assert!(
port.write_data.is_empty(),
"no bytes sent for out-of-range offset"
);
match result.unwrap_err() {
McpError::Protocol(msg) => {
assert!(
msg.contains("8.0") || msg.contains("out of range") || msg.contains("volts"),
"error must describe ±8.0 V limit; got: {msg}"
);
}
other => panic!("expected McpError::Protocol, got: {other:?}"),
}
}
#[test]
fn set_offset_minus_8_point_1_rejected_no_device_bytes() {
let mut port = MockPort::with_responses(&[]);
let args = json!({"volts": -8.1});
let result = handle_wavegen_set_offset(&mut port, &args);
assert!(result.is_err(), "offset -8.1 V must be rejected (min -8.0)");
assert!(
port.write_data.is_empty(),
"no bytes sent for out-of-range offset"
);
}
#[test]
fn set_amplitude_zero_boundary_accepted() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let args = json!({"vpp": 0.0});
let result = handle_wavegen_set_amplitude(&mut port, &args).unwrap();
assert_eq!(result["set"], true);
assert!((result["vpp"].as_f64().unwrap() - 0.0).abs() < 1e-9);
}
#[test]
fn set_offset_negative_8_boundary_accepted() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let args = json!({"volts": -8.0});
let result = handle_wavegen_set_offset(&mut port, &args).unwrap();
assert_eq!(result["set"], true);
assert!((result["volts"].as_f64().unwrap() - (-8.0)).abs() < 1e-9);
}
#[test]
fn set_offset_positive_8_boundary_accepted() {
let frame = MockPort::ok_frame();
let mut port = MockPort::with_responses(&[&frame]);
let args = json!({"volts": 8.0});
let result = handle_wavegen_set_offset(&mut port, &args).unwrap();
assert_eq!(result["set"], true);
}
}