Present a REAL game controller to an iOS device and set its state. The device registers a virtual DualShock 4 and receives genuine controller HID, so NATIVE apps see a GCExtendedGamepad and a web page sees a W3C standard-mapping pad in navigator.getGamepads(). It shows up in Settings > General > Game Controller. No Safari page is required.
HOW TO FIRE INPUT PROPERLY:
• One call = ONE frame = one instant in time. A button stays pressed until you send a frame WITHOUT it, so every press needs a matching release frame — exactly like keydown then keyup. Sending press after press just holds them all down.
• For anything that should look played rather than stepped — mashing, combos, a stick sweep — pass `frames` instead of calling repeatedly. The device plays the whole sequence out at `intervalMs` (default 33 ms ≈ 30 fps). A round trip per frame cannot reach that cadence, so a rapid sequence built out of single calls will always read as held buttons.
• Alternate press and release inside `frames`: [{buttons:[1]}, {}, {buttons:[0,1]}, {}] is tap A, release, tap A+B, release.
• Axes are [leftX, leftY, rightX, rightY], -1..1. Sweep them across frames to roll a stick; omitted axes read as 0.
• READ THIS BEFORE REPORTING A STICK BUG: a stick axis can only express RIGHT and UP on `axes[0..1]`, because the report's joystick fields are unsigned. The left stick's full 360-degree analog push IS delivered, but it arrives on the D-PAD — `buttons[12..15]`, and `GCExtendedGamepad.dpad.xAxis/yAxis` for native apps, which are bipolar. Reconstruct it as x = right - left, y = down - up. Pushing left and reading `axes[0] === 0` is the EXPECTED, structural behaviour, not a fault to debug.
• While a sequence is playing, this call owns the pad — a controller streamed from a live viewer is paused and resumes on its own shortly after.
• Confirm the pad EXISTS with ios_gamepad_status; confirm VALUES in the app under test.
Input routes to whatever holds focus, so foreground the app under test first — measuring while Settings or another app is in front reads as "nothing works".
ConnectorOAuth