//! Transport-independent contract for the virtual **Steam Controller 2** (2026, Valve "Ibex" / //! SDL "Triton", wired `28DE:1302`) — the as-is passthrough sibling of [`super::steam_proto`]. //! //! Unlike the Deck/classic-SC backends, this device is NOT re-synthesized from typed wire state: //! the client captures the physical controller (USB Puck / wired / BLE) and forwards its raw //! input reports verbatim ([`RichInput::HidReport`](punktfunk_core::quic::RichInput)); the host //! mirrors them out unchanged, and everything the host's hidraw consumer writes back (Steam's //! lizard-off / IMU-enable feature reports, `0x80` rumble output reports) is forwarded raw to the //! client for replay on the real controller ([`HidOutput::HidRaw`](punktfunk_core::quic::HidOutput)). //! Mainline `hid-steam` does not bind this PID, so no kernel evdev exists — **Steam Input is the //! consumer**, driving the hidraw node exactly as it drives the physical pad. //! //! Protocol ground truth: SDL's `SDL_hidapi_steam_triton.c` + `steam/controller_structs.h` //! (Valve-maintained). Input report ids `0x42`/`0x45` (`TritonMTUNoQuat_t`, 46 bytes with id), //! `0x47` (adds a trackpad timestamp), `0x43` battery, `0x46`/`0x79` wireless status. Feature //! reports are 64 bytes on report id `1`; haptics are OUTPUT reports `0x80..=0x85`. //! //! A typed **fallback synthesizer** is kept for the degraded case (a client that declared the //! kind but sends no raw feed): buttons/sticks/triggers from the ordinary gamepad plane are //! serialized into a minimal `0x42` state report. The first raw report permanently switches the //! pad to as-is mode. use punktfunk_core::input::gamepad as gs; /// Valve vendor id (same as [`super::steam_proto::STEAM_VENDOR`], repeated to keep this module /// self-contained). pub const TRITON_VENDOR: u32 = 0x28DE; /// The wired Steam Controller 2 identity the virtual pad presents. The BLE (`0x1303`) and Puck /// dongle (`0x1304`/`0x1305`) identities are client-side transports only — Steam treats the wired /// PID as the canonical controller. pub const TRITON_WIRED_PRODUCT: u32 = 0x1302; /// Triton input-report ids (`ETritonReportIDTypes`, SDL `controller_structs.h`). pub const ID_TRITON_CONTROLLER_STATE: u8 = 0x42; pub const ID_TRITON_BATTERY_STATUS: u8 = 0x43; pub const ID_TRITON_CONTROLLER_STATE_BLE: u8 = 0x45; pub const ID_TRITON_CONTROLLER_STATE_TIMESTAMP: u8 = 0x47; /// Haptic OUTPUT report ids (`ID_OUT_REPORT_*`). Only rumble is parsed host-side (for the /// universal 0xCA plane); every output report is forwarded raw regardless. pub const ID_OUT_REPORT_HAPTIC_RUMBLE: u8 = 0x80; /// Physical `0x42` state report size: one report-id byte plus 53 payload bytes. pub const TRITON_REPORT_LEN: usize = 64; pub const TRITON_STATE_LEN: usize = 54; /// The physical Triton HID report descriptor, captured byte-for-byte from both wired `28DE:1302` /// and Puck `28DE:1304` controller interfaces. Its numbered reports are part of the protocol: /// inputs `0x40`–`0x45`/`0x79`/`0x7B`, outputs `0x80`–`0x89`, and feature channels `1` and `2`. /// In particular, Puck connection and bond queries use feature report 2; an unnumbered minimal /// descriptor makes hidraw frame those queries incorrectly and Steam eventually closes the device. #[rustfmt::skip] pub const TRITON_RDESC: &[u8] = &[ 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x85, 0x40, 0x09, 0x01, 0xA1, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x02, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x02, 0x81, 0x02, 0x75, 0x06, 0x95, 0x01, 0x81, 0x01, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x02, 0x81, 0x06, 0x95, 0x01, 0x09, 0x38, 0x81, 0x06, 0x05, 0x0C, 0x0A, 0x38, 0x02, 0x95, 0x01, 0x81, 0x06, 0xC0, 0xC0, 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x85, 0x41, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x81, 0x01, 0x19, 0x00, 0x29, 0x65, 0x15, 0x00, 0x25, 0x65, 0x75, 0x08, 0x95, 0x06, 0x81, 0x00, 0xC0, 0x06, 0x00, 0xFF, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x42, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x35, 0x09, 0x42, 0x81, 0x02, 0x85, 0x44, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x05, 0x09, 0x44, 0x81, 0x02, 0x85, 0x79, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01, 0x09, 0x79, 0x81, 0x02, 0x85, 0x43, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x0E, 0x09, 0x43, 0x81, 0x02, 0x85, 0x7B, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x0C, 0x09, 0x7B, 0x81, 0x02, 0x85, 0x45, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x2D, 0x09, 0x45, 0x81, 0x02, 0x85, 0x80, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x09, 0x09, 0x80, 0x91, 0x02, 0x85, 0x81, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x07, 0x09, 0x81, 0x91, 0x02, 0x85, 0x82, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x82, 0x91, 0x02, 0x85, 0x83, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x09, 0x09, 0x83, 0x91, 0x02, 0x85, 0x84, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x08, 0x09, 0x84, 0x91, 0x02, 0x85, 0x85, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x85, 0x91, 0x02, 0x85, 0x86, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x03, 0x09, 0x86, 0x91, 0x02, 0x85, 0x87, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F, 0x09, 0x87, 0x91, 0x02, 0x85, 0x89, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F, 0x09, 0x89, 0x91, 0x02, 0x85, 0x88, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x3F, 0x09, 0x88, 0x91, 0x02, 0x85, 0x01, 0x95, 0x3F, 0x09, 0x01, 0xB1, 0x02, 0x85, 0x02, 0x95, 0x3F, 0x09, 0x01, 0xB1, 0x02, 0xC0, ]; /// Triton button bits in the state report's `buttons` u32 — transcribed verbatim from SDL's /// `TritonButtons`. Only the bits the typed fallback synthesizes are named; the raw path carries /// whatever the physical pad set. pub mod tbtn { pub const A: u32 = 0x0000_0001; pub const B: u32 = 0x0000_0002; pub const X: u32 = 0x0000_0004; pub const Y: u32 = 0x0000_0008; pub const QAM: u32 = 0x0000_0010; pub const R3: u32 = 0x0000_0020; pub const VIEW: u32 = 0x0000_0040; pub const R4: u32 = 0x0000_0080; pub const R5: u32 = 0x0000_0100; pub const RB: u32 = 0x0000_0200; pub const DPAD_DOWN: u32 = 0x0000_0400; pub const DPAD_RIGHT: u32 = 0x0000_0800; pub const DPAD_LEFT: u32 = 0x0000_1000; pub const DPAD_UP: u32 = 0x0000_2000; pub const MENU: u32 = 0x0000_4000; pub const L3: u32 = 0x0000_8000; pub const STEAM: u32 = 0x0001_0000; pub const L4: u32 = 0x0002_0000; pub const L5: u32 = 0x0004_0000; pub const LB: u32 = 0x0008_0000; pub const RPAD_TOUCH: u32 = 0x0020_0000; pub const RPAD_CLICK: u32 = 0x0040_0000; pub const RT_CLICK: u32 = 0x0080_0000; pub const LPAD_TOUCH: u32 = 0x0200_0000; pub const LPAD_CLICK: u32 = 0x0400_0000; pub const LT_CLICK: u32 = 0x0800_0000; } /// One virtual Triton pad's report state. In as-is mode (`raw_len > 0`) the raw report IS the /// state; the typed fields only feed the fallback synthesizer until the first raw report lands. #[derive(Clone, Copy)] pub struct TritonState { /// The last raw report the client forwarded (report-id byte first); `raw_len == 0` until the /// first one arrives, after which the typed fields below stop mattering. pub raw: [u8; TRITON_REPORT_LEN], pub raw_len: u8, /// Typed fallback fields (Triton bit layout / raw axis units), from the ordinary wire plane. pub buttons: u32, pub lt: u16, pub rt: u16, pub lx: i16, pub ly: i16, pub rx: i16, pub ry: i16, } impl TritonState { pub fn neutral() -> TritonState { TritonState { raw: [0u8; TRITON_REPORT_LEN], raw_len: 0, buttons: 0, lt: 0, rt: 0, lx: 0, ly: 0, rx: 0, ry: 0, } } /// Typed fallback: fold one wire button/stick frame into Triton fields. Mapping follows the /// Deck backend's conventions (PADDLE1/2/3/4 = R4/L4/R5/L5, MISC1 = QAM, the DualSense /// touchpad-click wire bit = right-pad click); sticks are already the device convention /// (+y up), triggers scale 0..255 → 0..32767. pub fn from_gamepad( buttons: u32, lx: i16, ly: i16, rx: i16, ry: i16, lt: u8, rt: u8, ) -> TritonState { let on = |bit: u32| buttons & bit != 0; let trig = |v: u8| ((v as u32 * 32767) / 255) as u16; let mut b = 0u32; let set = |b: &mut u32, on: bool, m: u32| { if on { *b |= m; } }; set(&mut b, on(gs::BTN_A), tbtn::A); set(&mut b, on(gs::BTN_B), tbtn::B); set(&mut b, on(gs::BTN_X), tbtn::X); set(&mut b, on(gs::BTN_Y), tbtn::Y); set(&mut b, on(gs::BTN_LB), tbtn::LB); set(&mut b, on(gs::BTN_RB), tbtn::RB); set(&mut b, on(gs::BTN_BACK), tbtn::VIEW); set(&mut b, on(gs::BTN_START), tbtn::MENU); set(&mut b, on(gs::BTN_GUIDE), tbtn::STEAM); set(&mut b, on(gs::BTN_LS_CLICK), tbtn::L3); set(&mut b, on(gs::BTN_RS_CLICK), tbtn::R3); set(&mut b, on(gs::BTN_DPAD_UP), tbtn::DPAD_UP); set(&mut b, on(gs::BTN_DPAD_DOWN), tbtn::DPAD_DOWN); set(&mut b, on(gs::BTN_DPAD_LEFT), tbtn::DPAD_LEFT); set(&mut b, on(gs::BTN_DPAD_RIGHT), tbtn::DPAD_RIGHT); set(&mut b, on(gs::BTN_TOUCHPAD), tbtn::RPAD_CLICK); set(&mut b, on(gs::BTN_PADDLE1), tbtn::R4); set(&mut b, on(gs::BTN_PADDLE2), tbtn::L4); set(&mut b, on(gs::BTN_PADDLE3), tbtn::R5); set(&mut b, on(gs::BTN_PADDLE4), tbtn::L5); set(&mut b, on(gs::BTN_MISC1), tbtn::QAM); // "Fully pressed" digital shadow of the analog triggers (the physical pad's own // threshold is a hard pull, not first-contact). set(&mut b, lt >= 240, tbtn::LT_CLICK); set(&mut b, rt >= 240, tbtn::RT_CLICK); TritonState { raw: [0u8; TRITON_REPORT_LEN], raw_len: 0, buttons: b, lt: trig(lt), rt: trig(rt), lx, ly, rx, ry, } } } /// Serialize the typed fallback state into a minimal `0x42` `TritonMTUNoQuat_t` report: /// `[0x42][seq u8][buttons u32][trigL i16][trigR i16][sticks i16×4][lpad x/y + pressure] /// [rpad x/y + pressure][imu ts u32 + accel i16×3 + gyro i16×3]` — pads and IMU stay zero /// (no raw feed = no trackpad/motion source; Steam only sees IMU data after enabling /// `SETTING_IMU_MODE` on a real feed anyway). pub fn serialize_triton_state(buf: &mut [u8; TRITON_STATE_LEN], st: &TritonState, seq: u8) { buf.fill(0); buf[0] = ID_TRITON_CONTROLLER_STATE; buf[1] = seq; buf[2..6].copy_from_slice(&st.buttons.to_le_bytes()); buf[6..8].copy_from_slice(&(st.lt as i16).to_le_bytes()); buf[8..10].copy_from_slice(&(st.rt as i16).to_le_bytes()); buf[10..12].copy_from_slice(&st.lx.to_le_bytes()); buf[12..14].copy_from_slice(&st.ly.to_le_bytes()); buf[14..16].copy_from_slice(&st.rx.to_le_bytes()); buf[16..18].copy_from_slice(&st.ry.to_le_bytes()); // [18..30] left/right pad + pressures stay zero; [30..46] IMU stays zero. } /// One service pass's extracted feedback: the raw reports to forward (kind-tagged for /// [`HidOutput::HidRaw`](punktfunk_core::quic::HidOutput)) plus the rumble level parsed out of a /// `0x80` report for the universal 0xCA plane (drives the phone-mirror path on clients whose /// physical pad already gets the raw report). #[derive(Default)] pub struct TritonFeedback { /// `(low, high)` — `left.speed`/`right.speed` of the last rumble output report seen. pub rumble: Option<(u16, u16)>, /// Raw reports to forward: `(kind, bytes)` with kind = `HID_RAW_OUTPUT`/`HID_RAW_FEATURE`. pub raw: Vec<(u8, Vec)>, } /// Parse a Triton haptic-rumble OUTPUT report (`MsgHapticRumble`, 10 bytes with id): /// `[0x80][type u8][intensity u16][left.speed u16][left.gain i8][right.speed u16][right.gain i8]`. /// Returns `(left_speed, right_speed)` as `(low, high)`. pub fn parse_triton_rumble(data: &[u8]) -> Option<(u16, u16)> { if data.len() < 10 || data[0] != ID_OUT_REPORT_HAPTIC_RUMBLE { return None; } let le = |o: usize| u16::from_le_bytes([data[o], data[o + 1]]); Some((le(4), le(7))) } /// Strip the hidraw unnumbered-report `0x00` prefix if present: Triton report/command ids are all /// non-zero (`0x42+` input, `0x80+` output, `1` feature), so a leading zero can only be the /// synthetic report-id byte hidraw prepends on this unnumbered virtual descriptor. pub fn strip_report_prefix(data: &[u8]) -> &[u8] { match data { [0, rest @ ..] if !rest.is_empty() => rest, d => d, } } /// Per-instance unit id stamped into the fake `0x83` attributes (`'T','R','I'` + index). pub fn triton_unit_id(index: u8) -> u32 { 0x5452_4900 | index as u32 } /// The virtual pad's serial, FVPF-prefixed: the physical-Steam-controller conflict gate /// recognizes `FVPF…` (`HID_UNIQ`) as one of punktfunk's own virtual pads, so a concurrent /// session never mistakes this device for real hardware. Shaped like the real `FXA…` serials /// (13 chars). Shared by the UHID and usbip legs (identity + `0xAE` replies must agree). pub fn triton_serial(index: u8) -> String { format!("FVPF1302{index:02}D03") } /// Build the reply to a feature GET_REPORT — the answer half of the Valve query dance. Steam's /// `GetControllerInfo` SETs a query (`0x83` attributes / `0xAE` string) and then GETs the answer; /// **the reply's command byte must echo the LAST SET's command** or Steam treats the pad as /// broken and never adopts it (confirmed on-glass 2026-07-15: answering every GET with a serial /// blob left the virtual pad unpicked). Mirrors the Deck's validated /// [`feature_reply`](super::steam_proto::feature_reply), with two Triton deltas: the frame rides /// feature report id **1** (`[0x01][cmd][len][payload…]`, matching SDL's send framing for this /// device), and the `0x83` blob carries the Triton's product id. The attribute VALUES beyond the /// product id mirror the Deck's hidraw capture (same firmware family conventions) — replace them /// with a capture from a physical pad if Steam still balks. /// /// `last_set` is the id-first SET payload (`[0x01, cmd, …]`); a stack that already stripped the /// id byte (`[cmd, …]`, cmd ≥ 0x80) is handled too. pub fn triton_feature_reply(last_set: &[u8], serial: &str, unit_id: u32) -> [u8; 64] { const ID_GET_ATTRIBUTES_VALUES: u8 = 0x83; const ID_GET_STRING_ATTRIBUTE: u8 = 0xAE; const ID_GET_FIRMWARE_INFO: u8 = 0xF2; const ATTRIB_STR_UNIT_SERIAL: u8 = 0x01; let body = match last_set { [0x01, rest @ ..] => rest, d => d, }; let cmd = body.first().copied().unwrap_or(ID_GET_STRING_ATTRIBUTE); let mut r = [0u8; 64]; r[0] = 0x01; match cmd { ID_GET_ATTRIBUTES_VALUES => { // Captured controller response: 25-byte payload containing five id/u32 attributes. r[1] = ID_GET_ATTRIBUTES_VALUES; r[2] = 0x19; let attrs = [ (0x01, TRITON_WIRED_PRODUCT), (0x02, 0), (0x0A, unit_id), (0x04, unit_id ^ 0x0296_DAF9), (0x09, 0x49), ]; let mut o = 3; for (id, val) in attrs { r[o] = id; r[o + 1..o + 5].copy_from_slice(&val.to_le_bytes()); o += 5; } } ID_GET_STRING_ATTRIBUTE => { // Captured replies always declare 20 bytes: attribute id plus a 19-byte padded string. let attr = body.get(2).copied().unwrap_or(ATTRIB_STR_UNIT_SERIAL); let b = serial.as_bytes(); let len = b.len().min(19); r[..4].copy_from_slice(&[0x01, ID_GET_STRING_ATTRIBUTE, 0x14, attr]); r[4..4 + len].copy_from_slice(&b[..len]); } ID_GET_FIRMWARE_INFO => { let index = body.get(2).copied().unwrap_or(0); r[1] = ID_GET_FIRMWARE_INFO; r[3] = index; match index { 0 => { r[2] = 0x29; r[4..8].copy_from_slice(&(unit_id ^ 0x0296_DAF9).to_le_bytes()); r[8] = 0x49; r[12..24].copy_from_slice(b"603f69218a85"); let b = serial.as_bytes(); let len = b.len().min(16); r[28..28 + len].copy_from_slice(&b[..len]); } 1 => { r[2] = 0x22; r[4..37].copy_from_slice(&[ 0x00, 0x57, 0xD0, 0x18, 0x6A, 0x37, 0x30, 0x35, 0x34, 0x32, 0x35, 0x37, 0x64, 0x32, 0x64, 0x61, 0x37, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x6D, 0x02, 0x00, ]); } _ => { r[2] = 0x09; r[4..12].copy_from_slice(&[0x7C, 0x4F, 0x01, 0x00, 0x01, 0, 0, 0]); } } } _ => { let n = body.len().min(63); r[1..1 + n].copy_from_slice(&body[..n]); } } r } #[cfg(test)] mod tests { use super::*; /// The typed fallback lands the canonical wire mapping on the SDL-documented bit positions /// and byte offsets. #[test] fn fallback_state_serializes_sdl_layout() { let st = TritonState::from_gamepad( gs::BTN_A | gs::BTN_START | gs::BTN_PADDLE1 | gs::BTN_MISC1, 1000, -2000, 3000, -32768, 255, 0, ); assert_eq!( st.buttons, tbtn::A | tbtn::MENU | tbtn::R4 | tbtn::QAM | tbtn::LT_CLICK ); assert_eq!(st.lt, 32767); // exact full-scale, not the *128 approximation let mut r = [0u8; TRITON_STATE_LEN]; serialize_triton_state(&mut r, &st, 7); assert_eq!(r[0], ID_TRITON_CONTROLLER_STATE); assert_eq!(r[1], 7); assert_eq!(u32::from_le_bytes([r[2], r[3], r[4], r[5]]), st.buttons); assert_eq!(i16::from_le_bytes([r[6], r[7]]), 32767); // sTriggerLeft assert_eq!(i16::from_le_bytes([r[10], r[11]]), 1000); // sLeftStickX assert_eq!(i16::from_le_bytes([r[16], r[17]]), -32768); // sRightStickY assert!(r[18..].iter().all(|&b| b == 0)); // pads + IMU zero } /// A rumble output report parses to `(left_speed, right_speed)`; other ids don't. #[test] fn rumble_output_report_parses() { // [0x80, type, intensity(2), left.speed(2), left.gain, right.speed(2), right.gain] let mut d = [0u8; 10]; d[0] = ID_OUT_REPORT_HAPTIC_RUMBLE; d[4..6].copy_from_slice(&0x1234u16.to_le_bytes()); d[7..9].copy_from_slice(&0x5678u16.to_le_bytes()); assert_eq!(parse_triton_rumble(&d), Some((0x1234, 0x5678))); d[0] = 0x81; // haptic pulse — not rumble assert_eq!(parse_triton_rumble(&d), None); assert_eq!(parse_triton_rumble(&d[..8]), None); // short } /// The hidraw `0x00` unnumbered prefix strips; genuine command bytes survive. #[test] fn report_prefix_strips_only_leading_zero() { assert_eq!(strip_report_prefix(&[0x00, 0x80, 1, 2]), &[0x80, 1, 2]); assert_eq!(strip_report_prefix(&[0x80, 1, 2]), &[0x80, 1, 2]); assert_eq!(strip_report_prefix(&[0x01, 0x87]), &[0x01, 0x87]); // feature id 1 kept assert_eq!(strip_report_prefix(&[0x00]), &[0x00]); // lone zero: nothing to strip to } /// The GET reply echoes the LAST SET's command — the Valve query dance Steam's /// `GetControllerInfo` runs; a mismatched command type makes Steam drop the pad. #[test] fn feature_reply_echoes_the_queried_command() { let serial = triton_serial(0); let uid = triton_unit_id(0); // 0x83 attributes: id-first frame, 5 captured blocks, product id = 0x1302 in the first. let r = triton_feature_reply(&[0x01, 0x83, 0x00], &serial, uid); assert_eq!(&r[..3], &[0x01, 0x83, 0x19]); assert_eq!(r[3], 0x01); // ATTRIB product-id tag assert_eq!( u32::from_le_bytes([r[4], r[5], r[6], r[7]]), TRITON_WIRED_PRODUCT ); // 0xAE serial: the captured fixed 20-byte payload — attribute id + padded string. let r = triton_feature_reply(&[0x01, 0xAE, 0x01, 0x01], &serial, uid); assert_eq!(&r[..3], &[0x01, 0xAE, 0x14]); assert_eq!(r[3], 0x01); assert_eq!(&r[4..4 + serial.len()], serial.as_bytes()); // A stack that stripped the id byte still resolves the command. let r = triton_feature_reply(&[0x83u8, 0x00], &serial, uid); assert_eq!(&r[..3], &[0x01, 0x83, 0x19]); // Anything else (settings write) reads back as an echo. let r = triton_feature_reply(&[0x01, 0x87, 3, 9, 0, 0], &serial, uid); assert_eq!(&r[..6], &[0x01, 0x87, 3, 9, 0, 0]); } }