Summary
DownloadSdCardFileAsync returns a successful 0-byte result when the device serves an empty, marker-only transfer (the __END_OF_FILE__ marker with no file bytes in front of it), instead of signaling an error. This silent success makes a transient/unready device look like a data or import bug to callers, and it cost a multi-day downstream investigation to localize.
Where it happens
SdCardFileReceiver.ReceiveAsync distinguishes its outcomes by exception:
- A marker-less empty stream throws
TimeoutException — src/Daqifi.Core/Device/SdCard/SdCardFileReceiver.cs:98-104.
- But the
foundEof branch returns totalBytesReceived without throwing even when it is 0 — src/Daqifi.Core/Device/SdCard/SdCardFileReceiver.cs:110-151.
DaqifiStreamingDevice.DownloadSdCardFileAsync then sets fileSize = bytesReceived (DaqifiStreamingDevice.cs:694) and returns new SdCardDownloadResult(fileName, 0, …) — a clean "success" with FileSize == 0.
How a real device produces this
When the device's SD subsystem is wedged or not-yet-ready, the firmware opens the file successfully but the first SYS_FS_FileRead returns 0, so it transmits only the __END_OF_FILE__ marker and closes (daqifi-nyquist-firmware sd_card_manager.c:1407). Core faithfully measures 0 content bytes and reports it as a successful download.
This is distinct from "no SD card" (firmware returns No SD Card Detected → SdCardNotPresentException) and from a failed open (no marker → Core times out). The empty-but-clean transfer is the only path that yields a non-throwing FileSize == 0.
Evidence (bench, 2026-06-23)
The same file, same host code, same card — only the device's SD state varied (proven by driving the device directly over USB-serial, bypassing Core):
| Device state |
GET "log_…bin" result |
DownloadSdCardFileAsync |
| Healthy |
full file bytes + marker |
FileSize=14668, imports 18,745 samples |
| Wedged |
marker only (0 file bytes) |
FileSize=0, "success" → downstream "empty (0 bytes)" import failure |
The device-side intermittent wedge itself is a firmware matter (tracked separately as #593 in the firmware repo) and recovers on a power-cycle. This issue is only about Core failing loudly / recovering instead of returning a silent 0-byte success, so that callers can tell "device not ready, retry/power-cycle" apart from "downloaded a file that legitimately has no data."
Recommended fix (either or both)
- Treat an empty transfer as a failure. In
SdCardFileReceiver.ReceiveAsync, when foundEof && totalBytesReceived == 0, throw a typed exception (e.g. SdCardOperationException / a new EmptySdCardTransferException) at SdCardFileReceiver.cs:150 rather than returning 0. An immediate EOF marker for a file the directory listing reports as non-empty is never a valid download.
- Retry the GET like the LIST path already does. Wrap the GET in
DownloadSdCardFileAsync (DaqifiStreamingDevice.cs:674-695) in a bounded retry mirroring GetSdCardFilesAsync's SD_LIST_MAX_RETRIES loop — a transient wedge is exactly what the LIST retry already absorbs.
Optionally, also surface SdCardNotPresentException on the download path (not just on LIST) when the device reports No SD Card Detected mid-download.
Acceptance
- A device that serves an empty marker-only transfer for a non-empty file causes
DownloadSdCardFileAsync to throw a descriptive, typed exception (and/or transparently recover via retry), not return FileSize == 0.
- Unit coverage for the empty-marker-first stream in
SdCardFileReceiverTests.
Summary
DownloadSdCardFileAsyncreturns a successful 0-byte result when the device serves an empty, marker-only transfer (the__END_OF_FILE__marker with no file bytes in front of it), instead of signaling an error. This silent success makes a transient/unready device look like a data or import bug to callers, and it cost a multi-day downstream investigation to localize.Where it happens
SdCardFileReceiver.ReceiveAsyncdistinguishes its outcomes by exception:TimeoutException—src/Daqifi.Core/Device/SdCard/SdCardFileReceiver.cs:98-104.foundEofbranch returnstotalBytesReceivedwithout throwing even when it is0—src/Daqifi.Core/Device/SdCard/SdCardFileReceiver.cs:110-151.DaqifiStreamingDevice.DownloadSdCardFileAsyncthen setsfileSize = bytesReceived(DaqifiStreamingDevice.cs:694) and returnsnew SdCardDownloadResult(fileName, 0, …)— a clean "success" withFileSize == 0.How a real device produces this
When the device's SD subsystem is wedged or not-yet-ready, the firmware opens the file successfully but the first
SYS_FS_FileReadreturns 0, so it transmits only the__END_OF_FILE__marker and closes (daqifi-nyquist-firmwaresd_card_manager.c:1407). Core faithfully measures 0 content bytes and reports it as a successful download.This is distinct from "no SD card" (firmware returns
No SD Card Detected→SdCardNotPresentException) and from a failed open (no marker → Core times out). The empty-but-clean transfer is the only path that yields a non-throwingFileSize == 0.Evidence (bench, 2026-06-23)
The same file, same host code, same card — only the device's SD state varied (proven by driving the device directly over USB-serial, bypassing Core):
GET "log_…bin"resultDownloadSdCardFileAsyncFileSize=14668, imports 18,745 samplesFileSize=0, "success" → downstream "empty (0 bytes)" import failureThe device-side intermittent wedge itself is a firmware matter (tracked separately as
#593in the firmware repo) and recovers on a power-cycle. This issue is only about Core failing loudly / recovering instead of returning a silent 0-byte success, so that callers can tell "device not ready, retry/power-cycle" apart from "downloaded a file that legitimately has no data."Recommended fix (either or both)
SdCardFileReceiver.ReceiveAsync, whenfoundEof && totalBytesReceived == 0, throw a typed exception (e.g.SdCardOperationException/ a newEmptySdCardTransferException) atSdCardFileReceiver.cs:150rather than returning 0. An immediate EOF marker for a file the directory listing reports as non-empty is never a valid download.DownloadSdCardFileAsync(DaqifiStreamingDevice.cs:674-695) in a bounded retry mirroringGetSdCardFilesAsync'sSD_LIST_MAX_RETRIESloop — a transient wedge is exactly what the LIST retry already absorbs.Optionally, also surface
SdCardNotPresentExceptionon the download path (not just on LIST) when the device reportsNo SD Card Detectedmid-download.Acceptance
DownloadSdCardFileAsyncto throw a descriptive, typed exception (and/or transparently recover via retry), not returnFileSize == 0.SdCardFileReceiverTests.