obsbot-mcp 0.6.3 → 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.
Files changed (71) hide show
  1. package/README.md +97 -20
  2. package/dist/build-info.json +5 -0
  3. package/dist/device/manager.d.ts +8 -0
  4. package/dist/device/manager.js +37 -4
  5. package/dist/device/manager.js.map +1 -1
  6. package/dist/geometry/aim.d.ts +24 -13
  7. package/dist/geometry/aim.js +26 -13
  8. package/dist/geometry/aim.js.map +1 -1
  9. package/dist/ipc/build-id.d.ts +25 -0
  10. package/dist/ipc/build-id.js +60 -0
  11. package/dist/ipc/build-id.js.map +1 -0
  12. package/dist/ipc/client.d.ts +36 -0
  13. package/dist/ipc/client.js +106 -4
  14. package/dist/ipc/client.js.map +1 -1
  15. package/dist/ipc/coordinator.d.ts +91 -12
  16. package/dist/ipc/coordinator.js +293 -57
  17. package/dist/ipc/coordinator.js.map +1 -1
  18. package/dist/ipc/gate.d.ts +14 -0
  19. package/dist/ipc/gate.js +45 -0
  20. package/dist/ipc/gate.js.map +1 -0
  21. package/dist/ipc/owner.d.ts +27 -1
  22. package/dist/ipc/owner.js +67 -4
  23. package/dist/ipc/owner.js.map +1 -1
  24. package/dist/ipc/protocol.d.ts +30 -0
  25. package/dist/ipc/protocol.js +40 -0
  26. package/dist/ipc/protocol.js.map +1 -1
  27. package/dist/ipc/rendezvous.d.ts +37 -11
  28. package/dist/ipc/rendezvous.js +143 -30
  29. package/dist/ipc/rendezvous.js.map +1 -1
  30. package/dist/mcp/server.js +40 -12
  31. package/dist/mcp/server.js.map +1 -1
  32. package/dist/mcp/tools.d.ts +6 -0
  33. package/dist/mcp/tools.js +123 -17
  34. package/dist/mcp/tools.js.map +1 -1
  35. package/dist/tail2/api.d.ts +218 -0
  36. package/dist/tail2/api.js +267 -0
  37. package/dist/tail2/api.js.map +1 -0
  38. package/dist/tail2/mdns.d.ts +48 -0
  39. package/dist/tail2/mdns.js +216 -0
  40. package/dist/tail2/mdns.js.map +1 -0
  41. package/dist/tail2/registry.d.ts +100 -0
  42. package/dist/tail2/registry.js +200 -0
  43. package/dist/tail2/registry.js.map +1 -0
  44. package/dist/tail2/snapshot.d.ts +54 -0
  45. package/dist/tail2/snapshot.js +157 -0
  46. package/dist/tail2/snapshot.js.map +1 -0
  47. package/dist/tail2/tools.d.ts +7 -0
  48. package/dist/tail2/tools.js +370 -0
  49. package/dist/tail2/tools.js.map +1 -0
  50. package/dist/transport/helper-process.d.ts +11 -0
  51. package/dist/transport/helper-process.js +10 -0
  52. package/dist/transport/helper-process.js.map +1 -1
  53. package/dist/transport/linux.d.ts +59 -9
  54. package/dist/transport/linux.js +163 -21
  55. package/dist/transport/linux.js.map +1 -1
  56. package/dist/transport/macos.d.ts +8 -0
  57. package/dist/transport/macos.js +11 -0
  58. package/dist/transport/macos.js.map +1 -1
  59. package/dist/transport/transport.d.ts +22 -0
  60. package/dist/transport/transport.js +20 -0
  61. package/dist/transport/transport.js.map +1 -1
  62. package/dist/transport/windows.d.ts +6 -0
  63. package/dist/transport/windows.js +9 -0
  64. package/dist/transport/windows.js.map +1 -1
  65. package/dist/version.d.ts +1 -1
  66. package/dist/version.js +1 -1
  67. package/native/prebuilt/darwin-arm64/obsbot-helper +0 -0
  68. package/native/prebuilt/darwin-x64/obsbot-helper +0 -0
  69. package/native/prebuilt/linux-x64/obsbot-helper +0 -0
  70. package/native/prebuilt/win32-x64/obsbot-helper.exe +0 -0
  71. 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[]>;