Same wire format as the Tiny 2 — credit to lxman/obsbot-mcp for working it out. The commands differ (see ai-modes.md), the framing does not.
| USB VID / PID | 0x3564 / 0xFEF8 |
| XU descriptor GUID | {9A1E7291-6843-4683-6D92-39BC7906EE49} |
| Control selector | 0x02 |
| Payload | 60 bytes, zero-padded |
On Windows: IBaseFilter → IKsTopologyInfo, find the node whose type is
KSNODETYPE_DEV_SPECIFIC ({941C7AC0-C559-11D0-8A2B-00A0C9255AC1}), CreateNodeInstance for
IKsControl, then KsProperty with a KSP_NODE whose Property.Set is the XU descriptor
GUID above — not the node's type GUID. Using the node type GUID is the classic mistake and
yields ERROR_SET_NOT_FOUND (0x80070492).
Some drivers only dispatch node properties through the filter's IKsControl rather than the
per-node one. If you get ERROR_SET_NOT_FOUND, retry via filter.QueryInterface(IKsControl).
off 0 0xAA magic
off 1 flags 0x25 = SET (with nested payload)
0x01 = header-only GET <-- required for reads
off 2-3 seq u16 LE increments per frame
off 4-5 len u16 LE = 12 bytes covered by the header token
off 6-7 token u16 LE CRC-16/USB over frame[0:12], token zeroed
off 8 sender = 0x0A (host)
off 9 receiver = 0x02 camera / 0x03 gimbal / 0x04 AI
off 10-11 cmd u16 LE
off 12+ nested payload segment, when payload is non-empty:
len2 u16 @12
tok2 u16 @14 CRC-16/USB over frame[12 : 16+len2], tok2 zeroed
data @16
Poly 0xA001 (reflected 0x8005), init 0xFFFF, refin/refout true, xorout 0xFFFF.
def crc16_usb(data: bytes) -> int:
crc = 0xFFFF
for b in data:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
return crc ^ 0xFFFFGETs only work with frame[1] = 0x01 and an empty payload. A GET sent with the 0x25 SET
flavour is never answered — which is what leads people to conclude the read path is broken.
Sequence: write the GET frame to selector 0x02, wait ~100 ms, then read selector 0x02. A real
answer starts aa 29; all-zero means no reply.
xu_set(2, build_frame(seq, 0x0104, 0x04, b"", flags=0x01)) # AI_GET_QUICK_STATUS
time.sleep(0.1)
reply = xu_get(2, 60)
n = int.from_bytes(reply[12:14], "little")
payload = reply[16:16+n]Confirmed answering on Tiny 3: AI_GET_QUICK_STATUS 0x0104, AI_GET_GIM_STATE 0x6604,
AI_GET_HAND_TRACK_STATE 0x2004, AI_GET_CONTROL_PARAMETER 0x5484,
AI_GET_GIMBAL_PARAMETER 0x3F84.
| Purpose | cmd | receiver | payload |
|---|---|---|---|
| Gimbal velocity | 0x6484 |
0x04 |
roll, pitch, yaw — 3× f32 LE, deg/s (yaw inverted) |
| Gimbal absolute | 0x6444 |
0x04 |
roll, pitch, yaw — 3× f32 LE, motor degrees |
| Recenter | 0x00C3 |
0x03 |
6 zero bytes |
| Sleep / wake | 0xA0C2 |
0x02 |
u32: 0 wake, 1 sleep |
| AI engine on/off | 0x0244 |
0x04 |
u32 0/1 |
| Tracking on (+ mode) | 0x0584 |
0x04 |
u32 subject, u32 view |
| Tracking off | 0x0504 |
0x04 |
(empty) |
| AI mode | 0x0284 |
0x04 |
u32 0–5 — see ai-modes.md |
| Read AI state | 0x0104 |
0x04 |
(empty, flags 0x01) |
Zoom is not here — use standard UVC CT_ZOOM_ABSOLUTE. The vendor zoom-ratio command
(0x6884) is ignored by this firmware.
The Tiny 2 spec warns that some command families can drop the device off the USB bus. Steer clear
of boot-position, firmware/upgrade and media families when experimenting. tools/sweep.py skips
them by default.
Also: do not open the camera from two processes at once. Concurrent KsProperty calls
hard-crash one of them with no Python traceback.