refactor(firmware): split FirmwareUpdateService into focused collaborators (part of #344) - #419
Conversation
…ators (part of #344) FirmwareUpdateService had grown to 2,656 lines co-mingling two independent flows behind one class: the PIC32 bootloader path and the WiFi-module path, plus shared retry/state helpers and an embedded verify-progress parser. Split behind the unchanged IFirmwareUpdateService facade: - FirmwareUpdateContext - shared state machine, progress, retry and per-state timeout plumbing - Pic32BootloaderSession - the low-level HID bootloader exchanges - Pic32FirmwareUpdater - PIC32 flow orchestration + diagnostics + cleanup - WifiModuleUpdater - WINC version probe + external flash-tool run - WifiFlashProgressParser - lifted out of its nested position Pure refactor: no public API change and no behavior change. Verified by diffing the normalized statement multiset of the original file against all six new files - every difference is structural (signatures, fields, wiring), with zero logic statements added, removed or altered. Adds one test. The StateChanged sender was previously an implicit `this` on a field-like event and so could not be wrong; it is now forwarded from the context, which nothing asserted. The new test pins it (mutation-verified: it fails when the sender is wrong). WifiFlashProgressParser was internal nested in a public class, so its external accessibility is unchanged; only the in-assembly name moved, which is why three test references were updated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoRefactor firmware updates: extract PIC32/WiFi updaters behind FirmwareUpdateService facade
AI Description
Diagram
High-Level Assessment
Files changed (7)
|
Code Review by Qodo
1.
|
… skipping the action Qodo round 1 on #419. FirmwareUpdateContext.ExecuteWithRetryAsync returned successfully without ever invoking the action when maxAttempts was 0 or negative - the for loop body simply never ran. For a helper that carries flash-critical steps (erase, program, verify) that is a silent success which skipped the operation with no failure signal. Throwing rather than clamping to 1: a caller asking for zero attempts has a bug and should hear about it, not have it quietly corrected. This behavior is PRE-EXISTING - identical on origin/main, where the same unguarded loop lived in FirmwareUpdateService. The split relocated it verbatim. Adding the guard is therefore a deliberate, narrow exception to the PR's pure-refactor claim, not a regression the split introduced. Adds three tests: the 0 and -1 cases (throws, action never runs) and the maxAttempts == 1 boundary (still runs exactly once). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Qodo round 1 addressed — 1 bug, 0 rule violationsChecked all three surfaces (inline review comments, the issue-comment summary, and Retry loop can no-op — agreed, fixed in f65dc7d.
Added an upfront Worth recording: this is pre-existing behavior, not a regression from the split. The identical unguarded loop is on Three tests added: Suite now 4,707 green across net9.0 and net10.0; build still clean with |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit f65dc7d |
Why
FirmwareUpdateServicehad grown to 2,656 lines. It held two completely independent jobs in one class — flashing the PIC32 over its HID bootloader, and flashing the WiFi module via an external tool — plus the shared retry/state machinery both lean on. It's one of the two biggest collision points in the repo: unrelated changes keep landing in the same file.This is the firmware half of #344. The
DaqifiStreamingDevicehalf is separate.What
Same public API, same behavior, same tests — the code just lives in sensible places now.
FirmwareUpdateServiceFirmwareUpdateContextPic32BootloaderSessionPic32FirmwareUpdaterWifiModuleUpdaterWifiFlashProgressParserWas: one file, 2,656 lines.
How
Rather than trust my own reading, I diffed the normalized statement multiset of the original file against all six new files — stripping comments and canonicalizing the mechanical renames. Every remaining difference is structural (signatures, field declarations, wiring). Zero logic statements were added, removed, or changed.
One test added. The
StateChangedevent'ssenderused to be an implicitthison a field-like event, so it couldn't be wrong. It's now forwarded from the context, and nothing asserted it — so I pinned it, and mutation-verified the test actually fails when the sender is wrong.WifiFlashProgressParserwasinternalnested inside apublicclass, so it was never externally visible. Moving it to top-levelinternalis not a public API change; only the in-assembly name moved, which is why three test references were updated.No changes outside
src/Daqifi.Core/Firmware/**and its tests.Bench evidence
Nyquist 1, FW 3.7.2 / HW 2.0.0, on
/dev/cu.usbmodem1101and over WiFi at192.168.1.30. Example CLI built against this working copy.PIC32 bootloader diagnostics loop — the meaningful test for this refactor, and non-destructive by design:
That exercises the whole new PIC32 stack end to end on real hardware:
RunBootloaderDiagnosticAsync(facade lock + Idle gate),Pic32FirmwareUpdater.RunHealthCheckAsync/RunSoftResetAsync, andPic32BootloaderSession's enumerate / connect / version / jump. Worth noting the run emitted no state-transition events — that's correct, and confirms the diagnostics still deliberately leaveCurrentStateatIdle.Streaming, to show the shared assembly is undisturbed:
(Sample counts run below the nominal rate because of this unit's known FW 3.7.2 clock offset, not anything here.)
Not validated on hardware
No erase and no program was run — deliberately. There's no macOS recovery path if a flash goes wrong, so the actual
UpdateFirmwareAsyncandUpdateWifiModuleAsyncwrite paths were not exercised on the bench. Their correctness here rests on the statement-level diff plus the existing unit tests, not on hardware. The WiFi external-tool path in particular is Windows-only today and cannot run on this machine at all.Two things worth a reviewer's eye
WaitingForBootloaderTimeoutDetailProvideris a settable property on the context, assigned once after construction. It's a mild smell — if it were ever null, theWaitingForBootloadertimeout message would quietly lose its diagnostic detail. The cause is a real cycle: the context must exist to build the session, but the detail lives on the session. An existing test covers the message text, so a regression would be caught. I left it rather than add a lazier indirection.WifiModuleUpdateris 813 lines, marginally over the issue's "~800" target. The obvious next seam is the external-tool orchestration — which is exactly the code WiFi (WINC) flash depends on a Windows-only external tool — blocks cross-platform #271 replaces when it introduces theIWincFlasherseam for the native cross-platform flasher. Splitting it here would be churn I'd immediately redo, so I left it for that PR.Test suite
4,701 tests green across net9.0 and net10.0 (+1 from the sender guard). Build clean — 0 warnings, and
TreatWarningsAsErrorsis on.🤖 Generated with Claude Code