Determined on VITURE Pro 2 XR Glasses, VID 0x35CA / PID 0x1301, firmware
30.0.00.002_20260804, bcdDevice 0x0200.
Method: usbmon capture of the official SDK (libglasses.so) matched against the
SDK's own frame logging, which prints MsgID, payload length and checksum for every
command it builds. That yields a labelled dataset rather than guesswork. Every finding
below was then re-verified by an independent implementation that talks to the device
without any vendor library.
Reverse engineered for interoperability with hardware we own.
| Property | Value |
|---|---|
| USB | 2.01, full speed, 1 configuration, 1 interface |
| Interface | class 03 (HID), subclass 00, protocol 00 |
| Endpoints | 0x01 interrupt OUT, 0x81 interrupt IN, 64 B each, 1 ms interval |
| HID report descriptor | usage page 0xFF00, usage 0x01, one opaque 64-byte IN and OUT report, no report IDs |
Commands are written to the OUT endpoint at their actual frame length (short packets are
fine). Responses arrive zero-padded to 64 bytes. Over hidraw a leading report-ID byte
0x00 must be prepended; over usbfs the frame goes on the wire unchanged.
All fields little-endian.
Offset Size Field
0 2 Preamble, constant 0x0010
2 2 MsgID
4 2 PayloadLen
6 2 Checksum = sum of all payload bytes (mod 2^16)
8 n Payload
Total length is 8 + PayloadLen. The checksum is a plain byte sum, not a CRC —
verified against a command (0x01 + 0x02 = 0x0003) and a data packet (sum 2204 =
0x089C).
Reply convention: the response to a request carries MsgID + 0x2000 and starts with
a status byte, 0x00 meaning success.
Payload: 2 bytes [stream, rate].
stream is a bitmask, and it does not match the SDK header enum:
| Value | Meaning |
|---|---|
0x00 |
streams off |
0x01 |
pose stream (fused quaternion) |
0x02 |
raw stream (gyro + accelerometer) |
The SDK header defines VITURE_IMU_MODE_RAW = 0 and ..._POSE = 1; on the wire those
are 2 and 1. The API values are translated, not passed through. This is the single
most likely thing to trip up an independent implementation.
rate: 0 = 60 Hz, 1 = 90 Hz, 2 = 120 Hz, 3 = 240 Hz, 4 = 500 Hz, 5 = 1000 Hz.
The Pro 2 supports raw up to 1000 Hz but pose only up to 240 Hz (queried through the
SDK's is_product_support_imu_frequency).
10 00 01 03 02 00 03 00 01 02 pose stream, 120 Hz
10 00 01 03 02 00 04 00 02 02 raw stream, 120 Hz
10 00 01 03 02 00 00 00 00 00 both off
Payload: 1 status byte, 0x00 = success.
Payload: 24 bytes.
0 u32 unknown (always < 2^16, jumps around — not a counter)
4 u32 timestamp
8 f32 qw
12 f32 qx
16 f32 qy
20 f32 qz
The quaternion is normalised (measured |q| = 1.0000). Roll/pitch/yaw are never
transmitted — the Euler angles the SDK hands to its callbacks are computed host-side.
Payload: 56 bytes.
0 u32 unknown (as above)
4 u32 timestamp, increments by 1 per sample
8 u16 unknown, ~186/187, drifts slightly
10 f32 gyro X [rad/s]
14 f32 gyro Y
18 f32 gyro Z
22 f32 accel X [g]
26 f32 accel Y
30 f32 accel Z
34 f32 constant 117.30 — presumably calibration/scale
38 f32 constant 60.75
42 f32 constant 254.85
46 ... remainder varies, not plausible as f32
Mind the offset difference: pose floats start at 8, raw floats at 10 because of the extra u16.
Cross-check: accelerometer magnitude |(0.0024, -0.3221, -0.9287)| = 0.983 g, and
atan2(0.322, 0.929) = 19.1° matches the pitch reported by the pose stream at the same
moment.
A query is a frame with PayloadLen = 0, i.e. 8 bytes. The reply carries MsgID + 0x2000
and begins with a status byte followed by the value.
| Function | MsgID | Reply | Payload | Measured on a Pro 2 |
|---|---|---|---|---|
| Serial number | 0x3002 |
0x5002 |
status + 15 ASCII | plaintext, not a hash |
| Firmware version | 0x3003 |
0x5003 |
status + 20 ASCII | 30.0.00.002_20260804 |
| Brightness | 0x3122 |
0x5122 |
status + u8 | 3 |
| Duty cycle | 0x3125 |
0x5125 |
status + u8 (percent) | 98 |
| Display mode | 0x3141 |
0x5141 |
status + u8 | 0x31 |
| Volume | 0x3201 |
0x5201 |
status + u8 | 5 |
| Wear status | 0x3321 |
0x5321 |
status + u8 | 0 = not worn |
OUT 10 00 22 31 00 00 00 00
IN 10 00 22 51 02 00 03 00 00 03 status 0, value 3
MsgIDs are grouped: 0x30xx device information, 0x31xx display, 0x32xx audio,
0x33xx sensors, 0x03xx IMU control, 0x73xx IMU events. Setters presumably live in
the same groups but have not been mapped, since probing them changes device state.
The serial number travels in plaintext. The SDK computes the SHA-256 host-side, even though its own header states the raw number is never exposed.
xr_device_provider_get_wear_status() returns 0 for success but does not write the
output parameter — a sentinel placed in the buffer survived the call unchanged. The wire
carries the value correctly. Reading the device directly is more accurate than the vendor
SDK here.
Constants from viture_protocol_public.h, confirmed through 0x3141:
| Value | Mode |
|---|---|
0x31 |
1920×1080 @ 60 Hz |
0x32 |
3840×1080 @ 60 Hz (SBS) |
0x33 |
1920×1080 @ 90 Hz |
0x34 |
1920×1080 @ 120 Hz |
0x35 |
3840×1080 @ 90 Hz (SBS) |
The 1200p variants (0x41–0x45) belong to the Luma models.
xr_device_provider_initialize() and start() send nothing on the interrupt
endpoint. A full SDK session captured on the wire consists of two OUT commands (stream on,
stream off), two ACKs, and the data packets. A client only has to open the device and send
a single 10-byte command.
native_get_* and get_film_mode return -4 (NOT_SUPPORTED) and emit no command at
all. That independently confirms: no on-glasses DOF, no electrochromic film. The
marketing claim that the Pro 2 "has no head tracking" means exactly this — the IMU is
present and streams at up to 1000 Hz, but the fusion happens on the host.
The meaning of the leading u32 in event packets, and of the u16 at offset 8 in raw
packets. Setter MsgIDs. Mapping them is mechanical: call the corresponding SDK function,
capture, and match against the SDK's ProtocolBuilderV2: Built command - MsgID: ... log
line.