Skip to content

DownloadSdCardFileAsync silently returns a 0-byte success on an empty (marker-only) transfer #264

Description

@tylerkron

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 TimeoutExceptionsrc/Daqifi.Core/Device/SdCard/SdCardFileReceiver.cs:98-104.
  • But the foundEof branch returns totalBytesReceived without throwing even when it is 0src/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 DetectedSdCardNotPresentException) 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)

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions