fix(mcp): reject over-max sample rates instead of clamping - #412
Conversation
Core's StreamingFrequency setter already throws on an out-of-range rate; Daqifi.Mcp.SetSampleRateAsync silently clamped instead, so the same input produced opposite outcomes depending on which layer a caller went through. Standardize on throw so a client can tell its request was refused rather than silently honored at a lower rate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoMCP set_sample_rate: reject over-cap sample rates (no clamping)
AI Description
Diagram
High-Level Assessment
Files changed (5)
|
Code Review by Qodo
1.
|
The README and tool description already said "reject"; ServerOptions.HelpText (what --help actually prints) still said "clamp". (Qodo review on #412) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The over-cap check landed in the previous commit validated against Capabilities.MaxSamplingRate, which is the sampling ISR's absolute hardware envelope, not what the device will actually accept for the channels enabled right now (CapabilityStreaming.CurrentMaximumRateHz). Confirmed on a real Nq1 (fw 3.7.2): ISR ceiling reports 22000 Hz while the real cap for 2 enabled channels was 6924 Hz and for 16 channels was 3518 Hz — a request in that gap (e.g. 5000-10000 Hz) sailed through the earlier check silently and would only have failed later, at stream start, via the firmware's SCPI -222. SetSampleRateAsync now prefers CurrentMaximumRateHz when the capability document has one, falling back to the board-derived ceiling otherwise. ConfigureAnalogChannelsAsync/ConfigureDigitalChannelsAsync re-read the capability document (best-effort) after changing the enabled set, so the cap set_sample_rate sees reflects the live configuration rather than whatever was true at connect time. Also corrected the set_sample_rate tool description and README line, which still claimed a flat 1-1000 Hz range. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
While bench-testing this PR against a real Nq1 (fw 3.7.2), found that the over-cap check validated against Fixed in fe0d131 rather than filing separately:
Re-verified end to end against the real device:
/agentic_review |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit fe0d131 |
…lures Three issues from Qodo review of fe0d131: 1. RefreshCapabilityDocumentAsync ran unconditionally after channel config, but ReadCapabilityDocumentAsync's own docs say not to call it while streaming (it pauses the protobuf consumer) -- SD logging sets IsStreaming too. Now skipped outright when streaming.IsStreaming; the cap just stays at its last-known value until the next quiescent refresh. Verified on real hardware: start_sd_logging -> configure_analog_channels mid-log no longer touches the capability document, and logging survives the reconfigure cleanly. 2. SetSampleRateAsync's deviceCap now bounds CurrentMaximumRateHz to hardwareMax, so a self-inconsistent capability document can't produce a cap above the ceiling StreamingFrequency itself enforces (which would otherwise let the check pass and then throw a different exception type one line down). 3. RefreshCapabilityDocumentAsync's catch-all was silent. DaqifiAgent now takes an optional ILogger<DaqifiAgent> (resolved via DI in Program.cs; defaults to NullLogger for the existing test constructor calls) and logs a warning when a refresh fails for a reason other than cancellation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed all 3 findings from the last review (commit 9dbf622):
CI green, 23/23 Mcp unit tests pass, re-verified against the real Nq1 bench unit. /agentic_review |
Summary
Closes #410.
DaqifiStreamingDevice.StreamingFrequency's setter already throwsArgumentOutOfRangeExceptionwhen a requested rate exceeds the device's hardware ceiling, and firmware ≥ #524 rejects an over-max rate outright (SCPI-222, no streaming started).Daqifi.Mcp.DaqifiAgent.SetSampleRateAsyncwas the odd one out: it silently clamped to the effective cap and reported the adjustment in the result, so the same "rate too high" input produced opposite outcomes depending on which entry point a caller used.This standardizes on throw:
SetSampleRateAsyncnow rejects a request above the effective cap (device hardware max, or a lower--max-sample-rate-hz) with a clearInvalidOperationException, which the MCP tool layer's existingGuard/GuardAsyncwrapper already surfaces to the calling agent as a tool-call error with that message — no new plumbing needed.SampleRateResultshrinks from(DeviceId, RequestedRateHz, AppliedRateHz, Clamped, Note)to(DeviceId, RequestedRateHz)since there's no longer an "applied but different" case to report. This is a breaking change to theset_sample_rateMCP tool's JSON output for any existing client.ScpiMessageProducerremains intentionally unvalidating (wire-format layer; already correctly documented per feat(device): read the live capability document and merge it into DeviceCapabilities (closes #390) #404) — untouched by this PR.Test plan
dotnet test Daqifi.Core.sln— 2185 Core tests + 23 Mcp tests pass./dev/cu.usbmodem1101): built the Core example CLI against this worktree's localDaqifi.Core, ran a 3s smoke stream on channels 0+1 at 10 Hz — clean connect/stream/disconnect, exit 0. (This change isDaqifi.Mcp-only, not wire-format, so hardware doesn't exercise the throw path directly; that's covered by the unit tests.)🤖 Generated with Claude Code