// The motion frame conversion, pinned against the readings it was derived from. // // On 2026-08-07 one physical DualSense was read twice on one desk — over raw HID (the pad's own // report) and through GameController — so both frames come from the same controller in the same // orientations rather than from two documents: // // DualSense report frame: (Right, Up, Backward) axis 0 pitch, 1 yaw, 2 roll // GameController frame: (Right, Forward, Up) // // The numbers below are those measurements. They are the reason the conversion is `(x, z, -y)` and // not one of the five other permutations that also move gravity to slot 1, so they belong in a test // rather than only in a commit message. import XCTest @testable import PunktfunkKit final class GamepadMotionFrameTests: XCTestCase { private func wire(_ v: (Float, Float, Float)) -> (Float, Float, Float) { GamepadWire.appleMotionToWire(v) } /// Gravity at rest, face up. MEASURED: GameController read (+0.005, -0.192, +0.992) g while raw /// HID on the same pad read (+0.021, +0.997, +0.160). The conversion has to carry one into the /// other — including the small tilt term, which is what distinguishes this mapping from the one /// that merely gets gravity onto the right slot. func testRestingGravityLandsInTheDualSenseFrame() { let apple: (Float, Float, Float) = (0.005, -0.192, 0.992) let w = wire(apple) XCTAssertEqual(w.0, 0.005, accuracy: 0.001, "right stays on slot 0") XCTAssertEqual(w.1, 0.992, accuracy: 0.001, "up moves to slot 1 — the pad reads +1 g here") XCTAssertEqual(w.2, 0.192, accuracy: 0.001, "slot 2 is Backward, so GC's Forward negates") // The hardware's own reading of the same pose, to the precision two sessions of holding a // controller by hand can agree to. XCTAssertEqual(w.1, 0.997, accuracy: 0.02) XCTAssertEqual(w.2, 0.160, accuracy: 0.05) } /// The tilt term's SIGN is the whole point: before this conversion the client sent Apple's y /// straight through, so a pad tilted nose-up reported itself tilted nose-down. func testTheForeAftAxisIsNegatedNotJustMoved() { XCTAssertEqual(wire((0, 1, 0)).2, -1, "GC +y (Forward) is the wire's -Backward") XCTAssertEqual(wire((0, -1, 0)).2, 1) XCTAssertEqual(wire((0, 1, 0)).0, 0, "and it must not leak into the other slots") XCTAssertEqual(wire((0, 1, 0)).1, 0) } /// Each rotation, as measured, must reach the slot the wire reads it from: the wire's gyro is /// documented pitch/yaw/roll in slots 0/1/2, and the raw-HID run confirmed the pad agrees. func testEachRotationReachesItsWireSlot() { // Yaw is the reliable direct measurement — a continuous one-way spin, clockwise from above, // read as NEGATIVE on GC's z. It must arrive negative on slot 1, where the pad puts yaw. let yaw = wire((-0.2, 21.7, -122.2)) XCTAssertEqual(yaw.1, -122.2, accuracy: 0.01) XCTAssertLessThan(yaw.1, 0, "clockwise-from-above is negative about +Up, both frames agree") // Pitch: nose-down about Right stays on slot 0 and keeps its sign. let pitch = wire((-79.4, 0, 0)) XCTAssertEqual(pitch.0, -79.4, accuracy: 0.01) // Roll: about the fore-aft axis, which moves to slot 2 AND flips. let roll = wire((0, 61.8, 0)) XCTAssertEqual(roll.2, -61.8, accuracy: 0.01) } /// A change of basis is linear and orthonormal: it may not stretch a vector, and applying it to /// gyro and to acceleration must be the same operation. Both are asserted because the capture /// path calls it twice, on two different quantities. func testConversionIsAnIsometry() { for v in [(1, 2, 3), (-4, 5, -6), (0, 0, 1), (7, 0, 0)] as [(Float, Float, Float)] { let w = wire(v) let before = (v.0 * v.0 + v.1 * v.1 + v.2 * v.2).squareRoot() let after = (w.0 * w.0 + w.1 * w.1 + w.2 * w.2).squareRoot() XCTAssertEqual(before, after, accuracy: 1e-4, "must not change magnitude") } } /// Right-handed in, right-handed out. A permutation with the wrong number of sign flips is a /// REFLECTION, which reads as plausible on every single axis and inverts every rotation — the /// exact failure this measurement exists to prevent. func testHandednessIsPreserved() { let x = wire((1, 0, 0)) let y = wire((0, 1, 0)) // x cross y must equal the image of z, not its negative. let cx = (x.1 * y.2 - x.2 * y.1, x.2 * y.0 - x.0 * y.2, x.0 * y.1 - x.1 * y.0) let z = wire((0, 0, 1)) XCTAssertEqual(cx.0, z.0, accuracy: 1e-5) XCTAssertEqual(cx.1, z.1, accuracy: 1e-5) XCTAssertEqual(cx.2, z.2, accuracy: 1e-5) } }