Skip to content

Feature/core device orientation#1727

Merged
doronz88 merged 10 commits into
masterfrom
feature/core-device-orientation
Jun 15, 2026
Merged

Feature/core device orientation#1727
doronz88 merged 10 commits into
masterfrom
feature/core-device-orientation

Conversation

@doronz88

Copy link
Copy Markdown
Owner

No description provided.

doronz88 added 10 commits June 15, 2026 23:43
Decoded from a Xcode-mirror sniff of four back-to-back rotate-left clicks.
Each request is an OrientationRequest envelope on the new
'com.apple.coredevice.devicecontrol' RemoteXPC service:

    {featureIdentifier: 'com.apple.coredevice.feature.remote.devicecontrol.orientation',
     messageType: 'OrientationRequest',
     payload: {rotate: {_0: 'left'}}}

The reply carries currentDeviceOrientation + currentDeviceNonFlatOrientation
+ currentDeviceOrientationLocked. Four 'left' requests cycle the device
through portrait -> landscapeLeft -> portraitUpsideDown -> landscapeRight.

Exposed as 'pymobiledevice3 developer core-device rotate [left|right]'.
Adds Rotate ↺ / Rotate ↻ buttons in the viewer's util-tray and a
POST /rotate endpoint on the server that forwards to
OrientationService.rotate(direction).

The rotation is applied INSIDE the canvas (ctx.translate + ctx.rotate
+ drawImage on each frame), not via CSS transform on the canvas
element, so the canvas's own CSS box matches the rotated footprint
and the surrounding flex layout + cosmetic bezel wrap it naturally —
no overflow, no separate bezel rotation.

drawPending watches the buffer's aspect ratio; if it flips between
portrait and landscape (i.e. iOS is re-rendering for the new
orientation) the in-canvas rotation auto-resets to 0 so we don't
double-rotate already-oriented content. Touch coordinates project
through the inverse rotation back into the device buffer's frame
before normalising to HID 0..65535.
New util-tray button that pauses keydown/keyup forwarding so the
browser receives keys normally (Cmd-L, Cmd-W, devtools shortcuts,
etc). The toggle is purely client-side — no device-side release flush
on toggle-off, since the device only knows about the keys we've
sent. Preference persisted in localStorage.
Modal lists the Ctrl-hotkeys and a brief explainer of how the
keyboard-capture toggle and rotation buttons interact. Opens via the
'?' key or the new util-tray '?' button; closes on Esc, click outside
the card, or the X. While open all keystrokes are gated from the
device so typing into the page (e.g. searching for help) doesn't
leak through.
The virtual keyboard surface is host-registered against the live
media stream via UniversalHIDServiceService.create_keyboard_service.
When the stall watchdog restarts the stream, dtuhidd publishes fresh
HID surfaces and the previous _ServiceID becomes stale — every
report posted to it is silently dropped by backboardd.

_stop_hid already drops the UHS/Indigo channel handles; also drop
_kb_service_id so the next /key triggers _ensure_keyboard to
register a fresh surface against the new stream context.
Moves the utility buttons (Sound / Style / Frame / Restart / Help /
Rotate / Keyboard) into a left-side panel that participates in the
document flow instead of position:fixed, so a wide / rotated canvas
can't slide underneath them. Each tray has a chevron toggle to
collapse to a thin strip and reclaim canvas room; the open/closed
state persists in localStorage.

Also lands:
- 'Screenshot' button + Ctrl+P hotkey -> canvas.toBlob saved as PNG
  (uses the canvas backing store as-is so the file matches what the
  user sees, including in-canvas rotation).
- 'Reload' button -> location.reload() for a full client reset
  distinct from /restart (which only restarts the device stream).
- Offline overlay over the canvas when frameCount hasn't advanced
  in ~3 s while subscribed -- gives the user a clear 'stream is
  stuck, here's why your input isn't doing anything' signal.

fitCanvasToViewport now MEASURES the trays' actual widths instead of
the previous hard-coded 160 px reserve, so collapse/uncollapse
immediately reflows the canvas to the new available space.
Right-side collapsible tray that mirrors 'pymobiledevice3 developer
accessibility settings show' -- each AXAuditDeviceSetting renders as
a checkbox (bool) or 0..1 slider (float, e.g. DYNAMIC_TYPE), and
edits round-trip through AccessibilityAudit.set_setting on the
device. 'Reset all' calls reset_settings.

Lockdown / DTX comes up lazily on the first /accessibility request,
so a serve-web run that never opens the panel doesn't pay the
usbmuxd handshake cost.
The accessibility sidebar leaked one fresh AccessibilityAudit per
request -- each held an open DTX channel whose reader loop got
cancelled at process exit, producing a stack of 'Channel reader
loop cancelled' ERROR-level tracebacks in the log.

Cache the audit handle on the server, reuse it across panel
interactions, and close it in the shutdown sequence so the DTX
channel exits via the normal close() path (channel._closed=True
suppresses the cancellation traceback). serve-web shutdown is
now clean: one 'shutting down…' line, 'hid worker cancelled'
acknowledgement, 'shutdown complete'.
Rotation used to snap instantly -- the canvas dim swap + content
redraw all happened in one frame. Add a 300 ms eased CSS transform
on #device-frame so the visual rotation animates while the
canvas-internal rotation still snaps to the final orientation
underneath.

Mechanism: on each setVisualRotation() the canvas dims + content
update immediately (so the surrounding layout reflows now), then
#device-frame gets transform=rotate(-delta) with transition=none
so its visual position matches where it was a moment ago; a
double-RAF then enables a cubic-bezier transition and animates
the transform back to identity. The result is the canvas content
appearing to rotate smoothly into place over 300 ms while the
flex layout reflows in step.
Hitting /accessibility right before SIGINT produced a 'coroutine
ignored GeneratorExit' RuntimeError on Python 3.14: the request
handler was mid-bplist decode (synchronous plistlib code, not at
an await point) when shutdown closed the audit out from under it,
and the loop's coroutine cleanup couldn't unwind through the
non-awaiting frame.

_stop_accessibility now takes _accessibility_lock first, so any
in-flight handler completes its decode (or hits the outer 3s
shutdown bound) before we tear the DTX channel down. Verified
across 5 stress trials: 0 channel-reader tracebacks, 0 GeneratorExit
warnings, same clean three-line shutdown log.
@doronz88 doronz88 merged commit 23cc5ea into master Jun 15, 2026
17 checks passed
@doronz88 doronz88 deleted the feature/core-device-orientation branch June 15, 2026 23:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant