Skip to content

feat(sd): enable SD-card offload over WiFi/TCP (closes #1) - #2

Merged
cptkoolbeenz merged 14 commits into
mainfrom
feat/sd-offload-over-wifi
Jul 28, 2026
Merged

cptkoolbeenz merged 14 commits into
mainfrom
feat/sd-offload-over-wifi

Conversation

@cptkoolbeenz

Copy link
Copy Markdown
Member

Summary

SD-card file LIST / download / delete now works over WiFi/TCP, not just USB. The USB-only restriction was a stale client-side policy that predated the firmware's TCP SD-reply routing (#598/#599). This is a big mobile win — WiFi SD offload with no OTG cable.

Why this is safe (transport-agnostic all the way down)

  • Firmware (daqifi-nyquist-firmware #598/#599): sd_reply_write_tcp() writes SD READ/LIST replies to the TCP server; sd_card_manager_DataReadyCB picks the target from replyTarget == SD_CARD_REPLY_WIFI_TCP and tracks the originating connection generation (mid-transfer client change aborts cleanly — no cross-client leak).
  • Core (Daqifi.Core 1.0.0): GetSdCardFilesAsync / DownloadSdCardFileAsync drive a protobuf-consumer pause/swap against IStreamTransport.Stream — identical for TCP and serial. They defensively StopStreaming first, toggle the SD interface, and restore the LAN interface in a finally (correct for a WiFi link). No transport-type gate anywhere in the Core SD path or SdCardFileReceiver.
  • So the only blocker was the app's own ConnectionType == Usb checks.

Changes (SD file access path only)

File Change
WiFiDevice/DaqifiStreamingDevice.cs override CoreDeviceForSd => CoreDevice (was null → SD unavailable by construction), mirroring the serial/USB devices
AbstractStreamingDevice.RefreshSdCardFiles drop the ConnectionType != Usb throw; availability gated by CoreDeviceForSd (null → SD_UNAVAILABLE). EnsureSdOperationsQuiesced still blocks an active stream/log ("Stop streaming first") — covers the issue's streaming-interaction caveat, transport-agnostically. Download/Delete already relied on CoreDeviceForSd.
DeviceLogsViewModel CanAccessSdCard + RefreshFilesAsync guard accept Usb or Wifi; ConnectionTypeMessage WiFi branch → "SD Card Access Available"; Has* doc comments de-USB-ified
DeviceLogsView.axaml / DeviceLogsMobileView.axaml the !CanAccessSdCard callout is now a generic "connect a device (USB or Wi-Fi)" empty state, not "USB required"

Scope boundary

SD logging (StartSdCardLogging/StopSdCardLogging — streaming to the SD card) stays USB-gated. That's a separate feature outside this issue.

Divergence note

Touches @port-marked generated files. This is a downstream enhancement beyond upstream WPF (daqifi/daqifi-desktop carries the same CanAccessSdCard => Usb gate) — to be reconciled/pushed upstream on next sync.

Verification

  • ✅ Desktop head + shared Daqifi.Avalonia library build 0 errors (mobile XAML compiles in the shared library).
  • ⚠️ Android head not built here (workload absent in this env; head is validated on the dev phone in prior sessions).
  • ⚠️ Pending: on-device WiFi SD:LIST? + SD:GET validation (large-file download + __END_OF_FILE__ framing) per the issue's must-verify caveat. Code-complete, hardware-unverified.

Closes #1.

🤖 Generated with Claude Code

@qodo-code-review

qodo-code-review Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

PR Summary by Qodo

Enable SD-card file offload over WiFi/TCP

✨ Enhancement 🐞 Bug fix 🕐 40+ Minutes

Grey Divider

AI Description

• Allow SD-card file list/download/delete over WiFi/TCP in addition to USB.
• Gate SD access by device capability + connectivity, not connection type.
• Update desktop/mobile empty states and status messaging for WiFi SD access.
Diagram

graph TD
  UI["DeviceLogs views"] --> VM["DeviceLogsViewModel"] --> DEV["IStreamingDevice (USB/WiFi)"] --> ASD["AbstractStreamingDevice SD ops"] --> CORE["CoreStreamingDevice SD APIs"] --> XPORT["Transport stream (TCP/Serial)"]
  CM["ConnectionManager"] --> VM
  DEV -. "IsConnected changed" .-> VM
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Capability flag on device interface
  • ➕ Centralizes SD file-access eligibility (e.g., SupportsSdFileAccess) instead of repeating ConnectionType checks
  • ➕ Allows future transports (BLE, etc.) without updating UI gating logic
  • ➖ Requires interface and implementations change across device types
  • ➖ Still needs connectivity handling to avoid stale UI state
2. Push SD access gating into device layer only
  • ➕ ViewModel becomes thinner (just calls Refresh and reflects results)
  • ➕ Single source of truth for “can perform SD ops now”
  • ➖ Harder to express UI-only states like “selected but disconnected” messaging
  • ➖ Still requires ViewModel to subscribe for timely UX updates

Recommendation: Current approach is solid: it removes the stale USB-only policy where the actual SD implementation is transport-agnostic, and it adds the missing connectivity-driven invalidation needed for WiFi drop scenarios. If SD access expands further, consider a first-class capability property on the device abstraction to avoid future connection-type branching in UI code.

Files changed (5) +187 / -31

Enhancement (4) +30 / -18
AbstractStreamingDevice.csMake SD file refresh transport-agnostic (remove USB-only gate) +8/-7

Make SD file refresh transport-agnostic (remove USB-only gate)

• Removes the ConnectionType==Usb restriction from RefreshSdCardFiles and documents that SD file access is gated by CoreDeviceForSd availability instead. Keeps EnsureSdOperationsQuiesced protections and leaves SD logging (Start/StopSdCardLogging) USB-gated.

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs

DaqifiStreamingDevice.csExpose CoreDeviceForSd for WiFi devices +12/-2

Expose CoreDeviceForSd for WiFi devices

• Overrides CoreDeviceForSd to return the WiFi device’s CoreDevice so SD file operations can execute over the TCP transport. Updates class documentation to reflect WiFi/TCP SD offload support.

Daqifi.Avalonia/Daqifi.Desktop/Device/WiFiDevice/DaqifiStreamingDevice.cs

DeviceLogsView.axamlUpdate desktop empty-state messaging for WiFi SD access +4/-4

Update desktop empty-state messaging for WiFi SD access

• Replaces the USB-required empty state with a generic “no device” message and SD icon. Text now instructs users to connect over USB or Wi‑Fi to browse SD contents.

Daqifi.Avalonia/Daqifi.Desktop/View/DeviceLogsView.axaml

DeviceLogsMobileView.axamlUpdate mobile callout text to reflect WiFi/TCP SD offload +6/-5

Update mobile callout text to reflect WiFi/TCP SD offload

• Rewords the mobile SD offload callout from “USB required” to “connect a device (USB or Wi‑Fi)”, aligning the UX with the new ViewModel gating. Updates header comment to describe WiFi SD offload behavior.

Daqifi.Avalonia/Views/Mobile/DeviceLogsMobileView.axaml

Bug fix (1) +157 / -13
DeviceLogsViewModel.csAllow SD access over WiFi and harden gate against disconnect races +157/-13

Allow SD access over WiFi and harden gate against disconnect races

• Updates CanAccessSdCard and RefreshFilesAsync to accept USB or WiFi, and adds an IsConnected requirement to prevent operations against dead transports. Introduces selected-device connectivity subscription (INotifyPropertyChanged) with UI-thread marshaling, recomputes gate on connected-device list changes, and clears stale SD state/files when access becomes unavailable.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs

@qodo-code-review

qodo-code-review Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Refresh overwrites disconnect reset ✓ Resolved 🐞 Bug ≡ Correctness
Description
RaiseSdGateChanged() clears SdCardState and DeviceFiles when CanAccessSdCard becomes false,
but an in-flight RefreshFilesAsync() can still complete afterward and repopulate DeviceFiles and
set SdCardState = Ok without re-checking IsConnected. This can reintroduce the stale “SD card OK
/ N files” status after a disconnect and contradict the new disconnect UX.
Code

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[R272-276]

+        if (!CanAccessSdCard)
+        {
+            SdCardState = SdCardState.Unknown;
+            SdCardErrorMessage = string.Empty;
+            DeviceFiles.Clear();
Evidence
The new code clears SD state on lost access, but RefreshFilesAsync() can still set `SdCardState =
Ok and add files after the disconnect because it does not validate connectivity post-await`, only
selection equality. Since the disconnect message is driven by IsConnected, the SD status can
become inconsistent with the connection status again.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[268-277]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[281-312]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[133-139]

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

### Issue description
`RaiseSdGateChanged()` now clears SD state when SD access is lost, but `RefreshFilesAsync()` can still apply results after the device disconnects because it only checks `SelectedDevice != device` post-`await`.

### Issue Context
A device can remain selected while `IsConnected` flips to `false`. In that case, the disconnect reset runs, but the refresh continuation later repopulates `DeviceFiles` and sets `SdCardState = Ok`, reintroducing stale UI state.

### Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[268-312]

### Suggested fix
After `await Task.Run(() => device.RefreshSdCardFiles())`, add a post-await guard before mutating UI state, e.g.:
- `if (SelectedDevice != device || !device.IsConnected) return;`

Optionally strengthen further with a refresh “generation” counter or a cancellation token tied to selection/disconnect events so stale refresh results cannot apply.

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


2. Stale SD status line ✓ Resolved 🐞 Bug ≡ Correctness
Description
After a disconnect, the new gate recomputation updates CanAccessSdCard/ConnectionTypeMessage but
does not reset SdCardState/DeviceFiles, so SdCardStatusLine can keep showing "SD card OK · N files"
for a disconnected selected device. This yields inconsistent UI ("Device disconnected" while SD
status still looks OK) and can mislead users about SD availability.
Code

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[R203-208]

+            // Recompute the SD gate on every connected-set change. CanAccessSdCard now depends on
+            // the selected device's IsConnected (issue #1), and an explicit Disconnect() or a USB
+            // removal fires ConnectionManager's "ConnectedDevices" even when the SelectedDevice
+            // *reference* is unchanged — so the gate must recompute here, not only on selection
+            // change (Qodo "SD access state stale").
+            RaiseSdGateChanged();
Evidence
The new code recomputes the SD-access gate on connected-device set changes and introduces a
disconnected-specific message, but the SD status line is still derived solely from
SdCardState/DeviceFiles and is displayed in the UI regardless of CanAccessSdCard, so it can remain
stale after a disconnect where the selected device reference does not change.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[99-112]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[129-139]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[187-219]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[246-267]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[141-154]
Daqifi.Avalonia/Daqifi.Desktop/View/DeviceLogsView.axaml[207-229]

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

### Issue description
`CanAccessSdCard` now depends on `SelectedDevice.IsConnected`, and `UpdateConnectedDevices()` calls `RaiseSdGateChanged()` on connected-set changes. However, when the selected device disconnects (same reference, `IsConnected` flips false), the VM does not clear `DeviceFiles` / reset `SdCardState`, and `SdCardStatusLine` remains computed from the previous state, so the connection header can show a disconnect message while the SD status still shows "SD card OK".

### Issue Context
- `ConnectionTypeMessage` now explicitly reports `{ IsConnected: false }`.
- `SdCardStatusLine` is rendered independently in the view whenever a `SelectedDevice` exists.

### Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[187-209]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[246-267]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[141-154]

### Suggested fix
Implement one of these (or both):
1) When the selected device is disconnected during `UpdateConnectedDevices()` (or inside `RaiseSdGateChanged()`), clear `DeviceFiles`, set `SdCardState = Unknown`, and clear `SdCardErrorMessage`.
2) Make `SdCardStatusLine` return `string.Empty` when `!CanAccessSdCard`, and ensure `RaiseSdGateChanged()` also raises `OnPropertyChanged(nameof(SdCardStatusLine))` so the UI updates immediately on disconnect/reconnect.

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


3. SD access state stale ✓ Resolved 🐞 Bug ☼ Reliability
Description
CanAccessSdCard now depends on SelectedDevice.IsConnected, but the view model only raises
CanAccessSdCard/command CanExecute changes when SelectedDevice changes, not when the selected
device’s IsConnected changes. This can leave SD UI and RefreshFilesCommand enabled/visible after a
disconnect until selection changes, despite the new RefreshFilesAsync guard returning early.
Code

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[R107-108]

+        SelectedDevice is { IsConnected: true } &&
+        SelectedDevice.ConnectionType is ConnectionType.Usb or ConnectionType.Wifi;
Evidence
The VM now gates SD access on IsConnected, but it only triggers CanAccessSdCard/command
re-evaluation on selection change, so connection drops on the same selected device won’t necessarily
update the UI/command state.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[98-108]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[151-158]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[205-234]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[236-244]

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

### Issue description
`CanAccessSdCard` is computed from `SelectedDevice.IsConnected` and `SelectedDevice.ConnectionType`, but the VM doesn’t react to changes of those properties on the selected device. This makes the SD UI and `RefreshFilesCommand` `CanExecute` state stale after a disconnect.

### Issue Context
- `CanAccessSdCard` is now: connected + (USB or WiFi).
- `RefreshFilesCommand` is constructed with `() => CanAccessSdCard` and `NotifyCanExecuteChanged()` is only invoked from `OnSelectedDeviceChanged`.
- There is no subscription to `SelectedDevice.PropertyChanged` to forward `IsConnected` / `ConnectionType` changes into VM property notifications.

### Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[98-108]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[151-158]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[205-234]

### Implementation notes
- In `OnSelectedDeviceChanged`, unsubscribe from the previous device’s `PropertyChanged` and subscribe to the new one.
- When the selected device raises changes for `IsConnected` or `ConnectionType`, call:
 - `OnPropertyChanged(nameof(CanAccessSdCard))`
 - `OnPropertyChanged(nameof(HasNoFiles))`, `HasFiles`, `HasSdCardNotPresent`, `HasSdCardError`, `ConnectionTypeMessage` as needed
 - `RefreshFilesCommand.NotifyCanExecuteChanged()`
- If `IStreamingDevice` doesn’t implement `INotifyPropertyChanged` consistently, consider routing connection state changes through `ConnectionManager` so the VM gets notified reliably.

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


View more (1)
4. SD access ignores disconnect ✓ Resolved 🐞 Bug ☼ Reliability
Description
SD access/operations are gated primarily by transport type (Usb/Wifi) and non-null Core device
references rather than actual connection state, so when a device drops (especially over WiFi) the
UI/commands can remain enabled and SD refresh/import attempts run against a disconnected device.
This leads to avoidable transport exceptions and SD errors instead of disabling commands or showing
the correct empty-state UI.
Code

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[R99-105]

+    // SD file access (LIST / download) is transport-agnostic (issue #1): the firmware routes SD
+    // replies to the requesting link (USB or WiFi/TCP — daqifi-nyquist-firmware #598/#599) and
+    // Core's SD path drives its consumer swap against the transport stream regardless of type.
+    // Gate on "a real connected device" (USB or WiFi), not USB-only.
+    public bool CanAccessSdCard =>
+        SelectedDevice?.ConnectionType is ConnectionType.Usb or ConnectionType.Wifi;
+
Evidence
The cited logic makes CanAccessSdCard true based on ConnectionType (Usb/Wifi) without checking
IsConnected, so command enablement and empty-state visibility can remain “enabled” even if the
underlying device has disconnected; the codebase explicitly models this via
IStreamingDevice.IsConnected, with AbstractStreamingDevice.IsConnected backed by
CoreDevice.IsConnected. Additionally, WiFi SD support now routes CoreDeviceForSd to
CoreDevice, but GetCoreDevice(...) only validates non-null and not connectivity, meaning a
non-null yet disconnected Core device can still be used for SD operations after a transport drop,
resulting in unnecessary exceptions and inconsistent error handling.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[98-156]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[232-240]
Daqifi.Avalonia/Daqifi.Desktop/Device/IStreamingDevice.cs[21-44]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[259-266]
Daqifi.Avalonia/Daqifi.Desktop/Device/WiFiDevice/DaqifiStreamingDevice.cs[35-42]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[975-983]

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

## Issue description
SD commands/UI are currently enabled based on transport type (Usb/Wifi) and/or non-null Core device references, not true connection state. When a device disconnects (notably on WiFi drops), SD actions can remain available and operations proceed until Core throws, surfacing as SD/transport errors instead of cleanly disabling commands and showing the appropriate empty-state.

## Issue Context
- `IStreamingDevice` exposes `IsConnected`, and `AbstractStreamingDevice.IsConnected` is backed by `CoreDevice.IsConnected`.
- `DeviceLogsViewModel.CanAccessSdCard` is used to gate SD behaviors such as enabling/disabling `RefreshFilesCommand`, showing/hiding the “no device” empty state, and guarding `RefreshFilesAsync`, `ImportFile`, and `ImportAllFiles`.
- WiFi SD enablement returns `CoreDevice` for `CoreDeviceForSd`, but the SD helper `GetCoreDevice(CoreDeviceForSd, ...)` currently only rejects `null` and does not reject disconnected cores, allowing a non-null but disconnected Core device reference to be used.

## Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[98-155]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[201-230]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[232-240]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[975-983]
- Daqifi.Avalonia/Daqifi.Desktop/Device/WiFiDevice/DaqifiStreamingDevice.cs[35-42]

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



Informational

5. StatusLine docs incomplete ✓ Resolved 🐞 Bug ⚙ Maintainability ⭐ New
Description
SdCardStatusLine now returns an empty string when CanAccessSdCard is false (e.g., disconnected),
but its XML doc only mentions returning empty when the SD state is unknown. This mismatch can cause
future maintainers/tests to make incorrect assumptions about when the status line should be present.
Code

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[146]

+    public string SdCardStatusLine => !CanAccessSdCard ? string.Empty : SdCardState switch
Evidence
The property’s implementation now explicitly returns string.Empty when CanAccessSdCard is false,
while the doc comment above it only describes the empty-string case for unknown SD state; the UI
also hides the status element when the bound string is empty.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[141-154]
Daqifi.Avalonia/Daqifi.Desktop/View/DeviceLogsView.axaml[218-228]

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

## Issue description
`SdCardStatusLine` is now gated by `CanAccessSdCard`, so it can return an empty string even when `SdCardState` is not `Unknown`. The XML documentation above the property does not describe this additional condition.

## Issue Context
This behavior is intentional for UX (hide SD status when SD access is unavailable), but the comment should reflect the actual contract to avoid incorrect assumptions later.

## Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[141-154]

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


6. SelectedDevice double-read race ✓ Resolved 🐞 Bug ≡ Correctness
Description
CanAccessSdCard reads SelectedDevice twice, so if SelectedDevice becomes null between the two
evaluations (e.g., selection/disconnect reentrancy), the second access can throw. Capturing
SelectedDevice in a local before checking avoids the race and keeps the result consistent.
Code

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[R107-108]

+        SelectedDevice is { IsConnected: true } &&
+        SelectedDevice.ConnectionType is ConnectionType.Usb or ConnectionType.Wifi;
Evidence
SelectedDevice is an observable property that is treated as nullable elsewhere, and CanAccessSdCard
dereferences SelectedDevice a second time without a null-conditional access.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[46-57]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[106-108]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[126-131]

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

### Issue description
`CanAccessSdCard` evaluates `SelectedDevice` twice; if `SelectedDevice` changes to `null` between evaluations, `SelectedDevice.ConnectionType` can throw. Even without null, the result can be inconsistent across two different instances.

### Issue Context
This is defensive hardening: it narrows a small but real race window and makes the property evaluation atomic.

### Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[46-57]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[106-108]

### Suggested change
Rewrite as:
```csharp
public bool CanAccessSdCard
{
   get
   {
       var device = SelectedDevice;
       return device is { IsConnected: true } &&
              device.ConnectionType is ConnectionType.Usb or ConnectionType.Wifi;
   }
}
```

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


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Previous review results

Review updated until commit 1d9cae4

Results up to commit dfe4788


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. SD access ignores disconnect ✓ Resolved 🐞 Bug ☼ Reliability
Description
SD access/operations are gated primarily by transport type (Usb/Wifi) and non-null Core device
references rather than actual connection state, so when a device drops (especially over WiFi) the
UI/commands can remain enabled and SD refresh/import attempts run against a disconnected device.
This leads to avoidable transport exceptions and SD errors instead of disabling commands or showing
the correct empty-state UI.
Code

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[R99-105]

+    // SD file access (LIST / download) is transport-agnostic (issue #1): the firmware routes SD
+    // replies to the requesting link (USB or WiFi/TCP — daqifi-nyquist-firmware #598/#599) and
+    // Core's SD path drives its consumer swap against the transport stream regardless of type.
+    // Gate on "a real connected device" (USB or WiFi), not USB-only.
+    public bool CanAccessSdCard =>
+        SelectedDevice?.ConnectionType is ConnectionType.Usb or ConnectionType.Wifi;
+
Evidence
The cited logic makes CanAccessSdCard true based on ConnectionType (Usb/Wifi) without checking
IsConnected, so command enablement and empty-state visibility can remain “enabled” even if the
underlying device has disconnected; the codebase explicitly models this via
IStreamingDevice.IsConnected, with AbstractStreamingDevice.IsConnected backed by
CoreDevice.IsConnected. Additionally, WiFi SD support now routes CoreDeviceForSd to
CoreDevice, but GetCoreDevice(...) only validates non-null and not connectivity, meaning a
non-null yet disconnected Core device can still be used for SD operations after a transport drop,
resulting in unnecessary exceptions and inconsistent error handling.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[98-156]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[232-240]
Daqifi.Avalonia/Daqifi.Desktop/Device/IStreamingDevice.cs[21-44]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[259-266]
Daqifi.Avalonia/Daqifi.Desktop/Device/WiFiDevice/DaqifiStreamingDevice.cs[35-42]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[975-983]

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

## Issue description
SD commands/UI are currently enabled based on transport type (Usb/Wifi) and/or non-null Core device references, not true connection state. When a device disconnects (notably on WiFi drops), SD actions can remain available and operations proceed until Core throws, surfacing as SD/transport errors instead of cleanly disabling commands and showing the appropriate empty-state.

## Issue Context
- `IStreamingDevice` exposes `IsConnected`, and `AbstractStreamingDevice.IsConnected` is backed by `CoreDevice.IsConnected`.
- `DeviceLogsViewModel.CanAccessSdCard` is used to gate SD behaviors such as enabling/disabling `RefreshFilesCommand`, showing/hiding the “no device” empty state, and guarding `RefreshFilesAsync`, `ImportFile`, and `ImportAllFiles`.
- WiFi SD enablement returns `CoreDevice` for `CoreDeviceForSd`, but the SD helper `GetCoreDevice(CoreDeviceForSd, ...)` currently only rejects `null` and does not reject disconnected cores, allowing a non-null but disconnected Core device reference to be used.

## Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[98-155]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[201-230]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[232-240]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[975-983]
- Daqifi.Avalonia/Daqifi.Desktop/Device/WiFiDevice/DaqifiStreamingDevice.cs[35-42]

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


Results up to commit 74bc67e


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. SD access state stale ✓ Resolved 🐞 Bug ☼ Reliability
Description
CanAccessSdCard now depends on SelectedDevice.IsConnected, but the view model only raises
CanAccessSdCard/command CanExecute changes when SelectedDevice changes, not when the selected
device’s IsConnected changes. This can leave SD UI and RefreshFilesCommand enabled/visible after a
disconnect until selection changes, despite the new RefreshFilesAsync guard returning early.
Code

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[R107-108]

+        SelectedDevice is { IsConnected: true } &&
+        SelectedDevice.ConnectionType is ConnectionType.Usb or ConnectionType.Wifi;
Evidence
The VM now gates SD access on IsConnected, but it only triggers CanAccessSdCard/command
re-evaluation on selection change, so connection drops on the same selected device won’t necessarily
update the UI/command state.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[98-108]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[151-158]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[205-234]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[236-244]

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

### Issue description
`CanAccessSdCard` is computed from `SelectedDevice.IsConnected` and `SelectedDevice.ConnectionType`, but the VM doesn’t react to changes of those properties on the selected device. This makes the SD UI and `RefreshFilesCommand` `CanExecute` state stale after a disconnect.

### Issue Context
- `CanAccessSdCard` is now: connected + (USB or WiFi).
- `RefreshFilesCommand` is constructed with `() => CanAccessSdCard` and `NotifyCanExecuteChanged()` is only invoked from `OnSelectedDeviceChanged`.
- There is no subscription to `SelectedDevice.PropertyChanged` to forward `IsConnected` / `ConnectionType` changes into VM property notifications.

### Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[98-108]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[151-158]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[205-234]

### Implementation notes
- In `OnSelectedDeviceChanged`, unsubscribe from the previous device’s `PropertyChanged` and subscribe to the new one.
- When the selected device raises changes for `IsConnected` or `ConnectionType`, call:
 - `OnPropertyChanged(nameof(CanAccessSdCard))`
 - `OnPropertyChanged(nameof(HasNoFiles))`, `HasFiles`, `HasSdCardNotPresent`, `HasSdCardError`, `ConnectionTypeMessage` as needed
 - `RefreshFilesCommand.NotifyCanExecuteChanged()`
- If `IStreamingDevice` doesn’t implement `INotifyPropertyChanged` consistently, consider routing connection state changes through `ConnectionManager` so the VM gets notified reliably.

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



Informational
2. SelectedDevice double-read race ✓ Resolved 🐞 Bug ≡ Correctness
Description
CanAccessSdCard reads SelectedDevice twice, so if SelectedDevice becomes null between the two
evaluations (e.g., selection/disconnect reentrancy), the second access can throw. Capturing
SelectedDevice in a local before checking avoids the race and keeps the result consistent.
Code

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[R107-108]

+        SelectedDevice is { IsConnected: true } &&
+        SelectedDevice.ConnectionType is ConnectionType.Usb or ConnectionType.Wifi;
Evidence
SelectedDevice is an observable property that is treated as nullable elsewhere, and CanAccessSdCard
dereferences SelectedDevice a second time without a null-conditional access.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[46-57]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[106-108]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[126-131]

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

### Issue description
`CanAccessSdCard` evaluates `SelectedDevice` twice; if `SelectedDevice` changes to `null` between evaluations, `SelectedDevice.ConnectionType` can throw. Even without null, the result can be inconsistent across two different instances.

### Issue Context
This is defensive hardening: it narrows a small but real race window and makes the property evaluation atomic.

### Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[46-57]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[106-108]

### Suggested change
Rewrite as:
```csharp
public bool CanAccessSdCard
{
   get
   {
       var device = SelectedDevice;
       return device is { IsConnected: true } &&
              device.ConnectionType is ConnectionType.Usb or ConnectionType.Wifi;
   }
}
```

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


Results up to commit 18f7971


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Stale SD status line ✓ Resolved 🐞 Bug ≡ Correctness
Description
After a disconnect, the new gate recomputation updates CanAccessSdCard/ConnectionTypeMessage but
does not reset SdCardState/DeviceFiles, so SdCardStatusLine can keep showing "SD card OK · N files"
for a disconnected selected device. This yields inconsistent UI ("Device disconnected" while SD
status still looks OK) and can mislead users about SD availability.
Code

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[R203-208]

+            // Recompute the SD gate on every connected-set change. CanAccessSdCard now depends on
+            // the selected device's IsConnected (issue #1), and an explicit Disconnect() or a USB
+            // removal fires ConnectionManager's "ConnectedDevices" even when the SelectedDevice
+            // *reference* is unchanged — so the gate must recompute here, not only on selection
+            // change (Qodo "SD access state stale").
+            RaiseSdGateChanged();
Evidence
The new code recomputes the SD-access gate on connected-device set changes and introduces a
disconnected-specific message, but the SD status line is still derived solely from
SdCardState/DeviceFiles and is displayed in the UI regardless of CanAccessSdCard, so it can remain
stale after a disconnect where the selected device reference does not change.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[99-112]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[129-139]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[187-219]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[246-267]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[141-154]
Daqifi.Avalonia/Daqifi.Desktop/View/DeviceLogsView.axaml[207-229]

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

### Issue description
`CanAccessSdCard` now depends on `SelectedDevice.IsConnected`, and `UpdateConnectedDevices()` calls `RaiseSdGateChanged()` on connected-set changes. However, when the selected device disconnects (same reference, `IsConnected` flips false), the VM does not clear `DeviceFiles` / reset `SdCardState`, and `SdCardStatusLine` remains computed from the previous state, so the connection header can show a disconnect message while the SD status still shows "SD card OK".

### Issue Context
- `ConnectionTypeMessage` now explicitly reports `{ IsConnected: false }`.
- `SdCardStatusLine` is rendered independently in the view whenever a `SelectedDevice` exists.

### Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[187-209]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[246-267]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[141-154]

### Suggested fix
Implement one of these (or both):
1) When the selected device is disconnected during `UpdateConnectedDevices()` (or inside `RaiseSdGateChanged()`), clear `DeviceFiles`, set `SdCardState = Unknown`, and clear `SdCardErrorMessage`.
2) Make `SdCardStatusLine` return `string.Empty` when `!CanAccessSdCard`, and ensure `RaiseSdGateChanged()` also raises `OnPropertyChanged(nameof(SdCardStatusLine))` so the UI updates immediately on disconnect/reconnect.

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


Results up to commit dec05fc


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Refresh overwrites disconnect reset ✓ Resolved 🐞 Bug ≡ Correctness
Description
RaiseSdGateChanged() clears SdCardState and DeviceFiles when CanAccessSdCard becomes false,
but an in-flight RefreshFilesAsync() can still complete afterward and repopulate DeviceFiles and
set SdCardState = Ok without re-checking IsConnected. This can reintroduce the stale “SD card OK
/ N files” status after a disconnect and contradict the new disconnect UX.
Code

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[R272-276]

+        if (!CanAccessSdCard)
+        {
+            SdCardState = SdCardState.Unknown;
+            SdCardErrorMessage = string.Empty;
+            DeviceFiles.Clear();
Evidence
The new code clears SD state on lost access, but RefreshFilesAsync() can still set `SdCardState =
Ok and add files after the disconnect because it does not validate connectivity post-await`, only
selection equality. Since the disconnect message is driven by IsConnected, the SD status can
become inconsistent with the connection status again.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[268-277]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[281-312]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[133-139]

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

### Issue description
`RaiseSdGateChanged()` now clears SD state when SD access is lost, but `RefreshFilesAsync()` can still apply results after the device disconnects because it only checks `SelectedDevice != device` post-`await`.

### Issue Context
A device can remain selected while `IsConnected` flips to `false`. In that case, the disconnect reset runs, but the refresh continuation later repopulates `DeviceFiles` and sets `SdCardState = Ok`, reintroducing stale UI state.

### Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[268-312]

### Suggested fix
After `await Task.Run(() => device.RefreshSdCardFiles())`, add a post-await guard before mutating UI state, e.g.:
- `if (SelectedDevice != device || !device.IsConnected) return;`

Optionally strengthen further with a refresh “generation” counter or a cancellation token tied to selection/disconnect events so stale refresh results cannot apply.

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


Qodo Logo

Comment thread Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs
@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review

qodo-code-review Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to 5b540c3

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

CategorySuggestion                                                                                                                                    Impact
General
Handle unsupported transports accurately

Update ConnectionTypeMessage to explicitly check for ConnectionType.Wifi instead
of using a catch-all. Add a default fallback for unsupported connection types to
prevent displaying contradictory UI messages.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs [58-64]

 public string ConnectionTypeMessage => SelectedDevice switch
 {
     null => string.Empty,
     { IsConnected: false } => "Device disconnected - reconnect to access the SD card",
     { ConnectionType: ConnectionType.Usb } => "USB Connected - SD Card Access Available",
-    _ => "WiFi Connected - SD Card Access Available",
+    { ConnectionType: ConnectionType.Wifi } => "WiFi Connected - SD Card Access Available",
+    _ => "This connection does not support SD card access",
 };

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: This is an excellent catch that prevents a bug where other connection types (e.g., Serial) would incorrectly display a WiFi success message while being rejected by the CanAccessSdCard logic.

Medium
Possible issue
Unsubscribe connectivity handlers on disposal

Implement a cleanup mechanism to unsubscribe the PropertyChanged handler when
the view model is discarded. This prevents memory leaks caused by long-lived
device objects retaining references to the view model.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs [146-149]

 if (device is INotifyPropertyChanged current)
 {
     current.PropertyChanged += OnSelectedDeviceConnectivityChanged;
 }
 
+...
+
+public void Dispose()
+{
+    if (_connectivityWatchedDevice is INotifyPropertyChanged current)
+    {
+        current.PropertyChanged -= OnSelectedDeviceConnectivityChanged;
+    }
+
+    _connectivityWatchedDevice = null;
+}
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: Properly detaching event handlers is crucial to prevent memory leaks, especially when subscribing to longer-lived objects like device models from a view model.

Medium
  • Update

Previous suggestions

Suggestions up to commit 77bed16
CategorySuggestion                                                                                                                                    Impact
General
Detach the final device subscription

Implement a disposal lifecycle method to unsubscribe from the currently selected
device's events.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs [144-149]

 _connectivityWatchedDevice = device;
 
 if (device is INotifyPropertyChanged current)
 {
     current.PropertyChanged += OnSelectedDeviceConnectivityChanged;
 }
 
+// Invoke this from the view-model's disposal/deactivation lifecycle.
+private void StopWatchingSelectedDeviceConnectivity()
+{
+    if (_connectivityWatchedDevice is INotifyPropertyChanged current)
+    {
+        current.PropertyChanged -= OnSelectedDeviceConnectivityChanged;
+    }
+
+    _connectivityWatchedDevice = null;
+}
+
Suggestion importance[1-10]: 7

__

Why: Provides a valid memory leak prevention strategy by detaching the event handler when the view-model is disposed, preventing the device from keeping the view-model alive.

Medium
Avoid mislabeling unsupported connection types

Replace the default switch case _ with explicit matching for
ConnectionType.Wifi. Add a default case indicating that SD card access is
unavailable.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs [58-64]

 public string ConnectionTypeMessage => SelectedDevice switch
 {
     null => string.Empty,
     { IsConnected: false } => "Device disconnected - reconnect to access the SD card",
     { ConnectionType: ConnectionType.Usb } => "USB Connected - SD Card Access Available",
-    _ => "WiFi Connected - SD Card Access Available",
+    { ConnectionType: ConnectionType.Wifi } => "WiFi Connected - SD Card Access Available",
+    _ => "Connected - SD Card Access Unavailable",
 };
Suggestion importance[1-10]: 7

__

Why: Improves robustness by explicitly matching the Wi-Fi connection type, preventing incorrect status messages for potential future connection types that do not support SD card access.

Medium
✅ Suggestions up to commit dec05fc
CategorySuggestion                                                                                                                                    Impact
Possible issue
✅ Marshal state updates to UI
Suggestion Impact:The new connectivity-change handler checks UI-thread access and posts RaiseSdGateChanged to Dispatcher.UIThread when necessary, preventing its property notifications and DeviceFiles.Clear() from running on the transport thread. The check was implemented at the background-thread event entry point rather than inside RaiseSdGateChanged itself.

code diff:

+    /// <summary>
+    /// Recomputes the SD gate when the selected device reports a connectivity change.
+    /// </summary>
+    /// <remarks>
+    /// The device raises this from Core's transport thread on a silent drop, so marshal before
+    /// touching view-model state. An empty/null property name is the
+    /// <see cref="INotifyPropertyChanged"/> "everything changed" convention and must be treated as
+    /// a possible <c>IsConnected</c> change.
+    /// </remarks>
+    private void OnSelectedDeviceConnectivityChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (!string.IsNullOrEmpty(e.PropertyName) &&
+            e.PropertyName != nameof(IStreamingDevice.IsConnected))
+        {
+            return;
+        }
+
+        // Ignore a late event from a device that is no longer the subscribed one.
+        if (!ReferenceEquals(sender, _connectivityWatchedDevice))
+        {
+            return;
+        }
+
+        if (Dispatcher.UIThread.CheckAccess())
+        {
+            RaiseSdGateChanged();
+            return;
+        }
+
+        // Non-throwing, for the same reason AbstractStreamingDevice.RaiseIsConnectedChanged and
+        // BootloaderWatcher guard theirs: this runs on Core's transport thread, and the disconnect
+        // that triggers it is exactly what app shutdown produces — so Post can be called as the
+        // dispatcher goes away. An escape here would propagate into Core's callback.
+        try
+        {
+            Dispatcher.UIThread.Post(RaiseSdGateChanged);
+        }
+        catch (Exception)
+        {
+            // Dispatcher unavailable / shutting down — drop the gate refresh; nothing is bound.
+        }
+    }

Add a check to ensure RaiseSdGateChanged is executed on the Avalonia UI thread.
Use Dispatcher.UIThread.Post to marshal the execution to the UI thread if
necessary.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs [79-99]

 private void RaiseSdGateChanged()
 {
+    if (!Dispatcher.UIThread.CheckAccess())
+    {
+        Dispatcher.UIThread.Post(RaiseSdGateChanged);
+        return;
+    }
+
     OnPropertyChanged(nameof(CanAccessSdCard));
     OnPropertyChanged(nameof(HasNoFiles));
     OnPropertyChanged(nameof(HasSdCardNotPresent));
     OnPropertyChanged(nameof(HasSdCardError));
     OnPropertyChanged(nameof(HasFiles));
     OnPropertyChanged(nameof(ConnectionTypeMessage));
     RefreshFilesCommand.NotifyCanExecuteChanged();
 
-    // When SD is no longer accessible (e.g. the selected device disconnected), drop the now-stale
-    // SD state + file list so SdCardStatusLine doesn't keep showing a phantom "SD card OK · N
-    // files" next to a "Device disconnected" message (Qodo "Stale SD status line"). Setting
-    // SdCardState and clearing DeviceFiles each re-raise SdCardStatusLine / Has* on their own.
     if (!CanAccessSdCard)
     {
         SdCardState = SdCardState.Unknown;
         SdCardErrorMessage = string.Empty;
         DeviceFiles.Clear();
     }
 }
Suggestion importance[1-10]: 9

__

Why: The newly added RaiseSdGateChanged method manipulates an ObservableCollection (DeviceFiles.Clear()) and triggers UI property changes, which will cause cross-thread exceptions if called from a background thread before the dispatcher check.

High
✅ Suggestions up to commit 18f7971
CategorySuggestion                                                                                                                                    Impact
Possible issue
✅ Marshal notifications onto UI thread
Suggestion Impact:The commit added UI-thread checking and dispatching before RaiseSdGateChanged is invoked from selected-device connectivity notifications, preventing those notifications from running on a transport/background thread.

code diff:

+    /// <summary>
+    /// Recomputes the SD gate when the selected device reports a connectivity change.
+    /// </summary>
+    /// <remarks>
+    /// The device raises this from Core's transport thread on a silent drop, so marshal before
+    /// touching view-model state. An empty/null property name is the
+    /// <see cref="INotifyPropertyChanged"/> "everything changed" convention and must be treated as
+    /// a possible <c>IsConnected</c> change.
+    /// </remarks>
+    private void OnSelectedDeviceConnectivityChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (!string.IsNullOrEmpty(e.PropertyName) &&
+            e.PropertyName != nameof(IStreamingDevice.IsConnected))
+        {
+            return;
+        }
+
+        // Ignore a late event from a device that is no longer the subscribed one.
+        if (!ReferenceEquals(sender, _connectivityWatchedDevice))
+        {
+            return;
+        }
+
+        if (Dispatcher.UIThread.CheckAccess())
+        {
+            RaiseSdGateChanged();
+            return;
+        }
+
+        // Non-throwing, for the same reason AbstractStreamingDevice.RaiseIsConnectedChanged and
+        // BootloaderWatcher guard theirs: this runs on Core's transport thread, and the disconnect
+        // that triggers it is exactly what app shutdown produces — so Post can be called as the
+        // dispatcher goes away. An escape here would propagate into Core's callback.
+        try
+        {
+            Dispatcher.UIThread.Post(RaiseSdGateChanged);
+        }
+        catch (Exception)
+        {
+            // Dispatcher unavailable / shutting down — drop the gate refresh; nothing is bound.
+        }
+    }

Move the RaiseSdGateChanged() call after the UI thread dispatch check to ensure
UI bindings and notifications are safely updated on the correct thread.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs [52-60]

-            // Recompute the SD gate on every connected-set change. CanAccessSdCard now depends on
-            // the selected device's IsConnected (issue #1), and an explicit Disconnect() or a USB
-            // removal fires ConnectionManager's "ConnectedDevices" even when the SelectedDevice
-            // *reference* is unchanged — so the gate must recompute here, not only on selection
-            // change (Qodo "SD access state stale").
-            RaiseSdGateChanged();
         }
 
         if (!Dispatcher.UIThread.CheckAccess())
+        {
+            Dispatcher.UIThread.Post(Update);
+            return;
+        }
 
+        // Recompute the SD gate only after reaching the UI thread.
+        RaiseSdGateChanged();
+
Suggestion importance[1-10]: 8

__

Why: Firing property and command change notifications on a background thread can cause cross-thread exceptions in Avalonia; moving the call after the UI thread check prevents this potential crash.

Medium
Suggestions up to commit 74bc67e
CategorySuggestion                                                                                                                                    Impact
General
Report disconnected devices accurately

Refactor ConnectionTypeMessage to handle disconnected states correctly. Utilize
pattern matching to return accurate status messages based on IsConnected and
ConnectionType.

Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs [32-35]

-public string ConnectionTypeMessage => SelectedDevice == null ? string.Empty :
-    SelectedDevice.ConnectionType == ConnectionType.Usb ?
-        "USB Connected - SD Card Access Available" :
-        "WiFi Connected - SD Card Access Available";
+public string ConnectionTypeMessage => SelectedDevice switch
+{
+    null => string.Empty,
+    { IsConnected: false } => "Device Disconnected - SD Card Access Unavailable",
+    { ConnectionType: ConnectionType.Usb } => "USB Connected - SD Card Access Available",
+    { ConnectionType: ConnectionType.Wifi } => "WiFi Connected - SD Card Access Available",
+    _ => "Connected - SD Card Access Unavailable"
+};
Suggestion importance[1-10]: 7

__

Why: Updating ConnectionTypeMessage to account for IsConnected ensures UI consistency with the updated CanAccessSdCard property logic, preventing misleading status messages for disconnected devices.

Medium

Comment thread Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs Outdated
Comment on lines +107 to +108
SelectedDevice is { IsConnected: true } &&
SelectedDevice.ConnectionType is ConnectionType.Usb or ConnectionType.Wifi;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

2. Selecteddevice double-read race 🐞 Bug ≡ Correctness

CanAccessSdCard reads SelectedDevice twice, so if SelectedDevice becomes null between the two
evaluations (e.g., selection/disconnect reentrancy), the second access can throw. Capturing
SelectedDevice in a local before checking avoids the race and keeps the result consistent.
Agent Prompt
### Issue description
`CanAccessSdCard` evaluates `SelectedDevice` twice; if `SelectedDevice` changes to `null` between evaluations, `SelectedDevice.ConnectionType` can throw. Even without null, the result can be inconsistent across two different instances.

### Issue Context
This is defensive hardening: it narrows a small but real race window and makes the property evaluation atomic.

### Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[46-57]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[106-108]

### Suggested change
Rewrite as:
```csharp
public bool CanAccessSdCard
{
    get
    {
        var device = SelectedDevice;
        return device is { IsConnected: true } &&
               device.ConnectionType is ConnectionType.Usb or ConnectionType.Wifi;
    }
}
```

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

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 74bc67e

@cptkoolbeenz

Copy link
Copy Markdown
Member Author

Pass-2 dispositions (commit 18f7971):

  • SelectedDevice double-read race — fixed: CanAccessSdCard pattern-captures the local, one read.
  • SD access state stale — substantially fixed: the SD gate now re-raises on every connected-set change (explicit disconnect + USB removal), and every SD op re-checks IsConnected at call time so none runs against a dead transport. The residual (a fully-silent WiFi drop raises no notification anywhere — IsConnected is a computed property the device layer never signals) is an app-wide device-layer gap tracked in reliability: device layer never notifies IsConnected transitions (stale command/UI state on silent drops) #3, out of scope for this SD-enablement PR.
  • Report disconnected devices accurately (imp 7) — applied: ConnectionTypeMessage is now IsConnected-aware.

@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Persistent suggestions updated to latest commit 18f7971

Comment thread Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 18f7971

@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Persistent suggestions updated to latest commit dec05fc

Comment thread Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit dec05fc

@cptkoolbeenz

Copy link
Copy Markdown
Member Author

Batch disposition of the open /improve suggestions (non-blocking tier):

  • Marshal state updates to UI / Marshal notifications onto UI thread — declined, already correct. RaiseSdGateChanged() is only ever invoked on the UI thread: from UpdateConnectedDevices' Update(), which the CheckAccess()/Dispatcher.UIThread.Invoke pattern marshals to the UI thread, and from OnSelectedDeviceChanged, which is UI-thread by construction (the SelectedDevice setter runs on the UI thread). Adding a Dispatcher.UIThread.Post guard would be dead defensive code.
  • Report disconnected devices accurately — applied in commit 18f7971 (ConnectionTypeMessage is now a single-read IsConnected-aware switch).

The blocking-tier bug Refresh overwrites disconnect reset is fixed in b249fff (post-await connectivity re-check in RefreshFilesAsync).

@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b249fff

@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/agentic_review

Comment thread Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 48b1b97

@cptkoolbeenz
cptkoolbeenz marked this pull request as draft July 18, 2026 19:36
@cptkoolbeenz

Copy link
Copy Markdown
Member Author

⚠️ Blocker found by adversarial audit: Core 1.0.0 hard-blocks SD download over WiFi

Converting to draft. The adversarial audit (which Qodo missed) found that the pinned Daqifi.Core 1.0.0 does not actually support SD download over WiFi — only listing:

  • DaqifiStreamingDevice.GetSdCardFilesAsync (LIST) guards on IsConnected only → works over WiFi. ✅
  • DaqifiStreamingDevice.DownloadSdCardFileAsync (line 1431) throws when !IsUsbConnection:

    "SD card file download is only supported over USB (serial) connections. The SD card and WiFi/LAN share the SPI bus, so file downloads require a USB connection."
    IsUsbConnection => Transport is SerialStreamTransport — false for WiFi/TCP. ❌

  • StartSdCardLoggingAsync is likewise USB-only (!IsUsbConnection throw) — but that's SD logging, already out of scope here.

So as-is this PR would let a WiFi user list SD files but Import would fail with the Core exception — a broken half-feature. This is exactly the "tracked upstream in daqifi-core" dependency issue #1 called out; the app-side gate wasn't the only USB-only policy.

The app pins Core 1.0.0 as a NuGet package (not a project ref), so it can't consume an unreleased Core fix. And issue #1 itself flags that SD:GET over TCP is not yet hardware-verified — Core's guard cites a real SD/WiFi SPI-bus-sharing constraint, so lifting it needs on-device proof first.

What's already solid in this PR (independent of the WiFi-download question)

The SD-pane disconnect-handling hardening is correct and valuable for USB too, and survived the audit:

  • gate SD access on IsConnected (not just connection type), pattern-captured single-read;
  • RaiseSdGateChanged recomputes + clears stale SD state on the reachable disconnect signal;
  • ConnectionTypeMessage / SdCardStatusLine are disconnect-aware and gate-consistent;
  • RefreshFilesAsync success and catch paths guard against stale-device clobber.

Proposed path (see the decision I'm asking on)

  1. Hardware-validate SD:LIST? + SD:GET over TCP on the bench Nyquist (issue feat(sd): enable SD-card offload over WiFi — the USB-only gate is stale (firmware #598/#599 route SD replies to TCP) #1's own must-verify).
  2. If it works: daqifi-core PR to lift the DownloadSdCardFileAsync IsUsbConnection guard → release Core 1.0.1.
  3. Bump the app to 1.0.1 and finalize this PR (or ship a list-only interim).

@cptkoolbeenz

Copy link
Copy Markdown
Member Author

Upstream enabler opened: daqifi-core PR #347 — allow SD file transfer over WiFi on firmware >= v3.7.0. It replaces Core's blanket USB-only SD-download guard with a firmware-capability gate (WiFi SD requires fw >= v3.7.0, per firmware #598/#599). Once that merges + releases, this PR bumps the Core package and comes out of draft. Per user direction (full path, firmware-version-gated).

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6677367

…ading by device state

The previous commit's fix over-corrected. Downgrading every failure to Warning whenever
the device was no longer current also silenced genuine app defects that merely coincided
with a disconnect or a device switch, dropping their Error/Sentry report — the inverse of
the false-positive it set out to fix.

Move the decision to where it belongs: the classifier, keyed on exception TYPE. A link
that dies under an in-flight SD operation throws TransportNotConnectedException (the
typed error AbstractStreamingDevice.IsTransportDisconnectedError already keys on) or
ObjectDisposedException from a torn-down stream; both are now expected device conditions
— Warning, no Sentry, "reconnect and try again" — and card-unavailable, so a batch import
stops rather than re-failing every remaining file against a dead link.

RefreshFilesAsync goes back to logging unconditionally through LogFailure. Whether an
exception is a disconnect artifact is a property of the exception, not of the ambient
device state at the moment it was caught, so severity is now decided in exactly one place.

Claude-Session: https://claude.ai/code/session_019fhAAGrHshwojdLJ99DUhc
@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/agentic_review

Comment thread Daqifi.Avalonia/Daqifi.Desktop/ViewModels/SdCardFailureClassifier.cs Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 95c096a

… exception only

ObjectDisposedException was too broad. An SD import also disposes a DbContext and file
streams, so a disposal defect in parsing or the database write would have been classified
as transport loss — downgraded to Warning, told the user to reconnect, and marked the card
unavailable, hiding a real bug behind a wrong diagnosis.

Keep only TransportNotConnectedException, which is unambiguous. A suppressed Error costs
more than a noisy one, so anything less specific stays on the Error path.

This is where the disconnect-logging cascade converges: state-based downgrade (too broad,
hid coincidental defects) -> type-based (right idea) -> typed transport exception only
(precise).

Claude-Session: https://claude.ai/code/session_019fhAAGrHshwojdLJ99DUhc
@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 928e68e

…untyped disconnect guard

1. HIGH — an ordinary disconnect still reached the Error/Sentry path. Core does NOT use its
   typed transport exception for its own connectivity guards: every SD entry point opens
   with a plain InvalidOperationException("Device is not connected."), and the text path
   adds "...disposing or disconnecting" variants. Worse, Disconnect() sets _isDisconnecting
   and updates Status BEFORE the typed check deeper in ExecuteTextCommandAsync is reached,
   so in a real race the untyped guard always wins. Narrowing to the typed exception last
   pass therefore left the original false positive intact — pressing Disconnect mid-refresh
   would file a Sentry issue.

   Match those guards explicitly. Message matching is fragile and is used only because Core
   exposes no type here; filed upstream as daqifi-core#395 to give the guards a typed
   exception, and this arm retires when that lands. Our own SD_UNAVAILABLE_MESSAGE is
   deliberately NOT matched — "no SD support" is a different condition, and reporting it as
   a lost connection would send the user to reconnect a healthy device.

2. MEDIUM — an unreachable device was rendered as "SD card OK · 0 files". Core's text
   exchange returns the lines collected so far when its response window elapses rather than
   throwing, and its list parser reads "no error lines, no content lines" as an empty
   directory — so a device that never answered is indistinguishable from an empty card. Over
   USB this barely mattered (an unplug faults the serial transport and Core throws), but
   issue #1 puts SD on WiFi, where a silently dead or merely congested link produces exactly
   this shape. The user would be told the card is fine and empty while their files sit on an
   unreachable device.

   Corroborate an empty list with the storage query, whose parser cannot read a missing reply
   as success and throws when nothing comes back. The exception propagates into the existing
   catch, so SdCardFailureClassifier reports it correctly with no view-model change. One
   extra round-trip, only on the empty-list path.

Claude-Session: https://claude.ai/code/session_019fhAAGrHshwojdLJ99DUhc
@cptkoolbeenz

Copy link
Copy Markdown
Member Author

/agentic_review

Comment on lines +1173 to +1175
if (files.Count == 0)
{
Task.Run(() => coreDevice.GetSdCardStorageAsync()).GetAwaiter().GetResult();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Storage check cannot validate list 🐞 Bug ≡ Correctness

A successful storage query only proves the device answered a later request; it cannot distinguish an
actually empty directory from a selectively lost, truncated, or timed-out LIST response. In that
case, RefreshSdCardFiles publishes the ambiguous empty result and the UI reports the card as
healthy with no files.
Agent Prompt
## Issue description
The independent storage query does not validate that the preceding empty LIST response was complete. A LIST-specific timeout or response loss followed by a successful storage request is still presented as a healthy empty card.

## Issue Context
The storage request is useful for detecting a completely unresponsive device, but it cannot prove directory emptiness. Require an explicitly complete LIST response, retry LIST, or preserve an unknown/error state when LIST completion cannot be established.

## Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[1159-1177]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[428-446]

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

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit e89ea7f

…am root cause

Qodo is right that a successful storage query proves the device answered a LATER request,
not that the listing was complete — a LIST lost or truncated while the link then recovered
still reads as an empty card. The corroboration narrows the ambiguity rather than removing
it, and the comment now says so instead of implying a guarantee (the fourth comment on this
PR corrected for over-claiming).

The residual is not fixable at this layer: only Core knows a reply never arrived. Filed as
daqifi-core#396, with three suggested fixes — the useful one also covers truncation, which
no downstream mitigation can reach. Accepted here as the strict improvement over reporting
every unreachable device as a healthy empty card.

Claude-Session: https://claude.ai/code/session_019fhAAGrHshwojdLJ99DUhc
@cptkoolbeenz

Copy link
Copy Markdown
Member Author

Converged — dispositioning the last finding and merging

Adversarial audit: PASS / clean on the final state (2/2 legs, 636 KB of transcript, engagement verified — the auditors traced Core's guards and the storage-query parser directly).

Disposition: "Storage check cannot validate LIST" — ACCEPTED AS A KNOWN RESIDUAL

Qodo is correct, and the point is now documented in the code and filed upstream. A successful storage query proves the device answered a later request; it cannot prove the listing itself was complete. A LIST that was lost or truncated while the link then recovered still reads as an empty card.

Why this is dispositioned rather than fixed:

  • It is not fixable at this layer. Only Core knows a reply never arrived — its text exchange returns the lines collected so far on timeout, and its list parser reads no-error-no-content as an empty directory. Filed as daqifi-core#396 with three concrete fixes; the useful ones also cover truncation, which no downstream mitigation can reach at all.
  • The change is a strict improvement. Before: every unreachable device rendered as "SD card OK · 0 files". After: that is caught unless the link dies and recovers inside one round-trip.
  • No data loss, no destructive action, self-correcting on the next successful refresh.
  • The comment no longer claims otherwise — it states the residual and cites fix(#327): LoggedDataPanePrototype's 102 bindings were all on reflection — root scope, two declared template scopes, two existing item scopes #396.

The cascade, and where it settled

Five audit rounds on this PR, and the disconnect-classification question alone took four iterations, each correcting the previous over-correction:

  1. state-based downgrade (log Warning when the device was no longer current) → too broad: silenced genuine defects that merely coincided with a disconnect
  2. → type-based classification in the classifier → right idea
  3. → narrowed to TransportNotConnectedException only, after ObjectDisposedException was found to swallow disposed-DbContext/FileStream defects from import
  4. → too narrow: Core doesn't use its typed exception for its own guards — every SD entry point throws plain InvalidOperationException("Device is not connected."), and Disconnect() sets _isDisconnecting/Status before the typed check is reachable, so the untyped guard always wins a real race. Now matched explicitly, with the fragility called out and daqifi-core#395 filed to make it typed.

Severity across rounds: high → clean → high/medium → clean → low-residual. That flattening is the convergence signal.

Upstream filed from this PR

  • daqifi-core#394 — TcpStreamTransport never reports a silent link death (no read-error hook or keepalive), so IsConnected on TCP tracks intent, not reality.
  • daqifi-core#395 — connectivity guards throw untyped InvalidOperationException, forcing clients to string-match.
  • daqifi-core#396 — a timed-out SD LIST is indistinguishable from an empty card (this finding's root cause).

@cptkoolbeenz
cptkoolbeenz merged commit f768e4c into main Jul 28, 2026
@cptkoolbeenz
cptkoolbeenz deleted the feat/sd-offload-over-wifi branch July 28, 2026 20:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(sd): enable SD-card offload over WiFi — the USB-only gate is stale (firmware #598/#599 route SD replies to TCP)

1 participant