Skip to main content

idevice/services/core_device/
hid.rs

1//! HID back-channel for CoreDevice remote control. Served by the DDI daemon `dtuhidd`.
2//!
3//! **Authentication gate (applies to EVERY event kind here):** the device drops
4//! the synthetic HID events `dtuhidd` posts unless a displayservice media stream
5//! is active. This is true for buttons and keyboard just as much as touch.
6//! Without the stream the event decodes and dispatches cleanly (the daemon even
7//! logs `received peer event`) but nothing happens. Starting a
8//! displayservice media stream (see `display_stream`) authenticates the HID
9//! surfaces so the events route through to the system as real input.
10//! The stream only needs to exist for the duration of the events;
11//! its RTP payload can be discarded.
12
13use serde::Deserialize;
14use std::borrow::Cow;
15use web_time::{SystemTime, UNIX_EPOCH};
16
17use crate::{
18    IdeviceError, ReadWrite, RemoteXpcClient, RsdService, obf,
19    services::core_device::CoreDeviceError,
20    xpc::{Dictionary, XPCObject},
21};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ButtonState {
25    Down,
26    Up,
27}
28
29impl ButtonState {
30    pub fn raw(self) -> u64 {
31        match self {
32            ButtonState::Down => 1,
33            ButtonState::Up => 2,
34        }
35    }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum DigitizerEventType {
40    Start,
41    Position,
42    End,
43}
44
45impl DigitizerEventType {
46    pub fn raw(self) -> u64 {
47        match self {
48            DigitizerEventType::Start => 0,
49            DigitizerEventType::Position => 1,
50            DigitizerEventType::End => 2,
51        }
52    }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum DigitizerEdge {
57    None,
58    Top,
59    Left,
60    Bottom,
61    Right,
62}
63
64impl DigitizerEdge {
65    pub fn raw(self) -> u64 {
66        match self {
67            DigitizerEdge::None => 0,
68            DigitizerEdge::Top => 1,
69            DigitizerEdge::Left => 2,
70            DigitizerEdge::Bottom => 3,
71            DigitizerEdge::Right => 4,
72        }
73    }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum DigitizerTarget {
78    MainScreen,
79    Display(u64),
80}
81
82impl DigitizerTarget {
83    pub fn raw(self) -> u64 {
84        match self {
85            DigitizerTarget::MainScreen => 0,
86            DigitizerTarget::Display(n) => n,
87        }
88    }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ScrollTarget {
93    DigitalCrown,
94    Dial,
95}
96
97impl ScrollTarget {
98    pub fn raw(self) -> u64 {
99        match self {
100            ScrollTarget::DigitalCrown => 0,
101            ScrollTarget::Dial => 1,
102        }
103    }
104}
105
106pub mod scroll_phase {
107    pub const UNDEFINED: u64 = 0x0;
108    pub const BEGAN: u64 = 0x1;
109    pub const CHANGED: u64 = 0x2;
110    pub const ENDED: u64 = 0x4;
111    pub const CANCELLED: u64 = 0x8;
112    pub const MAY_BEGIN: u64 = 0x80;
113}
114
115pub mod scroll_momentum {
116    pub const UNDEFINED: u64 = 0x0;
117    pub const CONTINUE: u64 = 0x1;
118    pub const START: u64 = 0x2;
119    pub const END: u64 = 0x4;
120    pub const WILL_BEGIN: u64 = 0x8;
121    pub const INTERRUPTED: u64 = 0x10;
122}
123
124pub const DIGITIZER_REPORT_ID: u8 = 0x13;
125pub const TOUCHSCREEN_REPORT_ID: u8 = 0x09;
126pub const TOUCHSCREEN_STATE_CONTACT: u8 = 0xC2;
127pub const TOUCHSCREEN_STATE_RELEASE: u8 = 0x02;
128
129pub const DIGITIZER_SURFACE_MAIN_TOUCHSCREEN: u64 = 257;
130pub const DIGITIZER_SURFACE_TOUCHSCREEN_GESTURE: u64 = 1281;
131
132/// A 48-bit monotonic timestamp for HID reports. The gesture recognizer only
133/// cares about monotonicity and inter-frame deltas, so wall-clock nanoseconds
134/// (truncated to 48 bits) are sufficient.
135fn default_timestamp() -> u64 {
136    let nanos = SystemTime::now()
137        .duration_since(UNIX_EPOCH)
138        .map(|d| d.as_nanos() as u64)
139        .unwrap_or(0);
140    nanos & ((1u64 << 48) - 1)
141}
142
143/// Build a 19-byte gesture/pointer HID report
144///
145/// `x`/`y` are signed 32-bit. `timestamp` is a 48-bit monotonic value; pass
146/// `None` to use the current wall clock.
147///
148/// Layout: `[0x13][x:i32 LE][y:i32 LE][00 00][ts:6 LE][00 00]`.
149pub fn build_digitizer_report(x: i32, y: i32, timestamp: Option<u64>) -> Vec<u8> {
150    let ts = timestamp.unwrap_or_else(default_timestamp) & ((1u64 << 48) - 1);
151    let mut r = Vec::with_capacity(19);
152    r.push(DIGITIZER_REPORT_ID);
153    r.extend_from_slice(&x.to_le_bytes());
154    r.extend_from_slice(&y.to_le_bytes());
155    r.extend_from_slice(&[0, 0]);
156    r.extend_from_slice(&ts.to_le_bytes()[..6]);
157    r.extend_from_slice(&[0, 0]);
158    r
159}
160
161/// Build a 58-byte `mainTouchscreen` HID report (report ID `0x09`).
162///
163/// `state` is [`TOUCHSCREEN_STATE_CONTACT`] (a touch sample at `x`/`y`) or
164/// [`TOUCHSCREEN_STATE_RELEASE`] (lift). `x`/`y` are unsigned 16-bit. Pass
165/// `timestamp = None` to use the current wall clock.
166///
167/// Layout: `[0x09 0x01 0x05 state][x:u16 LE][y:u16 LE][32×00][02 00 00 00][ts:6 LE][8×00]`.
168pub fn build_touchscreen_report(state: u8, x: u16, y: u16, timestamp: Option<u64>) -> Vec<u8> {
169    let ts = timestamp.unwrap_or_else(default_timestamp) & ((1u64 << 48) - 1);
170    let mut r = Vec::with_capacity(58);
171    r.extend_from_slice(&[TOUCHSCREEN_REPORT_ID, 0x01, 0x05, state]);
172    r.extend_from_slice(&x.to_le_bytes());
173    r.extend_from_slice(&y.to_le_bytes());
174    r.extend_from_slice(&[0u8; 32]);
175    r.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]);
176    r.extend_from_slice(&ts.to_le_bytes()[..6]);
177    r.extend_from_slice(&[0u8; 8]);
178    r
179}
180
181/// Generic Indigo HID events.
182///
183/// `com.apple.coredevice.hid.indigo`.
184#[derive(Debug)]
185pub struct IndigoHidClient<R: ReadWrite> {
186    inner: RemoteXpcClient<R>,
187}
188
189impl RsdService for IndigoHidClient<Box<dyn ReadWrite>> {
190    fn rsd_service_name() -> Cow<'static, str> {
191        obf!("com.apple.coredevice.hid.indigo")
192    }
193
194    async fn from_stream(stream: Box<dyn ReadWrite>) -> Result<Self, IdeviceError> {
195        let mut inner = RemoteXpcClient::new(stream).await?;
196        inner.do_handshake().await?;
197        Ok(Self { inner })
198    }
199}
200
201impl<R: ReadWrite> IndigoHidClient<R> {
202    pub fn new(inner: RemoteXpcClient<R>) -> Self {
203        Self { inner }
204    }
205
206    /// Wrap `payload` in the shared `{messageType, payload, featureIdentifier}`
207    /// envelope and send it one-way (no reply expected). This is the single
208    /// dispatch path every Indigo event kind shares.
209    async fn send_event(
210        &mut self,
211        message_type: &str,
212        feature_identifier: Cow<'static, str>,
213        payload: Dictionary,
214    ) -> Result<(), IdeviceError> {
215        let mut msg = Dictionary::new();
216        msg.insert(
217            "messageType".into(),
218            XPCObject::String(message_type.to_string()),
219        );
220        msg.insert("payload".into(), XPCObject::Dictionary(payload));
221        msg.insert(
222            "featureIdentifier".into(),
223            XPCObject::String(feature_identifier.into()),
224        );
225        self.inner.send_object(msg, false).await
226    }
227
228    /// Send an `IndigoButtonEvent`: a single hardware-button state change.
229    ///
230    /// * `usage_page` - HID usage page (e.g. `0x0C` Consumer for media keys,
231    ///   `0x01` Generic Desktop for power/sleep).
232    /// * `usage_code` - HID usage within that page.
233    /// * `state` - [`ButtonState::Down`] or [`ButtonState::Up`].
234    pub async fn send_button(
235        &mut self,
236        usage_page: u64,
237        usage_code: u64,
238        state: ButtonState,
239    ) -> Result<(), IdeviceError> {
240        let mut payload = Dictionary::new();
241        payload.insert("state".into(), XPCObject::UInt64(state.raw()));
242        payload.insert("usagePage".into(), XPCObject::UInt64(usage_page));
243        payload.insert("usageCode".into(), XPCObject::UInt64(usage_code));
244        self.send_event(
245            "IndigoButtonEvent",
246            obf!("com.apple.coredevice.feature.remote.hid.button"),
247            payload,
248        )
249        .await
250    }
251
252    /// Send an `IndigoKeyboardButtonEvent`: a single keyboard key state change.
253    ///
254    /// * `usage_code` - HID Keyboard/Keypad page (`0x07`) usage, e.g. `0x04`=`a`,
255    ///   `0x28`=Return, `0x2A`=Backspace, `0xE1`=Left Shift. The usage page is
256    ///   implicit (keyboard); the device routes this to its `mainKeyboard`
257    ///   surface.
258    /// * `state` - [`ButtonState::Down`] or [`ButtonState::Up`].
259    ///
260    /// To type a character that needs a modifier (uppercase, symbols), press the
261    /// modifier key (e.g. `0xE1`) down, then the key down/up, then the modifier
262    /// up.
263    pub async fn send_keyboard(
264        &mut self,
265        usage_code: u64,
266        state: ButtonState,
267    ) -> Result<(), IdeviceError> {
268        let mut payload = Dictionary::new();
269        payload.insert("usageCode".into(), XPCObject::UInt64(usage_code));
270        payload.insert("state".into(), XPCObject::UInt64(state.raw()));
271        self.send_event(
272            "IndigoKeyboardButtonEvent",
273            obf!("com.apple.coredevice.feature.remote.hid.keyboard"),
274            payload,
275        )
276        .await
277    }
278
279    /// Send an `IndigoDigitizerEvent`.
280    ///
281    /// This is the higher-level digitizer path (distinct from the raw report
282    /// path on [`UniversalHidServiceClient`]). With `edge` = [`DigitizerEdge::None`]
283    /// it is a plain touch/drag at `point_one` (and optionally a second contact
284    /// `point_two`); with a non-`None` edge it becomes an edge-swipe system
285    /// gesture. Coordinates are `f64` in the display's pixel space.
286    pub async fn send_digitizer(
287        &mut self,
288        point_one: (f64, f64),
289        point_two: Option<(f64, f64)>,
290        event_type: DigitizerEventType,
291        edge: DigitizerEdge,
292        target: DigitizerTarget,
293    ) -> Result<(), IdeviceError> {
294        fn point(x: f64, y: f64) -> XPCObject {
295            let mut p = Dictionary::new();
296            p.insert("x".into(), XPCObject::Double(x));
297            p.insert("y".into(), XPCObject::Double(y));
298            XPCObject::Dictionary(p)
299        }
300
301        let mut payload = Dictionary::new();
302        payload.insert("pointOne".into(), point(point_one.0, point_one.1));
303        // `pointTwo` is an `Optional` decoded with `decodeIfPresent`; omit the
304        // key entirely when there's no second contact.
305        if let Some((x, y)) = point_two {
306            payload.insert("pointTwo".into(), point(x, y));
307        }
308        payload.insert("eventType".into(), XPCObject::UInt64(event_type.raw()));
309        payload.insert("edge".into(), XPCObject::UInt64(edge.raw()));
310        payload.insert("target".into(), XPCObject::UInt64(target.raw()));
311        self.send_event(
312            "IndigoDigitizerEvent",
313            obf!("com.apple.coredevice.feature.remote.hid.digitizer"),
314            payload,
315        )
316        .await
317    }
318
319    /// Send an `IndigoScrollEvent` (digital crown / dial scrolling).
320    ///
321    /// * `point` - scroll delta `(x, y, z)` as `f64`.
322    /// * `phase` - bitmask from [`scroll_phase`].
323    /// * `momentum` - bitmask from [`scroll_momentum`].
324    /// * `target` - [`ScrollTarget::DigitalCrown`] or [`ScrollTarget::Dial`].
325    pub async fn send_scroll(
326        &mut self,
327        point: (f64, f64, f64),
328        phase: u64,
329        momentum: u64,
330        target: ScrollTarget,
331    ) -> Result<(), IdeviceError> {
332        let mut p = Dictionary::new();
333        p.insert("x".into(), XPCObject::Double(point.0));
334        p.insert("y".into(), XPCObject::Double(point.1));
335        p.insert("z".into(), XPCObject::Double(point.2));
336
337        let mut payload = Dictionary::new();
338        payload.insert("point".into(), XPCObject::Dictionary(p));
339        payload.insert("phase".into(), XPCObject::UInt64(phase));
340        payload.insert("momentum".into(), XPCObject::UInt64(momentum));
341        payload.insert("target".into(), XPCObject::UInt64(target.raw()));
342        self.send_event(
343            "IndigoScrollEvent",
344            obf!("com.apple.coredevice.feature.remote.hid.scroll"),
345            payload,
346        )
347        .await
348    }
349
350    /// Send an `IndigoVendorDefinedEvent`: a raw vendor-defined HID report
351    /// (routed to the device's `avpCustom` surface).
352    ///
353    /// * `usage_page` / `usage` - the vendor usage.
354    /// * `version` - vendor event version.
355    /// * `data` - the opaque report bytes.
356    pub async fn send_vendor_defined(
357        &mut self,
358        usage_page: u64,
359        usage: u64,
360        version: u64,
361        data: Vec<u8>,
362    ) -> Result<(), IdeviceError> {
363        let mut payload = Dictionary::new();
364        payload.insert("usagePage".into(), XPCObject::UInt64(usage_page));
365        payload.insert("usage".into(), XPCObject::UInt64(usage));
366        payload.insert("version".into(), XPCObject::UInt64(version));
367        payload.insert("data".into(), XPCObject::Data(data));
368        self.send_event(
369            "IndigoVendorDefinedEvent",
370            obf!("com.apple.coredevice.feature.remote.hid.vendordefined"),
371            payload,
372        )
373        .await
374    }
375}
376
377/// A HID surface the device has registered, as returned by
378/// [`UniversalHidServiceClient::list_connected_services`]. The device also
379/// reports a verbose `_CoreDevice_codablePropertyStorage` mirror of these
380/// fields, which this skips.
381#[derive(Debug, Clone, Deserialize)]
382pub struct HidSurface {
383    /// The surface's identifier — the `service_id` to pass to
384    /// [`UniversalHidServiceClient::send_report`].
385    #[serde(rename = "_ServiceID")]
386    pub service_id: u64,
387    /// Human-readable product string, e.g. `"CoreDevice touchscreen(nil)"`.
388    #[serde(rename = "Product")]
389    pub product: Option<String>,
390    /// The surface's primary HID usage.
391    #[serde(rename = "PrimaryUsage")]
392    pub primary_usage: Option<u64>,
393    /// The surface's primary HID usage page.
394    #[serde(rename = "PrimaryUsagePage")]
395    pub primary_usage_page: Option<u64>,
396}
397
398/// Inspect and drive the device's registered HID surfaces.
399#[derive(Debug)]
400pub struct UniversalHidServiceClient<R: ReadWrite> {
401    inner: RemoteXpcClient<R>,
402}
403
404impl RsdService for UniversalHidServiceClient<Box<dyn ReadWrite>> {
405    fn rsd_service_name() -> Cow<'static, str> {
406        obf!("com.apple.coredevice.hid.universalhidservice")
407    }
408
409    async fn from_stream(stream: Box<dyn ReadWrite>) -> Result<Self, IdeviceError> {
410        let mut inner = RemoteXpcClient::new(stream).await?;
411        inner.do_handshake().await?;
412        Ok(Self { inner })
413    }
414}
415
416impl<R: ReadWrite> UniversalHidServiceClient<R> {
417    pub fn new(inner: RemoteXpcClient<R>) -> Self {
418        Self { inner }
419    }
420
421    /// Build the `{featureIdentifier, messageType: "Request", payload}` envelope
422    /// these requests share.
423    fn request(payload: Dictionary) -> Dictionary {
424        let mut msg = Dictionary::new();
425        let universal_hid_feature: Cow<'static, str> =
426            obf!("com.apple.coredevice.feature.remote.universalhidservice");
427        msg.insert(
428            "featureIdentifier".into(),
429            XPCObject::String(universal_hid_feature.into()),
430        );
431        msg.insert("messageType".into(), XPCObject::String("Request".into()));
432        msg.insert("payload".into(), XPCObject::Dictionary(payload));
433        msg
434    }
435
436    /// Enumerate the device's currently-registered HID surfaces.
437    pub async fn list_connected_services(&mut self) -> Result<Vec<HidSurface>, IdeviceError> {
438        let mut payload = Dictionary::new();
439        payload.insert(
440            "connectedServices".into(),
441            XPCObject::Dictionary(Dictionary::new()),
442        );
443        let msg = Self::request(payload);
444        self.inner.send_object(msg, true).await?;
445        let res = self.inner.recv().await?;
446
447        let services = res
448            .as_dictionary()
449            .and_then(|d| d.get("connectedServices"))
450            .ok_or(CoreDeviceError::MissingField("connectedServices"))?;
451        plist::from_value(services)
452            .map_err(|_| CoreDeviceError::MalformedField("connectedServices").into())
453    }
454
455    /// Deliver a raw HID report to one of the device's HID surfaces.
456    pub async fn send_report(
457        &mut self,
458        service_id: u64,
459        report: Vec<u8>,
460    ) -> Result<(), IdeviceError> {
461        // `send` is a Swift tuple `(_0: report, _1: serviceID)`.
462        let payload = crate::xpc!({
463            "send": {
464                "_0": report,
465                "_1": service_id
466            }
467        })
468        .to_dictionary()
469        .unwrap();
470
471        let msg = Self::request(payload);
472        self.inner.send_object(msg, false).await
473    }
474
475    /// Send a single 19-byte gesture/pointer report at (`x`, `y`).
476    /// For an actual on-screen touch use
477    /// [`send_touchscreen`](Self::send_touchscreen).
478    pub async fn send_digitizer(
479        &mut self,
480        x: i32,
481        y: i32,
482        service_id: u64,
483        timestamp: Option<u64>,
484    ) -> Result<(), IdeviceError> {
485        self.send_report(service_id, build_digitizer_report(x, y, timestamp))
486            .await
487    }
488
489    /// Send a single 58-byte `mainTouchscreen` report. `state` is
490    /// [`TOUCHSCREEN_STATE_CONTACT`] for an in-progress touch sample or
491    /// [`TOUCHSCREEN_STATE_RELEASE`] to lift.
492    pub async fn send_touchscreen(
493        &mut self,
494        state: u8,
495        x: u16,
496        y: u16,
497        timestamp: Option<u64>,
498    ) -> Result<(), IdeviceError> {
499        self.send_report(
500            DIGITIZER_SURFACE_MAIN_TOUCHSCREEN,
501            build_touchscreen_report(state, x, y, timestamp),
502        )
503        .await
504    }
505
506    /// A tap on the touchscreen: one contact sample, a short hold, then a
507    /// release at the same point.
508    pub async fn tap(&mut self, x: u16, y: u16) -> Result<(), IdeviceError> {
509        self.send_touchscreen(TOUCHSCREEN_STATE_CONTACT, x, y, None)
510            .await?;
511        crate::time::sleep(std::time::Duration::from_millis(50)).await;
512        self.send_touchscreen(TOUCHSCREEN_STATE_RELEASE, x, y, None)
513            .await
514    }
515
516    /// A drag on the touchscreen from (`x1`, `y1`) to (`x2`, `y2`): a stream of
517    /// `steps` contact samples advancing linearly, a final contact at the end
518    /// point, then a release. `delay_ms` is slept between samples so the gesture
519    /// recognizer sees a velocity (a too-fast drag reads as a tap). This is the
520    /// real touch-drag used for scrolling/swiping content. `steps` is clamped to
521    /// at least 1.
522    pub async fn drag(
523        &mut self,
524        x1: u16,
525        y1: u16,
526        x2: u16,
527        y2: u16,
528        steps: u32,
529        delay_ms: u64,
530    ) -> Result<(), IdeviceError> {
531        let steps = steps.max(1);
532        for i in 0..steps {
533            let t = i as f64 / steps as f64;
534            let x = (x1 as f64 + (x2 as f64 - x1 as f64) * t).round() as u16;
535            let y = (y1 as f64 + (y2 as f64 - y1 as f64) * t).round() as u16;
536            self.send_touchscreen(TOUCHSCREEN_STATE_CONTACT, x, y, None)
537                .await?;
538            if delay_ms > 0 {
539                crate::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
540            }
541        }
542        self.send_touchscreen(TOUCHSCREEN_STATE_CONTACT, x2, y2, None)
543            .await?;
544        self.send_touchscreen(TOUCHSCREEN_STATE_RELEASE, x2, y2, None)
545            .await
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    #[test]
554    fn digitizer_report_layout() {
555        let r = build_digitizer_report(100, -50, Some(0x0102030405));
556        assert_eq!(r.len(), 19);
557        assert_eq!(r[0], DIGITIZER_REPORT_ID);
558        assert_eq!(&r[1..5], &100i32.to_le_bytes());
559        assert_eq!(&r[5..9], &(-50i32).to_le_bytes());
560        assert_eq!(&r[9..11], &[0, 0]);
561        assert_eq!(&r[11..17], &0x0102030405u64.to_le_bytes()[..6]);
562        assert_eq!(&r[17..19], &[0, 0]);
563    }
564
565    #[test]
566    fn touchscreen_report_layout() {
567        let r = build_touchscreen_report(TOUCHSCREEN_STATE_CONTACT, 375, 812, Some(0xAABBCCDD));
568        assert_eq!(r.len(), 58);
569        assert_eq!(&r[0..4], &[0x09, 0x01, 0x05, 0xC2]);
570        assert_eq!(&r[4..6], &375u16.to_le_bytes());
571        assert_eq!(&r[6..8], &812u16.to_le_bytes());
572        assert_eq!(&r[8..40], &[0u8; 32]);
573        assert_eq!(&r[40..44], &[0x02, 0x00, 0x00, 0x00]);
574        assert_eq!(&r[44..50], &0xAABBCCDDu64.to_le_bytes()[..6]);
575        assert_eq!(&r[50..58], &[0u8; 8]);
576    }
577
578    #[test]
579    fn timestamp_is_truncated_to_48_bits() {
580        // A timestamp above 48 bits must be masked, not overflow the 6-byte field.
581        let r = build_digitizer_report(0, 0, Some(u64::MAX));
582        assert_eq!(&r[11..17], &[0xFF; 6]);
583    }
584}