obsbot-mcp 0.3.1 → 0.4.0

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.
Files changed (69) hide show
  1. package/README.md +197 -38
  2. package/dist/codec/commands.d.ts +42 -4
  3. package/dist/codec/commands.js +72 -10
  4. package/dist/codec/commands.js.map +1 -1
  5. package/dist/codec/frame.d.ts +1 -0
  6. package/dist/codec/frame.js +1 -1
  7. package/dist/codec/frame.js.map +1 -1
  8. package/dist/codec/preset.d.ts +40 -0
  9. package/dist/codec/preset.js +198 -0
  10. package/dist/codec/preset.js.map +1 -0
  11. package/dist/codec/types.d.ts +4 -0
  12. package/dist/device/helper-factory.d.ts +25 -0
  13. package/dist/device/helper-factory.js +40 -0
  14. package/dist/device/helper-factory.js.map +1 -0
  15. package/dist/device/manager.d.ts +326 -2
  16. package/dist/device/manager.js +720 -24
  17. package/dist/device/manager.js.map +1 -1
  18. package/dist/ipc/client.d.ts +19 -0
  19. package/dist/ipc/client.js +81 -0
  20. package/dist/ipc/client.js.map +1 -0
  21. package/dist/ipc/coordinator.d.ts +29 -0
  22. package/dist/ipc/coordinator.js +94 -0
  23. package/dist/ipc/coordinator.js.map +1 -0
  24. package/dist/ipc/owner.d.ts +21 -0
  25. package/dist/ipc/owner.js +59 -0
  26. package/dist/ipc/owner.js.map +1 -0
  27. package/dist/ipc/protocol.d.ts +21 -0
  28. package/dist/ipc/protocol.js +56 -0
  29. package/dist/ipc/protocol.js.map +1 -0
  30. package/dist/ipc/rendezvous.d.ts +26 -0
  31. package/dist/ipc/rendezvous.js +93 -0
  32. package/dist/ipc/rendezvous.js.map +1 -0
  33. package/dist/mcp/log-sink.d.ts +12 -0
  34. package/dist/mcp/log-sink.js +27 -0
  35. package/dist/mcp/log-sink.js.map +1 -0
  36. package/dist/mcp/ready.d.ts +16 -4
  37. package/dist/mcp/ready.js +11 -8
  38. package/dist/mcp/ready.js.map +1 -1
  39. package/dist/mcp/render.js +1 -1
  40. package/dist/mcp/render.js.map +1 -1
  41. package/dist/mcp/server.js +68 -18
  42. package/dist/mcp/server.js.map +1 -1
  43. package/dist/mcp/tools.d.ts +5 -3
  44. package/dist/mcp/tools.js +787 -186
  45. package/dist/mcp/tools.js.map +1 -1
  46. package/dist/transport/helper-process.d.ts +36 -0
  47. package/dist/transport/helper-process.js +190 -10
  48. package/dist/transport/helper-process.js.map +1 -1
  49. package/dist/transport/linux.d.ts +40 -0
  50. package/dist/transport/linux.js +92 -2
  51. package/dist/transport/linux.js.map +1 -1
  52. package/dist/transport/macos.d.ts +13 -0
  53. package/dist/transport/macos.js +59 -3
  54. package/dist/transport/macos.js.map +1 -1
  55. package/dist/transport/read-serial.d.ts +26 -0
  56. package/dist/transport/read-serial.js +87 -0
  57. package/dist/transport/read-serial.js.map +1 -0
  58. package/dist/transport/transport.d.ts +24 -0
  59. package/dist/transport/windows.d.ts +4 -0
  60. package/dist/transport/windows.js +31 -4
  61. package/dist/transport/windows.js.map +1 -1
  62. package/native/prebuilt/darwin-arm64/obsbot-helper +0 -0
  63. package/native/prebuilt/darwin-x64/obsbot-helper +0 -0
  64. package/native/prebuilt/linux-x64/obsbot-helper +0 -0
  65. package/native/prebuilt/win32-x64/obsbot-helper.exe +0 -0
  66. package/package.json +3 -2
  67. package/dist/device/session.d.ts +0 -22
  68. package/dist/device/session.js +0 -37
  69. 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 `obsbot_probe` tool (raw XU byte get/set/query) and the
46
- `raw` 60-byte status block on `obsbot_get_status`:
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
- | `obsbot_list_devices` | — | List connected OBSBOT-compatible video capture devices. |
68
- | `obsbot_set_run_status` | `state`: `"run" \| "sleep"` | Wake (`"run"`) or sleep the camera/gimbal. |
69
- | `obsbot_get_status` | — | Read the live status block: `{ awake, hdr, aiMode, trackSpeed }`. Under `--debug`, also returns the raw 60-byte block as hex. |
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
- | `obsbot_ptz_move_angle` | `yaw`, `pitch`, `roll` (degrees, `roll` defaults `0`) | 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. |
76
- | `obsbot_ptz_move_speed` | `yaw`, `pitch`, `roll` (deg/s, `roll` defaults `0`), `autoStopMs` (default `800`) | Drive the gimbal at a speed, then auto-stop after `autoStopMs` so it can't run away. Same yaw/pitch sign convention as `move_angle`. |
77
- | `obsbot_gimbal_recenter` | — | Recenter the gimbal (return to home position). |
78
- | `obsbot_gimbal_position` | — | Read the gimbal's current absolute `{ yaw, pitch }` in degrees via standard UVC Pan/Tilt. May lag a move still in progress. |
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
- | `obsbot_zoom_absolute` | `ratio` (`1.0`–`2.0`) | Set absolute zoom ratio, clamped to `[1.0, 2.0]`. |
85
- | `obsbot_zoom_speed` | `ratio` (`1.0`–`2.0`), `speed` (default `0`) | Zoom to a ratio at a chosen speed: `0` = device default, `1`–`10` slow→fast, `255` = maximum. |
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
- | `obsbot_ai_tracking` | `enabled` (bool), `mode` (default `"normal"`) | Enable/disable AI subject tracking and choose framing: `normal \| upper-body \| close-up \| headless \| lower-body`. Polls status and returns `{ verified, matched }` (`matched:false` = no subject tracked yet). |
92
- | `obsbot_ai_track_speed` | `speed`: `"standard" \| "sport"` | Set the tracking-speed preset (Center's Standard/Sport): `standard` (slower follow) or `sport` (snappier). |
93
- | `obsbot_face_focus` | `enabled` (bool) | Enable or disable face-priority autofocus. |
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
- | `obsbot_fov` | `fov`: `"wide" \| "medium" \| "narrow"` | Set the field of view: wide (86°), medium (78°), narrow (65°). |
100
- | `obsbot_hdr` | `enabled` (bool) | Toggle HDR/WDR imaging on or off. |
101
- | `obsbot_focus` | `mode`: `"auto" \| "manual"`, `position` (`0`–`100`, default `50`) | `auto` = continuous autofocus; `manual` = set the focus motor to `position` (near→far). |
102
- | `obsbot_exposure` | `mode`: `"auto" \| "manual"`, `level` (`0`–`100`, default `50`) | `auto` = auto-exposure; `manual` = set `level` (0 darkest → 100 brightest). |
103
- | `obsbot_white_balance` | `mode`: `"auto" \| "manual"`, `temperature` (Kelvin, default `5000`) | `auto` = auto white balance; `manual` = set a colour temperature (clamped to device range). |
104
- | `obsbot_image_control` | `control`, `level` (`0`–`100`) | Adjust `brightness \| contrast \| hue \| saturation \| sharpness \| gain \| backlight-compensation`; `level` maps onto the device range. |
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
- | `obsbot_snapshot` | `maxDim` (`256`–`1920`, default `1024`), `quality` (`1`–`100`, default `80`), `settleMs` (default `600`), `source` (default `"device"`) | Grab one still frame and return it as an image (for framing/lighting/exposure checks). `source`: `device \| virtual \| ndi`. |
111
- | `obsbot_record_start` | `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 under `Videos/OBSBOT`. Returns a `sessionId`. **Needs ffmpeg.**¹ |
112
- | `obsbot_preview_start` | `source` (default `"device"`) | Open a live preview window. Returns a `sessionId`. **Needs ffplay.**¹ |
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
- | `obsbot_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`. |
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, AI tracking,
133
- wake/sleep, HDR, FOV). Snapshots capture a MJPEG or YUYV frame via V4L2 mmap streaming and encode
134
- to JPEG using **libjpeg**. The `linux-x64` prebuilt binary ships with the published npm package.
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,39 @@ 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. This is a Linux kernel-driver
229
+ gap, not a firmware limitation: hardware testing (2026-07-21) confirmed the OBSBOT Tiny 2's
230
+ `CT_PANTILT_ABSOLUTE` control genuinely tracks live position — a raw USB read of that same control,
231
+ bypassing the kernel, showed a real slew progressing in real time. The reason plain V4L2
232
+ (`VIDIOC_G_CTRL`) never sees that is that `uvcvideo` doesn't mark `V4L2_CID_PAN_ABSOLUTE`/
233
+ `TILT_ABSOLUTE` as `V4L2_CTRL_FLAG_VOLATILE` on this kernel (confirmed via `VIDIOC_QUERY_EXT_CTRL`),
234
+ so the V4L2 core control framework serves its own cache of the last `VIDIOC_S_CTRL` value instead of
235
+ re-querying the device.
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 has been submitted upstream** to mark these controls volatile in `uvcvideo`,
245
+ matching precedent — this device already has one OBSBOT-specific quirk merged
246
+ (`UVC_QUIRK_OBSBOT_MIN_SETTINGS`, a different bug). If it lands, `obsbot_gimbal_position` would
247
+ become live through plain V4L2 with no code changes needed here. Until then:
248
+
249
+ - `obsbot_gimbal_move` and `obsbot_gimbal_recenter` work normally — hardware-verified,
250
+ repeatedly, via direct V4L2 `VIDIOC_S_CTRL` writes. Their target values are known and clamped
251
+ before being sent, so they can't exceed the gimbal's mechanical range regardless of the missing
252
+ feedback.
253
+ - **`obsbot_gimbal_move_speed` is not available on Linux** (hidden from the tool list entirely,
254
+ not just refused at runtime). A speed×duration burst has no target position to clamp — without a
255
+ live reading to confirm where the gimbal actually is, there's no way to bound it against the
256
+ mechanical limits before it gets there. It remains available on Windows/macOS.
257
+
162
258
  ### Building the native helper (macOS)
163
259
 
164
260
  ```bash
@@ -174,9 +270,9 @@ What has actually been exercised against hardware, and what hasn't:
174
270
 
175
271
  | Platform | Status |
176
272
  |---|---|
177
- | `win32-x64` | Builds in CI |
178
- | `linux-x64` | Builds in CI |
179
- | `darwin-arm64` | **Hardware-verified** — control, gimbal, zoom, and snapshot on a real Tiny 2 |
273
+ | `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 |
274
+ | `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 |
275
+ | `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
276
  | `darwin-x64` | **Build-verified only** — compiles with the right architecture and deployment target, never executed |
181
277
 
182
278
  - **The Intel (`darwin-x64`) helper has never been run.** No Intel Mac was available to test it. It
@@ -192,13 +288,76 @@ What has actually been exercised against hardware, and what hasn't:
192
288
  with no bundle identifier, so macOS attributes camera access to whichever app spawned it — your
193
289
  MCP client — and that app is named in the prompt and holds the grant. Approve once; the grant
194
290
  survives helper updates, since it is keyed to the client rather than to the helper's signature.
291
+ - **AI tracking overrides manual gimbal moves.** When AI tracking is active (the Tiny 2's default
292
+ on wake), a commanded pan/tilt executes and is then pulled back to the tracked subject —
293
+ `obsbot_gimbal_position` shows the yaw/pitch move out and decay back to rest. This is the camera's
294
+ behaviour, not a bug: turn tracking off for unopposed manual control.
195
295
  - **The camera may not enumerate through a USB hub or dock.** A Tiny 2 connected through a USB-C
196
296
  dock was invisible to `ioreg` and `system_profiler` entirely — not just to this server. If
197
- `obsbot_list_devices` comes back empty, try a direct connection before assuming a software fault.
198
- - **Only the OBSBOT Tiny 2 is supported.** On macOS the USB vendor/product IDs are hardcoded
199
- (`0x3564`/`0xFEF8`), so no other model is detected at all. Windows and Linux match devices by
200
- name, so a different OBSBOT may be *found* — but the vendor command set is Tiny 2 specific
201
- either way.
297
+ `obsbot_devices` comes back empty, try a direct connection before assuming a software fault.
298
+ - **Only the OBSBOT Tiny 2 is supported.** On Windows and macOS candidacy is gated on the Remo USB
299
+ vendor ID plus a known-model product ID (`0x3564`/`0xFEF8`), so no other model is detected at
300
+ all — and a name-matching software source, such as the "OBSBOT Virtual Camera" that OBSBOT Center
301
+ registers, is rejected because it reports no vid/pid. Linux still matches by name, because its
302
+ helper does not report vid/pid yet, so a different OBSBOT may be *found* there — but the vendor
303
+ command set is Tiny 2 specific either way. (On macOS the virtual camera cannot appear at all: the
304
+ helper enumerates USB devices through the IORegistry, which a software camera never enters.)
305
+ - **The vendor reply mailbox is unreliable for several seconds after a replug.** On 2026-07-21 a
306
+ Tiny 2 was seen returning only the host's own echoed request frame from the vendor reply mailbox
307
+ (XU selector 2) — magic byte `0xaa` cleared to `0x00`, every other byte identical — for a
308
+ continuous 3.2 s. `readSerial()` threw, `bind()` found no serial, and every tool needing a bound
309
+ camera failed with "no OBSBOT camera found" while the device was plainly healthy: correct
310
+ vid/pid, opened fine, XU node 2, live status block on selector 6.
311
+
312
+ That was unexplained for a while. It is now reproducible: **immediately after a USB
313
+ re-enumeration**. Polling `readSerial` every 50 ms across a replug failed 22 times in 80 attempts
314
+ spread over the first 14 s, against 0 in 120 in steady state; the first read after arrival showed
315
+ exactly the echoed-request signature above, and later failures showed the reply slot populated
316
+ but with its magic byte still zeroed. Ruled out as causes: stale per-process device state (the
317
+ same long-lived helper read a brand-new uniqueID cleanly at t+49 ms), re-opening the device
318
+ (0/40 either way), and the per-transport sequence counter restarting at 1 (0/80).
319
+
320
+ Consequence for callers: a bind attempted in the first seconds after a replug can fail even
321
+ though the camera is fine. Retrying works. The arrival-driven re-bind now retries on a bounded
322
+ ladder for this reason, and a `readSerial` failure reports what the mailbox actually held
323
+ (echoed request / unparseable / a reply to another request) rather than only "no valid reply".
324
+ Ruled out earlier and still ruled out: reply latency (polled 3.2 s), the wrong extension unit
325
+ (the VideoControl interface exposes exactly one, `bUnitID 2`), the wrong `wLength` (every XU
326
+ selector is 60 bytes by `GET_LEN`), the reply arriving on another selector (1–19 swept), camera
327
+ sleep state, and contention from OBSBOT Center.
328
+ - **Recovery after a replug is proactive, but not in every case, and it differs by platform.**
329
+ The server subscribes to OS camera arrival/removal events, so in the common case a replugged
330
+ camera re-binds itself with no tool call — `obsbot_devices` reports it `bound` again on its own.
331
+ Every cell below is hardware-measured:
332
+
333
+ | scenario | macOS | Windows | Linux |
334
+ |---|---|---|---|
335
+ | same-port replug | proactive | proactive | next tool call |
336
+ | different-port replug | proactive | next tool call | next tool call |
337
+
338
+ Where it says "next tool call", nothing is stranded — the call that follows detects the stale
339
+ binding, prunes it and re-binds. It costs one failed call, which is exactly how every platform
340
+ behaved before these events existed. The Windows difference comes from its arrival filter
341
+ requiring a path it has already enumerated, which is also what stops the Tiny 2's *audio*
342
+ interface from being reported as a second camera; macOS has no equivalent problem because it
343
+ re-binds by serial and ignores the path. Linux emits no bus events at all yet.
344
+
345
+ Note the interaction with the mailbox entry above: a re-bind attempted immediately after a
346
+ replug can still lose the first attempt, so the server retries on a short bounded ladder.
347
+
348
+ - **Two-camera operation is not yet hardware-verified.** The `camera` selector and the
349
+ per-camera device registry are covered by the unit test suite against fake transports; running
350
+ two physical Tiny 2s attached at once has not been confirmed on real hardware (a second unit
351
+ wasn't available). Single-camera use is unaffected either way.
352
+ - **Linux gimbal position feedback is not live, and `obsbot_gimbal_move_speed` is unavailable
353
+ there as a result.** See ["Linux gimbal position feedback is not live"](#linux-gimbal-position-feedback-is-not-live)
354
+ above — a kernel patch has been submitted to fix this at the source. `obsbot_gimbal_move` and
355
+ `obsbot_gimbal_recenter` are unaffected; both are hardware-verified to work normally.
356
+ - **`obsbot_zoom_vendor`'s ratio scale doesn't match `obsbot_zoom_uvc`'s at the same `ratio`.** A
357
+ hardware snapshot comparison at `ratio: 2.0` showed the vendor path framed tighter than the UVC
358
+ path. Whether the vendor-side ratio encoding is off by a scale factor, or the two zoom controls
359
+ simply have different physical ranges, isn't determined yet — one comparison isn't enough to
360
+ tell. Tracked separately; use `obsbot_zoom_uvc` if you need the ratio to land exactly.
202
361
 
203
362
  ## No proprietary SDK
204
363
 
@@ -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 obsbot_probe tool to exercise unmapped GET/query opcodes
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
- export declare const encodeSetExposureMode: (manual: boolean) => VendorFrame;
29
- export declare const encodeSetExposureValue: (raw: number) => VendorFrame;
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
  }
@@ -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 obsbot_probe tool to exercise unmapped GET/query opcodes
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
- export const encodeSetExposureMode = (manual) => vendorOp("CAM_SET_EXPOSURE_MODE", i32le(manual ? 1 : 0));
79
- export const encodeSetExposureValue = (raw) => vendorOp("CAM_SET_EXPOSURE_TINY2", i32le(raw));
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
- export const encodeAiTracking = (on, mode = "normal") => {
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] = on ? 0x02 : 0x00;
138
- b[3] = on ? AI_FRAMING[mode] : 0x00;
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
- "6,0": "hand",
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":"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"}
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/codec/commands.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@ export interface FrameOpts {
4
4
  receiver: number;
5
5
  payload: Buffer;
6
6
  sender?: number;
7
+ flags?: number;
7
8
  }
8
9
  export declare function buildFrame(o: FrameOpts): Buffer;
9
10
  export declare class FrameParseError extends Error {
@@ -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; // flags: UVC + nested payload (const for v1 cmds)
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