✨ #72: Support for the OBSBOT Tiny 4K - #169
Open
hutzelmann wants to merge 7 commits into
Open
hutzelmann wants to merge 7 commits into
hutzelmann wants to merge 7 commits into
Conversation
The device hint was hardcoded to "OBSBOT Tiny 2" in the GUI and the CLI, and open_camera matches that hint against the V4L2 card name. A Tiny 4K reports "OBSBOT Tiny 4K: OBSBOT Tiny 4K", so NoCameraFound was returned and both applications claimed that no camera is connected, although the camera works everywhere else. Camera::detect() now tries "OBSBOT Tiny 2" first and falls back to "OBSBOT Tiny 4K". A connected Tiny 2 is still preferred, so nothing changes for Tiny 2 users, and Camera::new(hint) is untouched. Verified on an OBSBOT Tiny 4K (USB 3564:fef4): the camera is detected and its status is read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Tiny 4K accepts the Tiny 2 sleep frame on USB but ignores it. It is architecturally the original OBSBOT Tiny and speaks an older frame format, so sleep and wake now dispatch on the detected model. The Tiny 2 path is unchanged. Its sleep state is read from status byte 0x09, the device run status, rather than byte 0x02, which is a reserved and always zero field on the 4K. That is why info reported the Tiny 4K as awake while it was asleep. The frames and the status offset were captured from the vendor app driving a Tiny 4K and verified against the camera. The README gains the feature matrix for this model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Tiny 4K does not understand the Tiny 2 AI-mode setting. It splits the same function over three legacy frames: enable the AI engine, select the framing mode and select a target. Without the target selection the camera never starts following anyone. The frames also have to be paced. Sent back to back they are dropped and nothing happens at all; in captures of the vendor app no two legacy frames are ever closer than about 50 ms, so the sequence is sent with a 100 ms gap. set_ai_mode dispatches on the detected model, so the Tiny 2 path is unchanged. The 4K knows standard and headroom framing but not the other Tiny 2 modes, it has no tracking speed setting at all, and it takes manual exposure only through the standard UVC controls. Rather than accepting those commands and doing nothing, the CLI and the GUI now report that the camera does not have them. For the same reason info no longer prints an AI mode and a tracking speed that were decoded from bytes the 4K leaves at zero. Verified on a Tiny 4K: with tracking off the gimbal ignores a person moving across the frame, with tracking on it follows them by 18 degrees. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Tiny 4K has no command that recalls a preset. It stores the positions itself and hands the whole table out in one read on extension unit 2, selector 7: a count byte followed by one nine byte record per slot, holding the slot number, pitch and yaw in hundredths of a degree and the zoom factor. A recall is therefore a read followed by two frames: release the tracked target, because the AI would otherwise steer the gimbal straight back, and move to the stored angles. The positions themselves are still stored with OBSBOT Center; this only recalls them. goto_preset_position dispatches on the detected model, so the Tiny 2 path is unchanged. Recalling a slot the camera does not have used to panic through unwrap in both the CLI and the GUI, and now reports the problem instead. Verified on a Tiny 4K: after moving the gimbal to -9.91 degrees, "t4l preset 1" returns it to 21.05 degrees against a stored 21.16, and "t4l preset 2" to the default position. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Get & Dump 0x02" crashed the whole GUI on an OBSBOT Tiny 4K. That camera stalls a cold GET_CUR on extension unit 2, selector 2, dump_02 returns the resulting USB error and unwrap turned it into a panic. Selectors 6 and 7 answer normally, so only this one button was affected. The other three debug actions had the same problem for any USB error, and invalid hex input in the two command fields panicked as well. All four now report what went wrong and leave the window standing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On an OBSBOT Tiny 4K the tracking speed buttons, the tracking modes that camera does not know and the manual exposure button all looked clickable, did nothing and reported the problem on stderr, where a user of a graphical application never sees it. The status panel additionally stated a tracking speed on a camera that has no such setting, the same invented value that info used to print. Those controls are now disabled and faded well below the contrast of an active button, and the panel says the setting is not available. The fade deliberately has no outline: an outlined control reads as interactive, and the active buttons beside them are solid fills, so a border would have given the unavailable control the stronger visual cue of the two. The capability queries moved from Camera to CameraModel, because the view builds its buttons from the application state and has no camera handle there. Camera delegates to the model, so nothing else changes. While no camera is connected every button stays enabled, exactly as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
info showed "Unknown" for the AI mode on a Tiny 4K, because that camera leaves the Tiny 2 status bytes at zero. It answers a separate request instead: e3 3092 returns a status block whose second byte says whether the AI engine is enabled and whose byte 33 holds the framing mode, with the same values the tracking mode command takes. Reading it needs care. The camera keeps the previous answer on the command channel until the new one is ready, so a read can return the answer to an earlier request. Every request therefore carries a sequence number that the reply echoes, and the read is retried until the numbers match. That sequence also has to differ between runs: the stale answer outlives the process that asked for it, so a counter starting at the same value every time would accept the previous run's answer. This was not theoretical, it reported a stale mode until fixed. The GUI now also starts with the tracking mode the camera is really in, so the correct button is selected from the first frame. Verified on a Tiny 4K: setting static, normal and upper-body in turn is read back correctly every time, and info still returns in well under a tenth of a second. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds support for the OBSBOT Tiny 4K (#72), so that every command the CLI and the GUI
already offer either works on this camera or says why it cannot. No new commands, no new
controls, no new dependencies.
The Tiny 4K is not a cut-down Tiny 2. It is architecturally the original OBSBOT Tiny and
speaks an older protocol, which is why so much of it looked like it worked: the camera
accepts the Tiny 2 frames on USB, answers without an error, and ignores them.
Seven independent problems, one commit each:
inforeports the wrong stateinfoand the GUI cannot read the tracking mode backChanges
1 — detection.
Camera::detect()tries"OBSBOT Tiny 2"and then"OBSBOT Tiny 4K".A Tiny 2 is still preferred whenever one is connected, so nothing changes for Tiny 2
users;
Camera::new(hint)is untouched.2 — sleep and wake. A builder for the legacy frame next to the existing
command02.rs, plusCameraModelderived from the detection hint. The sleep state isread from status byte
0x09instead of0x02, which is permanently zero on this model —that is why
infoclaimed a sleeping camera was awake.3 — AI tracking. The 4K splits tracking across three frames: enable the engine, choose
the framing mode, select a target. Without the target selection it never follows anyone.
The frames also have to be paced: sent back to back they are dropped and nothing
happens, while the vendor app never sends two legacy frames closer than about 50 ms. The
modes it does not have, the tracking speed it lacks entirely and manual exposure are now
reported instead of silently accepted.
4 — preset positions. The 4K has no recall command. It stores the positions and hands
the table out in one read: a count byte, then one nine byte record per slot with slot
number, pitch and yaw in hundredths of a degree, and zoom. A recall is that read plus
release-the-target and one absolute move. Positions are still stored with OBSBOT Center.
5 — GUI crash. "Get & Dump 0x02" took the whole window down on a 4K, which stalls a
cold
GET_CURon selector 2. The other three debug actions had the same flaw for any USBerror, and invalid hex input panicked too.
6 — GUI honesty. Controls the camera does not have are disabled and faded, and the
status panel no longer states a tracking speed for a camera that has none.
7 — tracking readback.
e3 3092reports whether the AI engine is on and which framingmode is set, so
infoand the GUI show the truth instead of "Unknown".The Tiny 2 path is unchanged throughout, and each commit builds and passes its own
tests (74 → 85 → 95 → 99 → 99 → 99 → 106).
The protocol, so this is reviewable without the hardware
lenincludes the 12-byte header,typeis0x10to set and0x12to ask,routeis0xe1camera /0xe2gimbal /0xe3AI.crcis CRC-16/USB overframe[0..len]withthe checksum bytes zeroed, stored big-endian. Padded to the 60-byte XU buffer.
Two frames captured from OBSBOT Center driving a Tiny 4K:
The unit tests pin the builder to both, byte for byte, and check the CRC against the
standard CRC-16/USB check value (
"123456789"→0xB4C8). If the captured frames areright then the builder is, and a future change that breaks either fails the suite.
One trap worth knowing if you ever read this channel: the camera keeps the previous
answer available until the new one is ready. Requests therefore carry a sequence number
that the reply echoes, and the read retries until they match. The sequence also has to
differ between runs, because the stale answer outlives the process that asked for it —
that one was not theoretical, it reported a stale tracking mode until fixed.
Verification on a real Tiny 4K (USB 3564:fef4, firmware 1.2.5.4, kernel 6.6)
Everything below was reverse engineered from three USB captures of the Windows vendor app
(~325 MB, every legacy frame CRC-valid) and then replayed against the camera.
info0x093⇄1every time0x02throughout0x00— why the old decode could only ever say "Awake"0x09 = 4)t4l tracking normalt4l preset 9Sleeping while a video stream runs yields
0x09 = 4(privacy) rather than3; itcannot be set directly, so that is the only way to reach it. Both stop the stream, so both
decode as asleep. And a wake sent while the camera is still leaving privacy is silently
dropped — a second one works.
Scope
Three things still report themselves as unavailable, each a real property of the hardware:
other cameras, so there is nothing to implement.
reaches; this project documents that division deliberately.
I mapped and hardware-verified more of the protocol than this PR uses — field of view,
mirror, noise cancellation, microphone-while-asleep, portrait mode, gesture control, the
gimbal home position. None of it is here, because it would mean new commands, new GUI
controls and new strings in seven locale files. It is all written up at the bottom of this
description so it is not lost.
Notes for review
Tiny2Cameraeven though it now dispatches Tiny 4K commands.Renaming it is a refactor, and Support for different cameras #108's known-device list is the better place for it.
CameraStatus::decodeis no longer called from production code (transportcallsdecode_for). It stays as the Tiny 2 shim the existing status tests use, which keepsthe public API stable — deliberate, not an oversight.
rather than stylistic: both binaries
.unwrap()every camera call, so returningErrfor an unsupported command would have turned an ignored command into a panic.
unchanged by construction rather than by testing: every model-dependent call keeps the
original command and offsets in its
CameraModel::Tiny2arm,CameraModel::from_hintfalls back to
Tiny2for any unrecognised hint, and the existing tests still pass. Thebehaviour that does change for a Tiny 2 is that two error paths report instead of
panicking: an unknown preset number, and any failure in the GUI debug area.
enandde. The other five locales fall back to Englishthrough the existing
fallback = "en"; I did not want to invent translations in fivelanguages I do not speak.
not hold anything open waiting on me. Everything I know is in this description.
cargo build,cargo test --all-features(106 passed, previously 74),cargo fmt --checkand
npx prettier --checkall pass.cargo clippy --all-featuresis unchanged from thebase branch: the same 6 / 7 / 8 warnings in lib, CLI and GUI, none of them in new code.
Reference: everything else I verified on the Tiny 4K (not implemented here)
Reverse engineered from three USB captures of OBSBOT Center driving a Tiny 4K, then
replayed against the camera. Cross-checked against the vendor SDK symbol tables and
published work on the 1st-generation Tiny.
Two traps that make this hard to reproduce
usbvideodissector hijacks UVC class requests and renames the setupfields, so
usb.setup.wValue/usb.setup.wIndexcome back empty and every extensionunit filter matches zero packets — the capture looks like it contains no vendor traffic
at all. Pass
--disable-protocol usbvideoto tshark.v4l2-ctl --get-ctrl=pan_absoluteis not a position sensor. uvcvideo echoes back thelast value written, wherever the gimbal actually is. Use the camera's own attitude report
(
e2 2001) instead. This made a working command look broken.Extension unit 2, selector 6 — TLV settings
[tag, len, value]01 01 <0/1>0x0603 01 <0/1>0x0704 01 <0/1/2>0x110a 01 <0/1>0x080b 02 <int16 LE seconds>0x0a-0x0b0b 02 <negated timeout>0x0a-0x0b0c 01 <0/1>0x0c13 01 <0/1>0x1014 01 <0/1>0x13Switching portrait mode re-enumerates the USB device; anything holding the video node
has to reopen it.
Extension unit 2, selector 2 — legacy frames
e1 13c201 01wake /01 03sleepe1 13d2<0/1>, reads back at status0x0de2 2001e2 2006e3 3023[03][f32 LE roll=200.0][f32 pitch][f32 pan]e3 303ae3 305100 <0/1>e3 3066e3 306700— a constant, not a booleane3 3090[sub, value]:00target lock,01zoome3 309100standard /01headroom /02motione3 3092e3 3093e3 30ee<0/1>e8 8060 / 8010 / 8063 / 8065routeis a(dst<<4) | srcnibble pair with the host as0xE, which is why replies comeback with it swapped (
e3→3e). The camera does not validate the sequence number, andit ignores everything past
len, so trailing bytes of a captured 60-byte buffer are stale.Plain UVC on this model — no vendor commands needed
Zoom (
ZOOM_ABSOLUTE), exposure (AE_MODE+EXPOSURE_TIME_ABSOLUTE), focus(
FOCUS_AUTO,FOCUS_ABSOLUTE) and anti-flicker (POWER_LINE_FREQUENCY) are all standardUVC controls, reachable through V4L2.
Status buffer (selector 6, 60 bytes)
0x00ai target ·0x03anti-flicker ·0x04zoom/FOV position ·0x06HDR ·0x07face AE ·0x08noise cancellation ·0x09run status (1 run, 3 sleep, 4 privacy) ·0x0a-0x0bauto-sleep timeout int16 LE, negative = disabled ·0x0cportrait ·0x0dface autofocus ·0x0eautofocus ·0x0ffocus value, drifts constantly ·0x10mic while asleep ·0x11FOV level (3 = manual zoom active) ·0x13mirror.0x14upward stayed zero throughout.Sleep resets zoom and FOV to wide; settings do not survive a sleep/wake cycle.
Do not replay these blind
e3 3023is continuous — a nonzero frame runs the motor until an all-zero stop frame.Its floats are little-endian inside a big-endian header.
e2 2006's first three int16 are speeds, the last three are angles; getting the orderwrong commands a full-range slew. The deg*100 scale comes from the SDK binary and was not
confirmed on hardware.
e3 3051→00 00) before manual gimbal motion, or AI fights it.e1 1fffappeared once and matches no known SDK opcode. Leave it alone.🤖 Generated with Claude Code