apple / swift (pull_request) Successful in 1m30s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m37s
ci / bun-nix (pull_request) Successful in 30s
android / android (pull_request) Successful in 4m5s
ci / rust-arm64 (pull_request) Successful in 4m1s
ci / docs-site (pull_request) Successful in 1m26s
ci / rust (pull_request) Successful in 10m54s
Opt-in "Gyro from this phone" (gyro_on_phone, off by default): this device's IMU sources wire pad 0's motion while that pad is a controller with no motion source of its own. On Android that gate is exact — the only pads that forward motion are the capture links (USB DualSense / SC2, claimed as ExternalPads), so the mirror stands down per sample whenever GamepadRouter.padHasOwnMotion(0) says a capture link holds the index, and sends nothing while pad 0 has no slot at all (motion never creates a host pad). "Rumble on this phone"'s sibling, data flowing the other way: same read-once-at-attach settings plumbing, same hardware-gated rows in the touch and controller settings (a TV box has no gyroscope to mirror from). DeviceGyro registers TYPE_GYROSCOPE + TYPE_ACCELEROMETER at ~200 Hz on a dedicated HandlerThread with batching disabled (maxReportLatencyUs = 0 — batching is poison for gyro aim), converts with the wire contract shared with pf-client-core (rad/s → 20 LSB/°·s, m/s² → g → 10000 LSB/g; Android's accelerometer already reads specific force, the DualSense report's own convention), and rotates each sample from the natural-portrait sensor frame into the controller frame by display rotation — a phone clipped landscape yaws when the player yaws instead of rolling. The remap matrix and unit constants are pinned by DeviceGyroTest. A stand-down edge (capture link claims pad 0, or session teardown) sends one zero-gyro sample so the host's virtual pad never keeps integrating an angular velocity this device stopped producing — the gyro sweep's stale-rotation latch, avoided by construction here.