obsbot-mcp 0.4.1 → 0.6.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.
- package/README.md +133 -8
- package/dist/capture/ffmpeg-args.d.ts +1 -0
- package/dist/capture/ffmpeg-args.js +29 -2
- package/dist/capture/ffmpeg-args.js.map +1 -1
- package/dist/capture/manager.js +1 -1
- package/dist/capture/manager.js.map +1 -1
- package/dist/codec/commands.d.ts +11 -0
- package/dist/codec/commands.js +24 -0
- package/dist/codec/commands.js.map +1 -1
- package/dist/geometry/aim.d.ts +259 -0
- package/dist/geometry/aim.js +317 -0
- package/dist/geometry/aim.js.map +1 -0
- package/dist/mcp/ready.d.ts +1 -0
- package/dist/mcp/ready.js +3 -1
- package/dist/mcp/ready.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tools.d.ts +51 -1
- package/dist/mcp/tools.js +562 -16
- package/dist/mcp/tools.js.map +1 -1
- package/dist/transport/helper-process.js +4 -1
- package/dist/transport/helper-process.js.map +1 -1
- package/dist/transport/linux.js +17 -1
- package/dist/transport/linux.js.map +1 -1
- package/dist/transport/macos.js +13 -1
- package/dist/transport/macos.js.map +1 -1
- package/dist/transport/transport.d.ts +14 -0
- package/dist/transport/transport.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 +1 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { FovType } from "../codec/commands.js";
|
|
2
|
+
export interface Frame {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
}
|
|
6
|
+
export interface Pose {
|
|
7
|
+
yaw: number;
|
|
8
|
+
pitch: number;
|
|
9
|
+
}
|
|
10
|
+
export interface Optics {
|
|
11
|
+
/**
|
|
12
|
+
* Total linear magnification relative to the WIDE field, which is 1.0.
|
|
13
|
+
*
|
|
14
|
+
* One number, deliberately. The camera has a single magnification scale: the
|
|
15
|
+
* discrete FOV modes are points on it (FOV_MAGNIFICATION) and a continuous
|
|
16
|
+
* zoom writes to it directly (magnificationFromZoomRatio). Setting a zoom
|
|
17
|
+
* ratio REPLACES the mode's magnification rather than multiplying it —
|
|
18
|
+
* `narrow` plus ratio 1.5 measures 2.509, the same as `wide` plus 1.5, not
|
|
19
|
+
* 1.47 x 2.5. An earlier `{ fov, zoom }` shape made that double-counting easy
|
|
20
|
+
* to write and had to warn against it; this shape makes it unrepresentable.
|
|
21
|
+
*/
|
|
22
|
+
magnification: number;
|
|
23
|
+
/**
|
|
24
|
+
* Whether the capture path horizontally flips the preview. This inverts the
|
|
25
|
+
* yaw correction, so it is an explicit input rather than a baked-in
|
|
26
|
+
* assumption. Defaults to false.
|
|
27
|
+
*/
|
|
28
|
+
mirrored?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Horizontal field of view of the CAPTURE STREAM for each FOV setting, in
|
|
32
|
+
* degrees — DERIVED, not three separate measurements.
|
|
33
|
+
*
|
|
34
|
+
* Earlier revisions carried wide/medium/narrow as three independently measured
|
|
35
|
+
* absolutes at +/-3 degrees each. That threw away the most precise thing known
|
|
36
|
+
* about them: the RATIOS between the modes are measured to ~0.05%, roughly 60x
|
|
37
|
+
* better than any of the absolutes, so three free values let the modes drift out
|
|
38
|
+
* of proportion with each other for no reason.
|
|
39
|
+
*
|
|
40
|
+
* One anchor plus measured magnifications instead. The anchor's uncertainty
|
|
41
|
+
* still propagates, but it now moves all three together, which is the honest
|
|
42
|
+
* representation of what is known.
|
|
43
|
+
*
|
|
44
|
+
* MEASURED 2026-07-25 by solving the camera intrinsics from pure gimbal
|
|
45
|
+
* rotations. A camera that only rotates induces an exact homography
|
|
46
|
+
* H = K R K^-1 between views, with no dependence on scene depth, so frames at
|
|
47
|
+
* known gimbal angles determine the focal lengths outright. No distance is
|
|
48
|
+
* measured anywhere — the gimbal angle is the ruler. That is what makes this
|
|
49
|
+
* tighter than the tape-measured letter sheet behind the old +/-3 degrees.
|
|
50
|
+
*
|
|
51
|
+
* Six rotations (pitch +/-10, +/-20; yaw +/-10) over a static scene, 313-1243
|
|
52
|
+
* inliers each, gave fx = 1452-1455 px on a 1920-wide frame across every subset
|
|
53
|
+
* — a spread of 0.2% — which is HFOV 66.84-66.90. Rounded to 67. This also
|
|
54
|
+
* independently reproduces an earlier pan-and-track measurement of 66.4.
|
|
55
|
+
*
|
|
56
|
+
* The per-mode magnifications come from fitting a similarity transform between
|
|
57
|
+
* frames at each setting on a fixed scene (275 and 683 inliers, 0.48 and 0.44 px
|
|
58
|
+
* residual). They are pure ratios, so they are unaffected by which capture
|
|
59
|
+
* format the measurement went through — which matters, because the 1080p pixel
|
|
60
|
+
* formats do NOT share a field of view (see the note on the capture path below).
|
|
61
|
+
*
|
|
62
|
+
* Verified head-to-head on hardware, same feature and same start pose with only
|
|
63
|
+
* the constant differing: a target at u = +0.91 left a yaw residual of -0.823
|
|
64
|
+
* degrees under the old 68 and -0.274 under 67.
|
|
65
|
+
*
|
|
66
|
+
* CAPTURE FORMAT WARNING: at 1920x1080 this camera has two different windows
|
|
67
|
+
* onto the sensor, and FRAME RATE selects between them — not the codec.
|
|
68
|
+
* MEASURED 2026-07-25 at one pose and one zoom: MJPEG@30 vs YUYV@30 came out at
|
|
69
|
+
* scale 1.00001, t (0.2, 0.0) over 2382 inliers at 0.12 px, i.e. the same field
|
|
70
|
+
* to within a fifth of a pixel; MJPEG@60 is a 1.214x crop of BOTH (1.21422 and
|
|
71
|
+
* 1.21404). An earlier revision recorded this as "MJPEG is a 1.201x crop of
|
|
72
|
+
* YUYV" — that comparison was MJPEG@60 against YUYV@30 and charged the codec
|
|
73
|
+
* for what the frame rate did.
|
|
74
|
+
*
|
|
75
|
+
* These constants describe the WIDE (30fps) field. That is what
|
|
76
|
+
* `obsbot_capture_snapshot` delivers: its graph negotiates MJPG 1920x1080@30,
|
|
77
|
+
* which the reply now states outright in `sourceFormat`.
|
|
78
|
+
* `obsbot_capture_preview` pins 60fps to buy smooth motion and therefore shows
|
|
79
|
+
* ~21% less — so preview pixels are NOT interchangeable with snapshot pixels
|
|
80
|
+
* for aiming.
|
|
81
|
+
*
|
|
82
|
+
* Any future measurement must state pixel format AND frame rate; neither a
|
|
83
|
+
* resolution nor a codec alone identifies the field.
|
|
84
|
+
*
|
|
85
|
+
* SCOPE: 16:9 capture at any resolution. A 4:3 path would need re-measuring.
|
|
86
|
+
*/
|
|
87
|
+
export declare const WIDE_HFOV_DEG = 67;
|
|
88
|
+
/**
|
|
89
|
+
* Linear magnification of each FOV setting relative to the wide field.
|
|
90
|
+
*
|
|
91
|
+
* MEASURED 2026-07-25. These are the precise part of the pair: the ratios are
|
|
92
|
+
* good to ~0.05% where the anchor above is good to perhaps 0.5%.
|
|
93
|
+
*
|
|
94
|
+
* The continuous zoom writes to this same scale rather than multiplying on top
|
|
95
|
+
* of it — `narrow` plus zoom ratio 1.5 measures 2.509, the same as `wide` plus
|
|
96
|
+
* 1.5 (2.501), not 1.47 x 2.5. The discrete modes and the zoom control are two
|
|
97
|
+
* ways of writing to one magnification scale, which is why setting zoom ratio
|
|
98
|
+
* 1.0 does not reliably clear `custom`: it is the same optical state as `wide`.
|
|
99
|
+
*/
|
|
100
|
+
export declare const FOV_MAGNIFICATION: Record<FovType, number>;
|
|
101
|
+
export declare const HORIZONTAL_FOV_DEG: Record<FovType, number>;
|
|
102
|
+
/**
|
|
103
|
+
* Empirical correction applied on top of the aspect-derived vertical half-angle.
|
|
104
|
+
* MEASURED, not geometric.
|
|
105
|
+
*
|
|
106
|
+
* Square-pixel geometry says tan(V) = tan(H) * (height/width) — 0.5625 at 16:9.
|
|
107
|
+
* Hardware says the vertical field is shorter than that; this factor carries the
|
|
108
|
+
* difference.
|
|
109
|
+
*
|
|
110
|
+
* MEASURED 2026-07-25 from the same intrinsics solve as WIDE_HFOV_DEG above.
|
|
111
|
+
* fy came out 1502-1520 px across every subset of six rotations, implying this
|
|
112
|
+
* factor at 0.957-0.967. Rounded to 0.957.
|
|
113
|
+
*
|
|
114
|
+
* This REPLACES an earlier value of 0.898, which was 7% low. That figure came
|
|
115
|
+
* from a measurement this project's own history recorded as inconclusive: the
|
|
116
|
+
* constant itself was off by about 6.2%, versus the ~4.3% effect it was trying
|
|
117
|
+
* to capture — roughly 1.4x the effect itself, not an order of magnitude off.
|
|
118
|
+
*
|
|
119
|
+
* The up/down asymmetry is ~1%, not the ~5% once believed: solving from the
|
|
120
|
+
* up-tilt alone gives 0.957 and from the down-tilt alone 0.967. One constant
|
|
121
|
+
* captures that comfortably. Do NOT reintroduce a two-branch vertical constant
|
|
122
|
+
* on the strength of the old figure. Both candidate explanations for a genuine
|
|
123
|
+
* asymmetry were tested and eliminated — the principal point is centred (cx, cy
|
|
124
|
+
* within a few px of frame centre in every solve) and radial distortion is
|
|
125
|
+
* negligible (k1 ~= -0.02).
|
|
126
|
+
*
|
|
127
|
+
* Verified head-to-head on hardware, same feature and same start pose with only
|
|
128
|
+
* this constant differing: a target at v = -0.83 left a pitch residual of -0.597
|
|
129
|
+
* degrees under 0.898 and +0.072 under 0.957, an 8x improvement that lands
|
|
130
|
+
* inside the noise.
|
|
131
|
+
*
|
|
132
|
+
* Known limit: the intrinsics fit carries 2.4-2.8 px rms, not sub-pixel, so
|
|
133
|
+
* something is unmodelled — most likely the entrance pupil sitting off the
|
|
134
|
+
* gimbal's rotation axes, which translates the lens as it turns and is
|
|
135
|
+
* depth-dependent. Sampling was symmetric so it should not bias fx or fy, but do
|
|
136
|
+
* not claim more precision than that.
|
|
137
|
+
*
|
|
138
|
+
* SCOPE: measured on the 16:9 capture path. A 4:3 path needs its own measurement.
|
|
139
|
+
*/
|
|
140
|
+
export declare const VERTICAL_TANGENT_CORRECTION = 0.957;
|
|
141
|
+
/** Magnification of the wide field, and of the whole scale, at its extremes. */
|
|
142
|
+
export declare const MIN_MAGNIFICATION = 1;
|
|
143
|
+
export declare const MAX_MAGNIFICATION = 4;
|
|
144
|
+
/**
|
|
145
|
+
* Linear magnification for a UVC zoom ratio. MEASURED 2026-07-25: magnification
|
|
146
|
+
* is linear in the ratio, `m = 3r - 2`, holding to better than 0.05% at ratios
|
|
147
|
+
* 1.25, 1.5 and 2.0. So ratio 2.0 is 4x linear — carried for a long time as an
|
|
148
|
+
* unsourced note, now measured to four figures. See the spec's section 1.1.
|
|
149
|
+
*/
|
|
150
|
+
export declare const magnificationFromZoomRatio: (ratio: number) => number;
|
|
151
|
+
/** Inverse of {@link magnificationFromZoomRatio}: the ratio that yields `m`. */
|
|
152
|
+
export declare const zoomRatioFromMagnification: (m: number) => number;
|
|
153
|
+
/** Effective half-angles of the visible field, in degrees, after zoom and aspect. */
|
|
154
|
+
export declare function halfAngles(optics: Optics, frame: Frame): {
|
|
155
|
+
h: number;
|
|
156
|
+
v: number;
|
|
157
|
+
};
|
|
158
|
+
export interface Offset {
|
|
159
|
+
dYaw: number;
|
|
160
|
+
dPitch: number;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Convert a pixel in a captured frame to the yaw/pitch delta that would bring
|
|
164
|
+
* that point to the center of frame, treating each axis independently.
|
|
165
|
+
*
|
|
166
|
+
* PER-AXIS ONLY — this does NOT account for axis coupling. The gimbal's yaw
|
|
167
|
+
* axis is world-vertical, so yawing while pitched sweeps a cone; the two axes
|
|
168
|
+
* commute only at zero pitch or zero horizontal offset (see `aimAtPixel`,
|
|
169
|
+
* which composes a rotation instead and is what every tool actually points
|
|
170
|
+
* the camera with). Adding this function's dYaw/dPitch to a pose that is not
|
|
171
|
+
* level reproduces the additive bug `aimAtPixel` was fixed to remove. It is
|
|
172
|
+
* used only by tests now, to pin the per-axis tangent mapping in isolation
|
|
173
|
+
* from the coupling — do not reach for it as a general "point the camera"
|
|
174
|
+
* answer.
|
|
175
|
+
*
|
|
176
|
+
* A rectilinear lens maps angle through a tangent: tan(theta) = u * tan(hfov/2),
|
|
177
|
+
* where u is the normalized offset from center. The linear approximation is
|
|
178
|
+
* exact at the center and again at the edge, and wrong in between — always low,
|
|
179
|
+
* peaking near 1.58 degrees at u ~= 0.55 on the wide setting (WIDE_HFOV_DEG =
|
|
180
|
+
* 67). That error is the difference between landing on target and visibly
|
|
181
|
+
* hunting, so the tangent form is not optional.
|
|
182
|
+
*
|
|
183
|
+
* Signs: +yaw pans camera-LEFT and image x grows rightward, so the yaw term is
|
|
184
|
+
* negated. +pitch tilts DOWN and image y grows downward, so the pitch term is
|
|
185
|
+
* not. Both conventions are hardware-verified.
|
|
186
|
+
*/
|
|
187
|
+
export declare function pixelToOffset(x: number, y: number, frame: Frame, optics: Optics): Offset;
|
|
188
|
+
/**
|
|
189
|
+
* Mechanical limits of the Tiny 2's gimbal, in degrees. Hardware-verified.
|
|
190
|
+
*
|
|
191
|
+
* These live here rather than in the tool layer so there is exactly one
|
|
192
|
+
* definition: `obsbot_gimbal_move` imports them for its own clamping. Two copies
|
|
193
|
+
* of a bound that must agree is a defect waiting to happen.
|
|
194
|
+
*
|
|
195
|
+
* ±150 is the mechanical yaw range on EVERY platform, and these limits are not
|
|
196
|
+
* platform-conditional. Measured 2026-07-25: commanded 145 reads back 145, and
|
|
197
|
+
* 150 reads back 149. Position feedback comes from the camera's physical
|
|
198
|
+
* encoder, which is a property of the hardware and does not vary by OS.
|
|
199
|
+
*
|
|
200
|
+
* Do not "fix" this to 130. `transport/linux.ts` and `transport/macos.ts` record
|
|
201
|
+
* a `CT_PANTILT_ABSOLUTE` range of ±468000 arcsec = ±130°, which reads like a
|
|
202
|
+
* conflict and was raised as one during review. It is not: that is the range the
|
|
203
|
+
* UVC control *advertises* — a descriptor value that under-reports the mechanism
|
|
204
|
+
* it describes — not where the gimbal stops. Nothing clamps to it either;
|
|
205
|
+
* `LinuxTransport.gimbalSet` writes `yawDeg * ARCSEC_PER_DEG` unclamped, so the
|
|
206
|
+
* arcsec figure lives only in comments. See the spec's §8.
|
|
207
|
+
*/
|
|
208
|
+
export declare const GIMBAL_YAW_LIMIT_DEG = 150;
|
|
209
|
+
export declare const GIMBAL_PITCH_LIMIT_DEG = 90;
|
|
210
|
+
export interface Aim {
|
|
211
|
+
target: Pose;
|
|
212
|
+
/**
|
|
213
|
+
* The rotation REQUESTED to reach the raw, unclamped target —
|
|
214
|
+
* `rawTarget - current` — not the rotation actually applied. When `clamped`
|
|
215
|
+
* is true, `target` stops short of `current + offset`; `offset` still
|
|
216
|
+
* reports what was asked for, not what the gimbal was told to do.
|
|
217
|
+
*/
|
|
218
|
+
offset: Offset;
|
|
219
|
+
/** True if either axis saturated, meaning the target was not reachable. */
|
|
220
|
+
clamped: boolean;
|
|
221
|
+
/**
|
|
222
|
+
* True when the target ray's horizontal direction points opposite the
|
|
223
|
+
* camera's current heading — an "over the top" solution. The pixel lies
|
|
224
|
+
* past vertical from the current pose and is not reachable by any rotation
|
|
225
|
+
* that keeps the image upright; clamping it to the nearest yaw would slew
|
|
226
|
+
* the camera toward the far side of the room, not toward the target.
|
|
227
|
+
*/
|
|
228
|
+
overTheTop: boolean;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Absolute pose that brings the given pixel to the centre of frame.
|
|
232
|
+
*
|
|
233
|
+
* Composes a rotation rather than adding two scalars. The gimbal's yaw axis is
|
|
234
|
+
* world-vertical, so yawing while pitched sweeps a CONE: the two rotations do
|
|
235
|
+
* not commute, and `target = current + offset` is exact only at zero pitch or
|
|
236
|
+
* zero horizontal offset. Adding them cost 0.98 degrees at pitch 7.6 / yaw 31 on
|
|
237
|
+
* hardware, against `pitch * (1 - cos yaw)` = 1.09 predicted.
|
|
238
|
+
*
|
|
239
|
+
* The ray to the target in camera coordinates (x right, y down, z forward) is
|
|
240
|
+
* rotated into world coordinates by the current orientation, and the target pose
|
|
241
|
+
* is read back off that direction. The two axes reduce to the old additive sum
|
|
242
|
+
* under DIFFERENT conditions, not the same one: yaw reduces exactly to the old
|
|
243
|
+
* sum whenever pitch = 0, for any pixel, because yaw and pitch commute at zero
|
|
244
|
+
* tilt. Pitch reduces to the old sum only when u = 0 (the pixel sits on the
|
|
245
|
+
* vertical centre line) — composed pitch is asin(dy/n) where n includes dx, so
|
|
246
|
+
* away from u = 0 it differs from the old atan(dy) even when pitch is 0. That is
|
|
247
|
+
* what the invariant tests pin.
|
|
248
|
+
*
|
|
249
|
+
* Saturation is REPORTED, not silent: if the target lies outside the gimbal's
|
|
250
|
+
* range the caller has to know it landed short rather than assume the aim
|
|
251
|
+
* succeeded, since a silent clamp presents as "the camera aimed and missed".
|
|
252
|
+
*
|
|
253
|
+
* `current` must be where the camera actually was when the frame was captured.
|
|
254
|
+
* The module cannot verify that — see the spec's section 5 for what breaks it.
|
|
255
|
+
* Note that on Windows the pose the caller reads is FLOORED to whole degrees
|
|
256
|
+
* (spec section 4.2), which costs up to another degree; that is a separate
|
|
257
|
+
* defect in the pose source, not in this function.
|
|
258
|
+
*/
|
|
259
|
+
export declare function aimAtPixel(x: number, y: number, frame: Frame, optics: Optics, current: Pose): Aim;
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Pure geometry for aiming the gimbal at a point seen in a snapshot. No I/O, no
|
|
2
|
+
// transport, no device access — everything here is a function of its arguments,
|
|
3
|
+
// which is what makes it testable with no camera attached.
|
|
4
|
+
//
|
|
5
|
+
// Degrees in the public API (matching every other angle in this codebase),
|
|
6
|
+
// radians internal only.
|
|
7
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
8
|
+
const toDeg = (rad) => (rad * 180) / Math.PI;
|
|
9
|
+
/**
|
|
10
|
+
* Horizontal field of view of the CAPTURE STREAM for each FOV setting, in
|
|
11
|
+
* degrees — DERIVED, not three separate measurements.
|
|
12
|
+
*
|
|
13
|
+
* Earlier revisions carried wide/medium/narrow as three independently measured
|
|
14
|
+
* absolutes at +/-3 degrees each. That threw away the most precise thing known
|
|
15
|
+
* about them: the RATIOS between the modes are measured to ~0.05%, roughly 60x
|
|
16
|
+
* better than any of the absolutes, so three free values let the modes drift out
|
|
17
|
+
* of proportion with each other for no reason.
|
|
18
|
+
*
|
|
19
|
+
* One anchor plus measured magnifications instead. The anchor's uncertainty
|
|
20
|
+
* still propagates, but it now moves all three together, which is the honest
|
|
21
|
+
* representation of what is known.
|
|
22
|
+
*
|
|
23
|
+
* MEASURED 2026-07-25 by solving the camera intrinsics from pure gimbal
|
|
24
|
+
* rotations. A camera that only rotates induces an exact homography
|
|
25
|
+
* H = K R K^-1 between views, with no dependence on scene depth, so frames at
|
|
26
|
+
* known gimbal angles determine the focal lengths outright. No distance is
|
|
27
|
+
* measured anywhere — the gimbal angle is the ruler. That is what makes this
|
|
28
|
+
* tighter than the tape-measured letter sheet behind the old +/-3 degrees.
|
|
29
|
+
*
|
|
30
|
+
* Six rotations (pitch +/-10, +/-20; yaw +/-10) over a static scene, 313-1243
|
|
31
|
+
* inliers each, gave fx = 1452-1455 px on a 1920-wide frame across every subset
|
|
32
|
+
* — a spread of 0.2% — which is HFOV 66.84-66.90. Rounded to 67. This also
|
|
33
|
+
* independently reproduces an earlier pan-and-track measurement of 66.4.
|
|
34
|
+
*
|
|
35
|
+
* The per-mode magnifications come from fitting a similarity transform between
|
|
36
|
+
* frames at each setting on a fixed scene (275 and 683 inliers, 0.48 and 0.44 px
|
|
37
|
+
* residual). They are pure ratios, so they are unaffected by which capture
|
|
38
|
+
* format the measurement went through — which matters, because the 1080p pixel
|
|
39
|
+
* formats do NOT share a field of view (see the note on the capture path below).
|
|
40
|
+
*
|
|
41
|
+
* Verified head-to-head on hardware, same feature and same start pose with only
|
|
42
|
+
* the constant differing: a target at u = +0.91 left a yaw residual of -0.823
|
|
43
|
+
* degrees under the old 68 and -0.274 under 67.
|
|
44
|
+
*
|
|
45
|
+
* CAPTURE FORMAT WARNING: at 1920x1080 this camera has two different windows
|
|
46
|
+
* onto the sensor, and FRAME RATE selects between them — not the codec.
|
|
47
|
+
* MEASURED 2026-07-25 at one pose and one zoom: MJPEG@30 vs YUYV@30 came out at
|
|
48
|
+
* scale 1.00001, t (0.2, 0.0) over 2382 inliers at 0.12 px, i.e. the same field
|
|
49
|
+
* to within a fifth of a pixel; MJPEG@60 is a 1.214x crop of BOTH (1.21422 and
|
|
50
|
+
* 1.21404). An earlier revision recorded this as "MJPEG is a 1.201x crop of
|
|
51
|
+
* YUYV" — that comparison was MJPEG@60 against YUYV@30 and charged the codec
|
|
52
|
+
* for what the frame rate did.
|
|
53
|
+
*
|
|
54
|
+
* These constants describe the WIDE (30fps) field. That is what
|
|
55
|
+
* `obsbot_capture_snapshot` delivers: its graph negotiates MJPG 1920x1080@30,
|
|
56
|
+
* which the reply now states outright in `sourceFormat`.
|
|
57
|
+
* `obsbot_capture_preview` pins 60fps to buy smooth motion and therefore shows
|
|
58
|
+
* ~21% less — so preview pixels are NOT interchangeable with snapshot pixels
|
|
59
|
+
* for aiming.
|
|
60
|
+
*
|
|
61
|
+
* Any future measurement must state pixel format AND frame rate; neither a
|
|
62
|
+
* resolution nor a codec alone identifies the field.
|
|
63
|
+
*
|
|
64
|
+
* SCOPE: 16:9 capture at any resolution. A 4:3 path would need re-measuring.
|
|
65
|
+
*/
|
|
66
|
+
export const WIDE_HFOV_DEG = 67;
|
|
67
|
+
/**
|
|
68
|
+
* Linear magnification of each FOV setting relative to the wide field.
|
|
69
|
+
*
|
|
70
|
+
* MEASURED 2026-07-25. These are the precise part of the pair: the ratios are
|
|
71
|
+
* good to ~0.05% where the anchor above is good to perhaps 0.5%.
|
|
72
|
+
*
|
|
73
|
+
* The continuous zoom writes to this same scale rather than multiplying on top
|
|
74
|
+
* of it — `narrow` plus zoom ratio 1.5 measures 2.509, the same as `wide` plus
|
|
75
|
+
* 1.5 (2.501), not 1.47 x 2.5. The discrete modes and the zoom control are two
|
|
76
|
+
* ways of writing to one magnification scale, which is why setting zoom ratio
|
|
77
|
+
* 1.0 does not reliably clear `custom`: it is the same optical state as `wide`.
|
|
78
|
+
*/
|
|
79
|
+
export const FOV_MAGNIFICATION = {
|
|
80
|
+
wide: 1,
|
|
81
|
+
medium: 1.15060,
|
|
82
|
+
narrow: 1.47073,
|
|
83
|
+
};
|
|
84
|
+
export const HORIZONTAL_FOV_DEG = {
|
|
85
|
+
wide: 2 * toDeg(Math.atan(Math.tan(toRad(WIDE_HFOV_DEG / 2)) / FOV_MAGNIFICATION.wide)),
|
|
86
|
+
medium: 2 * toDeg(Math.atan(Math.tan(toRad(WIDE_HFOV_DEG / 2)) / FOV_MAGNIFICATION.medium)),
|
|
87
|
+
narrow: 2 * toDeg(Math.atan(Math.tan(toRad(WIDE_HFOV_DEG / 2)) / FOV_MAGNIFICATION.narrow)),
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Empirical correction applied on top of the aspect-derived vertical half-angle.
|
|
91
|
+
* MEASURED, not geometric.
|
|
92
|
+
*
|
|
93
|
+
* Square-pixel geometry says tan(V) = tan(H) * (height/width) — 0.5625 at 16:9.
|
|
94
|
+
* Hardware says the vertical field is shorter than that; this factor carries the
|
|
95
|
+
* difference.
|
|
96
|
+
*
|
|
97
|
+
* MEASURED 2026-07-25 from the same intrinsics solve as WIDE_HFOV_DEG above.
|
|
98
|
+
* fy came out 1502-1520 px across every subset of six rotations, implying this
|
|
99
|
+
* factor at 0.957-0.967. Rounded to 0.957.
|
|
100
|
+
*
|
|
101
|
+
* This REPLACES an earlier value of 0.898, which was 7% low. That figure came
|
|
102
|
+
* from a measurement this project's own history recorded as inconclusive: the
|
|
103
|
+
* constant itself was off by about 6.2%, versus the ~4.3% effect it was trying
|
|
104
|
+
* to capture — roughly 1.4x the effect itself, not an order of magnitude off.
|
|
105
|
+
*
|
|
106
|
+
* The up/down asymmetry is ~1%, not the ~5% once believed: solving from the
|
|
107
|
+
* up-tilt alone gives 0.957 and from the down-tilt alone 0.967. One constant
|
|
108
|
+
* captures that comfortably. Do NOT reintroduce a two-branch vertical constant
|
|
109
|
+
* on the strength of the old figure. Both candidate explanations for a genuine
|
|
110
|
+
* asymmetry were tested and eliminated — the principal point is centred (cx, cy
|
|
111
|
+
* within a few px of frame centre in every solve) and radial distortion is
|
|
112
|
+
* negligible (k1 ~= -0.02).
|
|
113
|
+
*
|
|
114
|
+
* Verified head-to-head on hardware, same feature and same start pose with only
|
|
115
|
+
* this constant differing: a target at v = -0.83 left a pitch residual of -0.597
|
|
116
|
+
* degrees under 0.898 and +0.072 under 0.957, an 8x improvement that lands
|
|
117
|
+
* inside the noise.
|
|
118
|
+
*
|
|
119
|
+
* Known limit: the intrinsics fit carries 2.4-2.8 px rms, not sub-pixel, so
|
|
120
|
+
* something is unmodelled — most likely the entrance pupil sitting off the
|
|
121
|
+
* gimbal's rotation axes, which translates the lens as it turns and is
|
|
122
|
+
* depth-dependent. Sampling was symmetric so it should not bias fx or fy, but do
|
|
123
|
+
* not claim more precision than that.
|
|
124
|
+
*
|
|
125
|
+
* SCOPE: measured on the 16:9 capture path. A 4:3 path needs its own measurement.
|
|
126
|
+
*/
|
|
127
|
+
export const VERTICAL_TANGENT_CORRECTION = 0.957;
|
|
128
|
+
/** Magnification of the wide field, and of the whole scale, at its extremes. */
|
|
129
|
+
export const MIN_MAGNIFICATION = 1;
|
|
130
|
+
export const MAX_MAGNIFICATION = 4;
|
|
131
|
+
/**
|
|
132
|
+
* Linear magnification for a UVC zoom ratio. MEASURED 2026-07-25: magnification
|
|
133
|
+
* is linear in the ratio, `m = 3r - 2`, holding to better than 0.05% at ratios
|
|
134
|
+
* 1.25, 1.5 and 2.0. So ratio 2.0 is 4x linear — carried for a long time as an
|
|
135
|
+
* unsourced note, now measured to four figures. See the spec's section 1.1.
|
|
136
|
+
*/
|
|
137
|
+
export const magnificationFromZoomRatio = (ratio) => 3 * ratio - 2;
|
|
138
|
+
/** Inverse of {@link magnificationFromZoomRatio}: the ratio that yields `m`. */
|
|
139
|
+
export const zoomRatioFromMagnification = (m) => (m + 2) / 3;
|
|
140
|
+
// The tangents are the useful form for every downstream calculation, so they are
|
|
141
|
+
// computed once here and the degree-valued halfAngles() is a thin wrapper. Going
|
|
142
|
+
// through degrees would mean an atan followed immediately by a tan.
|
|
143
|
+
//
|
|
144
|
+
// This is also the ONE place every public entry point (halfAngles, pixelToOffset,
|
|
145
|
+
// aimAtPixel) funnels through, which is why the magnification bound is enforced
|
|
146
|
+
// here rather than trusted from the caller. A magnification of 0 divides out to
|
|
147
|
+
// tanH = Infinity, which atan silently resolves to a 90-degree half-angle; a
|
|
148
|
+
// negative value flips the half-angle's sign with no error; NaN propagates all
|
|
149
|
+
// the way through aimAtPixel's output. Before this guard MIN_MAGNIFICATION/
|
|
150
|
+
// MAX_MAGNIFICATION were exported but enforced nowhere — decorative constants
|
|
151
|
+
// that a malformed device reading (this module's callers now derive magnification
|
|
152
|
+
// from a reported zoomPercent) could silently violate. Callers that already
|
|
153
|
+
// validate their own input (resolveMagnification in src/mcp/tools.ts) should
|
|
154
|
+
// never actually trip this; it exists as the backstop for whichever caller
|
|
155
|
+
// doesn't.
|
|
156
|
+
const halfAngleTangents = (optics, frame) => {
|
|
157
|
+
if (!Number.isFinite(optics.magnification) ||
|
|
158
|
+
optics.magnification < MIN_MAGNIFICATION ||
|
|
159
|
+
optics.magnification > MAX_MAGNIFICATION) {
|
|
160
|
+
throw new RangeError(`optics.magnification must be finite and within [${MIN_MAGNIFICATION}, ${MAX_MAGNIFICATION}], got ${optics.magnification}`);
|
|
161
|
+
}
|
|
162
|
+
const tanH = Math.tan(toRad(WIDE_HFOV_DEG / 2)) / optics.magnification;
|
|
163
|
+
// Vertical starts from the horizontal half-angle scaled by the frame aspect —
|
|
164
|
+
// what square-pixel geometry predicts — then takes the measured correction,
|
|
165
|
+
// because hardware says the real vertical field is ~4.3% shorter than that.
|
|
166
|
+
// See VERTICAL_TANGENT_CORRECTION for the intrinsics solve over six gimbal
|
|
167
|
+
// rotations behind it.
|
|
168
|
+
return { tanH, tanV: tanH * (frame.height / frame.width) * VERTICAL_TANGENT_CORRECTION };
|
|
169
|
+
};
|
|
170
|
+
/** Effective half-angles of the visible field, in degrees, after zoom and aspect. */
|
|
171
|
+
export function halfAngles(optics, frame) {
|
|
172
|
+
const { tanH, tanV } = halfAngleTangents(optics, frame);
|
|
173
|
+
return { h: toDeg(Math.atan(tanH)), v: toDeg(Math.atan(tanV)) };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Convert a pixel in a captured frame to the yaw/pitch delta that would bring
|
|
177
|
+
* that point to the center of frame, treating each axis independently.
|
|
178
|
+
*
|
|
179
|
+
* PER-AXIS ONLY — this does NOT account for axis coupling. The gimbal's yaw
|
|
180
|
+
* axis is world-vertical, so yawing while pitched sweeps a cone; the two axes
|
|
181
|
+
* commute only at zero pitch or zero horizontal offset (see `aimAtPixel`,
|
|
182
|
+
* which composes a rotation instead and is what every tool actually points
|
|
183
|
+
* the camera with). Adding this function's dYaw/dPitch to a pose that is not
|
|
184
|
+
* level reproduces the additive bug `aimAtPixel` was fixed to remove. It is
|
|
185
|
+
* used only by tests now, to pin the per-axis tangent mapping in isolation
|
|
186
|
+
* from the coupling — do not reach for it as a general "point the camera"
|
|
187
|
+
* answer.
|
|
188
|
+
*
|
|
189
|
+
* A rectilinear lens maps angle through a tangent: tan(theta) = u * tan(hfov/2),
|
|
190
|
+
* where u is the normalized offset from center. The linear approximation is
|
|
191
|
+
* exact at the center and again at the edge, and wrong in between — always low,
|
|
192
|
+
* peaking near 1.58 degrees at u ~= 0.55 on the wide setting (WIDE_HFOV_DEG =
|
|
193
|
+
* 67). That error is the difference between landing on target and visibly
|
|
194
|
+
* hunting, so the tangent form is not optional.
|
|
195
|
+
*
|
|
196
|
+
* Signs: +yaw pans camera-LEFT and image x grows rightward, so the yaw term is
|
|
197
|
+
* negated. +pitch tilts DOWN and image y grows downward, so the pitch term is
|
|
198
|
+
* not. Both conventions are hardware-verified.
|
|
199
|
+
*/
|
|
200
|
+
export function pixelToOffset(x, y, frame, optics) {
|
|
201
|
+
const { tanH, tanV } = halfAngleTangents(optics, frame);
|
|
202
|
+
const u = (2 * x) / frame.width - 1;
|
|
203
|
+
const v = (2 * y) / frame.height - 1;
|
|
204
|
+
const uEff = optics.mirrored ? -u : u;
|
|
205
|
+
return {
|
|
206
|
+
dYaw: -toDeg(Math.atan(uEff * tanH)),
|
|
207
|
+
dPitch: toDeg(Math.atan(v * tanV)),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Mechanical limits of the Tiny 2's gimbal, in degrees. Hardware-verified.
|
|
212
|
+
*
|
|
213
|
+
* These live here rather than in the tool layer so there is exactly one
|
|
214
|
+
* definition: `obsbot_gimbal_move` imports them for its own clamping. Two copies
|
|
215
|
+
* of a bound that must agree is a defect waiting to happen.
|
|
216
|
+
*
|
|
217
|
+
* ±150 is the mechanical yaw range on EVERY platform, and these limits are not
|
|
218
|
+
* platform-conditional. Measured 2026-07-25: commanded 145 reads back 145, and
|
|
219
|
+
* 150 reads back 149. Position feedback comes from the camera's physical
|
|
220
|
+
* encoder, which is a property of the hardware and does not vary by OS.
|
|
221
|
+
*
|
|
222
|
+
* Do not "fix" this to 130. `transport/linux.ts` and `transport/macos.ts` record
|
|
223
|
+
* a `CT_PANTILT_ABSOLUTE` range of ±468000 arcsec = ±130°, which reads like a
|
|
224
|
+
* conflict and was raised as one during review. It is not: that is the range the
|
|
225
|
+
* UVC control *advertises* — a descriptor value that under-reports the mechanism
|
|
226
|
+
* it describes — not where the gimbal stops. Nothing clamps to it either;
|
|
227
|
+
* `LinuxTransport.gimbalSet` writes `yawDeg * ARCSEC_PER_DEG` unclamped, so the
|
|
228
|
+
* arcsec figure lives only in comments. See the spec's §8.
|
|
229
|
+
*/
|
|
230
|
+
export const GIMBAL_YAW_LIMIT_DEG = 150;
|
|
231
|
+
export const GIMBAL_PITCH_LIMIT_DEG = 90;
|
|
232
|
+
const clampTo = (value, limit) => Math.min(limit, Math.max(-limit, value));
|
|
233
|
+
/**
|
|
234
|
+
* Absolute pose that brings the given pixel to the centre of frame.
|
|
235
|
+
*
|
|
236
|
+
* Composes a rotation rather than adding two scalars. The gimbal's yaw axis is
|
|
237
|
+
* world-vertical, so yawing while pitched sweeps a CONE: the two rotations do
|
|
238
|
+
* not commute, and `target = current + offset` is exact only at zero pitch or
|
|
239
|
+
* zero horizontal offset. Adding them cost 0.98 degrees at pitch 7.6 / yaw 31 on
|
|
240
|
+
* hardware, against `pitch * (1 - cos yaw)` = 1.09 predicted.
|
|
241
|
+
*
|
|
242
|
+
* The ray to the target in camera coordinates (x right, y down, z forward) is
|
|
243
|
+
* rotated into world coordinates by the current orientation, and the target pose
|
|
244
|
+
* is read back off that direction. The two axes reduce to the old additive sum
|
|
245
|
+
* under DIFFERENT conditions, not the same one: yaw reduces exactly to the old
|
|
246
|
+
* sum whenever pitch = 0, for any pixel, because yaw and pitch commute at zero
|
|
247
|
+
* tilt. Pitch reduces to the old sum only when u = 0 (the pixel sits on the
|
|
248
|
+
* vertical centre line) — composed pitch is asin(dy/n) where n includes dx, so
|
|
249
|
+
* away from u = 0 it differs from the old atan(dy) even when pitch is 0. That is
|
|
250
|
+
* what the invariant tests pin.
|
|
251
|
+
*
|
|
252
|
+
* Saturation is REPORTED, not silent: if the target lies outside the gimbal's
|
|
253
|
+
* range the caller has to know it landed short rather than assume the aim
|
|
254
|
+
* succeeded, since a silent clamp presents as "the camera aimed and missed".
|
|
255
|
+
*
|
|
256
|
+
* `current` must be where the camera actually was when the frame was captured.
|
|
257
|
+
* The module cannot verify that — see the spec's section 5 for what breaks it.
|
|
258
|
+
* Note that on Windows the pose the caller reads is FLOORED to whole degrees
|
|
259
|
+
* (spec section 4.2), which costs up to another degree; that is a separate
|
|
260
|
+
* defect in the pose source, not in this function.
|
|
261
|
+
*/
|
|
262
|
+
export function aimAtPixel(x, y, frame, optics, current) {
|
|
263
|
+
const { tanH, tanV } = halfAngleTangents(optics, frame);
|
|
264
|
+
const u = (2 * x) / frame.width - 1;
|
|
265
|
+
const v = (2 * y) / frame.height - 1;
|
|
266
|
+
const uEff = optics.mirrored ? -u : u;
|
|
267
|
+
// Ray to the target, in camera coordinates: x right, y down, z forward.
|
|
268
|
+
const dx = uEff * tanH;
|
|
269
|
+
const dy = v * tanV;
|
|
270
|
+
const dz = 1;
|
|
271
|
+
const n = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
272
|
+
const cy = Math.cos(toRad(current.yaw));
|
|
273
|
+
const sy = Math.sin(toRad(current.yaw));
|
|
274
|
+
const cp = Math.cos(toRad(current.pitch));
|
|
275
|
+
const sp = Math.sin(toRad(current.pitch));
|
|
276
|
+
// Pitch first, about the camera's own x-axis (+pitch tilts DOWN)...
|
|
277
|
+
const px = dx / n;
|
|
278
|
+
const py = (dy * cp + dz * sp) / n;
|
|
279
|
+
const pz = (-dy * sp + dz * cp) / n;
|
|
280
|
+
// ...then yaw, about the world vertical (+yaw pans camera-LEFT).
|
|
281
|
+
const wx = px * cy - pz * sy;
|
|
282
|
+
const wy = py;
|
|
283
|
+
const wz = px * sy + pz * cy;
|
|
284
|
+
const rawPitch = toDeg(Math.asin(Math.max(-1, Math.min(1, wy))));
|
|
285
|
+
// atan2 returns [-180, 180] — -180 IS included, and is exactly the case the
|
|
286
|
+
// following paragraph handles. Pick the representative nearest the current
|
|
287
|
+
// yaw so a target past +150 reads as +183 rather than -177. Without this a
|
|
288
|
+
// saturating aim clamps to the WRONG END of the range.
|
|
289
|
+
//
|
|
290
|
+
// This is a modulo wrap rather than `Math.round((current.yaw - rawYaw) /
|
|
291
|
+
// 360)`: at an exact 180-degree difference — dead behind the camera, which
|
|
292
|
+
// happens for real at u = 0 aiming past vertical — that rounds ties toward
|
|
293
|
+
// +Infinity regardless of sign (JS's Math.round(0.5) is 1, not 0), which
|
|
294
|
+
// silently flips -180 to +180 and clamps on the wrong end. The wrap below
|
|
295
|
+
// always resolves an exact tie to the lower edge of the window instead.
|
|
296
|
+
const rawYawAtan2 = toDeg(Math.atan2(-wx, wz));
|
|
297
|
+
const rawYaw = current.yaw + (((rawYawAtan2 - current.yaw + 180) % 360 + 360) % 360 - 180);
|
|
298
|
+
const yaw = clampTo(rawYaw, GIMBAL_YAW_LIMIT_DEG);
|
|
299
|
+
const pitch = clampTo(rawPitch, GIMBAL_PITCH_LIMIT_DEG);
|
|
300
|
+
// Over the top: the target ray's horizontal direction points opposite the
|
|
301
|
+
// camera's current heading. (hx, hz) is that heading; (wx, wz) is the target
|
|
302
|
+
// direction, both already computed above. A negative dot product means the
|
|
303
|
+
// only way to face the target while keeping the image upright is to go past
|
|
304
|
+
// vertical — clamping the resulting yaw to the nearest limit would slew the
|
|
305
|
+
// camera toward the OPPOSITE side of the room, not toward the target, so the
|
|
306
|
+
// caller must refuse this case rather than clamp-and-move it.
|
|
307
|
+
const hx = -sy;
|
|
308
|
+
const hz = cy;
|
|
309
|
+
const overTheTop = hx * wx + hz * wz < 0;
|
|
310
|
+
return {
|
|
311
|
+
target: { yaw, pitch },
|
|
312
|
+
offset: { dYaw: rawYaw - current.yaw, dPitch: rawPitch - current.pitch },
|
|
313
|
+
clamped: yaw !== rawYaw || pitch !== rawPitch,
|
|
314
|
+
overTheTop,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=aim.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aim.js","sourceRoot":"","sources":["../../src/geometry/aim.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp/ready.d.ts
CHANGED
package/dist/mcp/ready.js
CHANGED
|
@@ -48,7 +48,9 @@ export async function ensureReady(getTransport, reconnect, opts = {}) {
|
|
|
48
48
|
return { ok: false, reason: "unreachable", error: `camera not reachable: ${msg(e2)}` };
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
let woke = false;
|
|
51
52
|
if (!awake) {
|
|
53
|
+
woke = true;
|
|
52
54
|
await t.sendVendor(encodeSetRunStatus("run").buildFrame(t.nextSeq()));
|
|
53
55
|
let waited = 0;
|
|
54
56
|
while (waited < wakeTimeoutMs) {
|
|
@@ -69,6 +71,6 @@ export async function ensureReady(getTransport, reconnect, opts = {}) {
|
|
|
69
71
|
}
|
|
70
72
|
await sleep(settleMs); // let the gimbal finish rising before we drive it
|
|
71
73
|
}
|
|
72
|
-
return { ok: true, transport: t, reconnected: reconnect?.takeReconnected() ?? false };
|
|
74
|
+
return { ok: true, transport: t, reconnected: reconnect?.takeReconnected() ?? false, woke };
|
|
73
75
|
}
|
|
74
76
|
//# sourceMappingURL=ready.js.map
|
package/dist/mcp/ready.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ready.js","sourceRoot":"","sources":["../../src/mcp/ready.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"ready.js","sourceRoot":"","sources":["../../src/mcp/ready.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp/server.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function startServer(opts = {}) {
|
|
|
71
71
|
process.on("SIGTERM", () => { shutdown(); process.exit(0); });
|
|
72
72
|
const server = new Server(
|
|
73
73
|
// Must match package.json's version; test/version-sync.test.ts enforces it.
|
|
74
|
-
{ name: "obsbot-mcp", version: "0.
|
|
74
|
+
{ name: "obsbot-mcp", version: "0.6.0" }, { capabilities: { tools: {} } });
|
|
75
75
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
76
76
|
tools: tools.map((tool) => ({
|
|
77
77
|
name: tool.name,
|