obsbot-mcp 0.7.0 → 0.8.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 +95 -20
- package/dist/build-info.json +5 -0
- package/dist/device/manager.d.ts +8 -0
- package/dist/device/manager.js +37 -4
- package/dist/device/manager.js.map +1 -1
- package/dist/geometry/aim.d.ts +24 -13
- package/dist/geometry/aim.js +26 -13
- package/dist/geometry/aim.js.map +1 -1
- package/dist/ipc/build-id.d.ts +25 -0
- package/dist/ipc/build-id.js +60 -0
- package/dist/ipc/build-id.js.map +1 -0
- package/dist/ipc/client.d.ts +36 -0
- package/dist/ipc/client.js +106 -4
- package/dist/ipc/client.js.map +1 -1
- package/dist/ipc/coordinator.d.ts +91 -12
- package/dist/ipc/coordinator.js +293 -57
- package/dist/ipc/coordinator.js.map +1 -1
- package/dist/ipc/gate.d.ts +14 -0
- package/dist/ipc/gate.js +45 -0
- package/dist/ipc/gate.js.map +1 -0
- package/dist/ipc/owner.d.ts +27 -1
- package/dist/ipc/owner.js +67 -4
- package/dist/ipc/owner.js.map +1 -1
- package/dist/ipc/protocol.d.ts +30 -0
- package/dist/ipc/protocol.js +40 -0
- package/dist/ipc/protocol.js.map +1 -1
- package/dist/ipc/rendezvous.d.ts +37 -11
- package/dist/ipc/rendezvous.js +143 -30
- package/dist/ipc/rendezvous.js.map +1 -1
- package/dist/mcp/server.js +40 -12
- package/dist/mcp/server.js.map +1 -1
- package/dist/tail2/api.d.ts +218 -0
- package/dist/tail2/api.js +267 -0
- package/dist/tail2/api.js.map +1 -0
- package/dist/tail2/mdns.d.ts +48 -0
- package/dist/tail2/mdns.js +216 -0
- package/dist/tail2/mdns.js.map +1 -0
- package/dist/tail2/registry.d.ts +100 -0
- package/dist/tail2/registry.js +200 -0
- package/dist/tail2/registry.js.map +1 -0
- package/dist/tail2/snapshot.d.ts +54 -0
- package/dist/tail2/snapshot.js +157 -0
- package/dist/tail2/snapshot.js.map +1 -0
- package/dist/tail2/tools.d.ts +7 -0
- package/dist/tail2/tools.js +370 -0
- package/dist/tail2/tools.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +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 +4 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP/WS client for the OBSBOT Tail 2 control API.
|
|
3
|
+
*
|
|
4
|
+
* The Tail 2's network control plane is a lighttpd REST API plus a WebSocket
|
|
5
|
+
* status push (see TAIL2-PROTOCOL.md). Its USB-C UVC mode has a control
|
|
6
|
+
* surface of its own (§11) that this client does not use.
|
|
7
|
+
* Everything here is plain HTTP and therefore platform-independent: the same
|
|
8
|
+
* code runs on Windows, Linux and macOS with no native helper, which is the
|
|
9
|
+
* whole reason this module exists as a separate family from the Tiny 2's
|
|
10
|
+
* helper-process transports.
|
|
11
|
+
*
|
|
12
|
+
* Identity is the camera's MAC (what OBSBOT Center keys on); device_name and
|
|
13
|
+
* any of its IPs are accepted as selectors one level up, in the registry.
|
|
14
|
+
*/
|
|
15
|
+
/** `GET /camera/sdk/device_info` — the probe target and identity source. */
|
|
16
|
+
export interface Tail2DeviceInfo {
|
|
17
|
+
device_name: string;
|
|
18
|
+
wired_ip: string;
|
|
19
|
+
wireless_ip: string;
|
|
20
|
+
mac: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One push from `ws://<host>/ws/`. The camera sends the FULL status block
|
|
24
|
+
* ~1 Hz with no subscription message. Fields the tools rely on are typed;
|
|
25
|
+
* the block carries more (and firmware may add some) — unknown fields pass
|
|
26
|
+
* through untouched via the index signature.
|
|
27
|
+
*
|
|
28
|
+
* MEASURED 2026-09-26 against firmware 7.2.13.1; see TAIL2-PROTOCOL.md §4.
|
|
29
|
+
*/
|
|
30
|
+
export interface Tail2Status {
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
power_on?: boolean;
|
|
33
|
+
usb_mode?: number;
|
|
34
|
+
rec?: boolean;
|
|
35
|
+
rec_time?: number;
|
|
36
|
+
switch_portrait?: boolean;
|
|
37
|
+
ai_mode?: string;
|
|
38
|
+
zoom_ratio?: number;
|
|
39
|
+
roll_bias?: number;
|
|
40
|
+
focus_mode?: string;
|
|
41
|
+
auto_focus_mode?: string;
|
|
42
|
+
ndi?: {
|
|
43
|
+
enable?: boolean;
|
|
44
|
+
};
|
|
45
|
+
rtsp?: {
|
|
46
|
+
enable?: boolean;
|
|
47
|
+
};
|
|
48
|
+
srt?: {
|
|
49
|
+
enable?: boolean;
|
|
50
|
+
};
|
|
51
|
+
rtmp?: {
|
|
52
|
+
enable?: boolean;
|
|
53
|
+
};
|
|
54
|
+
preset_info?: Array<{
|
|
55
|
+
id: number;
|
|
56
|
+
pitch: number;
|
|
57
|
+
yaw: number;
|
|
58
|
+
roll: number;
|
|
59
|
+
ratio: number;
|
|
60
|
+
name: string;
|
|
61
|
+
}>;
|
|
62
|
+
device_status?: Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
/** A saved gimbal pose: degrees + zoom ratio. Names are base64 on the wire only. */
|
|
65
|
+
export interface Preset {
|
|
66
|
+
id: number;
|
|
67
|
+
pitch: number;
|
|
68
|
+
yaw: number;
|
|
69
|
+
roll: number;
|
|
70
|
+
ratio: number;
|
|
71
|
+
name: string;
|
|
72
|
+
}
|
|
73
|
+
export declare class Tail2HttpError extends Error {
|
|
74
|
+
readonly status: number;
|
|
75
|
+
readonly path: string;
|
|
76
|
+
readonly body: string;
|
|
77
|
+
constructor(status: number, path: string, body: string);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `{"code":200,"err_idx":0}` means ACKNOWLEDGED, not applied — MEASURED
|
|
81
|
+
* 2026-09-26: a rollbias write returned 200 while the portrait motor was in
|
|
82
|
+
* motion and never landed (TAIL2-PROTOCOL.md §8.1). This is the same failure
|
|
83
|
+
* class as the Tiny 2's post-replug vendor mailbox, so every setter here
|
|
84
|
+
* verifies by readback before reporting success, with a bounded retry ladder
|
|
85
|
+
* sized for the actuator involved (the zoom motor and the portrait rotation
|
|
86
|
+
* take seconds, not milliseconds).
|
|
87
|
+
*/
|
|
88
|
+
export interface VerifyOpts {
|
|
89
|
+
attempts?: number;
|
|
90
|
+
delayMs?: number;
|
|
91
|
+
}
|
|
92
|
+
export declare class Tail2Api {
|
|
93
|
+
readonly baseUrl: string;
|
|
94
|
+
private readonly timeoutMs;
|
|
95
|
+
private readonly wsUrl;
|
|
96
|
+
constructor(opts: {
|
|
97
|
+
baseUrl: string;
|
|
98
|
+
timeoutMs?: number;
|
|
99
|
+
});
|
|
100
|
+
private req;
|
|
101
|
+
private send;
|
|
102
|
+
/**
|
|
103
|
+
* Read `read()` until `match()` holds, on a bounded ladder. Returns
|
|
104
|
+
* `settled:false` (NOT a throw) when the ladder runs out — the command was
|
|
105
|
+
* sent and acknowledged, so the caller decides whether that is failure.
|
|
106
|
+
* Mirrors `obsbot_zoom_uvc`'s `settled` contract.
|
|
107
|
+
*/
|
|
108
|
+
verified<T>(read: () => Promise<T>, match: (v: T) => boolean, opts?: VerifyOpts): Promise<{
|
|
109
|
+
settled: boolean;
|
|
110
|
+
value: T;
|
|
111
|
+
}>;
|
|
112
|
+
info(): Promise<Tail2DeviceInfo>;
|
|
113
|
+
/** One-shot status: connect, take the first push, close. No persistent socket. */
|
|
114
|
+
status(): Promise<Tail2Status>;
|
|
115
|
+
ranges(): Promise<Record<string, unknown>>;
|
|
116
|
+
networkConfig(): Promise<Record<string, unknown>>;
|
|
117
|
+
imageState(): Promise<Record<string, unknown>>;
|
|
118
|
+
zoomGet(): Promise<{
|
|
119
|
+
ratio: number;
|
|
120
|
+
}>;
|
|
121
|
+
/**
|
|
122
|
+
* Absolute zoom ratio on the Tail 2's own 1.0–12.0 scale (`GET range`).
|
|
123
|
+
* `speed` is REQUIRED by the firmware (a ratio-only PUT is a 400) — both
|
|
124
|
+
* fields MEASURED 2026-09-26.
|
|
125
|
+
*/
|
|
126
|
+
zoomSet(ratio: number, speed: number, verify?: VerifyOpts): Promise<{
|
|
127
|
+
settled: boolean;
|
|
128
|
+
ratio: number;
|
|
129
|
+
}>;
|
|
130
|
+
/** Gimbal recenter (`POST ptz/reset`). Open-loop: the Tail 2 reports no live pose. */
|
|
131
|
+
recenter(): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Motorized 90° barrel rotation. Verified against the WS push rather than a
|
|
134
|
+
* REST readback (none exists) — and the readback MUST be tolerant of the
|
|
135
|
+
* motor taking ~1.5s, plus the swallowed-write hazard during motion.
|
|
136
|
+
*/
|
|
137
|
+
portraitSet(enable: boolean, verify?: VerifyOpts): Promise<{
|
|
138
|
+
settled: boolean;
|
|
139
|
+
switch_portrait: boolean;
|
|
140
|
+
}>;
|
|
141
|
+
rollBiasGet(): Promise<{
|
|
142
|
+
angle: number;
|
|
143
|
+
}>;
|
|
144
|
+
/** Roll trim in degrees. MEASURED write + readback 2026-09-26. */
|
|
145
|
+
rollBiasSet(angle: number, verify?: VerifyOpts): Promise<{
|
|
146
|
+
settled: boolean;
|
|
147
|
+
angle: number;
|
|
148
|
+
}>;
|
|
149
|
+
aiModeGet(): Promise<{
|
|
150
|
+
mode: string;
|
|
151
|
+
}>;
|
|
152
|
+
/**
|
|
153
|
+
* `mode` strings are the camera's own enum (bundle-verified, TAIL2-PROTOCOL
|
|
154
|
+
* §3): none | humanTrackingSingleMode | humanTrackingGroupMode |
|
|
155
|
+
* animalTrackingNormal | animalTrackingCloseUp | objectTracking |
|
|
156
|
+
* objectTrackingNormal | objectTrackingCloseUp.
|
|
157
|
+
*/
|
|
158
|
+
aiModeSet(mode: string, verify?: VerifyOpts): Promise<{
|
|
159
|
+
settled: boolean;
|
|
160
|
+
mode: string;
|
|
161
|
+
}>;
|
|
162
|
+
trackSpeedGet(): Promise<{
|
|
163
|
+
speed: string;
|
|
164
|
+
}>;
|
|
165
|
+
/**
|
|
166
|
+
* The Tail 2's own six-speed enum (bundle-verified): superLazy | lazy |
|
|
167
|
+
* slow | fast | crazy | customized. NOT the Tiny 2's standard/sport pair.
|
|
168
|
+
*/
|
|
169
|
+
trackSpeedSet(speed: string, verify?: VerifyOpts): Promise<{
|
|
170
|
+
settled: boolean;
|
|
171
|
+
speed: string;
|
|
172
|
+
}>;
|
|
173
|
+
/**
|
|
174
|
+
* A saved pose: angles in DEGREES plus zoom ratio — the same units the
|
|
175
|
+
* tools speak. `name` is base64 on the wire; encode/decode lives here so
|
|
176
|
+
* callers never see it raw.
|
|
177
|
+
*/
|
|
178
|
+
presetsGet(): Promise<Preset[]>;
|
|
179
|
+
/**
|
|
180
|
+
* `PUT ptz/preset {"operation": ..., "id": ..., "name": base64}` — one
|
|
181
|
+
* endpoint, four operations (grammar decoded from the camera's own web
|
|
182
|
+
* bundle and hardware-verified 2026-09-26; see TAIL2-PROTOCOL.md §3).
|
|
183
|
+
* IDs are 0-based (0–2); the tools expose 1-based slots for consistency
|
|
184
|
+
* with the Tiny 2's preset tools.
|
|
185
|
+
*
|
|
186
|
+
* `set` saves the CURRENT live pose — there is NO explicit-pose write
|
|
187
|
+
* anywhere in this API (measured), and `set` on an occupied slot
|
|
188
|
+
* OVERWRITES it (unlike the Tiny 2's create-once slots).
|
|
189
|
+
*/
|
|
190
|
+
private presetOp;
|
|
191
|
+
/**
|
|
192
|
+
* Save the current live pose into slot `id`. The preset LIST lags the
|
|
193
|
+
* write by up to ~1s (measured), so verification is a ladder, and the
|
|
194
|
+
* pose itself can't be verified at all — the camera reports no live pose,
|
|
195
|
+
* so "the slot exists afterwards" is the honest success criterion.
|
|
196
|
+
*/
|
|
197
|
+
presetSave(id: number, name: string, verify?: VerifyOpts): Promise<{
|
|
198
|
+
settled: boolean;
|
|
199
|
+
presets: Preset[];
|
|
200
|
+
}>;
|
|
201
|
+
/**
|
|
202
|
+
* Recall slot `id`. Physical arrival is verifiable through exactly one
|
|
203
|
+
* observable: the preset's zoom ratio landing in the live zoom_ratio (the
|
|
204
|
+
* WS push carries it). The gimbal axes themselves are open-loop.
|
|
205
|
+
*/
|
|
206
|
+
presetRecall(id: number, verify?: VerifyOpts): Promise<{
|
|
207
|
+
settled: boolean;
|
|
208
|
+
zoom: number;
|
|
209
|
+
}>;
|
|
210
|
+
presetDelete(id: number, verify?: VerifyOpts): Promise<{
|
|
211
|
+
settled: boolean;
|
|
212
|
+
presets: Preset[];
|
|
213
|
+
}>;
|
|
214
|
+
presetRename(id: number, name: string, verify?: VerifyOpts): Promise<{
|
|
215
|
+
settled: boolean;
|
|
216
|
+
presets: Preset[];
|
|
217
|
+
}>;
|
|
218
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
// The camera stores preset names as base64 of UTF-8 (decoded from the web
|
|
3
|
+
// bundle's own btoa-encoder, hardware-round-tripped 2026-09-26).
|
|
4
|
+
const b64encode = (s) => Buffer.from(s, "utf8").toString("base64");
|
|
5
|
+
const b64decode = (s) => Buffer.from(s, "base64").toString("utf8");
|
|
6
|
+
export class Tail2HttpError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
path;
|
|
9
|
+
body;
|
|
10
|
+
constructor(status, path, body) {
|
|
11
|
+
super(`Tail 2 API ${path} failed with HTTP ${status}${body ? `: ${body}` : ""}`);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.path = path;
|
|
14
|
+
this.body = body;
|
|
15
|
+
this.name = "Tail2HttpError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
19
|
+
export class Tail2Api {
|
|
20
|
+
baseUrl;
|
|
21
|
+
timeoutMs;
|
|
22
|
+
wsUrl;
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
// Accept a bare host too — the common case is "192.168.0.132".
|
|
25
|
+
this.baseUrl = opts.baseUrl.startsWith("http") ? opts.baseUrl : `http://${opts.baseUrl}`;
|
|
26
|
+
this.timeoutMs = opts.timeoutMs ?? 5000;
|
|
27
|
+
this.wsUrl = `${this.baseUrl.replace(/^http/, "ws")}/ws/`;
|
|
28
|
+
}
|
|
29
|
+
async req(method, path, body) {
|
|
30
|
+
let res;
|
|
31
|
+
try {
|
|
32
|
+
res = await fetch(this.baseUrl + path, {
|
|
33
|
+
method,
|
|
34
|
+
headers: body !== undefined ? { "Content-Type": "application/json" } : undefined,
|
|
35
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
36
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
throw new Error(`Tail 2 at ${this.baseUrl} is unreachable (${e instanceof Error ? e.message : String(e)}). ` +
|
|
41
|
+
`Check the camera is powered and on the network; if it changed IP, re-run obsbot_tail2_scan.`);
|
|
42
|
+
}
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const text = await res.text().catch(() => "");
|
|
45
|
+
throw new Tail2HttpError(res.status, path, text.slice(0, 300));
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return (await res.json());
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
// HTTP-level success but a body that isn't JSON: this is some OTHER
|
|
52
|
+
// device's web server on that address, not a Tail 2. Same actionable
|
|
53
|
+
// shape as unreachable — the fix is a different address, not a retry.
|
|
54
|
+
throw new Error(`Tail 2 at ${this.baseUrl} did not reply with JSON for ${path} ` +
|
|
55
|
+
`(${e instanceof Error ? e.message : String(e)}) — the address is probably another ` +
|
|
56
|
+
`device, not a Tail 2. Re-run obsbot_tail2_scan to find the camera.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async send(method, path, body) {
|
|
60
|
+
const ack = await this.req(method, path, body);
|
|
61
|
+
// Every confirmed endpoint replies {"code":200,"err_idx":0}; a non-200
|
|
62
|
+
// code with HTTP 200 has not been observed, but checking it is free and
|
|
63
|
+
// makes an err_idx≠0 reply loud instead of silent.
|
|
64
|
+
if (typeof ack.code === "number" && ack.code !== 200) {
|
|
65
|
+
throw new Error(`${path} rejected: code=${ack.code} err_idx=${ack.err_idx ?? "?"}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read `read()` until `match()` holds, on a bounded ladder. Returns
|
|
70
|
+
* `settled:false` (NOT a throw) when the ladder runs out — the command was
|
|
71
|
+
* sent and acknowledged, so the caller decides whether that is failure.
|
|
72
|
+
* Mirrors `obsbot_zoom_uvc`'s `settled` contract.
|
|
73
|
+
*/
|
|
74
|
+
async verified(read, match, opts = {}) {
|
|
75
|
+
const attempts = opts.attempts ?? 6;
|
|
76
|
+
const delayMs = opts.delayMs ?? 400;
|
|
77
|
+
let value = await read();
|
|
78
|
+
for (let i = 1; i < attempts && !match(value); i++) {
|
|
79
|
+
await sleep(delayMs);
|
|
80
|
+
value = await read();
|
|
81
|
+
}
|
|
82
|
+
return { settled: match(value), value };
|
|
83
|
+
}
|
|
84
|
+
// ---- reads ---------------------------------------------------------------
|
|
85
|
+
info() {
|
|
86
|
+
return this.req("GET", "/camera/sdk/device_info");
|
|
87
|
+
}
|
|
88
|
+
/** One-shot status: connect, take the first push, close. No persistent socket. */
|
|
89
|
+
status() {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const ws = new WebSocket(this.wsUrl, { handshakeTimeout: this.timeoutMs });
|
|
92
|
+
// One winner across message/error/close/timer. NOT removeAllListeners():
|
|
93
|
+
// the camera keeps pushing its ~1 Hz status while our close handshake is
|
|
94
|
+
// in flight, and a late teardown frame can raise 'error' AFTER we've
|
|
95
|
+
// stripped the handler — an unhandled 'error' on a WebSocket kills the
|
|
96
|
+
// process (measured on live hardware 2026-09-26: "Invalid WebSocket
|
|
97
|
+
// frame: invalid status code 12592"). The settled flag makes stray
|
|
98
|
+
// events no-ops; success tears the socket down with terminate() so no
|
|
99
|
+
// further frames are parsed at all.
|
|
100
|
+
let settled = false;
|
|
101
|
+
const settle = (fn) => {
|
|
102
|
+
if (settled)
|
|
103
|
+
return;
|
|
104
|
+
settled = true;
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
fn();
|
|
107
|
+
};
|
|
108
|
+
const timer = setTimeout(() => settle(() => {
|
|
109
|
+
ws.terminate();
|
|
110
|
+
reject(new Error(`Tail 2 status channel (${this.wsUrl}) timed out. Retry this call.`));
|
|
111
|
+
}), this.timeoutMs + 2000);
|
|
112
|
+
ws.on("error", (e) => settle(() => reject(new Error(`Tail 2 status channel failed: ${e.message}`))));
|
|
113
|
+
ws.on("close", () => settle(() => reject(new Error("status channel closed before a push arrived"))));
|
|
114
|
+
ws.on("message", (data) => {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(data.toString());
|
|
117
|
+
settle(() => {
|
|
118
|
+
ws.terminate();
|
|
119
|
+
resolve(parsed);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
settle(() => reject(e instanceof Error ? e : new Error(String(e))));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
ranges() {
|
|
129
|
+
return this.req("GET", "/camera/sdk/range");
|
|
130
|
+
}
|
|
131
|
+
networkConfig() {
|
|
132
|
+
return this.req("GET", "/camera/sdk/networkconfig");
|
|
133
|
+
}
|
|
134
|
+
imageState() {
|
|
135
|
+
return this.req("GET", "/camera/sdk/image");
|
|
136
|
+
}
|
|
137
|
+
// ---- PTZ -----------------------------------------------------------------
|
|
138
|
+
zoomGet() {
|
|
139
|
+
return this.req("GET", "/camera/sdk/ptz/zoom");
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Absolute zoom ratio on the Tail 2's own 1.0–12.0 scale (`GET range`).
|
|
143
|
+
* `speed` is REQUIRED by the firmware (a ratio-only PUT is a 400) — both
|
|
144
|
+
* fields MEASURED 2026-09-26.
|
|
145
|
+
*/
|
|
146
|
+
async zoomSet(ratio, speed, verify = {}) {
|
|
147
|
+
await this.send("PUT", "/camera/sdk/ptz/zoom", { ratio, speed });
|
|
148
|
+
// The zoom motor ramps; 4.5s of polling covers a full 1x→12x sweep at
|
|
149
|
+
// default speed without hanging the call on a wedged motor.
|
|
150
|
+
const v = await this.verified(() => this.zoomGet(), (r) => Math.abs(r.ratio - ratio) <= 0.01, { attempts: 10, delayMs: 500, ...verify });
|
|
151
|
+
return { settled: v.settled, ratio: v.value.ratio };
|
|
152
|
+
}
|
|
153
|
+
/** Gimbal recenter (`POST ptz/reset`). Open-loop: the Tail 2 reports no live pose. */
|
|
154
|
+
async recenter() {
|
|
155
|
+
await this.send("POST", "/camera/sdk/ptz/reset", {});
|
|
156
|
+
}
|
|
157
|
+
// ---- rotation (no Tiny 2 equivalent) --------------------------------------
|
|
158
|
+
/**
|
|
159
|
+
* Motorized 90° barrel rotation. Verified against the WS push rather than a
|
|
160
|
+
* REST readback (none exists) — and the readback MUST be tolerant of the
|
|
161
|
+
* motor taking ~1.5s, plus the swallowed-write hazard during motion.
|
|
162
|
+
*/
|
|
163
|
+
async portraitSet(enable, verify = {}) {
|
|
164
|
+
await this.send("POST", "/camera/sdk/switch_portrait", { enable });
|
|
165
|
+
const v = await this.verified(() => this.status(), (s) => s.switch_portrait === enable, { attempts: 12, delayMs: 450, ...verify });
|
|
166
|
+
return { settled: v.settled, switch_portrait: v.value.switch_portrait === true };
|
|
167
|
+
}
|
|
168
|
+
rollBiasGet() {
|
|
169
|
+
return this.req("GET", "/camera/sdk/ptz/rollbias");
|
|
170
|
+
}
|
|
171
|
+
/** Roll trim in degrees. MEASURED write + readback 2026-09-26. */
|
|
172
|
+
async rollBiasSet(angle, verify = {}) {
|
|
173
|
+
await this.send("PUT", "/camera/sdk/ptz/rollbias", { angle });
|
|
174
|
+
const v = await this.verified(() => this.rollBiasGet(), (r) => Math.abs(r.angle - angle) <= 0.01, verify);
|
|
175
|
+
return { settled: v.settled, angle: v.value.angle };
|
|
176
|
+
}
|
|
177
|
+
// ---- AI tracking -----------------------------------------------------------
|
|
178
|
+
aiModeGet() {
|
|
179
|
+
return this.req("GET", "/camera/sdk/ai/workmode");
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* `mode` strings are the camera's own enum (bundle-verified, TAIL2-PROTOCOL
|
|
183
|
+
* §3): none | humanTrackingSingleMode | humanTrackingGroupMode |
|
|
184
|
+
* animalTrackingNormal | animalTrackingCloseUp | objectTracking |
|
|
185
|
+
* objectTrackingNormal | objectTrackingCloseUp.
|
|
186
|
+
*/
|
|
187
|
+
async aiModeSet(mode, verify = {}) {
|
|
188
|
+
await this.send("PUT", "/camera/sdk/ai/workmode", { mode });
|
|
189
|
+
const v = await this.verified(() => this.aiModeGet(), (r) => r.mode === mode, verify);
|
|
190
|
+
return { settled: v.settled, mode: v.value.mode };
|
|
191
|
+
}
|
|
192
|
+
trackSpeedGet() {
|
|
193
|
+
return this.req("GET", "/camera/sdk/ai/trackspeed");
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The Tail 2's own six-speed enum (bundle-verified): superLazy | lazy |
|
|
197
|
+
* slow | fast | crazy | customized. NOT the Tiny 2's standard/sport pair.
|
|
198
|
+
*/
|
|
199
|
+
async trackSpeedSet(speed, verify = {}) {
|
|
200
|
+
await this.send("PUT", "/camera/sdk/ai/trackspeed", { speed });
|
|
201
|
+
const v = await this.verified(() => this.trackSpeedGet(), (r) => r.speed === speed, verify);
|
|
202
|
+
return { settled: v.settled, speed: v.value.speed };
|
|
203
|
+
}
|
|
204
|
+
// ---- presets ----------------------------------------------------------------
|
|
205
|
+
/**
|
|
206
|
+
* A saved pose: angles in DEGREES plus zoom ratio — the same units the
|
|
207
|
+
* tools speak. `name` is base64 on the wire; encode/decode lives here so
|
|
208
|
+
* callers never see it raw.
|
|
209
|
+
*/
|
|
210
|
+
async presetsGet() {
|
|
211
|
+
const r = await this.req("GET", "/camera/sdk/ptz/preset");
|
|
212
|
+
return (r.presetList ?? []).map((p) => ({ ...p, name: b64decode(p.name) }));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* `PUT ptz/preset {"operation": ..., "id": ..., "name": base64}` — one
|
|
216
|
+
* endpoint, four operations (grammar decoded from the camera's own web
|
|
217
|
+
* bundle and hardware-verified 2026-09-26; see TAIL2-PROTOCOL.md §3).
|
|
218
|
+
* IDs are 0-based (0–2); the tools expose 1-based slots for consistency
|
|
219
|
+
* with the Tiny 2's preset tools.
|
|
220
|
+
*
|
|
221
|
+
* `set` saves the CURRENT live pose — there is NO explicit-pose write
|
|
222
|
+
* anywhere in this API (measured), and `set` on an occupied slot
|
|
223
|
+
* OVERWRITES it (unlike the Tiny 2's create-once slots).
|
|
224
|
+
*/
|
|
225
|
+
async presetOp(operation, id, name) {
|
|
226
|
+
await this.send("PUT", "/camera/sdk/ptz/preset", {
|
|
227
|
+
operation,
|
|
228
|
+
id,
|
|
229
|
+
...(name !== undefined ? { name: b64encode(name) } : {}),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Save the current live pose into slot `id`. The preset LIST lags the
|
|
234
|
+
* write by up to ~1s (measured), so verification is a ladder, and the
|
|
235
|
+
* pose itself can't be verified at all — the camera reports no live pose,
|
|
236
|
+
* so "the slot exists afterwards" is the honest success criterion.
|
|
237
|
+
*/
|
|
238
|
+
async presetSave(id, name, verify = {}) {
|
|
239
|
+
await this.presetOp("set", id, name);
|
|
240
|
+
const v = await this.verified(() => this.presetsGet(), (list) => list.some((p) => p.id === id), { attempts: 6, delayMs: 350, ...verify });
|
|
241
|
+
return { settled: v.settled, presets: v.value };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Recall slot `id`. Physical arrival is verifiable through exactly one
|
|
245
|
+
* observable: the preset's zoom ratio landing in the live zoom_ratio (the
|
|
246
|
+
* WS push carries it). The gimbal axes themselves are open-loop.
|
|
247
|
+
*/
|
|
248
|
+
async presetRecall(id, verify = {}) {
|
|
249
|
+
const target = (await this.presetsGet()).find((p) => p.id === id);
|
|
250
|
+
if (!target)
|
|
251
|
+
throw new Error(`preset slot ${id} is empty — nothing to recall`);
|
|
252
|
+
await this.presetOp("call", id);
|
|
253
|
+
const v = await this.verified(() => this.zoomGet(), (r) => Math.abs(r.ratio - target.ratio) <= 0.05, { attempts: 14, delayMs: 450, ...verify });
|
|
254
|
+
return { settled: v.settled, zoom: v.value.ratio };
|
|
255
|
+
}
|
|
256
|
+
async presetDelete(id, verify = {}) {
|
|
257
|
+
await this.presetOp("delete", id);
|
|
258
|
+
const v = await this.verified(() => this.presetsGet(), (list) => !list.some((p) => p.id === id), { attempts: 6, delayMs: 350, ...verify });
|
|
259
|
+
return { settled: v.settled, presets: v.value };
|
|
260
|
+
}
|
|
261
|
+
async presetRename(id, name, verify = {}) {
|
|
262
|
+
await this.presetOp("rename", id, name);
|
|
263
|
+
const v = await this.verified(() => this.presetsGet(), (list) => (list.find((p) => p.id === id)?.name ?? null) === name, { attempts: 6, delayMs: 350, ...verify });
|
|
264
|
+
return { settled: v.settled, presets: v.value };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/tail2/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passive mDNS discovery for the OBSBOT Tail 2.
|
|
3
|
+
*
|
|
4
|
+
* The Tail 2 has no mDNS query responder — it ANNOUNCES. Every few seconds it
|
|
5
|
+
* multicasts a response-framed packet to 224.0.0.251:5353 (IPv4, and the same
|
|
6
|
+
* digest over IPv6 link-local) carrying, in the TXT record of its
|
|
7
|
+
* `_remo_mdns._tcp.local` service, a full device digest: both MACs, both IPs,
|
|
8
|
+
* the hex-encoded device name, battery and sleep flags. Measured 2026-09-30 by
|
|
9
|
+
* capturing OBSBOT Center's discovery: Center sent no queries either — the
|
|
10
|
+
* announcements alone are how everything on the LAN finds the camera.
|
|
11
|
+
* TAIL2-PROTOCOL.md §2a.
|
|
12
|
+
*
|
|
13
|
+
* So discovery here is a listen, not a sweep: bind 5353 with SO_REUSEADDR
|
|
14
|
+
* (the OS's own responder holds the port on every platform; multicast
|
|
15
|
+
* delivery to multiple REUSEADDR sockets is uniform across Windows, Linux
|
|
16
|
+
* and macOS — it is how NDI tools coexist), join the group on every
|
|
17
|
+
* non-internal IPv4 interface, and collect Tail 2-shaped announcements.
|
|
18
|
+
* TAIL2-PROTOCOL.md §7a.
|
|
19
|
+
*/
|
|
20
|
+
/** What one camera's announcement tells us — identity straight from the camera. */
|
|
21
|
+
export interface Tail2Announcement {
|
|
22
|
+
mac: string;
|
|
23
|
+
name: string;
|
|
24
|
+
/** Wired first, then wireless, then the address the packet arrived from. */
|
|
25
|
+
hosts: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Extract a Tail 2 announcement from one mDNS packet, or null for anything
|
|
29
|
+
* else on the wire. Detection is by the digest's own shape — TXT strings that
|
|
30
|
+
* JSON-parse into an object carrying a `ble_mac` (the registry's identity
|
|
31
|
+
* MAC) plus a reachable IP — so other devices' mDNS traffic parses to nothing
|
|
32
|
+
* and registers nothing. `srcIp` is where the packet came from; it becomes a
|
|
33
|
+
* host of last resort and a live-reachability hint.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseTail2Announcement(buf: Buffer, srcIp?: string): Tail2Announcement | null;
|
|
36
|
+
/** Accumulates announcements across packets, keyed by MAC. Testable without
|
|
37
|
+
* sockets: push() buffers, results() the deduplicated list. */
|
|
38
|
+
export declare function createAnnouncementCollector(): {
|
|
39
|
+
push(buf: Buffer, srcIp?: string): void;
|
|
40
|
+
results(): Tail2Announcement[];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Listen for Tail 2 announcements for `ms` milliseconds (the camera repeats
|
|
44
|
+
* every few seconds, so 5s is ample) and return what was heard. Never throws:
|
|
45
|
+
* a bind failure or a network without multicast yields an empty list, and the
|
|
46
|
+
* caller falls back to the HTTP sweep.
|
|
47
|
+
*/
|
|
48
|
+
export declare function listenForTail2Announcements(ms?: number): Promise<Tail2Announcement[]>;
|