forked from unom/punktfunk
Supersedes the synchronous calibration readf6de620fshipped an hour ago. The ordering it protected is kept; the blocking it cost is not.f6de620fread the pad's calibration inline in DsCapture.startUsb, which runs on the main thread — the stream's setup path, and the USB-permission broadcast. The read is a blocking EP0 control transfer: a pad that is there answers in about a millisecond, but a pad that is stalling takes the link's whole 250 ms write timeout, and either way the interface was waiting on a controller. That is the wrong thread for it. It now runs on its own daemon thread, one per claim, named pf-ds-cal — the same shape HidUsbLink already uses for its reader rather than a second style. A pathological stall now delays the pad's motion by a moment instead of freezing the UI. What kept the ordering honest before was "assign the calibration before `model`", since `model` is what lets the link thread into the parse. That reasoning stands, so the gate simply moved: MotionCalHandoff holds the claim's calibration, starts null, and onReport parses nothing until it lands. No report is ever scaled by the last pad's numbers — those are per unit — nor by the nominal fallback the real read is about to replace. Dropping the first millisecond of a capture costs nothing: the reports carry absolute state, so the next one says everything the dropped one would have. The calibration is what got deferred, not `model`, and that is deliberate. Keeping `model` synchronous keeps isActive, the teardown writes, the feedback sinks and the active-changed true/false pairing meaning exactly what they meant yesterday — and, more to the point, it makes a late completion structurally unable to resurrect a dead capture. A straggler can only ever publish a calibration, and nothing is parsed while `model` is null. Teardown, which is where this sort of change actually bites. Both stop() and the unplug path end the claim before they close anything: ending burns the token, so a read that lands afterwards publishes nothing and says so in the log. They then wait, bounded at 500 ms and normally already over, for the read to let go of the connection they are about to close — closing a descriptor with a transfer in flight pulls it out from under the kernel, the same rule the pad-audio borrow follows. It cannot deadlock: the reading thread blocks on the EP0 transfer and on the hand-off's own monitor, never on anything a teardown holds. If a pad has stopped answering entirely the wait elapses and teardown proceeds regardless, which is the same exposure the feedback writes already carry and better than an interface that never comes back. Tested where it is testable. MotionCalHandoff is the piece that carries the hazard and it is pure, so it has its own test: nothing is visible until the read lands, a read that outlived its claim publishes nothing, a re-claim never inherits the previous pad's calibration, and a doubled end still refuses every outstanding token. Mutation-checked both ways — deleting the token check fails 3 of them, deleting begin's clear fails the fourth. Not covered: DsCapture's own claim/teardown ordering is not unit-testable in this module — there is no Robolectric, and the class builds a main-Looper Handler and needs a UsbManager — so it is argued in comments rather than pinned. The on-glass re-verificationf6de620fowes is unchanged and still owed. Gate: `:kit:compileDebugKotlin`, `:kit:testDebugUnitTest` (61 cases across the module, 0 failed) and `:app:compileDebugKotlin` green, with the four new cases confirmed present in the JUnit XML rather than assumed from a green build.