Skip to content

chore: retire the four daqifi-core#398 bandaids now that Core v1.4.0 owns them - #808

Merged
tylerkron merged 2 commits into
mainfrom
chore/retire-core-398-bandaids
Aug 4, 2026
Merged

chore: retire the four daqifi-core#398 bandaids now that Core v1.4.0 owns them#808
tylerkron merged 2 commits into
mainfrom
chore/retire-core-398-bandaids

Conversation

@tylerkron

Copy link
Copy Markdown
Contributor

Closes #803

Stacked on #806 (chore/core-1.4.0-bump). Base is that branch, not main.

daqifi-core#398 shipped in Core v1.4.0, so the four desktop workarounds that stood in for it (#776, #779, #780, #782) can be deleted. Every claim below was verified by reading the Core source at v1.4.0, not the XML docs or the ticket text — two of the ticket's claims turned out to be wrong (see "Where the issue was wrong").


1. Timestamp reset — daqifi-core#398 gap 3

Deleted: _timestampProcessorSync, _timestampFrequencyApplied, the ~30 lines of doc explaining why the lock was load-bearing, and TimestampProcessorSerializationTests in full.

Why it's safe. src/Daqifi.Core/Device/TimestampProcessor.cs at v1.4.0:

public void ResetAll()
{
    // Session baselines only. Device tick periods are static device configuration...
    _deviceStates.Clear();
}

_deviceTickPeriods is untouched. The desktop's two-part invariant — "the gate and the frequency it stands for have to change together" — no longer exists, because there is no desktop-side gate: ITimestampProcessor.HasTimestampFrequency(deviceId) (new in v1.4.0, confirmed public on the interface) is now the record of whether the apply happened, and it lives inside the same concurrent dictionary the apply writes. TimestampProcessor was already thread-safe on its own.

Lock audit (judgment call 1). _timestampProcessorSync had exactly three uses, all of them the apply/gate/reset interleaving:

Site What was under it
ProcessStreamMessage apply + gate write + ProcessTimestamp
InitializeStreaming ResetAll() + gate clear
StopStreaming ResetAll() + gate clear

Nothing else had crept under it, so it is removed entirely rather than narrowed.

Bonus taken in the same PR. TimestampResult.UsedFallbackTickPeriod is new in v1.4.0 and is set from the same dictionary read that chooses the tick period, so it cannot disagree with what was actually used. The failure mode behind #782 — a 42 MHz device silently reconstructed against the 50 MHz default, scaling every timestamp by ~1.19 with no error — is now logged. Warning, not Error, and once per device: it is recoverable device configuration, Error is the Sentry path, and a per-frame log at streaming rate would bury everything else.

2. Firmware classifier — daqifi-core#398 gap 4

Deleted: the FirmwareFlashPhase enum, the phase parameter on both classifier methods, and the phase variable threaded through FirmwareUpdateCoordinator and DaqifiViewModel.

Why it's safe. FirmwareUpdateState.ReconnectingAfterFlash = 12 is new in v1.4.0. git grep ReconnectingAfterFlash v1.4.0 -- src/Daqifi.Core/ returns exactly one emitting site — WifiModuleUpdater.cs:133, immediately after the WINC success-marker check and wrapping WaitForSerialReconnectAsync + the LAN restore. Pic32FirmwareUpdater.cs still transitions to JumpingToApp after the CRC pass and never enters ReconnectingAfterFlash. The two states are therefore disjoint across the two flows, which is what makes the caller-supplied phase unnecessary.

Core also fixed its own BuildRecoveryGuidance in the process: ReconnectingAfterFlash gets "the firmware was flashed and verified successfully…" instead of the PIC32 CRC text a successful WiFi flash used to be handed.

Every switch over FirmwareUpdateState (judgment call 4). There were none in the desktop other than the classifier's own switch over FirmwareFlashPhase (now deleted). Rather than rely on that staying true, the classifier tests now carry an exhaustiveness guard that compares Enum.GetValues<FirmwareUpdateState>() against the union of the downgraded and not-downgraded lists — a future Core state fails the suite instead of being silently defaulted.

3. SD transfer stalls — daqifi-core#398 gap 1

Deleted: Daqifi.Desktop/Loggers/SdCardDownloadStalledException.cs in full.

Why it's safe. SdCardTransferStalledException (v1.4.0) carries Reason, BytesReceived, FileName and Timeout — strictly more than the binary IsProlongedFailure it replaces. The classifier now keys on Reason, and the mapping preserves the old behaviour exactly:

Reason Guidance Stops a batch? Old equivalent
TransferTimeout power-cycle yes IsProlongedFailure == true
TransportClosed reconnect (new text) yes n/a — was folded into the above
NoDataReceived incomplete-transfer no IsProlongedFailure == false

Over USB serial — the only transport SD import supports — a wedged device produces NoDataReceived in about half a second, which stays per-file. That is the #779 case, and it stays off the Error/Sentry arm.

The Sentry arm (judgment call 2). Deleting the type without care would have reopened #779 in two different ways, and both are guarded:

  • SdCardTransferStalledException derives from SdCardOperationException, so the new arm is placed before the generic SdCardOperationException arm. Landing on that arm would have silently turned a device-wide stall into a per-file one with "the card may be corrupt" advice. There is a dedicated regression test for the arm order.
  • Core still throws a bare TimeoutException from SdCardOperations.RunWithHardDeadlineAsync when it abandons a parked worker. Unnormalized that reaches the classifier's default arm — a Sentry issue plus "check the device connection", exactly what bug: SD-download stall watchdog is dead code over USB serial — Core's plain TimeoutException hits the classifier's default (Sentry) arm #779 removed. The scoped catch (TimeoutException) at the download call site therefore stays; it now rethrows as Core's SdCardTransferStalledException(TransferTimeout) instead of ours. A bare TimeoutException reaching the classifier from anywhere else still keeps the Error path, and that test is unchanged.

Assertions are on log level, not "did not throw": ImportFile_TransportDetectedStall_LogsWarningNotError and the new ImportFile_TransportClosedStall_LogsWarningNotError both verify Warning once and Error never.

Watchdog kept, deliberately. Core does bound the download now (daqifi-core#399/#401), but DaqifiStreamingDevice.SdCardDownloadTimeout is internal virtual at 30 minutes and not settable from here. Shrinking the desktop's 90-second bound to 30 minutes would regress the #754 busy-overlay symptom, so the watchdog stays; only the exception type it raises changed. Worth a follow-up once Core exposes the knob.

4. Empty transfers — daqifi-core#398 gap 2

Deleted: the importer's fileInfo.Length == 0 throw.

Why it's safe. SdCardFileReceiver.ReceiveAsync now takes listedFileSizeBytes and only raises SdCardEmptyTransferException when totalBytesReceived == 0 && listedFileSizeBytes != 0 — so a file the listing reports as 0 bytes returns a legitimate empty download. SdCardOperations.TryGetListedFileSize sources that from the last GetSdCardFilesAsync listing, and the desktop's RefreshSdCardFiles goes through exactly that Core call, so the discrimination is genuinely wired up here rather than just theoretically available. A 0-byte log now imports as an empty session and logs a Warning naming the file.

Batch continuation (judgment call 3). #780 is preserved and slightly improved: the SdCardEmptyTransferException arm is still IsCardUnavailable: false (Core's unknown-listed-size case keeps its conservative throw, so it can still be one file), and the common empty-log case no longer fails at all, so it does not even consume a "skipped" slot. ImportAllFiles_WhenTheFirstFileIsEmpty_StillImportsEveryHealthyFileAfterIt and ImportAllFiles_WhenTheTransportTimesOut_KeepsGoingAndStaysOffTheErrorPath are unchanged and green.

The separate string.IsNullOrEmpty(FilePath) guard is kept but reclassified: Core's temp-file overload always returns result with { FilePath = tempPath }, so an empty path can only be a broken IStreamingDevice. It now throws InvalidOperationException and keeps the Error/Sentry path, where a contract violation belongs — it is no longer reported to the user as a wedged SD subsystem.


Where the issue was wrong

Both found by reading Core at the tag:

  1. "the classifier reduces to keying on FailedState == ReconnectingAfterFlash". No — PIC32's benign post-flash state is still JumpingToApp, which Core did not touch. Both arms remain. What the split actually bought is that the two states are disjoint across the two flows, which is what removes the phase parameter, not one of the arms. Dropping the JumpingToApp arm would have regressed Daqifi.Core.Firmware.FirmwareUpdateException: Firmware update failed in state 'JumpingToApp' while Jumping to application firmware.. #738.

  2. "the download is bounded now, so a wedged card can no longer hang the call" — true, but the bound is enforced by throwing a bare TimeoutException from RunWithHardDeadlineAsync. The catch (TimeoutException) normalization the issue lists for deletion is still load-bearing; deleting it puts a wedged card back on the Sentry arm.

Verification

  • dotnet build -tl:on — clean, zero warnings.
  • dotnet test --filter "TestCategory!=Ui&FullyQualifiedName!~WindowsFirewallWrapperTests" -tl:on868 passed, 0 failed (parent branch baseline: 871).
    • −3 TimestampProcessorSerializationTests (tested the deleted lock), +4 StreamTimestampReconstructionTests (assert the user-visible outcomes that survive).
    • −5 FirmwareFailureClassifierTests phase-crossproduct rows, replaced by one exhaustiveness guard over the whole enum.
    • −2 SD stall tests that asserted SilentFor/IsProlongedFailure on the deleted type; +1 TransportClosed case, +1 arm-order regression guard, +1 listed-0-byte import test.
  • Not bench-verified. Per the issue's own verification list this still wants: an SD import including a 0-byte file, a PIC32 flash, and a WiFi-module flash. No hardware was touched by this PR.

Rebase note

When #806 squash-merges, rebase this branch with:

git rebase --onto origin/main <parent-sha>

rather than merging main — the squash creates a duplicate of the pre-squash commit, and merging resolves it into a conflicted mess by hand.

🤖 Generated with Claude Code

@tylerkron
tylerkron requested a review from a team as a code owner August 3, 2026 17:38
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Retire Core#398 desktop workarounds now covered by Core v1.4.0

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Remove desktop timestamp-frequency gate/lock; rely on Core v1.4.0 ResetAll +
 HasTimestampFrequency.
• Simplify firmware reconnect-timeout classification using Core ReconnectingAfterFlash; drop phase
 plumbing.
• Normalize SD download stalls around Core SdCardTransferStalledException; update importer,
 classifier, and tests.
Diagram

graph TD
  C{{"Daqifi.Core v1.4.0"}} --> TS["Streaming timestamps"] --> LOG["Warn on fallback"]
  C --> FW["Firmware update"] --> FWC["Reconnect classifier"]
  C --> SDI["SD import"] --> SDC["Stall classification"]
  TST["Desktop tests"] --> TS --> LOG
  TST --> FW --> FWC
  TST --> SDI --> SDC
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep desktop workarounds behind Core-version checks
  • ➕ Could keep compatibility if desktop can run with Core <1.4.0
  • ➕ Allows incremental rollout if Core behavior differs in the wild
  • ➖ Adds conditional complexity in hot paths and error handling
  • ➖ Risks long-term divergence from Core semantics; more test matrix surface
2. Wrap all SD download timeouts into a single desktop exception type
  • ➕ Simplifies downstream classification logic to one exception type
  • ➖ Loses Core-provided stall reason/bytesReceived detail
  • ➖ Makes it easier to regress switch-arm ordering and misclassify device vs app faults
3. Rely solely on Core’s 30-minute SD deadline and remove desktop watchdog
  • ➕ Removes duplicate timeout mechanisms and related tests
  • ➖ 30-minute busy overlay is unacceptable UX for the desktop importer
  • ➖ Desktop still needs finer-grained ‘silence’ behavior and guidance

Recommendation: The PR’s approach—deleting now-redundant desktop gates/enums and keying off Core v1.4.0’s explicit signals (HasTimestampFrequency, UsedFallbackTickPeriod, ReconnectingAfterFlash, SdCardTransferStalledException)—is the best long-term maintenance choice. Keeping compatibility shims via version checks was considered, but given this is stacked on the Core v1.4.0 bump, removing the bandaids reduces complexity and prevents semantic drift. Retaining the desktop SD watchdog remains justified for UX (shorter bound) while still preserving Core’s richer stall typing where available.

Files changed (13) +957 / -517

Bug fix (3) +214 / -172
AbstractStreamingDevice.csRemove timestamp apply/reset lock and add warn-on-fallback tick period +83/-83

Remove timestamp apply/reset lock and add warn-on-fallback tick period

• Deletes the desktop-side _timestampProcessorSync lock and _timestampFrequencyApplied gate, switching to Core's HasTimestampFrequency for one-time per-device frequency application. Uses TimestampResult.UsedFallbackTickPeriod to emit a single Warning per device when reconstructing against Core’s fallback tick period, and simplifies InitializeStreaming/StopStreaming ResetAll calls accordingly.

Daqifi.Desktop/Device/AbstractStreamingDevice.cs

SdCardSessionImporter.csUse Core stalled-transfer exceptions; treat listed 0-byte logs as valid empty sessions +76/-63

Use Core stalled-transfer exceptions; treat listed 0-byte logs as valid empty sessions

• Updates ImportFromDeviceAsync and DownloadWithStallWatchdogAsync to throw/propagate SdCardTransferStalledException with explicit stall reasons. Removes legacy ‘empty file == device wedge’ behavior: a listed 0-byte file now logs a Warning and imports as an empty session, while a missing FilePath is treated as an InvalidOperationException (contract violation).

Daqifi.Desktop/Loggers/SdCardSessionImporter.cs

SdCardFailureClassifier.csClassify SD stalls by Core stall reason and add reconnect guidance for TransportClosed +55/-26

Classify SD stalls by Core stall reason and add reconnect guidance for TransportClosed

• Replaces desktop SdCardDownloadStalledException handling with SdCardTransferStalledException handling keyed on SdCardTransferStallReason. Introduces TRANSPORT_CLOSED_GUIDANCE, ensures stall handling precedes the SdCardOperationException base-type arm, and keeps bare TimeoutException off the SD classifier path (normalization happens at the importer call site).

Daqifi.Desktop/ViewModels/SdCardFailureClassifier.cs

Refactor (3) +54 / -82
FirmwareFailureClassifier.csDrop FirmwareFlashPhase and classify reconnect timeouts by FailedState alone +27/-54

Drop FirmwareFlashPhase and classify reconnect timeouts by FailedState alone

• Removes the FirmwareFlashPhase enum and updates IsPostFlashReconnectTimeout to downgrade only JumpingToApp (PIC32) and ReconnectingAfterFlash (WiFi). Updates message selection to key off FailedState rather than a caller-supplied phase.

Daqifi.Desktop/Device/Firmware/FirmwareFailureClassifier.cs

FirmwareUpdateCoordinator.csRemove phase threading from firmware update coordinator exception handling +19/-24

Remove phase threading from firmware update coordinator exception handling

• Eliminates the flashPhase variable and passes only the FirmwareUpdateException to the handler. The handler now relies on the updated firmware failure classifier and clarifies that Verifying is never downgraded post-Core v1.4.0.

Daqifi.Desktop/Device/Firmware/FirmwareUpdateCoordinator.cs

DaqifiViewModel.csRemove WiFi firmware phase parameter and use FailedState for installed messaging +8/-4

Remove WiFi firmware phase parameter and use FailedState for installed messaging

• Updates WiFi firmware-only update flow to use the new phase-free classifier and to build the installed/power-cycle dialog message based on Core’s FailedState. Keeps installed-but-not-reconnected behavior on the Warning (non-Sentry) path.

Daqifi.Desktop/ViewModels/DaqifiViewModel.cs

Tests (7) +689 / -263
FirmwareFailureClassifierTests.csUpdate firmware reconnect-timeout tests for Core v1.4.0 states +69/-74

Update firmware reconnect-timeout tests for Core v1.4.0 states

• Removes phase-based expectations and rewrites tests to treat JumpingToApp and ReconnectingAfterFlash as the only downgraded reconnect-timeout states. Adds an exhaustiveness assertion to force deliberate handling when Core adds new FirmwareUpdateState values.

Daqifi.Desktop.Test/Device/Firmware/FirmwareFailureClassifierTests.cs

StreamTimestampReconstructionTests.csAdd timestamp reconstruction regression tests for stop/start and fallback logging +383/-0

Add timestamp reconstruction regression tests for stop/start and fallback logging

• Introduces a transport-less streaming device fixture to verify stop/start preserves device timestamp frequency without reapplying per session. Adds coverage that missing timestamp frequency triggers exactly one Warning and never an Error/Sentry path.

Daqifi.Desktop.Test/Device/StreamTimestampReconstructionTests.cs

SdCardDownloadFailureTests.csAlign SD download failure tests to Core stalled-transfer typing +56/-106

Align SD download failure tests to Core stalled-transfer typing

• Switches expectations from the removed desktop SdCardDownloadStalledException to Core's SdCardTransferStalledException and its Reason/Timeout/BytesReceived fields. Adds coverage for Core’s hard-deadline TimeoutException normalization and clarifies contract-violation vs device-condition cases.

Daqifi.Desktop.Test/Loggers/SdCardDownloadFailureTests.cs

SdCardSessionImporterTests.csAdd test for importing legitimate listed 0-byte SD logs +43/-0

Add test for importing legitimate listed 0-byte SD logs

• Adds a regression test ensuring a device-listed 0-byte file imports as an empty session (0 samples) rather than being rejected by legacy desktop guards. Uses a mocked IStreamingDevice and a real temp file to simulate the Core v1.4.0 behavior.

Daqifi.Desktop.Test/Loggers/SdCardSessionImporterTests.cs

DaqifiViewModelFirmwareUpdateTests.csUpdate ViewModel firmware tests for ReconnectingAfterFlash guidance +47/-37

Update ViewModel firmware tests for ReconnectingAfterFlash guidance

• Updates modeled Core exceptions/operation strings to v1.4.0 sources and adds the new ReconnectingAfterFlash recovery guidance constant. Adjusts the WiFi reconnect-timeout test to assert the installed/power-cycle path without CRC guidance leakage.

Daqifi.Desktop.Test/ViewModels/DaqifiViewModelFirmwareUpdateTests.cs

DeviceLogsViewModelImportTests.csUpdate SD import ViewModel tests for new stall reasons + add TransportClosed case +33/-14

Update SD import ViewModel tests for new stall reasons + add TransportClosed case

• Migrates helper exceptions to SdCardTransferStalledException with explicit reasons (NoDataReceived, TransferTimeout). Adds a new test ensuring TransportClosed stalls are Warning-level (no Sentry) and show reconnect guidance while aborting the batch.

Daqifi.Desktop.Test/ViewModels/DeviceLogsViewModelImportTests.cs

SdCardFailureClassifierTests.csUpdate SD failure classifier tests for Core v1.4.0 stalled-transfer semantics +58/-32

Update SD failure classifier tests for Core v1.4.0 stalled-transfer semantics

• Reworks tests to validate stall classification by SdCardTransferStallReason, including TransferTimeout (power-cycle), NoDataReceived (per-file), and TransportClosed (reconnect). Adds a regression guard ensuring the stalled-transfer arm matches before its SdCardOperationException base-type arm.

Daqifi.Desktop.Test/ViewModels/SdCardFailureClassifierTests.cs

Base automatically changed from chore/core-1.4.0-bump to main August 3, 2026 17:39
@qodo-code-review

qodo-code-review Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 49 rules

Grey Divider


Remediation recommended

1. Act inlined in Assert.IsTrue ✓ Resolved 📘 Rule violation ▣ Testability
Description
Modified unit tests call FirmwareFailureClassifier.IsPostFlashReconnectTimeout(...) inside the
assertion rather than separating an explicit Act step, so the Arrange/Act/Assert structure is not
clearly delineated. This reduces test readability and makes future debugging/refactoring harder.
Code

Daqifi.Desktop.Test/Device/Firmware/FirmwareFailureClassifierTests.cs[52]

+        Assert.IsTrue(FirmwareFailureClassifier.IsPostFlashReconnectTimeout(exception));
Relevance

●●● Strong

AAA/explicit Act enforcement frequently accepted; reviewers ask not to inline Act in Assert.* (e.g.,
PRs #741, #770).

PR-#741
PR-#770
PR-#729

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 244801 requires new or modified tests to clearly separate Arrange, Act, and Assert.
In the modified test, the call to FirmwareFailureClassifier.IsPostFlashReconnectTimeout(exception)
is embedded directly inside Assert.IsTrue(...), so there is no explicit/clearly separated Act
step.

Rule 244801: Enforce Arrange-Act-Assert structure in unit tests
Daqifi.Desktop.Test/Device/Firmware/FirmwareFailureClassifierTests.cs[52-52]
Daqifi.Desktop.Test/Device/Firmware/FirmwareFailureClassifierTests.cs[67-67]

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

## Issue description
Some modified tests inline the primary Act call inside `Assert.*(...)` instead of having a distinct Act step between Arrange and Assert, which violates the required Arrange-Act-Assert structure.

## Issue Context
Compliance requires new/modified tests to have clearly separated Arrange, Act, and Assert sections (comments or blank lines are acceptable), with a single primary Act step.

## Fix Focus Areas
- Daqifi.Desktop.Test/Device/Firmware/FirmwareFailureClassifierTests.cs[43-68]

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


2. Fallback warning not per-device ✓ Resolved 🐞 Bug ◔ Observability
Description
AbstractStreamingDevice.WarnOnceAboutFallbackTickPeriod uses a single boolean flag, so once any
device triggers the fallback warning, the same wrapper will never warn again even if it later
represents a different device/serial number. Because serial-device discovery reuses an existing
SerialStreamingDevice instance per port and overwrites DeviceSerialNo, a newly connected device can
lose this diagnostic warning.
Code

Daqifi.Desktop/Device/AbstractStreamingDevice.cs[R796-801]

+        if (_warnedAboutFallbackTickPeriod)
+        {
+            return;
+        }
+
+        _warnedAboutFallbackTickPeriod = true;
Relevance

●● Moderate

Team fixes state leaking across device re-association (PR #693), but no direct precedent for
per-device WarnOnce flags.

PR-#693
PR-#456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The warning suppression state is a single boolean that ignores the deviceId parameter, so it is
inherently scoped to the device wrapper instance rather than to a device identity. Separately,
serial discovery explicitly mutates DeviceSerialNo on an existing SerialStreamingDevice
instance, meaning one wrapper can represent different serial numbers over time—so the wrapper-scoped
boolean can suppress a warning for a later device.

Daqifi.Desktop/Device/AbstractStreamingDevice.cs[794-808]
Daqifi.Desktop/ViewModels/ConnectionDialogViewModel.cs[780-790]

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

## Issue description
`WarnOnceAboutFallbackTickPeriod(string deviceId)` is documented and shaped to warn once *per device*, but it currently suppresses warnings via a wrapper-wide `_warnedAboutFallbackTickPeriod` boolean. If the same wrapper instance later represents a different physical device (new serial number), the warning is incorrectly suppressed.

## Issue Context
The connection/discovery layer updates `SerialStreamingDevice.DeviceSerialNo` on an existing instance for the same port, so wrapper reuse across physical devices is plausible.

## Fix Focus Areas
- Daqifi.Desktop/Device/AbstractStreamingDevice.cs[140-145]
- Daqifi.Desktop/Device/AbstractStreamingDevice.cs[794-808]

### Suggested change
- Replace `_warnedAboutFallbackTickPeriod` with a device-identity-aware mechanism, e.g.:
 - `private string? _warnedFallbackTickPeriodDeviceId;` and suppress only when it matches the current `deviceId`, or
 - a `HashSet<string>`/`ConcurrentDictionary<string, byte>` of warned deviceIds if you want to support multiple ids over the wrapper lifetime.
- (Optional hardening) If stream callbacks can be concurrent, make the “check then set” atomic (e.g., `Interlocked.CompareExchange` pattern) to avoid duplicate warnings.

ⓘ 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.

Qodo Logo

Comment thread Daqifi.Desktop.Test/Device/Firmware/FirmwareFailureClassifierTests.cs Outdated
Comment thread Daqifi.Desktop/Device/AbstractStreamingDevice.cs Outdated
@tylerkron
tylerkron force-pushed the chore/retire-core-398-bandaids branch from b0b442c to 1fe0e7b Compare August 3, 2026 17:44
@tylerkron

Copy link
Copy Markdown
Contributor Author

Rebased onto main — #806 has merged

#806 squash-merged as ded1ad1, which retargeted this PR to main automatically and left it carrying a duplicate of the pre-squash bump commit. Rebased with git rebase --onto origin/main 3597765 and force-pushed, per the rebase note above.

The note above is now stale and can be ignored — it has been executed.

Effect on this PR's diff: it no longer re-displays the already-merged bump. Reviewers see only this change.

Unit gate re-run after the rebase, not just before it.

@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 60d65af

@tylerkron

Copy link
Copy Markdown
Contributor Author

Status: ready for human review

On Qodo's "fallback warning not per-device" finding

The conclusion was right and the fix shipped, but the mechanism in the suggestion was wrong, and the difference mattered. Recording it because the wrong version is superficially convincing.

The suggestion said discovery "overwrites DeviceSerialNo" on the reused instance, implying a fix keyed on that property. But this warning never reads DeviceSerialNo — the id comes from the frame itself, message.DeviceSn (AbstractStreamingDevice.cs:715).

My first regression test modelled the suggested mechanism literally, mutating DeviceSerialNo. It passed against the unfixed code, which is how the mechanism was caught as wrong. Had the fix been keyed that way, it would have been a guard that never fires plus a test that proves nothing.

The real vector is the same instance reuse one step later: AddSerialDeviceFromDiscovery reuses the device registered for a COM port, so swapping units leaves one wrapper decoding another device's frames — and those frames carry the new unit's serial. The fix keys on that, via Interlocked.Exchange so the claim and the already-warned check are one atomic step (covering the concurrency hardening that was suggested as optional). The new test fails against the old implementation.

Outstanding: needs a human at the bench

Not covered by the unit gate:

  1. SD import including at least one 0-byte file — confirm the listed-size discrimination returns a legitimate empty download instead of raising, and that Import All still continues past a bad file (bug: Import All aborts on the first empty/ambiguous SD file, silently skipping later healthy files #780).
  2. A PIC32 flash and a WiFi-module flash — confirm the classifier still downgrades the benign WiFi reconnect timeout and still escalates a real CRC failure (Daqifi.Core.Firmware.FirmwareUpdateException: Firmware update failed in state 'JumpingToApp' while Jumping to application firmware.. #738, bug: firmware JumpingToApp carve-out misses the symmetric WiFi-module post-flash reconnect timeout (FailedState=Verifying) #776).

Note when testing SD: firmware#703 (SD read buffer allocation failing under low heap — LIST works, GET returns marker-only 0 bytes) is a firmware-side failure that presents as a desktop bug. Rule it out before attributing a failure here.

claude added 2 commits August 3, 2026 14:59
…owns them

daqifi-core#398 shipped in Core v1.4.0, so the four desktop workarounds that
stood in for it (#776, #779, #780, #782) can go.

1. Timestamp reset (gap 3). Core's ResetAll() now clears session baselines
   only and keeps per-device frequencies, so the desktop's
   _timestampFrequencyApplied gate and the _timestampProcessorSync lock that
   kept the gate and the frequency changing together are both gone. The apply
   is now once per device, keyed on Core's own HasTimestampFrequency. Bonus
   from the same Core release: TimestampResult.UsedFallbackTickPeriod makes
   the previously silent 50 MHz fallback observable, so a device that reports
   no clock now logs one Warning per device instead of nothing at all.

2. Firmware classifier (gap 4). Core added
   FirmwareUpdateState.ReconnectingAfterFlash, emitted only by
   WifiModuleUpdater after the WINC success marker. Verifying is now
   unambiguously the PIC32 flash CRC check, so the FirmwareFlashPhase enum and
   the phase parameter threaded through the coordinator and DaqifiViewModel
   are deleted and the classifier keys on FailedState alone.

   Note the issue text is wrong that this "reduces to keying on
   ReconnectingAfterFlash": PIC32's benign post-flash state is still
   JumpingToApp, which Core did not change. Both arms remain; what the split
   bought is that they are disjoint across the two flows, which is what makes
   the phase parameter unnecessary.

3. SD transfer stalls (gap 1). SdCardDownloadStalledException is deleted and
   the classifier keys on Core's SdCardTransferStalledException.Reason:
   TransferTimeout and TransportClosed stop a batch, NoDataReceived does not
   (the old IsProlongedFailure binary, with strictly more information).

   Core still throws a bare TimeoutException from its own hard download
   deadline, so the scoped normalization at the download call site stays - it
   now produces Core's type instead of ours. Deleting it outright would have
   put a wedged card back on the Sentry arm, which is the whole point of #779.

4. Empty transfers (gap 2). Core discriminates a wedged SD subsystem from a
   legitimately 0-byte file using the directory listing's reported size, so
   the importer's own "0 bytes on disk" throw is deleted - a listed 0-byte
   file now imports as an empty session rather than failing and burning a slot
   in an Import All batch (#780). The separate null-FilePath guard is kept but
   reclassified as a contract violation (Error/Sentry), because Core's
   temp-file overload always sets FilePath.

Tests: TimestampProcessorSerializationTests is deleted along with the lock it
tested, and replaced by StreamTimestampReconstructionTests, which asserts the
user-visible outcomes that survive (a stop/start leaves the session on the
device clock; a device with no reported clock warns once and never at Error).
The SD and firmware classifier suites keep their log-level assertions.

Closes #803

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WarnOnceAboutFallbackTickPeriod takes a deviceId but suppressed on a
wrapper-wide bool, so the signature promised per-device and the implementation
delivered per-wrapper.

That gap is reachable: discovery reuses the SerialStreamingDevice already
registered for a COM port rather than constructing a new one, so swapping units
on that port leaves one instance decoding a different device's frames. The
second unit would lose the diagnostic entirely.

Key on the frame's own serial via Interlocked.Exchange, which makes the claim
and the already-warned test a single atomic step so racing frames emit once.

Also separates Act from Assert in the firmware classifier tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron
tylerkron force-pushed the chore/retire-core-398-bandaids branch from 60d65af to 95bdafc Compare August 3, 2026 21:02
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

Summary

Summary
Generated on: 8/3/2026 - 9:05:33 PM
Coverage date: 8/3/2026 - 9:03:50 PM - 8/3/2026 - 9:05:28 PM
Parser: MultiReport (2x Cobertura)
Assemblies: 2
Classes: 141
Files: 164
Line coverage: 57.5% (6011 of 10448)
Covered lines: 6011
Uncovered lines: 4437
Coverable lines: 10448
Total lines: 34798
Branch coverage: 43.6% (1614 of 3697)
Covered branches: 1614
Total branches: 3697
Method coverage: Feature is only available for sponsors

Coverage

DAQiFi - 57.6%
Name Line Branch
DAQiFi 57.6% 43.8%
Daqifi.Desktop.App 2.9% 0%
Daqifi.Desktop.Channel.AbstractChannel 74.7% 57.1%
Daqifi.Desktop.Channel.AnalogChannel 46.8% 25%
Daqifi.Desktop.Channel.ChannelColorManager 100% 100%
Daqifi.Desktop.Channel.DataSample 91.6%
Daqifi.Desktop.Channel.DigitalChannel 86.5% 61.1%
Daqifi.Desktop.Configuration.FirewallConfiguration 90.6% 66.6%
Daqifi.Desktop.Configuration.WindowsFirewallWrapper 63% 64.2%
Daqifi.Desktop.ConnectionManager 81.2% 75%
Daqifi.Desktop.Converters.BoolAndToVisibilityConverter 100% 100%
Daqifi.Desktop.Converters.BrushColorMatchConverter 100% 100%
Daqifi.Desktop.Converters.ConnectionTypeToColorConverter 88.8% 83.3%
Daqifi.Desktop.Converters.InvertedBoolToVisibilityConverter 100% 100%
Daqifi.Desktop.Converters.ListToStringConverter 100% 94.4%
Daqifi.Desktop.Converters.NotNullToVisibilityConverter 100% 100%
Daqifi.Desktop.Converters.OxyColorToBrushConverter 100% 100%
Daqifi.Desktop.Device.AbstractStreamingDevice 70.7% 64%
Daqifi.Desktop.Device.ConnectionLostEventArgs 100%
Daqifi.Desktop.Device.DeviceMessage 92.8%
Daqifi.Desktop.Device.Firmware.BootloaderDiscoveredEventArgs 100% 50%
Daqifi.Desktop.Device.Firmware.BootloaderHoldDroppedEventArgs 100% 50%
Daqifi.Desktop.Device.Firmware.BootloaderHoldService 82.7% 83.3%
Daqifi.Desktop.Device.Firmware.BootloaderSessionStreamingDeviceAdapter 10% 8.3%
Daqifi.Desktop.Device.Firmware.BootloaderWatcher 90.7% 72.6%
Daqifi.Desktop.Device.Firmware.FirmwareFailureClassifier 100% 100%
Daqifi.Desktop.Device.Firmware.FirmwareUpdateCoordinator 74.1% 63.4%
Daqifi.Desktop.Device.Firmware.FirmwareUpdateServiceConfig 100%
Daqifi.Desktop.Device.Firmware.HeldBootloader 100% 50%
Daqifi.Desktop.Device.Firmware.HidBootloaderDiscovery 0% 0%
Daqifi.Desktop.Device.NativeMethods 100%
Daqifi.Desktop.Device.SerialDevice.SerialStreamingDevice 56.5% 55.4%
Daqifi.Desktop.Device.WiFiDevice.DaqifiStreamingDevice 59.2% 52.5%
Daqifi.Desktop.DialogService.DialogService 0% 0%
Daqifi.Desktop.DialogService.ServiceLocator 0% 0%
Daqifi.Desktop.DiskSpace.DiskSpaceCheckResult 100%
Daqifi.Desktop.DiskSpace.DiskSpaceEventArgs 100%
Daqifi.Desktop.DiskSpace.DiskSpaceMonitor 88.2% 86.6%
Daqifi.Desktop.DiskSpace.DiskSpaceMonitorCoordinator 100% 100%
Daqifi.Desktop.DiskSpace.DiskSpaceStartDecision 100%
Daqifi.Desktop.DuplicateDeviceCheckResult 100%
Daqifi.Desktop.Exporter.LoggingSessionSampleSource 96.3% 75%
Daqifi.Desktop.Exporter.OptimizedLoggingSessionExporter 68.9% 54.5%
Daqifi.Desktop.Helpers.BooleanConverter`1 100% 100%
Daqifi.Desktop.Helpers.BooleanToInverseBoolConverter 100% 100%
Daqifi.Desktop.Helpers.BooleanToVisibilityConverter 100%
Daqifi.Desktop.Helpers.EnumDescriptionConverter 100% 91.6%
Daqifi.Desktop.Helpers.IntToVisibilityConverter 100% 100%
Daqifi.Desktop.Helpers.MinMaxDownsampler 100% 96.4%
Daqifi.Desktop.Helpers.NaturalSortHelper 100% 100%
Daqifi.Desktop.Helpers.OxyPlotDarkTheme 100%
Daqifi.Desktop.Helpers.ReferenceComparer`1 100%
Daqifi.Desktop.Helpers.TileBrushes 100%
Daqifi.Desktop.Helpers.UiThreadHelper 26.6% 25%
Daqifi.Desktop.Logger.DatabaseLogger 0% 0%
Daqifi.Desktop.Logger.DatabaseMigrator 0% 0%
Daqifi.Desktop.Logger.DeviceLegendGroup 100% 100%
Daqifi.Desktop.Logger.InitialSessionLoad 100%
Daqifi.Desktop.Logger.LoggedSeriesLegendItem 94.1% 44.4%
Daqifi.Desktop.Logger.LoggingContext 100%
Daqifi.Desktop.Logger.LoggingContextDesignTimeFactory 0%
Daqifi.Desktop.Logger.LoggingManager 25.8% 26.7%
Daqifi.Desktop.Logger.LoggingSession 36.5% 13%
Daqifi.Desktop.Logger.MinimapPlotComponents 100%
Daqifi.Desktop.Logger.PlotLogger 68.8% 63.6%
Daqifi.Desktop.Logger.PlotModelFactory 99.4% 62.5%
Daqifi.Desktop.Logger.SessionChannelInfo 100%
Daqifi.Desktop.Logger.SessionDataRepository 97.9% 89.5%
Daqifi.Desktop.Logger.SessionDeviceMetadata 80%
Daqifi.Desktop.Logger.SessionSampleWriter 96% 91.3%
Daqifi.Desktop.Logger.SummaryLogger 88.2% 71.7%
Daqifi.Desktop.Logger.TimestampGapDetector 94.7% 83.3%
Daqifi.Desktop.Loggers.AppLoggerLoggerProvider 59.5% 56.6%
Daqifi.Desktop.Loggers.ImportOptions 100%
Daqifi.Desktop.Loggers.ImportProgress 0% 0%
Daqifi.Desktop.Loggers.ImportTimestampQuality 100% 100%
Daqifi.Desktop.Loggers.SdCardImportResult 100%
Daqifi.Desktop.Loggers.SdCardSessionImporter 79.4% 70%
Daqifi.Desktop.MainWindow 0% 0%
Daqifi.Desktop.Migrations.AddSamplesSessionTimeIndex 97.8%
Daqifi.Desktop.Migrations.AddSessionDeviceMetadata 98.6%
Daqifi.Desktop.Migrations.AddSessionSampleCount 98.1%
Daqifi.Desktop.Migrations.DropChannelTable 65.5%
Daqifi.Desktop.Migrations.InitialSQLiteMigration 97.4%
Daqifi.Desktop.Migrations.LoggingContextModelSnapshot 0%
Daqifi.Desktop.Models.DaqifiSettings 83.7% 100%
Daqifi.Desktop.Models.DebugDataHistory 6.6% 0%
Daqifi.Desktop.Models.DebugDataModel 0% 0%
Daqifi.Desktop.Models.FirmwareOption 61.5% 37.5%
Daqifi.Desktop.Models.Notifications 80%
Daqifi.Desktop.Models.Profile 100%
Daqifi.Desktop.Models.ProfileChannel 100%
Daqifi.Desktop.Models.ProfileDevice 100%
Daqifi.Desktop.Models.SdCardFile 42.8% 0%
Daqifi.Desktop.Models.SdCardLogFormatInfo 94.7% 87.5%
Daqifi.Desktop.Services.NoOpMessageBoxService 0%
Daqifi.Desktop.Services.WindowsPrincipalAdminChecker 0%
Daqifi.Desktop.Services.WpfMessageBoxService 0%
Daqifi.Desktop.UpdateVersion.VersionNotification 84% 54.1%
Daqifi.Desktop.View.ConnectionDialog 0% 0%
Daqifi.Desktop.View.DebugWindow 0% 0%
Daqifi.Desktop.View.DeviceLogsView 0% 0%
Daqifi.Desktop.View.DuplicateDeviceDialog 0% 0%
Daqifi.Desktop.View.ErrorDialog 0% 0%
Daqifi.Desktop.View.ExportDialog 0% 0%
Daqifi.Desktop.View.FirmwareDialog 0% 0%
Daqifi.Desktop.View.Flyouts.LiveGraphFlyout 0% 0%
Daqifi.Desktop.View.Flyouts.NotificationsFlyout 0% 0%
Daqifi.Desktop.View.Flyouts.SummaryFlyout 0% 0%
Daqifi.Desktop.View.MigrationStatusWindow 0% 0%
Daqifi.Desktop.View.MinimapInteractionController 0% 0%
Daqifi.Desktop.View.ProfilesPane 0% 0%
Daqifi.Desktop.View.Prototype.ChannelsPanePrototype 0% 0%
Daqifi.Desktop.View.Prototype.DevicesPanePrototype 0% 0%
Daqifi.Desktop.View.Prototype.LiveGraphPane 0% 0%
Daqifi.Desktop.View.Prototype.LoggedDataPanePrototype 0% 0%
Daqifi.Desktop.View.SuccessDialog 0% 0%
Daqifi.Desktop.ViewModels.ChannelsPaneViewModel 57.8% 34.8%
Daqifi.Desktop.ViewModels.ChannelTileViewModel 72.6% 66.1%
Daqifi.Desktop.ViewModels.ConfirmOverlayViewModel 100% 100%
Daqifi.Desktop.ViewModels.ConnectionDialogViewModel 66.1% 55.7%
Daqifi.Desktop.ViewModels.DaqifiViewModel 19.4% 14.3%
Daqifi.Desktop.ViewModels.DeviceLogsViewModel 80.4% 60.8%
Daqifi.Desktop.ViewModels.DevicesPaneViewModel 0% 0%
Daqifi.Desktop.ViewModels.DeviceTileViewModel 35.7% 15.2%
Daqifi.Desktop.ViewModels.DuplicateDeviceDialogViewModel 0%
Daqifi.Desktop.ViewModels.ErrorDialogViewModel 0%
Daqifi.Desktop.ViewModels.ExportDialogViewModel 65.5% 50%
Daqifi.Desktop.ViewModels.FirmwareDialogViewModel 55.1% 46.8%
Daqifi.Desktop.ViewModels.ImportAllOutcome 100% 100%
Daqifi.Desktop.ViewModels.LoggingSessionListViewModel 95.6% 88.8%
Daqifi.Desktop.ViewModels.NewProfileChannelItem 0%
Daqifi.Desktop.ViewModels.NewProfileDeviceItem 0% 0%
Daqifi.Desktop.ViewModels.ProfilesPaneViewModel 0% 0%
Daqifi.Desktop.ViewModels.SdCardFailure 100%
Daqifi.Desktop.ViewModels.SdCardFailureClassifier 100% 91.6%
Daqifi.Desktop.ViewModels.SettingsViewModel 0% 0%
Daqifi.Desktop.ViewModels.SuccessDialogViewModel 0%
Sentry.Generated.BuildPropertyInitializer 100%
Daqifi.Desktop.Common - 46%
Name Line Branch
Daqifi.Desktop.Common 46% 30.9%
Daqifi.Desktop.Common.AppDataPaths 84.2% 50%
Daqifi.Desktop.Common.Loggers.AppLogErrorException 0%
Daqifi.Desktop.Common.Loggers.AppLogger 40.9% 28.9%

Coverage report generated by ReportGeneratorView full report in build artifacts

@tylerkron

Copy link
Copy Markdown
Contributor Author

Rebased onto main (post-#812), and a bench-status note

Rebased after #812 merged, with the unit gate re-run after the rebase rather than before. All three PRs were already reported textually mergeable, but #808 and #810 modify the same file #812 touched, so "no conflict" was not sufficient reason to skip re-testing.

Hardware gate: partially blocked, and NOT on anything in these PRs

Device-gated FlaUI run on the chain-1 tip: 19/23. Four failures, none attributable to this work:

  • 2 SD imports (SdCardImport_ExistingFileOnCard_ImportsToSession, SdCardLogging_LogsToSdCard_ThenImportsToSession) — both report -1 samples on files dated 2026-06-23 and 2026-07-06. Known firmware SD-heap defect (firmware#703); the bench card has been wedged since June and writes no new files.
  • 2 connect/discovery timeouts (ConfigureLogging_SetsFrequencyAndChannels, RestartStreamingAfterGap_TimeAxisAnchorsOnNewSession) — No device discovered on Serial within 60s and Expected at least 1 connected device tile(s) but found fewer within 60s.

The last two are device state, not code. Established by control, not assumption:

Run Code Result
Earlier today #812 branch RestartStreamingAfterGap PASSED (twice)
Now main, which contains #812 FAILS — same code
Now chain-1 tip FAILS identically

Since the identical code passed earlier and fails now, the variable is the bench, not the branch. A Core-level probe on the same port confirms the device is still alive at the transport layer — connects, initializes, firmware 3.7.2, reports analog_in_port_enabled = FFFF, channel enables survive — so it responds to a direct port open while the app's discovery probe times out. That is consistent with the SD subsystem being wedged by the SD tests that ran immediately before, which per firmware#703 recovers only on a true USB re-enumeration.

This needs a physical power-cycle of the bench device, which I can't perform. I deliberately did not force re-enumeration via pnputil/Device Manager — this hardware has known recovery problems and that is not a risk to take unprompted.

Once the device is power-cycled the device-gated suite should be re-run against these branches to close out the gate. Everything else — build, unit gate, Qodo — is green.

@tylerkron

Copy link
Copy Markdown
Contributor Author

Hardware gate result

Run with parallelism disabled (-- MSTest.Parallelize.Workers=1), which is required for this suite to be deterministic at all — see #814.

Branch Device-gated result
chain-1 (#808 + #810) 21/23 — only the 2 known firmware SD failures
chain-2 (#809) 20/23 — the 2 SD failures + RestartStreamingAfterGap

The extra failure on #809 is not #809

RestartStreamingAfterGap_TimeAxisAnchorsOnNewSession failed with Expected at least 1 connected device tile(s) but found fewer within 60s. Checked rather than assumed:

Identical code passing earlier and failing now, plus main failing the same way, rules out #809. The variable is the bench: discovery degrades as the SD tests accumulate, which is the same behaviour a power cycle cleared earlier today (firmware#703 — recovers only on true USB re-enumeration).

Status

Everything else on all three: build clean, unit gates green, Qodo clean.

@tylerkron

Copy link
Copy Markdown
Contributor Author

Hardware gate: cleared

Re-run on a freshly power-cycled device with -- MSTest.Parallelize.Workers=1 (required — see #814).

The one outstanding question was RestartStreamingAfterGap_TimeAxisAnchorsOnNewSession, which failed in #809's full-suite runs but not #810's. Settled with a controlled back-to-back triplet — same test, run alone, same device state, one after another:

Branch Isolated result
main PASS
#810 tip PASS
#809 PASS

All three pass. The full-suite failure is a harness defect, not a branch one: a preceding test leaves its app running (Application failed to exit in the captured output), that app holds the COM port, and the next test is the one that reports the timeout. The same marker appears in runs on main. Now recorded on #814.

I was wrong twice on the way to this, and both are worth recording so the next person does not repeat them: I first attributed these failures to an SD-induced device wedge (excluding the SD tests made it worse), and then to device degradation across the session (a fresh device reproduced it identically). Neither held up.

Final state, all three PRs

#808 #809 #810
Build clean clean clean
Unit 876 896 872
Qodo Bugs 0, Rules 0 Bugs 0, Rules 0 Bugs 0, Rules 0
Device-gated 21/23 20/23 21/23

Every remaining device-gated failure is accounted for: the two SD imports are the known firmware SD-heap defect (firmware#703) on files dated June/July, and #809's third is the leaked-app harness issue above, disproved by isolated re-run.

@tylerkron
tylerkron added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 5ecf5ff Aug 4, 2026
2 checks passed
@tylerkron
tylerkron deleted the chore/retire-core-398-bandaids branch August 4, 2026 02:01
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.

chore: retire the four daqifi-core#398 bandaids now that Core v1.4.0 owns them

2 participants