obsbot-mcp 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -38
- package/dist/codec/commands.d.ts +42 -4
- package/dist/codec/commands.js +72 -10
- package/dist/codec/commands.js.map +1 -1
- package/dist/codec/frame.d.ts +1 -0
- package/dist/codec/frame.js +1 -1
- package/dist/codec/frame.js.map +1 -1
- package/dist/codec/preset.d.ts +40 -0
- package/dist/codec/preset.js +198 -0
- package/dist/codec/preset.js.map +1 -0
- package/dist/codec/types.d.ts +4 -0
- package/dist/device/helper-factory.d.ts +25 -0
- package/dist/device/helper-factory.js +40 -0
- package/dist/device/helper-factory.js.map +1 -0
- package/dist/device/manager.d.ts +326 -2
- package/dist/device/manager.js +720 -24
- package/dist/device/manager.js.map +1 -1
- package/dist/ipc/client.d.ts +19 -0
- package/dist/ipc/client.js +81 -0
- package/dist/ipc/client.js.map +1 -0
- package/dist/ipc/coordinator.d.ts +29 -0
- package/dist/ipc/coordinator.js +94 -0
- package/dist/ipc/coordinator.js.map +1 -0
- package/dist/ipc/owner.d.ts +21 -0
- package/dist/ipc/owner.js +59 -0
- package/dist/ipc/owner.js.map +1 -0
- package/dist/ipc/protocol.d.ts +21 -0
- package/dist/ipc/protocol.js +56 -0
- package/dist/ipc/protocol.js.map +1 -0
- package/dist/ipc/rendezvous.d.ts +26 -0
- package/dist/ipc/rendezvous.js +93 -0
- package/dist/ipc/rendezvous.js.map +1 -0
- package/dist/mcp/log-sink.d.ts +12 -0
- package/dist/mcp/log-sink.js +27 -0
- package/dist/mcp/log-sink.js.map +1 -0
- package/dist/mcp/ready.d.ts +16 -4
- package/dist/mcp/ready.js +11 -8
- package/dist/mcp/ready.js.map +1 -1
- package/dist/mcp/render.js +1 -1
- package/dist/mcp/render.js.map +1 -1
- package/dist/mcp/server.js +68 -18
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +5 -3
- package/dist/mcp/tools.js +787 -186
- package/dist/mcp/tools.js.map +1 -1
- package/dist/transport/helper-process.d.ts +36 -0
- package/dist/transport/helper-process.js +190 -10
- package/dist/transport/helper-process.js.map +1 -1
- package/dist/transport/linux.d.ts +40 -0
- package/dist/transport/linux.js +92 -2
- package/dist/transport/linux.js.map +1 -1
- package/dist/transport/macos.d.ts +13 -0
- package/dist/transport/macos.js +59 -3
- package/dist/transport/macos.js.map +1 -1
- package/dist/transport/read-serial.d.ts +26 -0
- package/dist/transport/read-serial.js +87 -0
- package/dist/transport/read-serial.js.map +1 -0
- package/dist/transport/transport.d.ts +24 -0
- package/dist/transport/windows.d.ts +4 -0
- package/dist/transport/windows.js +31 -4
- package/dist/transport/windows.js.map +1 -1
- package/native/prebuilt/darwin-arm64/obsbot-helper +0 -0
- package/native/prebuilt/darwin-x64/obsbot-helper +0 -0
- package/native/prebuilt/linux-x64/obsbot-helper +0 -0
- package/native/prebuilt/win32-x64/obsbot-helper.exe +0 -0
- package/package.json +3 -2
- package/dist/device/session.d.ts +0 -22
- package/dist/device/session.js +0 -37
- package/dist/device/session.js.map +0 -1
package/README.md
CHANGED
|
@@ -42,8 +42,8 @@ If you're running from a local checkout instead of an npm install, point `comman
|
|
|
42
42
|
### Debug / diagnostics tools
|
|
43
43
|
|
|
44
44
|
By default the server advertises only the normal control surface. Pass `--debug` to additionally
|
|
45
|
-
expose the diagnostics surface — the `
|
|
46
|
-
`raw` 60-byte status block on `
|
|
45
|
+
expose the diagnostics surface — the `obsbot_debug_probe` tool (raw XU byte get/set/query) and the
|
|
46
|
+
`raw` 60-byte status block on `obsbot_status`:
|
|
47
47
|
|
|
48
48
|
```json
|
|
49
49
|
{
|
|
@@ -60,64 +60,123 @@ With the installed binary, use `"command": "obsbot-mcp"` and `"args": ["--debug"
|
|
|
60
60
|
|
|
61
61
|
## Tools
|
|
62
62
|
|
|
63
|
+
34 tools total. All names below are current as of v0.4.0 — **every tool was renamed in this
|
|
64
|
+
release and there is no backward-compatible alias**; see [CHANGELOG.md](./CHANGELOG.md) for the
|
|
65
|
+
full old→new mapping if you're updating a caller.
|
|
66
|
+
|
|
67
|
+
### The `camera` selector
|
|
68
|
+
|
|
69
|
+
Every camera-addressing tool accepts an optional `camera` parameter: the target camera's serial
|
|
70
|
+
number. Omit it with a single camera attached and nothing changes — this matches the server's
|
|
71
|
+
pre-v0.4.0, single-camera behaviour exactly. With more than one camera attached, a call that omits
|
|
72
|
+
`camera` fails with an error naming every attached serial, so you always know what to pass next.
|
|
73
|
+
|
|
74
|
+
**Exempt** (no `camera` parameter, ever): `obsbot_devices` (enumerates the whole fleet),
|
|
75
|
+
`obsbot_capture_stop` / `obsbot_capture_list` (address a `sessionId`, not a device), and
|
|
76
|
+
`obsbot_debug_probe` (operates on the current diagnostics transport). Two more tools honor it only
|
|
77
|
+
partially — see **Capture** below.
|
|
78
|
+
|
|
79
|
+
Multi-camera support is new in v0.4.0. It's exercised by the unit test suite against fakes; running
|
|
80
|
+
two physical Tiny 2s at once has not yet been hardware-verified (see
|
|
81
|
+
[Known limitations](#known-limitations)).
|
|
82
|
+
|
|
83
|
+
`obsbot_devices` is the way to discover the serials you pass as `camera`: it reports each attached
|
|
84
|
+
camera's `serial` (where obtainable — reading it requires briefly opening the camera), `name`, and
|
|
85
|
+
`status` (`available` | `bound` | `busy`). A camera another process already holds comes back `busy`
|
|
86
|
+
with no serial, since it can't be opened to read one.
|
|
87
|
+
|
|
63
88
|
### Device & power
|
|
64
89
|
|
|
65
90
|
| Tool | Parameters | Description |
|
|
66
91
|
|------|------------|-------------|
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
92
|
+
| `obsbot_devices` | — | List attached OBSBOT cameras with each one's serial (where obtainable), name, and status (`available`/`bound`/`busy`). A `busy` camera is held by another process. |
|
|
93
|
+
| `obsbot_wake` | `camera`? | Wake the camera/gimbal (sends `"run"`). **Moves the camera:** un-stows the gimbal back to level (pitch ~0). Most control commands also wake it implicitly. |
|
|
94
|
+
| `obsbot_sleep` | `camera`? | Sleep the camera/gimbal (sends `"sleep"`). **Moves the camera:** stows the gimbal face-down at roughly pitch `84`, so `obsbot_gimbal_position` reads ~84 rather than the pose you left. |
|
|
95
|
+
| `obsbot_status` | `camera`? | Read the live status block: `{ awake, hdr, faceAe, aiMode, trackSpeed }` (`faceAe` = auto-exposure metering for a detected face). Under `--debug`, also returns the raw 60-byte block as hex. |
|
|
70
96
|
|
|
71
97
|
### Gimbal (PTZ)
|
|
72
98
|
|
|
73
99
|
| Tool | Parameters | Description |
|
|
74
100
|
|------|------------|-------------|
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `obsbot_gimbal_recenter` |
|
|
78
|
-
| `obsbot_gimbal_position` |
|
|
101
|
+
| `obsbot_gimbal_move` | `yaw`, `pitch`, `roll` (degrees, `roll` defaults `0`), `camera`? | Move the gimbal to an absolute angle. Positive yaw pans to the camera's left, positive pitch tilts down. Yaw clamped to `[-150, 150]`, pitch to `[-90, 90]`. Absolute 1:1 degrees, hardware-verified. |
|
|
102
|
+
| `obsbot_gimbal_move_speed` | `yaw`, `pitch`, `roll` (deg/s, clamped to `±150`, `roll` defaults `0`), `autoStopMs` (default `800`), `camera`? | Drive the gimbal at a speed, then auto-stop after `autoStopMs` so it can't run away. Same yaw/pitch sign convention as `gimbal_move`. Returns the speeds actually used. Past its limit the firmware ignores the command outright rather than saturating — 180 deg/s and above move the gimbal exactly 0° — so requests are clamped into the hardware-verified band. **Not available on Linux** — see [limitations](#linux-gimbal-position-feedback-is-not-live). |
|
|
103
|
+
| `obsbot_gimbal_recenter` | `camera`? | Recenter the gimbal — drives it to yaw `0` / pitch `0`. Returns as soon as the command is sent, so poll `obsbot_gimbal_position` if you need to know it arrived. |
|
|
104
|
+
| `obsbot_gimbal_position` | `camera`? | Read the gimbal's current absolute `{ yaw, pitch }` in degrees via standard UVC Pan/Tilt. Valid during a move as well as after one. On Linux this is the last-*commanded* value, not a live in-flight reading — see [limitations](#linux-gimbal-position-feedback-is-not-live). |
|
|
105
|
+
|
|
106
|
+
### Gimbal presets
|
|
107
|
+
|
|
108
|
+
Three on-device preset slots (1–3). Slots are **create-once**: `obsbot_preset_save` requires an
|
|
109
|
+
empty slot (delete first to reuse one); every other preset tool requires the slot to already be
|
|
110
|
+
occupied. Each tool re-reads the slot list after writing and returns a structured `{ ok:false }`
|
|
111
|
+
failure if the device didn't land the change.
|
|
112
|
+
|
|
113
|
+
| Tool | Parameters | Description |
|
|
114
|
+
|------|------------|-------------|
|
|
115
|
+
| `obsbot_preset_list` | `camera`? | Read the three preset slots: occupied/empty, name, and pose in degrees. |
|
|
116
|
+
| `obsbot_preset_save` | `slot` (`1`\|`2`\|`3`), `camera`? | Save the gimbal's current live pose into an **empty** slot. |
|
|
117
|
+
| `obsbot_preset_recall` | `slot` (`1`\|`2`\|`3`), `camera`? | Recall an **occupied** slot, driving the gimbal to its saved pose. |
|
|
118
|
+
| `obsbot_preset_update` | `slot` (`1`\|`2`\|`3`), `camera`? | Overwrite an **occupied** slot with the gimbal's current live pose. |
|
|
119
|
+
| `obsbot_preset_rename` | `slot` (`1`\|`2`\|`3`), `name`, `camera`? | Rename an **occupied** slot (names over 40 bytes are truncated). |
|
|
120
|
+
| `obsbot_preset_delete` | `slot` (`1`\|`2`\|`3`), `camera`? | Delete an **occupied** slot, freeing it for `obsbot_preset_save`. |
|
|
79
121
|
|
|
80
122
|
### Zoom
|
|
81
123
|
|
|
124
|
+
Two tools, not one — they ride different transports (standard UVC vs. the vendor command frame)
|
|
125
|
+
and produce different physical zoom at the same commanded `ratio`, so merging them would silently
|
|
126
|
+
change what `ratio` means. Pick by which behaviour you need.
|
|
127
|
+
|
|
82
128
|
| Tool | Parameters | Description |
|
|
83
129
|
|------|------------|-------------|
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
130
|
+
| `obsbot_zoom_uvc` | `ratio` (`1.0`–`2.0`), `camera`? | Standard UVC zoom: set an absolute zoom ratio, clamped to `[1.0, 2.0]`. Snaps to the requested target exactly. |
|
|
131
|
+
| `obsbot_zoom_vendor` | `ratio` (`1.0`–`2.0`), `speed` (default `0`), `camera`? | Vendor zoom path with adjustable speed: zoom to a ratio at a chosen speed (`0` = device default, `1`–`10` slow→fast, `255` = maximum). **Its ratio scale differs from `obsbot_zoom_uvc`'s** and may not land exactly on the requested target — see [Known limitations](#known-limitations). |
|
|
86
132
|
|
|
87
133
|
### AI tracking
|
|
88
134
|
|
|
89
135
|
| Tool | Parameters | Description |
|
|
90
136
|
|------|------------|-------------|
|
|
91
|
-
| `
|
|
92
|
-
| `obsbot_ai_track_speed` | `speed`: `"standard" \| "sport"` | Set the tracking-speed preset (Center's Standard/Sport): `standard` (slower follow) or `sport` (snappier). |
|
|
93
|
-
| `
|
|
137
|
+
| `obsbot_ai_track` | `enabled` (bool), `mode` (default `"normal"`), `camera`? | Enable/disable AI tracking and choose the mode: a human framing (`normal \| upper-body \| close-up \| headless \| lower-body`) or a scene mode (`group \| whiteboard \| desk \| hand`). Polls status and returns `{ verified, matched }` (`matched:false` = no subject tracked yet). |
|
|
138
|
+
| `obsbot_ai_track_speed` | `speed`: `"standard" \| "sport"`, `camera`? | Set the tracking-speed preset (Center's Standard/Sport): `standard` (slower follow) or `sport` (snappier). |
|
|
139
|
+
| `obsbot_focus_face` | `enabled` (bool), `camera`? | Enable or disable face-priority autofocus. |
|
|
94
140
|
|
|
95
141
|
### Image & lens
|
|
96
142
|
|
|
143
|
+
Focus, white balance, and exposure each split into a dedicated `_auto` and `_manual` tool in
|
|
144
|
+
v0.4.0 (previously one tool with a mode parameter) — auto and manual take different parameters, so
|
|
145
|
+
splitting them lets each schema say exactly what it needs.
|
|
146
|
+
|
|
97
147
|
| Tool | Parameters | Description |
|
|
98
148
|
|------|------------|-------------|
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
149
|
+
| `obsbot_image_fov` | `fov`: `"wide" \| "medium" \| "narrow"`, `camera`? | Set the field of view: wide (86°), medium (78°), narrow (65°). |
|
|
150
|
+
| `obsbot_image_hdr` | `enabled` (bool), `camera`? | Toggle HDR/WDR imaging on or off. |
|
|
151
|
+
| `obsbot_focus_auto` | `camera`? | Enable continuous autofocus. |
|
|
152
|
+
| `obsbot_focus_manual` | `position` (`0`–`100`, default `50`), `camera`? | Set the focus motor to `position` (near→far). |
|
|
153
|
+
| `obsbot_image_exposure_auto` | `priority` (`"global" \| "face"`, optional), `camera`? | Enable auto-exposure; optional `priority` selects global vs face metering. |
|
|
154
|
+
| `obsbot_image_exposure_manual` | `level` (`0`–`100`, default `50`), `camera`? | Set exposure `level` (0 darkest → 100 brightest). |
|
|
155
|
+
| `obsbot_image_wb_auto` | `camera`? | Enable auto white balance. |
|
|
156
|
+
| `obsbot_image_wb_manual` | `temperature` (Kelvin, default `5000`), `camera`? | Set a colour temperature (clamped to device range). |
|
|
157
|
+
| `obsbot_image_adjust` | `control`, `level` (`0`–`100`), `camera`? | Adjust `brightness \| contrast \| hue \| saturation \| sharpness \| gain \| backlight-compensation`; `level` maps onto the device range. **`gain` and `backlight-compensation` are not implemented on the Tiny 2** (it reports them as zero-length controls) and are refused with an error; the other five work. |
|
|
105
158
|
|
|
106
159
|
### Capture
|
|
107
160
|
|
|
161
|
+
**`obsbot_capture_record` and `obsbot_capture_preview` do not take `camera`.** They select a device
|
|
162
|
+
by `source` (`device`/`virtual`/`ndi`) through ffmpeg/ffplay, not by serial — there is no
|
|
163
|
+
serial-to-ffmpeg-device mapping yet. **`obsbot_capture_snapshot` honors `camera` only for
|
|
164
|
+
`source:"device"`**; for `source:"virtual"`/`"ndi"` the pixel source is still resolved by device
|
|
165
|
+
name, independent of `camera`.
|
|
166
|
+
|
|
108
167
|
| Tool | Parameters | Description |
|
|
109
168
|
|------|------------|-------------|
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `obsbot_capture_stop` | `sessionId` | Stop a recording or preview session (recordings are finalized gracefully). |
|
|
114
|
-
| `obsbot_capture_list` | — | List active recording/preview sessions. |
|
|
169
|
+
| `obsbot_capture_snapshot` | `resolution` (`256`–`1920`, default `640`), `quality` (`1`–`100`, default `80`), `settleMs` (default `600`), `source` (default `"device"`), `camera`? (source:"device" only) | Grab one still frame and return it as an image (for framing/lighting/exposure checks). `resolution` is the longest edge in pixels — larger costs proportionally more tokens. `source`: `device \| virtual \| ndi`. |
|
|
170
|
+
| `obsbot_capture_record` | `durationSec` (optional), `audio` (default `true`), `outputPath` (optional), `source` (default `"device"`) | Start recording to MP4. Open-ended recordings auto-stop after 60 min; audio uses the OBSBOT mic; defaults to `~/Videos/OBSBOT` on every platform, including macOS, where that is not the usual `~/Movies`. Returns a `sessionId`. **Needs ffmpeg.**¹ No `camera`. |
|
|
171
|
+
| `obsbot_capture_preview` | `source` (default `"device"`) | Open a live preview window. Returns a `sessionId`. **Needs ffplay.**¹ No `camera`. |
|
|
172
|
+
| `obsbot_capture_stop` | `sessionId` | Stop a recording or preview session (recordings are finalized gracefully). No `camera`. |
|
|
173
|
+
| `obsbot_capture_list` | — | List active recording/preview sessions. No `camera`. |
|
|
115
174
|
|
|
116
175
|
### Diagnostics (`--debug` only)
|
|
117
176
|
|
|
118
177
|
| Tool | Parameters | Description |
|
|
119
178
|
|------|------------|-------------|
|
|
120
|
-
| `
|
|
179
|
+
| `obsbot_debug_probe` | `mode`: `"get" \| "set" \| "query"`, plus `selector`, `length`, `hex`, `opcode`, `payloadHex` | RE/diagnostics only — raw XU byte get/set and framed table queries. Advertised only under `--debug`. No `camera`. |
|
|
121
180
|
|
|
122
181
|
¹ `record`/`preview` shell out to **ffmpeg**/**ffplay** (install: `winget install Gyan.FFmpeg`
|
|
123
182
|
on Windows, `brew install ffmpeg` on macOS, `apt install ffmpeg` on Linux). `snapshot` does **not**
|
|
@@ -129,10 +188,14 @@ need ffmpeg — it grabs the frame through the native helper.
|
|
|
129
188
|
(CMake + MSVC); the published npm package ships a prebuilt binary so end users need no toolchain.
|
|
130
189
|
- **Linux x64** — supported from v0.2. The native helper is in `native/linux/` (CMake + GCC);
|
|
131
190
|
it uses **V4L2** for standard UVC controls (zoom, focus, exposure, pan/tilt, white balance,
|
|
132
|
-
image controls) and `UVCIOC_CTRL_QUERY` for vendor Extension Unit commands (gimbal
|
|
133
|
-
wake/sleep, HDR, FOV). Snapshots capture a MJPEG or YUYV frame via V4L2 mmap streaming
|
|
134
|
-
to JPEG using **libjpeg**. The `linux-x64` prebuilt binary ships with the published npm
|
|
135
|
-
Build dependencies: `build-essential cmake libjpeg-dev libv4l-dev`.
|
|
191
|
+
image controls) and `UVCIOC_CTRL_QUERY` for vendor Extension Unit commands (gimbal speed/AI
|
|
192
|
+
tracking, wake/sleep, HDR, FOV). Snapshots capture a MJPEG or YUYV frame via V4L2 mmap streaming
|
|
193
|
+
and encode to JPEG using **libjpeg**. The `linux-x64` prebuilt binary ships with the published npm
|
|
194
|
+
package. Build dependencies: `build-essential cmake libjpeg-dev libv4l-dev`.
|
|
195
|
+
|
|
196
|
+
Gimbal *position* reads (`obsbot_gimbal_position`) reflect the last-commanded value, not live
|
|
197
|
+
in-flight position — see ["Linux gimbal position feedback"](#linux-gimbal-position-feedback-is-not-live)
|
|
198
|
+
below for why, and what would fix it.
|
|
136
199
|
- **macOS 14+ (Apple Silicon and Intel)** — supported. The native helper is in `native/macos/`
|
|
137
200
|
(Objective-C + **IOKit**/**AVFoundation**). It uses IOKit USB control transfers for both standard
|
|
138
201
|
UVC controls and vendor Extension Unit commands, and AVFoundation for enumeration and snapshots.
|
|
@@ -159,6 +222,37 @@ make -j$(nproc)
|
|
|
159
222
|
make install # copies to native/prebuilt/linux-x64/
|
|
160
223
|
```
|
|
161
224
|
|
|
225
|
+
### Linux gimbal position feedback is not live
|
|
226
|
+
|
|
227
|
+
`obsbot_gimbal_position` on Linux reports the last position `obsbot_gimbal_move`/
|
|
228
|
+
`obsbot_gimbal_recenter` commanded — not a live, in-flight reading. Hardware testing (2026-07-21)
|
|
229
|
+
confirmed the OBSBOT Tiny 2's `CT_PANTILT_ABSOLUTE` control genuinely tracks live position — a raw
|
|
230
|
+
USB read of that same control, bypassing the kernel, showed a real slew progressing in real time.
|
|
231
|
+
The reason plain V4L2 (`VIDIOC_G_CTRL`) never sees that is that `uvcvideo` caches the control's
|
|
232
|
+
value and serves the cache instead of re-querying the device (confirmed via
|
|
233
|
+
`VIDIOC_QUERY_EXT_CTRL`, which reports no `V4L2_CTRL_FLAG_VOLATILE`). The driver invalidates that
|
|
234
|
+
cache when the device sends a UVC Control Change interrupt — which this camera's firmware never
|
|
235
|
+
does, and never advertises support for.
|
|
236
|
+
|
|
237
|
+
Getting a genuinely live reading through V4L2 requires briefly detaching the kernel driver from
|
|
238
|
+
the camera's control interface and reading the control directly over raw USB — but detaching that
|
|
239
|
+
interface (even briefly, even without writing anything) breaks any concurrent video capture on this
|
|
240
|
+
device: streaming and control share one kernel-managed USB function, so pulling the driver off one
|
|
241
|
+
takes both down together. That makes a libusb-based workaround incompatible with anything actually
|
|
242
|
+
using the camera as a webcam at the same time, which ruled it out as a shipped default.
|
|
243
|
+
|
|
244
|
+
**A kernel patch is being worked on.** If one lands, `obsbot_gimbal_position` would become live
|
|
245
|
+
through plain V4L2 with no code changes needed here. Until then:
|
|
246
|
+
|
|
247
|
+
- `obsbot_gimbal_move` and `obsbot_gimbal_recenter` work normally — hardware-verified,
|
|
248
|
+
repeatedly, via direct V4L2 `VIDIOC_S_CTRL` writes. Their target values are known and clamped
|
|
249
|
+
before being sent, so they can't exceed the gimbal's mechanical range regardless of the missing
|
|
250
|
+
feedback.
|
|
251
|
+
- **`obsbot_gimbal_move_speed` is not available on Linux** (hidden from the tool list entirely,
|
|
252
|
+
not just refused at runtime). A speed×duration burst has no target position to clamp — without a
|
|
253
|
+
live reading to confirm where the gimbal actually is, there's no way to bound it against the
|
|
254
|
+
mechanical limits before it gets there. It remains available on Windows/macOS.
|
|
255
|
+
|
|
162
256
|
### Building the native helper (macOS)
|
|
163
257
|
|
|
164
258
|
```bash
|
|
@@ -174,9 +268,9 @@ What has actually been exercised against hardware, and what hasn't:
|
|
|
174
268
|
|
|
175
269
|
| Platform | Status |
|
|
176
270
|
|---|---|
|
|
177
|
-
| `win32-x64` |
|
|
178
|
-
| `linux-x64` |
|
|
179
|
-
| `darwin-arm64` | **Hardware-verified** — control, gimbal
|
|
271
|
+
| `win32-x64` | **Hardware-verified** — mid-session disconnect recovery (`ERROR_DEV_NOT_EXIST`, measured not guessed), camera arrival/removal push events, proactive re-bind across a **same-port** replug with no tool call, white balance, and the ProcAmp control ranges, on a real Tiny 2. A **different-port** replug recovers on the next tool call rather than proactively — see below |
|
|
272
|
+
| `linux-x64` | **Hardware-verified** — gimbal absolute moves and recenter via V4L2 (20 consecutive moves with a live preview running), and the arc-second scaling fix confirmed by physical swing. Gimbal *position* is not live and `obsbot_gimbal_move_speed` is unavailable — see below |
|
|
273
|
+
| `darwin-arm64` | **Hardware-verified** — control, gimbal movement **and per-axis position readback**, zoom, snapshot, USB vid/pid candidacy, serial readback and serial-keyed binding, single-owner IPC coordination, helper-death recovery, and **unaided recovery from an unplug/replug**, on a real Tiny 2 |
|
|
180
274
|
| `darwin-x64` | **Build-verified only** — compiles with the right architecture and deployment target, never executed |
|
|
181
275
|
|
|
182
276
|
- **The Intel (`darwin-x64`) helper has never been run.** No Intel Mac was available to test it. It
|
|
@@ -192,13 +286,76 @@ What has actually been exercised against hardware, and what hasn't:
|
|
|
192
286
|
with no bundle identifier, so macOS attributes camera access to whichever app spawned it — your
|
|
193
287
|
MCP client — and that app is named in the prompt and holds the grant. Approve once; the grant
|
|
194
288
|
survives helper updates, since it is keyed to the client rather than to the helper's signature.
|
|
289
|
+
- **AI tracking overrides manual gimbal moves.** When AI tracking is active (the Tiny 2's default
|
|
290
|
+
on wake), a commanded pan/tilt executes and is then pulled back to the tracked subject —
|
|
291
|
+
`obsbot_gimbal_position` shows the yaw/pitch move out and decay back to rest. This is the camera's
|
|
292
|
+
behaviour, not a bug: turn tracking off for unopposed manual control.
|
|
195
293
|
- **The camera may not enumerate through a USB hub or dock.** A Tiny 2 connected through a USB-C
|
|
196
294
|
dock was invisible to `ioreg` and `system_profiler` entirely — not just to this server. If
|
|
197
|
-
`
|
|
198
|
-
- **Only the OBSBOT Tiny 2 is supported.** On macOS
|
|
199
|
-
(`0x3564`/`0xFEF8`), so no other model is detected at
|
|
200
|
-
|
|
201
|
-
|
|
295
|
+
`obsbot_devices` comes back empty, try a direct connection before assuming a software fault.
|
|
296
|
+
- **Only the OBSBOT Tiny 2 is supported.** On Windows and macOS candidacy is gated on the Remo USB
|
|
297
|
+
vendor ID plus a known-model product ID (`0x3564`/`0xFEF8`), so no other model is detected at
|
|
298
|
+
all — and a name-matching software source, such as the "OBSBOT Virtual Camera" that OBSBOT Center
|
|
299
|
+
registers, is rejected because it reports no vid/pid. Linux still matches by name, because its
|
|
300
|
+
helper does not report vid/pid yet, so a different OBSBOT may be *found* there — but the vendor
|
|
301
|
+
command set is Tiny 2 specific either way. (On macOS the virtual camera cannot appear at all: the
|
|
302
|
+
helper enumerates USB devices through the IORegistry, which a software camera never enters.)
|
|
303
|
+
- **The vendor reply mailbox is unreliable for several seconds after a replug.** On 2026-07-21 a
|
|
304
|
+
Tiny 2 was seen returning only the host's own echoed request frame from the vendor reply mailbox
|
|
305
|
+
(XU selector 2) — magic byte `0xaa` cleared to `0x00`, every other byte identical — for a
|
|
306
|
+
continuous 3.2 s. `readSerial()` threw, `bind()` found no serial, and every tool needing a bound
|
|
307
|
+
camera failed with "no OBSBOT camera found" while the device was plainly healthy: correct
|
|
308
|
+
vid/pid, opened fine, XU node 2, live status block on selector 6.
|
|
309
|
+
|
|
310
|
+
That was unexplained for a while. It is now reproducible: **immediately after a USB
|
|
311
|
+
re-enumeration**. Polling `readSerial` every 50 ms across a replug failed 22 times in 80 attempts
|
|
312
|
+
spread over the first 14 s, against 0 in 120 in steady state; the first read after arrival showed
|
|
313
|
+
exactly the echoed-request signature above, and later failures showed the reply slot populated
|
|
314
|
+
but with its magic byte still zeroed. Ruled out as causes: stale per-process device state (the
|
|
315
|
+
same long-lived helper read a brand-new uniqueID cleanly at t+49 ms), re-opening the device
|
|
316
|
+
(0/40 either way), and the per-transport sequence counter restarting at 1 (0/80).
|
|
317
|
+
|
|
318
|
+
Consequence for callers: a bind attempted in the first seconds after a replug can fail even
|
|
319
|
+
though the camera is fine. Retrying works. The arrival-driven re-bind now retries on a bounded
|
|
320
|
+
ladder for this reason, and a `readSerial` failure reports what the mailbox actually held
|
|
321
|
+
(echoed request / unparseable / a reply to another request) rather than only "no valid reply".
|
|
322
|
+
Ruled out earlier and still ruled out: reply latency (polled 3.2 s), the wrong extension unit
|
|
323
|
+
(the VideoControl interface exposes exactly one, `bUnitID 2`), the wrong `wLength` (every XU
|
|
324
|
+
selector is 60 bytes by `GET_LEN`), the reply arriving on another selector (1–19 swept), camera
|
|
325
|
+
sleep state, and contention from OBSBOT Center.
|
|
326
|
+
- **Recovery after a replug is proactive, but not in every case, and it differs by platform.**
|
|
327
|
+
The server subscribes to OS camera arrival/removal events, so in the common case a replugged
|
|
328
|
+
camera re-binds itself with no tool call — `obsbot_devices` reports it `bound` again on its own.
|
|
329
|
+
Every cell below is hardware-measured:
|
|
330
|
+
|
|
331
|
+
| scenario | macOS | Windows | Linux |
|
|
332
|
+
|---|---|---|---|
|
|
333
|
+
| same-port replug | proactive | proactive | next tool call |
|
|
334
|
+
| different-port replug | proactive | next tool call | next tool call |
|
|
335
|
+
|
|
336
|
+
Where it says "next tool call", nothing is stranded — the call that follows detects the stale
|
|
337
|
+
binding, prunes it and re-binds. It costs one failed call, which is exactly how every platform
|
|
338
|
+
behaved before these events existed. The Windows difference comes from its arrival filter
|
|
339
|
+
requiring a path it has already enumerated, which is also what stops the Tiny 2's *audio*
|
|
340
|
+
interface from being reported as a second camera; macOS has no equivalent problem because it
|
|
341
|
+
re-binds by serial and ignores the path. Linux emits no bus events at all yet.
|
|
342
|
+
|
|
343
|
+
Note the interaction with the mailbox entry above: a re-bind attempted immediately after a
|
|
344
|
+
replug can still lose the first attempt, so the server retries on a short bounded ladder.
|
|
345
|
+
|
|
346
|
+
- **Two-camera operation is not yet hardware-verified.** The `camera` selector and the
|
|
347
|
+
per-camera device registry are covered by the unit test suite against fake transports; running
|
|
348
|
+
two physical Tiny 2s attached at once has not been confirmed on real hardware (a second unit
|
|
349
|
+
wasn't available). Single-camera use is unaffected either way.
|
|
350
|
+
- **Linux gimbal position feedback is not live, and `obsbot_gimbal_move_speed` is unavailable
|
|
351
|
+
there as a result.** See ["Linux gimbal position feedback is not live"](#linux-gimbal-position-feedback-is-not-live)
|
|
352
|
+
above — a kernel patch to fix this at the source is being worked on. `obsbot_gimbal_move` and
|
|
353
|
+
`obsbot_gimbal_recenter` are unaffected; both are hardware-verified to work normally.
|
|
354
|
+
- **`obsbot_zoom_vendor`'s ratio scale doesn't match `obsbot_zoom_uvc`'s at the same `ratio`.** A
|
|
355
|
+
hardware snapshot comparison at `ratio: 2.0` showed the vendor path framed tighter than the UVC
|
|
356
|
+
path. Whether the vendor-side ratio encoding is off by a scale factor, or the two zoom controls
|
|
357
|
+
simply have different physical ranges, isn't determined yet — one comparison isn't enough to
|
|
358
|
+
tell. Tracked separately; use `obsbot_zoom_uvc` if you need the ratio to land exactly.
|
|
202
359
|
|
|
203
360
|
## No proprietary SDK
|
|
204
361
|
|
package/dist/codec/commands.d.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { VendorFrame, RunState } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* RE/diagnostics: build ANY opcode-table entry as a V3 frame with an arbitrary
|
|
4
|
-
* payload. Used by the
|
|
5
|
-
* (e.g. AI_GET_QUICK_STATUS) while reverse-engineering the feedback surface.
|
|
4
|
+
* payload. Used by the obsbot_debug_probe tool to exercise unmapped GET/query
|
|
5
|
+
* opcodes (e.g. AI_GET_QUICK_STATUS) while reverse-engineering the feedback surface.
|
|
6
6
|
*/
|
|
7
7
|
export declare const encodeVendorProbe: (name: string, payload: Buffer) => VendorFrame;
|
|
8
|
+
/**
|
|
9
|
+
* Header-only vendor GET (flags 0x01, no nested payload). On this device the
|
|
10
|
+
* framed-V3 vendor GET path only answers when the frame's flags byte is 0x01;
|
|
11
|
+
* SET commands (the vendorOp default) use 0x25. wireCmd/receiver come from the
|
|
12
|
+
* opcode table, same as every other vendor encoder.
|
|
13
|
+
*/
|
|
14
|
+
export declare const encodeVendorGet: (name: string) => VendorFrame;
|
|
15
|
+
/**
|
|
16
|
+
* Decode a UG_GET_SN reply payload (14 ASCII bytes) into the serial string.
|
|
17
|
+
* Anything from the first NUL onward is dropped, then the result is trimmed.
|
|
18
|
+
*/
|
|
19
|
+
export declare const decodeSerial: (payload: Buffer) => string;
|
|
8
20
|
export declare const encodeSetRunStatus: (state: RunState) => VendorFrame;
|
|
9
21
|
export declare const encodePtzMoveAngle: (yaw: number, pitch: number, roll: number) => VendorFrame;
|
|
10
22
|
export declare const encodePtzMoveSpeed: (yaw: number, pitch: number, roll: number) => VendorFrame;
|
|
@@ -25,8 +37,27 @@ export declare const AI_TRACK_SPEEDS: AiTrackSpeed[];
|
|
|
25
37
|
export declare const encodeAiTrackSpeed: (speed: AiTrackSpeed) => VendorFrame;
|
|
26
38
|
export declare const encodeZoomWithSpeed: (ratioX100: number, speed: number) => VendorFrame;
|
|
27
39
|
export declare const encodeFaceFocus: (enable: boolean) => VendorFrame;
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Set exposure mode and value together.
|
|
42
|
+
*
|
|
43
|
+
* `CAM_SET_EXPOSURE_TINY2` takes a **5-byte** `[mode:u8][value:u32le]` payload and
|
|
44
|
+
* sets both fields at once. Payload width is load-bearing: the 4-byte `i32le`
|
|
45
|
+
* payload this previously shipped is SILENTLY DISCARDED — writing 500 left the
|
|
46
|
+
* readback at 330, while the 5-byte form landed immediately (verified 2026-07-20
|
|
47
|
+
* against `CAM_GET_EXPOSURE_TINY2`, which returns this same `[mode][value]` shape).
|
|
48
|
+
*
|
|
49
|
+
* The separate `CAM_SET_EXPOSURE_MODE` command is inert — writing 0 then 1 left the
|
|
50
|
+
* mode pinned — so it has been removed. Mode is only settable through this command.
|
|
51
|
+
*
|
|
52
|
+
* UNVERIFIED: which mode byte means auto and which means manual. Writing 0 reads
|
|
53
|
+
* back as 2; writing 1 reads back as 1, so the readback encoding is 1/2 rather than
|
|
54
|
+
* the 0/1 this code long assumed. Attempting to discriminate by observing whether a
|
|
55
|
+
* written value drifts was inconclusive, most likely because auto-exposure has no
|
|
56
|
+
* frames to meter while nothing is streaming. `manual` is therefore mapped to the
|
|
57
|
+
* byte the caller has always intended (1 = manual, 0 = auto) — which is at worst no
|
|
58
|
+
* worse than before, since previously neither field was written at all.
|
|
59
|
+
*/
|
|
60
|
+
export declare const encodeSetExposure: (manual: boolean, raw: number) => VendorFrame;
|
|
30
61
|
export declare const encodeGetExposureMode: () => VendorFrame;
|
|
31
62
|
export declare const encodeGetExposureValue: () => VendorFrame;
|
|
32
63
|
export declare const encodeGetExposureRange: () => VendorFrame;
|
|
@@ -46,9 +77,15 @@ export declare const UVC_XU_SELECTOR = 6;
|
|
|
46
77
|
export type FovType = "wide" | "medium" | "narrow";
|
|
47
78
|
export declare const FOV_TYPES: FovType[];
|
|
48
79
|
export declare const encodeFov: (fov: FovType) => Buffer;
|
|
80
|
+
export declare const encodeFaceAe: (face: boolean) => Buffer;
|
|
49
81
|
export declare const encodeHdr: (on: boolean) => Buffer;
|
|
50
82
|
export type AiFramingMode = "normal" | "upper-body" | "close-up" | "headless" | "lower-body";
|
|
51
83
|
export declare const AI_FRAMING_MODES: AiFramingMode[];
|
|
84
|
+
export type AiWorkMode = "none" | "group" | "human" | "hand" | "whiteboard" | "desk";
|
|
85
|
+
export declare const AI_WORK_MODES: AiWorkMode[];
|
|
86
|
+
export type AiSceneMode = "group" | "whiteboard" | "desk" | "hand";
|
|
87
|
+
export declare const AI_SCENE_MODES: AiSceneMode[];
|
|
88
|
+
export declare const encodeAiMode: (work: AiWorkMode, framing?: AiFramingMode) => Buffer;
|
|
52
89
|
export declare const encodeAiTracking: (on: boolean, mode?: AiFramingMode) => Buffer;
|
|
53
90
|
export declare const CAMERA_CONTROL_PAN = 0;
|
|
54
91
|
export declare const CAMERA_CONTROL_TILT = 1;
|
|
@@ -67,6 +104,7 @@ export type TrackSpeedStatus = "standard" | "sport" | "unknown";
|
|
|
67
104
|
export interface CameraStatus {
|
|
68
105
|
awake: boolean;
|
|
69
106
|
hdr: boolean;
|
|
107
|
+
faceAe: boolean;
|
|
70
108
|
aiMode: AiModeStatus;
|
|
71
109
|
trackSpeed: TrackSpeedStatus;
|
|
72
110
|
}
|
package/dist/codec/commands.js
CHANGED
|
@@ -5,20 +5,32 @@ import { OP_BY_NAME } from "./opcodes.js";
|
|
|
5
5
|
// from the reverse-engineered opcode table (src/codec/opcodes.ts) rather than
|
|
6
6
|
// magic numbers, so the same definitions serve every platform transport and
|
|
7
7
|
// new commands are a table row + a payload encoder.
|
|
8
|
-
const vendorOp = (name, payload) => {
|
|
8
|
+
const vendorOp = (name, payload, flags) => {
|
|
9
9
|
const op = OP_BY_NAME.get(name);
|
|
10
10
|
if (!op || op.wireCmd === null || op.receiver === null) {
|
|
11
11
|
throw new Error(`opcode "${name}" is not a sendable V3 command`);
|
|
12
12
|
}
|
|
13
13
|
const { wireCmd, receiver } = op;
|
|
14
|
-
return { kind: "vendor", buildFrame: (seq) => buildFrame({ seq, cmd: wireCmd, receiver, payload }) };
|
|
14
|
+
return { kind: "vendor", buildFrame: (seq) => buildFrame({ seq, cmd: wireCmd, receiver, payload, flags }) };
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
17
|
* RE/diagnostics: build ANY opcode-table entry as a V3 frame with an arbitrary
|
|
18
|
-
* payload. Used by the
|
|
19
|
-
* (e.g. AI_GET_QUICK_STATUS) while reverse-engineering the feedback surface.
|
|
18
|
+
* payload. Used by the obsbot_debug_probe tool to exercise unmapped GET/query
|
|
19
|
+
* opcodes (e.g. AI_GET_QUICK_STATUS) while reverse-engineering the feedback surface.
|
|
20
20
|
*/
|
|
21
21
|
export const encodeVendorProbe = (name, payload) => vendorOp(name, payload);
|
|
22
|
+
/**
|
|
23
|
+
* Header-only vendor GET (flags 0x01, no nested payload). On this device the
|
|
24
|
+
* framed-V3 vendor GET path only answers when the frame's flags byte is 0x01;
|
|
25
|
+
* SET commands (the vendorOp default) use 0x25. wireCmd/receiver come from the
|
|
26
|
+
* opcode table, same as every other vendor encoder.
|
|
27
|
+
*/
|
|
28
|
+
export const encodeVendorGet = (name) => vendorOp(name, Buffer.alloc(0), 0x01);
|
|
29
|
+
/**
|
|
30
|
+
* Decode a UG_GET_SN reply payload (14 ASCII bytes) into the serial string.
|
|
31
|
+
* Anything from the first NUL onward is dropped, then the result is trimmed.
|
|
32
|
+
*/
|
|
33
|
+
export const decodeSerial = (payload) => payload.toString("ascii").replace(/\0.*$/, "").trim();
|
|
22
34
|
// The gimbal move wire payload order is [roll, pitch, yaw] (data[0..3]=roll,
|
|
23
35
|
// [4..7]=pitch, [8..11]=yaw). Sending them in logical (yaw,pitch,roll) order put yaw
|
|
24
36
|
// into the roll slot (roll is unused on Tiny 2), which is why move-to-angle appeared
|
|
@@ -75,8 +87,27 @@ export const encodeFaceFocus = (enable) => vendorOp("CAM_SET_FACE_FOCUS", i32le(
|
|
|
75
87
|
// - CAM_SET_EXPOSURE_MODE (0x2442): i32le(0=auto, 1=manual)
|
|
76
88
|
// - CAM_SET_EXPOSURE_TINY2 (0x2982): i32le(value) 0..65535
|
|
77
89
|
// ---------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Set exposure mode and value together.
|
|
92
|
+
*
|
|
93
|
+
* `CAM_SET_EXPOSURE_TINY2` takes a **5-byte** `[mode:u8][value:u32le]` payload and
|
|
94
|
+
* sets both fields at once. Payload width is load-bearing: the 4-byte `i32le`
|
|
95
|
+
* payload this previously shipped is SILENTLY DISCARDED — writing 500 left the
|
|
96
|
+
* readback at 330, while the 5-byte form landed immediately (verified 2026-07-20
|
|
97
|
+
* against `CAM_GET_EXPOSURE_TINY2`, which returns this same `[mode][value]` shape).
|
|
98
|
+
*
|
|
99
|
+
* The separate `CAM_SET_EXPOSURE_MODE` command is inert — writing 0 then 1 left the
|
|
100
|
+
* mode pinned — so it has been removed. Mode is only settable through this command.
|
|
101
|
+
*
|
|
102
|
+
* UNVERIFIED: which mode byte means auto and which means manual. Writing 0 reads
|
|
103
|
+
* back as 2; writing 1 reads back as 1, so the readback encoding is 1/2 rather than
|
|
104
|
+
* the 0/1 this code long assumed. Attempting to discriminate by observing whether a
|
|
105
|
+
* written value drifts was inconclusive, most likely because auto-exposure has no
|
|
106
|
+
* frames to meter while nothing is streaming. `manual` is therefore mapped to the
|
|
107
|
+
* byte the caller has always intended (1 = manual, 0 = auto) — which is at worst no
|
|
108
|
+
* worse than before, since previously neither field was written at all.
|
|
109
|
+
*/
|
|
110
|
+
export const encodeSetExposure = (manual, raw) => vendorOp("CAM_SET_EXPOSURE_TINY2", concat(Buffer.from([manual ? 1 : 0]), u32le(raw)));
|
|
80
111
|
export const encodeGetExposureMode = () => vendorOp("CAM_GET_EXPOSURE_MODE", Buffer.alloc(0));
|
|
81
112
|
export const encodeGetExposureValue = () => vendorOp("CAM_GET_EXPOSURE_TINY2", Buffer.alloc(0));
|
|
82
113
|
export const encodeGetExposureRange = () => vendorOp("CAM_GET_EXPOSURE_RANGE_TINY2", Buffer.alloc(0));
|
|
@@ -120,6 +151,11 @@ const uvcExt = (tag, value) => {
|
|
|
120
151
|
const FOV_VALUE = { wide: 0, medium: 1, narrow: 2 };
|
|
121
152
|
export const FOV_TYPES = Object.keys(FOV_VALUE);
|
|
122
153
|
export const encodeFov = (fov) => uvcExt(0x04, FOV_VALUE[fov]);
|
|
154
|
+
// Face-priority auto-exposure toggle. Same sel-6 uvcExt family, tag 0x03:
|
|
155
|
+
// [0x03, 0x01, v] with v=1 face / 0 global. Hardware-verified 2026-07-18 — the write
|
|
156
|
+
// moves status offset 0x07 (1=face, 0=global). Precondition: auto-exposure must be on
|
|
157
|
+
// first. This is AE priority, distinct from face_focus (which is autofocus).
|
|
158
|
+
export const encodeFaceAe = (face) => uvcExt(0x03, face ? 1 : 0);
|
|
123
159
|
// HDR/WDR on/off.
|
|
124
160
|
export const encodeHdr = (on) => uvcExt(0x01, on ? 1 : 0);
|
|
125
161
|
const AI_FRAMING = {
|
|
@@ -130,14 +166,27 @@ const AI_FRAMING = {
|
|
|
130
166
|
"lower-body": 4,
|
|
131
167
|
};
|
|
132
168
|
export const AI_FRAMING_MODES = Object.keys(AI_FRAMING);
|
|
133
|
-
|
|
169
|
+
const AI_WORK_MODE = {
|
|
170
|
+
none: 0,
|
|
171
|
+
group: 1,
|
|
172
|
+
human: 2,
|
|
173
|
+
hand: 3,
|
|
174
|
+
whiteboard: 4,
|
|
175
|
+
desk: 5,
|
|
176
|
+
};
|
|
177
|
+
export const AI_WORK_MODES = Object.keys(AI_WORK_MODE);
|
|
178
|
+
export const AI_SCENE_MODES = ["group", "whiteboard", "desk", "hand"];
|
|
179
|
+
export const encodeAiMode = (work, framing = "normal") => {
|
|
134
180
|
const b = Buffer.alloc(60);
|
|
135
181
|
b[0] = 0x16;
|
|
136
182
|
b[1] = 0x02;
|
|
137
|
-
b[2] =
|
|
138
|
-
b[3] =
|
|
183
|
+
b[2] = AI_WORK_MODE[work];
|
|
184
|
+
b[3] = work === "human" ? AI_FRAMING[framing] : 0x00;
|
|
139
185
|
return b;
|
|
140
186
|
};
|
|
187
|
+
// Enable/disable human subject tracking with a framing sub-mode. This is the common
|
|
188
|
+
// case, kept as a thin wrapper over encodeAiMode: enable = human, disable = none.
|
|
189
|
+
export const encodeAiTracking = (on, mode = "normal") => encodeAiMode(on ? "human" : "none", mode);
|
|
141
190
|
// ---------------------------------------------------------------------------
|
|
142
191
|
// UVC standard controls (IAMCameraControl / IAMVideoProcAmp) — property ids
|
|
143
192
|
// and the auto/manual flag values for focus and white balance.
|
|
@@ -168,6 +217,7 @@ export const percentToRange = (pct, min, max) => Math.round(min + (max - min) *
|
|
|
168
217
|
// ---------------------------------------------------------------------------
|
|
169
218
|
const STATUS_OFF_SLEEP = 0x02; // 0 = awake, 1 = sleep
|
|
170
219
|
const STATUS_OFF_HDR = 0x06; // 0 = off, non-zero = on
|
|
220
|
+
const STATUS_OFF_FACE_AE = 0x07; // 0 = global AE, 1 = face-priority AE (HW-verified 2026-07-18)
|
|
171
221
|
const STATUS_OFF_AI_MODE_M = 0x18; // AI mode tuple, first value
|
|
172
222
|
const STATUS_OFF_AI_MODE_N = 0x1c; // AI mode tuple, second value
|
|
173
223
|
// Track speed lives at 0x24 on the Tiny 2 — NOT the reference's 0x21 (which reads
|
|
@@ -183,7 +233,18 @@ const AI_MODE_TABLE = {
|
|
|
183
233
|
"2,4": "lower-body",
|
|
184
234
|
"5,0": "desk",
|
|
185
235
|
"4,0": "whiteboard",
|
|
186
|
-
|
|
236
|
+
// Hand shows up as m=3 (= AiWorkModeType Hand) on the live Tiny 2 firmware
|
|
237
|
+
// (verified 2026-07-18); the Tiny4Linux reference lists m=6.
|
|
238
|
+
//
|
|
239
|
+
// Do NOT also map "6,0" to "hand" to cover the reference's numbering. On this
|
|
240
|
+
// firmware m=6 is the MID-SWITCH TRANSIENT the device parks at while changing
|
|
241
|
+
// framing, so it must fall through to "unknown" — verifyFraming() (src/mcp/framing.ts)
|
|
242
|
+
// relies on "unknown" meaning "not settled yet" and keeps polling. Decoding the
|
|
243
|
+
// transient as a real framing made every switch early-exit with a false-negative
|
|
244
|
+
// `verified:"hand", matched:false` on a write that had actually succeeded.
|
|
245
|
+
// Wire evidence (2026-07-21, XU sel 6 @ 60 ms, normal->upper-body):
|
|
246
|
+
// m=2,n=0 (before) -> m=6,n=0 (~200 ms transient) -> m=2,n=1 (landed).
|
|
247
|
+
"3,0": "hand",
|
|
187
248
|
"1,0": "group",
|
|
188
249
|
};
|
|
189
250
|
const TRACK_SPEED_TABLE = {
|
|
@@ -199,6 +260,7 @@ export const decodeStatus = (block) => {
|
|
|
199
260
|
return {
|
|
200
261
|
awake: block[STATUS_OFF_SLEEP] === 0,
|
|
201
262
|
hdr: block[STATUS_OFF_HDR] !== 0,
|
|
263
|
+
faceAe: block[STATUS_OFF_FACE_AE] === 1,
|
|
202
264
|
aiMode: AI_MODE_TABLE[`${m},${n}`] ?? "unknown",
|
|
203
265
|
trackSpeed: TRACK_SPEED_TABLE[block[STATUS_OFF_TRACK_SPEED]] ?? "unknown",
|
|
204
266
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/codec/commands.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,6EAA6E;AAC7E,8EAA8E;AAC9E,4EAA4E;AAC5E,oDAAoD;AACpD,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,OAAe,EAAe,EAAE;
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/codec/commands.ts"],"names":[],"mappings":"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"}
|
package/dist/codec/frame.d.ts
CHANGED
package/dist/codec/frame.js
CHANGED
|
@@ -3,7 +3,7 @@ import { u16le } from "./encoding.js";
|
|
|
3
3
|
export function buildFrame(o) {
|
|
4
4
|
const frame = Buffer.alloc(60); // zero-padded fixed buffer
|
|
5
5
|
frame[0] = 0xaa;
|
|
6
|
-
frame[1] = 0x25; //
|
|
6
|
+
frame[1] = o.flags ?? 0x25; // 0x25 = SET (nested payload); 0x01 = header-only GET
|
|
7
7
|
u16le(o.seq).copy(frame, 2);
|
|
8
8
|
u16le(12).copy(frame, 4); // len = 12 (header covered by token)
|
|
9
9
|
// token field (6-7) stays 0 for the CRC, filled after
|