chore(device): add BootloaderSessionDevice so Core owns the bootloader stand-in - #478
Conversation
…r stand-in (#477) Both consumer apps (daqifi-desktop, daqifi-avalonia) carry a byte-for-byte equivalent no-op IStreamingDevice implementation for their manual bootloader-only firmware update dialogs. Stripped of comments the two copies differ by 15 lines, and that delta is pure version drift. Because Core owns IStreamingDevice but the only hand-written implementers live downstream, every widening of that interface is a source break that surfaces later, twice, and is invisible from inside this repository. v1.5.0 added 12 members; #476 added 4 more. Neither flagged it. Moving the stand-in here means such a member is resolved once, in the change that adds it, and is caught by this repository's build. The three behaviours the PIC32 update flow actually depends on are documented and tested rather than left implicit: - IsConnected starts true — the flow opens with EnsureDeviceConnected - IsStreaming is always false — lets the flow skip StopStreaming() - Send() discards silently — the flow sends force-bootloader unconditionally Connect/disconnect use an atomic compare-and-set so StatusChanged is raised exactly once per real transition when a dialog teardown races the update flow. Tests include a reflection sweep over the whole IStreamingDevice surface, asserting no member throws; verified non-vacuous by mutation (a member made to throw does fail it). That is the guard that keeps the type honest as the interface grows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR Summary by QodoAdd BootloaderSessionDevice no-op IStreamingDevice to centralize bootloader updates
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
|
/agentic_review |
Code Review by Qodo
1.
|
…sweep Qodo review: the sweep awaited Task and non-generic ValueTask only. Task<T> needs no special case since it derives from Task, but ValueTask<T> is a distinct struct with no non-generic base, so a future IStreamingDevice member returning one could fault unobserved while the sweep still reported success — undermining the guard's whole purpose. Extracts the result-observing logic into ObserveAsync and adds a test that feeds it a faulted instance of each awaitable shape (Task, Task<T>, ValueTask, ValueTask<T>) and asserts every one surfaces. Verified non-vacuous by mutation: removing the ValueTask<T> branch fails that test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 55c71cc |
Summary
daqifi-desktopanddaqifi-avaloniaeach carry a hand-written no-opIStreamingDevicefor their manual bootloader-only firmware update dialogs. They are the same file — stripped of comments the two copies differ by 15 lines, and that delta is pure version drift (desktop, on Core 1.4.0, hasErrorOccurredand theadd {}/remove {}accessor style; Avalonia, on 1.3.0, does not).Core owns
IStreamingDevice, but the only hand-written implementers live downstream. So every widening of that interface is a source break that surfaces later, in two repositories, and is invisible from inside this one — Core's own tests and the MCP consumer all work with the concreteDaqifiStreamingDevice. Measured against each app's currentorigin/main:v1.5.0 added 12 members in one release (
ConnectAsync/DisconnectAsync,DisposeAsync, 9 ×IConfirmingDeviceAdministration.*Async); #476 added 4 more. Neither flagged the break.This PR moves the stand-in into Core, so a member added to
IStreamingDeviceis resolved once — in the change that adds it — and caught by this repository's build.Changes
BootloaderSessionDevice(src/Daqifi.Core/Device/) — a sealed, no-opIStreamingDevicefor a device that is already sitting in its bootloader.Three of its behaviours are load-bearing rather than incidental, because
Pic32FirmwareUpdater.RunUpdateAsynctouches the device before it ever reaches the bootloader. All three are documented on the type and covered by tests:IsConnectedtrueFirmwareUpdateContext.EnsureDeviceConnected, which throws on a disconnected deviceIsStreamingfalseStopStreaming()callSend(...)ForceBootloaderunconditionally; throwing would abort a valid updateConnect/disconnect use an atomic compare-and-set, so
StatusChangedfires exactly once per real transition when a dialog's teardown races the update flow's ownDisconnect().Two smaller judgement calls, both departures from what the apps do today:
PwmFrequencyHzreportsDaqifiStreamingDevice.DefaultPwmFrequencyHz, not0. The apps return 0 citing a "none commanded this session" sentinel, but that comment predates the current contract —IStreamingDevicenow documents this as defaulting to a commandable frequency, and 0 is not one.Metadatais a real empty instance, notnull!. Callers read it without a guard.Docs — a
BootloaderSessionDevicesubsection under Implementation Classes indocs/DEVICE_INTERFACES.md, including the load-bearing table and a note steering consumers away from hand-rolling their own stub.Testing
FirmwareUpdateContext.EnsureDeviceConnected) rather than a restatement of it.IStreamingDevicesurface — including inheritedIDevice,IConfirmingDeviceAdministration, andIAsyncDisposablemembers — and asserts none throws. This is the guard that earns the type its place in Core: a member added later must be implemented here as a no-op, or CI fails.DisableAllChannels()throwNotImplementedExceptiondoes fail the sweep. The test also asserts the discovered surface is >50 methods, so a reflection walk that silently stopped finding members can't pass while covering nothing.Daqifi.Core.Tests: 2890 passed, 2 skipped. FullDaqifi.Mcp.Tests: 43 passed.TreatWarningsAsErrors=trueand XML doc generation on, so everycrefin the new docs resolves.No bench test. The change is purely additive — one new file plus a docs section, touching no existing code path — and nothing in Core constructs or calls this type. Exercising it for real means driving a manual bootloader update dialog, which lives in the apps and cannot consume this until it ships. The apps' existing adapters, whose behaviour this reproduces, are the field-proven reference.
Follow-up (not in this PR)
The consumer-side deletions in #477 land after this is released, as part of each app's Core version bump — they can't compile against an unreleased Core. Desktop's next bump is a 16-member adapter update either way; 12 of those are already on
mainfrom v1.5.0 independent of this change.Closes #477
🤖 Generated with Claude Code