Skip to content

feat(sdcard): add SD card storage capacity query (closes #206)#214

Open
tylerkron wants to merge 5 commits into
mainfrom
claude/nifty-hypatia-514105
Open

feat(sdcard): add SD card storage capacity query (closes #206)#214
tylerkron wants to merge 5 commits into
mainfrom
claude/nifty-hypatia-514105

Conversation

@tylerkron
Copy link
Copy Markdown
Contributor

Summary

  • Adds GetSdCardStorageAsync on ISdCardOperations / DaqifiStreamingDevice, wrapping the firmware's SYSTem:STORage:SD:SPACe? query so callers get typed FreeBytes / TotalBytes / computed UsedBytes instead of raw strings.
  • New SdCardStorageInfo record (public sealed record to match sibling SD card types) and SdCardSpaceParser for the "free,total" response.
  • New ScpiMessageProducer.GetSdSpace.

Implementation follows the existing GetSdCardFilesAsync pattern: defensive StopStreaming, PrepareSdInterface with SPI-settle delay, single retry on transient **ERROR, PrepareLanInterface in finally, and typed SdCardNotPresentException / SdCardOperationException translation on failure.

Scope notes:

  • The optional SYSTem:STORage:SD:INFO? (CID register) wrapper from feat: Add SD card storage capacity query #206 is intentionally skipped — the issue marks it optional and the core acceptance criteria are met by the storage query.
  • No SdCardFilesystemException path: SPACe? reads FATFS metadata rather than iterating a directory, so the "Failed to open directory" marker doesn't apply.

Closes #206.

Test plan

  • dotnet test passes (net9.0 + net10.0): 1018 passed, 0 failed.
  • New unit tests for the parser (valid / whitespace / full card / malformed / negative / SCPI-error inputs, line-sequence parsing).
  • New device tests for GetSdCardStorageAsync covering: disconnected throws, correct SCPI commands sent, response parsing, LAN interface restoration, defensive stop streaming, retry-on-transient-error, persistent SCPI error → SdCardOperationException, no-card → SdCardNotPresentException, LAN restoration on error path.
  • New ScpiMessageProducer.GetSdSpace command test.
  • Smoke test against a real device with an SD card inserted.
  • Smoke test against a real device with no SD card (verify SdCardNotPresentException).

🤖 Generated with Claude Code

Adds GetSdCardStorageAsync on ISdCardOperations / DaqifiStreamingDevice
wrapping the firmware's SYSTem:STORage:SD:SPACe? query so callers can
read FreeBytes/TotalBytes (with computed UsedBytes) as typed values
instead of raw response strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tylerkron tylerkron requested a review from a team as a code owner May 16, 2026 21:12
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add SD card storage capacity query capability

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Adds GetSdCardStorageAsync method to query SD card free/total bytes
• Implements SdCardStorageInfo record with computed UsedBytes property
• Creates SdCardSpaceParser for parsing firmware response format
• Follows existing SD card operation patterns with retry and error handling
Diagram
flowchart LR
  A["Device Query"] -->|"SYSTem:STORage:SD:SPACe?"| B["Firmware Response"]
  B -->|"free,total"| C["SdCardSpaceParser"]
  C -->|"Parse"| D["SdCardStorageInfo"]
  D -->|"FreeBytes, TotalBytes, UsedBytes"| E["Caller"]
  B -->|"Error Detection"| F["Exception Translation"]
  F -->|"No Card"| G["SdCardNotPresentException"]
  F -->|"SCPI Error"| H["SdCardOperationException"]
Loading

Grey Divider

File Changes

1. src/Daqifi.Core/Device/SdCard/SdCardStorageInfo.cs ✨ Enhancement +15/-0

New SD card storage info record type

• New sealed record to hold SD card storage capacity data
• Contains FreeBytes and TotalBytes parameters
• Computes UsedBytes as TotalBytes - FreeBytes

src/Daqifi.Core/Device/SdCard/SdCardStorageInfo.cs


2. src/Daqifi.Core/Device/SdCard/SdCardSpaceParser.cs ✨ Enhancement +74/-0

New parser for SD card space response

• New parser for "free,total" response format from firmware
• TryParse method handles single line parsing with validation
• TryParseLines method searches sequence for first valid line
• Validates non-negative values and proper comma-separated format

src/Daqifi.Core/Device/SdCard/SdCardSpaceParser.cs


3. src/Daqifi.Core/Communication/Producers/ScpiMessageProducer.cs ✨ Enhancement +11/-0

Add SCPI message producer for space query

• Adds GetSdSpace static property returning SCPI query message
• Command: SYSTem:STORage:SD:SPACe?
• Includes XML documentation with usage example

src/Daqifi.Core/Communication/Producers/ScpiMessageProducer.cs


View more (5)
4. src/Daqifi.Core/Device/SdCard/ISdCardOperations.cs ✨ Enhancement +10/-0

Add storage query method to interface

• Adds GetSdCardStorageAsync method signature to interface
• Documents exception types and cancellation token support
• Specifies return type as SdCardStorageInfo

src/Daqifi.Core/Device/SdCard/ISdCardOperations.cs


5. src/Daqifi.Core/Device/DaqifiStreamingDevice.cs ✨ Enhancement +88/-0

Implement SD card storage query method

• Implements GetSdCardStorageAsync with defensive stop streaming
• Calls PrepareSdInterface with SPI settle delay before query
• Retries on transient SCPI errors up to SD_LIST_MAX_RETRIES
• Restores LAN interface in finally block
• Translates "No SD Card Detected" to SdCardNotPresentException
• Translates other errors to SdCardOperationException

src/Daqifi.Core/Device/DaqifiStreamingDevice.cs


6. src/Daqifi.Core.Tests/Communication/Producers/ScpiMessageProducerTests.cs 🧪 Tests +8/-0

Test SCPI message producer for space query

• Adds unit test for GetSdSpace SCPI message producer
• Verifies correct command string SYSTem:STORage:SD:SPACe?
• Validates message format consistency

src/Daqifi.Core.Tests/Communication/Producers/ScpiMessageProducerTests.cs


7. src/Daqifi.Core.Tests/Device/SdCard/SdCardSpaceParserTests.cs 🧪 Tests +100/-0

New parser unit tests with edge cases

• New test file with comprehensive parser validation
• Tests valid responses, whitespace handling, full card scenario
• Tests malformed inputs: missing comma, non-numeric, negative values
• Tests line sequence parsing and error detection
• Validates UsedBytes computation

src/Daqifi.Core.Tests/Device/SdCard/SdCardSpaceParserTests.cs


8. src/Daqifi.Core.Tests/Device/SdCard/SdCardOperationsTests.cs 🧪 Tests +133/-0

Add device tests for storage query operation

• Adds 8 new device integration tests for GetSdCardStorageAsync
• Tests disconnected state throws, correct SCPI commands sent
• Tests response parsing, LAN interface restoration
• Tests defensive stop streaming and retry-on-transient-error behavior
• Tests persistent error handling and exception translation
• Tests error path LAN restoration

src/Daqifi.Core.Tests/Device/SdCard/SdCardOperationsTests.cs


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 16, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Storage query stops logging ✓ Resolved 🐞 Bug ☼ Reliability
Description
GetSdCardStorageAsync always sends StopStreaming and sets IsStreaming=false without checking
IsLoggingToSdCard or clearing the internal _isLoggingToSdCard flag, so calling it during active SD
logging can stop recording and leave the device state inconsistent. After that, callers may see
incorrect "Cannot ... while logging" failures because the flag remains true while streaming has
already been stopped.
Code

src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[R413-416]

Evidence
The method stops streaming but never checks/updates SD logging state; the logging flag is only
set/cleared by Start/StopSdCardLoggingAsync, and other SD operations explicitly block while logging
to prevent state corruption/data loss.

src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[404-416]
src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[557-561]
src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[570-597]
src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[616-620]
src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[748-756]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`GetSdCardStorageAsync` unconditionally stops streaming but does not coordinate with SD logging state (`_isLoggingToSdCard`). This can silently stop SD logging and leave `_isLoggingToSdCard` stuck `true`, breaking subsequent SD operations.

### Issue Context
Other SD operations explicitly prevent running while `_isLoggingToSdCard` is true.

### Fix Focus Areas
- src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[404-416]
- src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[557-597]
- src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[616-620]
- src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[748-756]

### Implementation notes
- Add an early guard in `GetSdCardStorageAsync` similar to `DeleteSdCardFileAsync`/`DownloadSdCardFileAsync`, e.g. `if (_isLoggingToSdCard) throw new InvalidOperationException("Cannot query storage while logging to SD card.");`
- Alternatively, if you want to allow it, stop logging via `StopSdCardLoggingAsync(...)` (or equivalent internal routine) so `_isLoggingToSdCard` is updated consistently and interface restoration happens correctly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Thread.Sleep blocks async call ✓ Resolved 📎 Requirement gap ➹ Performance
Description
GetSdCardStorageAsync uses Thread.Sleep inside an async flow, which blocks a thread and cannot
be canceled during the settle delay. This violates the requirement that the API be asynchronous and
cancellable without blocking.
Code

src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[R420-445]

Evidence
PR Compliance requires a cancellable async API that does not block. The added
GetSdCardStorageAsync implementation calls Thread.Sleep(SD_INTERFACE_SETTLE_DELAY_MS) (twice)
while executing the command, which blocks and is not cancellable, conflicting with the
async/cancellation requirement.

Expose cancellable async API on ISdCardOperations: GetSdCardStorageAsync
Acceptance: Expose free/total bytes as numeric types and follow existing async/cancellation patterns
src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[420-445]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GetSdCardStorageAsync` blocks threads by calling `Thread.Sleep(SD_INTERFACE_SETTLE_DELAY_MS)` during the SD interface settle window. Because `Thread.Sleep` is not cancellable, the method cannot promptly honor `CancellationToken` and does not fully follow async/non-blocking patterns.

## Issue Context
The compliance checklist requires the new API to be async and cancellable (no blocking). The added implementation currently performs two blocking sleeps in the command execution path.

## Fix Focus Areas
- src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[420-445]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Retries no-card responses ✓ Resolved 🐞 Bug ≡ Correctness
Description
GetSdCardStorageAsync retries whenever a response contains a SCPI error, but the no-SD-card firmware
response includes a "**ERROR" line, so the method delays and retries even though the condition is
non-transient. If a retry response omits the "No SD Card Detected" marker, the method can
misclassify the condition as SdCardOperationException instead of SdCardNotPresentException.
Code

src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[R432-452]

Evidence
The retry condition is driven by presence of SCPI error lines, and tests demonstrate the no-card
response includes a SCPI error line, guaranteeing the retry will run. The no-card marker is only
evaluated after retries on the final lines value.

src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[420-452]
src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[467-473]
src/Daqifi.Core.Tests/Device/SdCard/SdCardOperationsTests.cs[1127-1135]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`GetSdCardStorageAsync` retries based solely on `ContainsScpiError(lines)`. The known no-card response includes both the firmware marker and a SCPI error line, so this path is retried unnecessarily and can be misclassified if the marker is not repeated.

### Issue Context
Unit tests show the no-card response contains:
- `Error !! No SD Card Detected`
- `**ERROR: -200, ...`
which triggers `ContainsScpiError`.

### Fix Focus Areas
- src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[417-452]
- src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[467-473]
- src/Daqifi.Core.Tests/Device/SdCard/SdCardOperationsTests.cs[1127-1135]

### Implementation notes
- Before entering the retry loop, detect the no-card marker (case-insensitive) and throw `SdCardNotPresentException` immediately.
- Alternatively, treat the no-card marker as "non-retriable" and break out of retry while preserving the original lines used for classification.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

4. No free/total validation ✓ Resolved 🐞 Bug ≡ Correctness
Description
SdCardSpaceParser accepts "free,total" pairs where free > total, and SdCardStorageInfo then computes
UsedBytes as TotalBytes - FreeBytes, yielding negative/invalid results. Reject (or otherwise handle)
free > total before constructing SdCardStorageInfo.
Code

src/Daqifi.Core/Device/SdCard/SdCardSpaceParser.cs[R45-51]

Evidence
The parser only checks for negative values and then constructs SdCardStorageInfo; UsedBytes is
computed via subtraction without constraints, so any free>total input will produce a negative
UsedBytes value.

src/Daqifi.Core/Device/SdCard/SdCardSpaceParser.cs[39-51]
src/Daqifi.Core/Device/SdCard/SdCardStorageInfo.cs[9-15]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`SdCardSpaceParser.TryParse` validates non-negative numbers but does not validate the relationship between them. If the device ever returns `free > total` (corrupt metadata / firmware bug), `UsedBytes` becomes negative.

### Issue Context
`UsedBytes` is derived as `TotalBytes - FreeBytes` with no guard.

### Fix Focus Areas
- src/Daqifi.Core/Device/SdCard/SdCardSpaceParser.cs[39-51]
- src/Daqifi.Core/Device/SdCard/SdCardStorageInfo.cs[9-15]

### Implementation notes
- Add `if (free > total) return false;` to `TryParse`.
- Consider adding a unit test for input like `"5000,1000"` to lock in behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/Daqifi.Core/Device/DaqifiStreamingDevice.cs
…rd retry; reject free>total

Addresses Qodo review on #214:

- Guard GetSdCardStorageAsync with _isLoggingToSdCard check to match the
  Delete/Download/Format pattern. Avoids silently stopping an active SD
  recording.
- Skip the retry loop when the firmware response contains the
  "No SD Card Detected" marker (non-transient). Prevents misclassifying
  a no-card condition as a generic SCPI error if the marker isn't
  repeated on retry.
- Parser rejects free > total as malformed, so callers always see a
  typed exception with raw response rather than a negative UsedBytes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tylerkron
Copy link
Copy Markdown
Contributor Author

Thanks for the review. Addressed in 62b8def:

1. Storage query stops logging — fixed. Added if (_isLoggingToSdCard) throw ... guard at the top of GetSdCardStorageAsync, matching the existing DeleteSdCardFileAsync / FormatSdCardAsync / DownloadSdCardFileAsync pattern. New test GetSdCardStorageAsync_WhenLogging_Throws covers it.

2. Thread.Sleep blocks async call — won't fix here. This is a real architectural concern, but mirrors GetSdCardFilesAsync exactly. The 100ms Thread.Sleep runs inside a synchronous Action (the setupAction parameter of ExecuteTextCommandAsync), so we can't await Task.Delay. Moving the prep+settle outside ExecuteTextCommandAsync would change the contract: the text consumer wouldn't be active to capture the DisableNetworkLan/EnableStorageSd responses, which the current pattern is careful to keep inside the consumer window. Refactoring ExecuteTextCommandAsync to accept Func<Task> is the right long-term fix but is out of scope for this PR — would prefer to track it as its own issue rather than diverge GetSdCardStorageAsync from GetSdCardFilesAsync in this PR.

3. Retries no-card responses — fixed. Added a ContainsNoSdCardMarker(lines) check that short-circuits the retry loop on both the initial response and any retry response, so the non-transient no-card condition is classified immediately as SdCardNotPresentException without an extra 100ms delay. Updated GetSdCardStorageAsync_WithNoSdCardDetected_ThrowsSdCardNotPresentException to enqueue a single response and assert ExecuteTextCommandCallCount == 1.

4. No free/total validation — fixed. SdCardSpaceParser.TryParse now rejects free > total alongside negatives. New InlineData("5000,1000") test case in TryParse_Malformed_ReturnsFalse locks in the behavior. Callers now see a typed SdCardOperationException with RawDeviceResponse instead of a negative UsedBytes.

All 1020 tests pass on net9.0 and net10.0.

@cptkoolbeenz
Copy link
Copy Markdown
Member

Hardware smoke test — NQ1, FW reports 1999.0 (`SYST:VERS?`)

Tested the new `SYSTem:STORage:SD:SPACe?` SCPI command directly on a live device (no SD card inserted — covers the second bullet of the test plan).

Sequence run (mirrors the C# `PrepareSdInterface` → query → `PrepareLanInterface` pattern):

```
SYSTem:COMMunicate:LAN:ENAbled 0 → OK
SYSTem:STORage:SD:ENAble 1 → OK
SYSTem:STORage:SD:SPACe? → 'Error !! No SD Card Detected\n**ERROR: -200, "Execution error"'
SYST:ERR? → '-200,"Execution error"'
SYSTem:STORage:SD:ENAble 0 → -200 (firmware quirk — see below)
SYSTem:COMMunicate:LAN:ENAbled 1 → OK
SYST:ERR? → '0,"No error"'
```

Verifications:

Check Result
Firmware accepts `SYSTem:STORage:SD:SPACe?` (not "Undefined header")
Returns a recognizable no-card marker ✅ — `Error !! No SD Card Detected`
C# `ContainsNoSdCardMarker` substring match (`"No SD Card Detected"`, case-insensitive) ✅ matches verbatim
Error surfaces as `**ERROR: -200, "Execution error"` per existing firmware convention
Error queue cleanly drains after consumption

So: when this PR's `GetSdCardStorageAsync` is invoked against a card-less device, the firmware delivers exactly the response shape the new parser keys off, and the existing `ContainsNoSdCardMarker` heuristic detects it. `SdCardNotPresentException` will fire correctly. Also confirms the no-retry path (`!ContainsScpiError(lines) || ContainsNoSdCardMarker(lines)` short-circuit) is triggered as designed — no wasted retries on a non-transient condition.

Two notes (not blocking — neither is PR #214 behavior):

  1. Pre-existing firmware quirk: `SYSTem:STORage:SD:ENAble 0` ALSO returns `-200` when no SD card is present. The C# `finally`-block `PrepareLanInterface` call will hit this error during cleanup. Since the spec is best-effort restoration and the subsequent `LAN:ENAbled 1` succeeded for me, this should be benign — but worth confirming the `finally` block doesn't propagate or log misleadingly. The `SYST:ERR?` queue does end up holding a `-200` from this cleanup attempt, which a subsequent unrelated operation would have to drain.

  2. With-SD-card path untested: I don't have a card inserted in this rig. Per the plan, that's bullet 1 of the test-plan checklist — needs someone with a card-loaded device to validate the success-path `free,total` parse end-to-end. Based on the parser tests (`"1048576000,2097152000"` style), I expect a clean comma-separated byte-count response from FW.

@cptkoolbeenz
Copy link
Copy Markdown
Member

With-SD-card path: ✅ VERIFIED on hardware (32 GB Silicon Power SDHC card, new firmware build):

```
SYSTem:COMMunicate:LAN:ENAbled 0 → OK
SYSTem:STORage:SD:ENAble 1 → OK
SYSTem:STORage:SD:SPACe? → '31899189248,31902400512'
SYST:ERR? → '0,"No error"'
SYSTem:STORage:SD:LIST? → 'DAQiFi/smoke5m.bin 3046387'
SYSTem:STORage:SD:INFO? → '3,"SD","SP32G",8.0,3824815414,2025-03'
SYSTem:STORage:SD:ENAble 0 → OK
SYSTem:COMMunicate:LAN:ENAbled 1 → OK
```

Response shape `','` matches the parser test fixtures (`SdCardSpaceParser.TryParse("1048576000,2097152000", ...)` style) verbatim. `SdCardSpaceParser.TryParse` on this input → `FreeBytes=31_899_189_248`, `TotalBytes=31_902_400_512`, computed `UsedBytes ≈ 3.21 MB` — consistent with the single `smoke5m.bin` (3,046,387 B) reported by `LIST?`.

Also cross-checks cleanly:

  • Error queue stayed clean throughout the query (no -200, no stale entries to drain on the way out)
  • Cleanup commands (`SD:ENAble 0`, `LAN:ENAbled 1`) returned OK — the spurious `-200` on cleanup we hit in the no-card path doesn't reproduce when a card is present, so the `finally`-block `PrepareLanInterface` is clean on the happy path
  • SD module log shows normal mount/list/unmount cycle, no driver-side warnings

Both test-plan checklist items now satisfied — no-card path verified earlier (`SdCardNotPresentException` substring match), with-card path verified here (clean parse + correct typed values).

(Earlier failures were on an older firmware build whose SD subsystem couldn't enumerate any card — unrelated to PR #214 and now fixed by the firmware rebuild.)

@cptkoolbeenz
Copy link
Copy Markdown
Member

Sign-off: both paths now verified end-to-end on the current firmware build.

Path SCPI response Parser outcome
With SD (32 GB Silicon Power SDHC) `'31899189248,31902400512'` `SdCardSpaceParser.TryParse` → `FreeBytes=31_899_189_248`, `TotalBytes=31_902_400_512`, computed `UsedBytes ≈ 3.21 MB` (consistent with the 3 MB `smoke5m.bin` reported by `LIST?`)
No SD (card physically removed, same firmware) `'Error !! No SD Card Detected\n**ERROR: -200, "Execution error"'` Contains `"No SD Card Detected"` substring → `ContainsNoSdCardMarker` matches verbatim → `SdCardNotPresentException` fires; no-retry short-circuit triggers as designed

Bonus observation on the new firmware: the `finally`-block `PrepareLanInterface` cleanup that I'd previously flagged as a concern (because `SD:ENAble 0` would return `-200` after a failed query on the older firmware) is now clean — both `SD:ENAble 0` and `LAN:ENAbled 1` return OK after a no-card `SPACe?` failure, and the error queue ends at `0,"No error"`. So the C# `finally` block won't leave a stale `-200` for the next unrelated operation to trip over.

Both unchecked items in the test plan are satisfied:

  • Smoke test against a real device with an SD card inserted
  • Smoke test against a real device with no SD card (verify `SdCardNotPresentException`)

LGTM from the hardware side — leaving formal PR approval to the assigned reviewer.

@cptkoolbeenz cptkoolbeenz self-requested a review May 26, 2026 22:18
Copy link
Copy Markdown
Member

@cptkoolbeenz cptkoolbeenz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardware-verified end-to-end on NQ1 (32 GB SDHC card with files / card removed). Both checklist items in the test plan satisfied; SCPI response shapes match the parser fixtures verbatim. Details in prior comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add SD card storage capacity query

2 participants