obsbot-mcp 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +197 -38
  2. package/dist/codec/commands.d.ts +42 -4
  3. package/dist/codec/commands.js +72 -10
  4. package/dist/codec/commands.js.map +1 -1
  5. package/dist/codec/frame.d.ts +1 -0
  6. package/dist/codec/frame.js +1 -1
  7. package/dist/codec/frame.js.map +1 -1
  8. package/dist/codec/preset.d.ts +40 -0
  9. package/dist/codec/preset.js +198 -0
  10. package/dist/codec/preset.js.map +1 -0
  11. package/dist/codec/types.d.ts +4 -0
  12. package/dist/device/helper-factory.d.ts +25 -0
  13. package/dist/device/helper-factory.js +40 -0
  14. package/dist/device/helper-factory.js.map +1 -0
  15. package/dist/device/manager.d.ts +326 -2
  16. package/dist/device/manager.js +720 -24
  17. package/dist/device/manager.js.map +1 -1
  18. package/dist/ipc/client.d.ts +19 -0
  19. package/dist/ipc/client.js +81 -0
  20. package/dist/ipc/client.js.map +1 -0
  21. package/dist/ipc/coordinator.d.ts +29 -0
  22. package/dist/ipc/coordinator.js +94 -0
  23. package/dist/ipc/coordinator.js.map +1 -0
  24. package/dist/ipc/owner.d.ts +21 -0
  25. package/dist/ipc/owner.js +59 -0
  26. package/dist/ipc/owner.js.map +1 -0
  27. package/dist/ipc/protocol.d.ts +21 -0
  28. package/dist/ipc/protocol.js +56 -0
  29. package/dist/ipc/protocol.js.map +1 -0
  30. package/dist/ipc/rendezvous.d.ts +26 -0
  31. package/dist/ipc/rendezvous.js +93 -0
  32. package/dist/ipc/rendezvous.js.map +1 -0
  33. package/dist/mcp/log-sink.d.ts +12 -0
  34. package/dist/mcp/log-sink.js +27 -0
  35. package/dist/mcp/log-sink.js.map +1 -0
  36. package/dist/mcp/ready.d.ts +16 -4
  37. package/dist/mcp/ready.js +11 -8
  38. package/dist/mcp/ready.js.map +1 -1
  39. package/dist/mcp/render.js +1 -1
  40. package/dist/mcp/render.js.map +1 -1
  41. package/dist/mcp/server.js +68 -18
  42. package/dist/mcp/server.js.map +1 -1
  43. package/dist/mcp/tools.d.ts +5 -3
  44. package/dist/mcp/tools.js +787 -186
  45. package/dist/mcp/tools.js.map +1 -1
  46. package/dist/transport/helper-process.d.ts +36 -0
  47. package/dist/transport/helper-process.js +190 -10
  48. package/dist/transport/helper-process.js.map +1 -1
  49. package/dist/transport/linux.d.ts +40 -0
  50. package/dist/transport/linux.js +92 -2
  51. package/dist/transport/linux.js.map +1 -1
  52. package/dist/transport/macos.d.ts +13 -0
  53. package/dist/transport/macos.js +59 -3
  54. package/dist/transport/macos.js.map +1 -1
  55. package/dist/transport/read-serial.d.ts +26 -0
  56. package/dist/transport/read-serial.js +87 -0
  57. package/dist/transport/read-serial.js.map +1 -0
  58. package/dist/transport/transport.d.ts +24 -0
  59. package/dist/transport/windows.d.ts +4 -0
  60. package/dist/transport/windows.js +31 -4
  61. package/dist/transport/windows.js.map +1 -1
  62. package/native/prebuilt/darwin-arm64/obsbot-helper +0 -0
  63. package/native/prebuilt/darwin-x64/obsbot-helper +0 -0
  64. package/native/prebuilt/linux-x64/obsbot-helper +0 -0
  65. package/native/prebuilt/win32-x64/obsbot-helper.exe +0 -0
  66. package/package.json +3 -2
  67. package/dist/device/session.d.ts +0 -22
  68. package/dist/device/session.js +0 -37
  69. package/dist/device/session.js.map +0 -1
@@ -1,13 +1,41 @@
1
+ import { encodePtzMoveSpeed } from "../codec/commands.js";
2
+ import { readSerialVia } from "./read-serial.js";
1
3
  const VENDOR_XU_SELECTOR = 0x02;
4
+ // Unproven per-command reply path — reads back zeros. See the WindowsTransport
5
+ // comment for the 2026-07-19 hardware sweep: sel 6 returns the status block (not
6
+ // a reply), and preset read-back lives on flat selectors 12/13 instead.
2
7
  const RESPONSE_SELECTOR = 0x02;
3
8
  const DEFAULT_REPLY_LEN = 60;
4
9
  const STATUS_SELECTOR = 0x06;
5
10
  const STATUS_BLOCK_LEN = 60;
11
+ // V4L2_CID_PAN_ABSOLUTE/TILT_ABSOLUTE map directly to the UVC CT_PANTILT_ABSOLUTE
12
+ // control (Camera Terminal, selector 0x0D), whose unit is arc-seconds per both the
13
+ // UVC and V4L2 specs — confirmed on hardware: pan range ±468000 / tilt ±324000,
14
+ // step 3600, i.e. exactly ±130°/±90° at 3600 units per degree. A prior version of
15
+ // this file divided by 1000 (mislabeled as "millidegrees"), which was wrong by a
16
+ // factor of 3.6x on both the read and write side — self-consistently wrong, since
17
+ // gimbalSet and camCtrlGet shared the same bad divisor, so a move-then-read check
18
+ // always reported 0 error while the true physical angle was ~28% of what was asked
19
+ // for.
20
+ const ARCSEC_PER_DEG = 3600;
6
21
  /**
7
22
  * Linux V4L2 transport — functionally identical to {@link WindowsTransport}
8
23
  * because both delegate to the native helper process over the same JSON-RPC
9
24
  * stdio protocol. The selector constants are camera-side constants (the UVC
10
25
  * Extension Unit), not OS constants, so they are shared.
26
+ *
27
+ * Key difference: gimbal absolute movement uses V4L2 pan_absolute/tilt_absolute
28
+ * (hardware-verified to physically move the gimbal, repeatedly, 2026-07-21),
29
+ * NOT vendor V3 frames. camCtrlGet for pan/tilt reads back the same V4L2
30
+ * controls — this is the last-*commanded* value, not a live in-flight
31
+ * reading: VIDIOC_QUERY_EXT_CTRL reports these controls as non-volatile on
32
+ * this kernel, so V4L2 core serves its own cache rather than re-querying the
33
+ * device. Getting a genuinely live reading requires a raw USB read with the
34
+ * kernel driver briefly detached, which conflicts with any concurrent video
35
+ * capture (preview/recording) and was deliberately dropped from the shipped
36
+ * transport for that reason — see README's Linux limitations section. A
37
+ * kernel patch marking these controls volatile would fix this at the source;
38
+ * until then, gimbal moves on Linux are open-loop, same as the position read.
11
39
  */
12
40
  export class LinuxTransport {
13
41
  helper;
@@ -44,10 +72,23 @@ export class LinuxTransport {
44
72
  await this.helper.camCtrlSet(property, value, flags);
45
73
  }
46
74
  async camCtrlRange(property) {
47
- return this.helper.camCtrlRange(property);
75
+ const result = await this.helper.camCtrlRange(property);
76
+ // Convert V4L2 arc-seconds → degrees for pan/tilt to match Windows convention
77
+ if (property === 0 || property === 1) {
78
+ result.min = Math.round(result.min / ARCSEC_PER_DEG);
79
+ result.max = Math.round(result.max / ARCSEC_PER_DEG);
80
+ }
81
+ return result;
48
82
  }
49
83
  async camCtrlGet(property) {
50
- return this.helper.camCtrlGet(property);
84
+ const result = await this.helper.camCtrlGet(property);
85
+ // V4L2 pan_absolute/tilt_absolute return arc-seconds, but the rest of
86
+ // the codebase expects degrees (Windows DirectShow convention). This is
87
+ // the last-commanded value, not a live reading — see the class comment.
88
+ if (property === 0 || property === 1) {
89
+ result.value = Math.round(result.value / ARCSEC_PER_DEG);
90
+ }
91
+ return result;
51
92
  }
52
93
  async procAmpSet(property, value, flags) {
53
94
  await this.helper.procAmpSet(property, value, flags);
@@ -55,6 +96,55 @@ export class LinuxTransport {
55
96
  async procAmpRange(property) {
56
97
  return this.helper.procAmpRange(property);
57
98
  }
99
+ /**
100
+ * Move the gimbal using V4L2 pan_absolute/tilt_absolute (arc-seconds).
101
+ * Hardware-verified to physically move the gimbal (2026-07-21, repeated
102
+ * across many absolute targets). Unlike vendor V3 frames, V4L2 writes keep
103
+ * camCtrlGet's echo in sync with what was actually commanded.
104
+ *
105
+ * Sign convention: V4L2 pan_absolute + = camera's left (matches our yaw sign).
106
+ * V4L2 tilt_absolute + = tilt up (opposite of our +pitch = down convention).
107
+ */
108
+ async gimbalSet(yawDeg, pitchDeg, _rollDeg) {
109
+ // V4L2 pan/tilt drives in parallel — both are independent controls.
110
+ await Promise.all([
111
+ this.camCtrlSet(0, Math.round(yawDeg * ARCSEC_PER_DEG), 2),
112
+ this.camCtrlSet(1, Math.round(-pitchDeg * ARCSEC_PER_DEG), 2),
113
+ ]);
114
+ }
115
+ /**
116
+ * Drive the gimbal at a speed for a duration, using vendor-frame velocity
117
+ * protocol (fire-and-forget, no position readback). Not reachable from the
118
+ * Linux tool surface (obsbot_gimbal_move_speed is hidden on this platform):
119
+ * without a live position reading there is no way to confirm a speed burst
120
+ * stays within the gimbal's mechanical range before it gets there, unlike
121
+ * gimbalSet's absolute target, which can be clamped up front regardless of
122
+ * current position. Kept implemented here for ObsbotTransport conformance
123
+ * and for any future internal use once live feedback exists.
124
+ */
125
+ async gimbalSpeed(yaw, pitch, roll, autoStopMs) {
126
+ // Firmware velocity-yaw is inverted relative to position-yaw (same vendor
127
+ // AI_SET_GIM_SPEED opcode as Windows/macOS) — negate so +yaw pans camera-left
128
+ // for both move-speed and move-angle.
129
+ await this.sendVendor(encodePtzMoveSpeed(-yaw, pitch, roll).buildFrame(this.nextSeq()));
130
+ if (autoStopMs > 0) {
131
+ await new Promise((r) => setTimeout(r, autoStopMs));
132
+ await this.sendVendor(encodePtzMoveSpeed(0, 0, 0).buildFrame(this.nextSeq()));
133
+ }
134
+ }
135
+ /**
136
+ * Recenter the gimbal via V4L2 pan_absolute=0, tilt_absolute=0.
137
+ * Hardware-verified to physically recenter the gimbal (2026-07-21).
138
+ */
139
+ async gimbalRecenter() {
140
+ await Promise.all([
141
+ this.camCtrlSet(0, 0, 2),
142
+ this.camCtrlSet(1, 0, 2),
143
+ ]);
144
+ }
145
+ async readSerial() {
146
+ return readSerialVia(this);
147
+ }
58
148
  nextSeq() {
59
149
  this.seq = this.seq >= 0xffff ? 1 : this.seq + 1;
60
150
  return this.seq;
@@ -1 +1 @@
1
- {"version":3,"file":"linux.js","sourceRoot":"","sources":["../../src/transport/linux.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"linux.js","sourceRoot":"","sources":["../../src/transport/linux.ts"],"names":[],"mappings":"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"}
@@ -20,6 +20,15 @@ export declare class MacosTransport implements ObsbotTransport {
20
20
  min: number;
21
21
  max: number;
22
22
  }>;
23
+ /**
24
+ * Read a camera-control property. Pan (0) and tilt (1) come from the standard
25
+ * UVC CT_PANTILT_ABSOLUTE control (selector 0x0D), which this firmware updates
26
+ * live during motion — including motion the host did not command (speed moves,
27
+ * recenter, tracking). Scaled arc-seconds → degrees.
28
+ *
29
+ * Sign convention: UVC pan positive = camera's left (matches our yaw sign);
30
+ * UVC tilt positive = up, which callers negate to get our +pitch = down.
31
+ */
23
32
  camCtrlGet(property: number): Promise<{
24
33
  value: number;
25
34
  flags: number;
@@ -29,6 +38,10 @@ export declare class MacosTransport implements ObsbotTransport {
29
38
  min: number;
30
39
  max: number;
31
40
  }>;
41
+ gimbalSet(yawDeg: number, pitchDeg: number, rollDeg?: number): Promise<void>;
42
+ gimbalSpeed(yaw: number, pitch: number, roll: number, autoStopMs: number): Promise<void>;
43
+ gimbalRecenter(): Promise<void>;
44
+ readSerial(): Promise<string>;
32
45
  nextSeq(): number;
33
46
  close(): Promise<void>;
34
47
  }
@@ -1,10 +1,26 @@
1
+ import { encodeRecenter, encodePtzMoveAngle, encodePtzMoveSpeed } from "../codec/commands.js";
2
+ import { readSerialVia } from "./read-serial.js";
1
3
  // Same XU selector constants as WindowsTransport — they're defined by the
2
4
  // OBSBOT protocol, not the OS:
3
5
  const VENDOR_XU_SELECTOR = 0x02;
6
+ // Unproven per-command reply path — reads back zeros. See the WindowsTransport
7
+ // comment for the 2026-07-19 hardware sweep: sel 6 returns the status block (not
8
+ // a reply), and preset read-back lives on flat selectors 12/13 instead.
4
9
  const RESPONSE_SELECTOR = 0x02;
5
10
  const DEFAULT_REPLY_LEN = 60;
6
11
  const STATUS_SELECTOR = 0x06;
7
12
  const STATUS_BLOCK_LEN = 60;
13
+ // UVC CT_PANTILT_ABSOLUTE (selector 0x0D) is expressed in arc-seconds. The
14
+ // helper returns the raw per-axis value; the rest of the codebase works in
15
+ // degrees (Windows DirectShow convention), so scale here — the same shape as
16
+ // LinuxTransport's millidegree scaling. Hardware-measured range is ±468000
17
+ // asec pan / ±324000 asec tilt, resolution 3600 asec = 1°.
18
+ const ARCSEC_PER_DEG = 3600;
19
+ const CAMCTRL_PAN = 0;
20
+ const CAMCTRL_TILT = 1;
21
+ function isGimbalAxis(property) {
22
+ return property === CAMCTRL_PAN || property === CAMCTRL_TILT;
23
+ }
8
24
  export class MacosTransport {
9
25
  helper;
10
26
  seq = 0;
@@ -37,13 +53,37 @@ export class MacosTransport {
37
53
  return this.helper.snapshot(opts);
38
54
  }
39
55
  async camCtrlSet(property, value, flags) {
56
+ // SET_CUR on CT_PANTILT_ABSOLUTE has never been exercised on this device —
57
+ // absolute moves go through vendor V3 frames instead. Refuse rather than
58
+ // issue an uncharacterized write.
59
+ if (isGimbalAxis(property)) {
60
+ throw new Error("camCtrlSet: pan/tilt writes are not supported on macOS — use gimbalSet (vendor V3 frames)");
61
+ }
40
62
  await this.helper.camCtrlSet(property, value, flags);
41
63
  }
42
64
  async camCtrlRange(property) {
43
- return this.helper.camCtrlRange(property);
44
- }
65
+ const result = await this.helper.camCtrlRange(property);
66
+ if (isGimbalAxis(property)) {
67
+ result.min = Math.round(result.min / ARCSEC_PER_DEG);
68
+ result.max = Math.round(result.max / ARCSEC_PER_DEG);
69
+ }
70
+ return result;
71
+ }
72
+ /**
73
+ * Read a camera-control property. Pan (0) and tilt (1) come from the standard
74
+ * UVC CT_PANTILT_ABSOLUTE control (selector 0x0D), which this firmware updates
75
+ * live during motion — including motion the host did not command (speed moves,
76
+ * recenter, tracking). Scaled arc-seconds → degrees.
77
+ *
78
+ * Sign convention: UVC pan positive = camera's left (matches our yaw sign);
79
+ * UVC tilt positive = up, which callers negate to get our +pitch = down.
80
+ */
45
81
  async camCtrlGet(property) {
46
- return this.helper.camCtrlGet(property);
82
+ const result = await this.helper.camCtrlGet(property);
83
+ if (isGimbalAxis(property)) {
84
+ result.value = Math.round(result.value / ARCSEC_PER_DEG);
85
+ }
86
+ return result;
47
87
  }
48
88
  async procAmpSet(property, value, flags) {
49
89
  await this.helper.procAmpSet(property, value, flags);
@@ -51,6 +91,22 @@ export class MacosTransport {
51
91
  async procAmpRange(property) {
52
92
  return this.helper.procAmpRange(property);
53
93
  }
94
+ async gimbalSet(yawDeg, pitchDeg, rollDeg = 0) {
95
+ await this.sendVendor(encodePtzMoveAngle(yawDeg, pitchDeg, rollDeg).buildFrame(this.nextSeq()));
96
+ }
97
+ async gimbalSpeed(yaw, pitch, roll, autoStopMs) {
98
+ await this.sendVendor(encodePtzMoveSpeed(-yaw, pitch, roll).buildFrame(this.nextSeq()));
99
+ if (autoStopMs > 0) {
100
+ await new Promise((r) => setTimeout(r, autoStopMs));
101
+ await this.sendVendor(encodePtzMoveSpeed(0, 0, 0).buildFrame(this.nextSeq()));
102
+ }
103
+ }
104
+ async gimbalRecenter() {
105
+ await this.sendVendor(encodeRecenter().buildFrame(this.nextSeq()));
106
+ }
107
+ async readSerial() {
108
+ return readSerialVia(this);
109
+ }
54
110
  nextSeq() {
55
111
  this.seq = this.seq >= 0xffff ? 1 : this.seq + 1;
56
112
  return this.seq;
@@ -1 +1 @@
1
- {"version":3,"file":"macos.js","sourceRoot":"","sources":["../../src/transport/macos.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"macos.js","sourceRoot":"","sources":["../../src/transport/macos.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The transport primitives readSerial needs. MacosTransport, LinuxTransport,
3
+ * and WindowsTransport already implement all three as public methods, so
4
+ * each of them satisfies this structurally with no extra glue beyond
5
+ * `readSerial() { return readSerialVia(this); }`.
6
+ */
7
+ export interface VendorReadPrimitives {
8
+ xuRaw(selector: number, data: Buffer): Promise<void>;
9
+ xuGetRaw(selector: number, length: number): Promise<Buffer>;
10
+ nextSeq(): number;
11
+ }
12
+ /**
13
+ * Send a header-only UG_GET_SN GET frame on the vendor XU selector and poll
14
+ * the selector-2 reply mailbox until a valid, matching reply appears.
15
+ *
16
+ * The mailbox retains the PREVIOUS reply until the new one lands (reply
17
+ * latency varies), so a single unvalidated read can return stale data. A
18
+ * reply is trusted only once it: parses cleanly (magic + header CRC +
19
+ * payload CRC via parseFrame), has cmd === UG_GET_SN's wire cmd, and echoes
20
+ * the seq this call just sent. Throws if no such reply shows up within
21
+ * POLL_ATTEMPTS reads.
22
+ *
23
+ * Shared by all three platform transports so this polling/validation logic
24
+ * exists exactly once instead of three near-identical copies.
25
+ */
26
+ export declare function readSerialVia(t: VendorReadPrimitives): Promise<string>;
@@ -0,0 +1,87 @@
1
+ import { encodeVendorGet, decodeSerial } from "../codec/commands.js";
2
+ import { parseFrame } from "../codec/frame.js";
3
+ // Protocol constant (not platform-specific) — the vendor SET_CUR request and
4
+ // the reply mailbox both live on XU selector 2. Kept local to this module
5
+ // rather than imported from a transport file so this stays a standalone unit;
6
+ // each platform transport separately defines the same constant for its own
7
+ // sendVendor/recvVendor use.
8
+ const VENDOR_XU_SELECTOR = 0x02;
9
+ const UG_GET_SN_CMD = 0x18c8;
10
+ const REPLY_LEN = 60;
11
+ const POLL_ATTEMPTS = 8;
12
+ // The device does not populate the reply mailbox instantly — the framed reply
13
+ // lands tens of ms after the request. Polling with NO delay drains all 8
14
+ // attempts in ~7ms, before the reply arrives, and reads only the stale previous
15
+ // frame → spurious "no reply". A short delay before each read gives the reply
16
+ // time to land. Hardware-verified 2026-07-20: 0ms delay fails every rapid read;
17
+ // 30ms is reliable. (Unit tests use a synchronous fake helper and never exercise
18
+ // this latency, which is why the bug shipped past them.)
19
+ const POLL_DELAY_MS = 30;
20
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21
+ /**
22
+ * Send a header-only UG_GET_SN GET frame on the vendor XU selector and poll
23
+ * the selector-2 reply mailbox until a valid, matching reply appears.
24
+ *
25
+ * The mailbox retains the PREVIOUS reply until the new one lands (reply
26
+ * latency varies), so a single unvalidated read can return stale data. A
27
+ * reply is trusted only once it: parses cleanly (magic + header CRC +
28
+ * payload CRC via parseFrame), has cmd === UG_GET_SN's wire cmd, and echoes
29
+ * the seq this call just sent. Throws if no such reply shows up within
30
+ * POLL_ATTEMPTS reads.
31
+ *
32
+ * Shared by all three platform transports so this polling/validation logic
33
+ * exists exactly once instead of three near-identical copies.
34
+ */
35
+ export async function readSerialVia(t) {
36
+ const seq = t.nextSeq();
37
+ const req = encodeVendorGet("UG_GET_SN").buildFrame(seq);
38
+ await t.xuRaw(VENDOR_XU_SELECTOR, req);
39
+ const seen = new Set();
40
+ let last = Buffer.alloc(0);
41
+ for (let i = 0; i < POLL_ATTEMPTS; i++) {
42
+ await sleep(POLL_DELAY_MS); // let the reply land before reading the mailbox
43
+ const raw = await t.xuGetRaw(VENDOR_XU_SELECTOR, REPLY_LEN);
44
+ last = raw;
45
+ seen.add(raw.toString("hex"));
46
+ try {
47
+ const f = parseFrame(raw);
48
+ if (f.cmd === UG_GET_SN_CMD && f.seq === seq && f.payload.length > 0) {
49
+ return decodeSerial(f.payload);
50
+ }
51
+ }
52
+ catch {
53
+ // Not our reply yet (stale mailbox, still in flight, or garbage) —
54
+ // keep polling.
55
+ }
56
+ }
57
+ // Report what the mailbox actually held. "No valid reply" alone cannot
58
+ // distinguish a mute device from one answering somebody else's request, and
59
+ // that ambiguity is why the 3.2s vendor-path outage of 2026-07-21 was never
60
+ // explained: the identifying datum was read eight times and discarded eight
61
+ // times. Reproduced since after a different-port replug, where the mailbox
62
+ // stays mute for the whole poll and answers ~420ms later.
63
+ const churn = seen.size === 1 ? "unchanged" : `${seen.size} distinct`;
64
+ throw new Error(`readSerial: no valid UG_GET_SN reply — ${POLL_ATTEMPTS} reads, ${churn}; ` +
65
+ `mailbox ${describeMailbox(last, req, seq)}`);
66
+ }
67
+ /** One short phrase naming what a non-reply actually was. */
68
+ function describeMailbox(raw, req, wantSeq) {
69
+ // The signature of the 2026-07-21 outage: our own request handed straight
70
+ // back with the magic byte zeroed.
71
+ if (raw.length === req.length &&
72
+ raw[0] === 0x00 &&
73
+ req[0] === 0xaa &&
74
+ raw.subarray(1).equals(req.subarray(1))) {
75
+ return "was our own request echoed back with the magic byte zeroed";
76
+ }
77
+ try {
78
+ const f = parseFrame(raw);
79
+ const empty = f.payload.length === 0 ? ", empty payload" : "";
80
+ return `held cmd 0x${f.cmd.toString(16)} seq ${f.seq}${empty} ` +
81
+ `(wanted cmd 0x${UG_GET_SN_CMD.toString(16)} seq ${wantSeq})`;
82
+ }
83
+ catch (e) {
84
+ return `unparseable: ${e.message}; first bytes ${raw.subarray(0, 8).toString("hex")}`;
85
+ }
86
+ }
87
+ //# sourceMappingURL=read-serial.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-serial.js","sourceRoot":"","sources":["../../src/transport/read-serial.ts"],"names":[],"mappings":"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"}
@@ -57,6 +57,30 @@ export interface ObsbotTransport {
57
57
  min: number;
58
58
  max: number;
59
59
  }>;
60
+ /**
61
+ * Move the gimbal to an absolute yaw/pitch angle in degrees. Positive yaw =
62
+ * camera's left; positive pitch = tilt down.
63
+ * Platform note: Linux uses V4L2 pan_absolute/tilt_absolute (proven to move
64
+ * the physical gimbal and keep V4L2 readback working). Windows/macOS use
65
+ * vendor V3 frames (AI_SET_GIM_MOTOR_DEG).
66
+ */
67
+ gimbalSet(yawDeg: number, pitchDeg: number, rollDeg?: number): Promise<void>;
68
+ /**
69
+ * Drive the gimbal at a yaw/pitch speed (platform-specific units), then
70
+ * automatically stop after autoStopMs. Positive yaw = camera's left.
71
+ * Platform note: Linux approximates by converting speed × duration to an
72
+ * absolute V4L2 pan_absolute/tilt_absolute move. Windows/macOS use vendor
73
+ * V3 frames (AI_SET_GIM_SPEED).
74
+ */
75
+ gimbalSpeed(yaw: number, pitch: number, roll: number, autoStopMs: number): Promise<void>;
76
+ /** Recenter the gimbal to yaw=0, pitch=0. */
77
+ gimbalRecenter(): Promise<void>;
78
+ /**
79
+ * Read the camera's 14-char ASCII serial number (UG_GET_SN): sends a
80
+ * header-only vendor GET on the vendor XU selector and polls the reply
81
+ * mailbox for a valid, matching reply. Throws if no valid reply arrives.
82
+ */
83
+ readSerial(): Promise<string>;
60
84
  nextSeq(): number;
61
85
  close(): Promise<void>;
62
86
  }
@@ -29,6 +29,10 @@ export declare class WindowsTransport implements ObsbotTransport {
29
29
  min: number;
30
30
  max: number;
31
31
  }>;
32
+ gimbalSet(yawDeg: number, pitchDeg: number, rollDeg?: number): Promise<void>;
33
+ gimbalSpeed(yaw: number, pitch: number, roll: number, autoStopMs: number): Promise<void>;
34
+ gimbalRecenter(): Promise<void>;
35
+ readSerial(): Promise<string>;
32
36
  nextSeq(): number;
33
37
  close(): Promise<void>;
34
38
  }
@@ -1,9 +1,18 @@
1
+ import { encodeRecenter, encodePtzMoveAngle, encodePtzMoveSpeed } from "../codec/commands.js";
2
+ import { readSerialVia } from "./read-serial.js";
1
3
  const VENDOR_XU_SELECTOR = 0x02;
2
4
  // Intended read-back selector for the (future) per-command V3-frame Get path
3
- // used by recvVendor. NOT YET verified on hardware — the per-command reply path
4
- // is unproven scaffolding; Slice 1 shipped the status-block read instead. A
5
- // separate constant from VENDOR_XU_SELECTOR because it may need to diverge once
6
- // that path is validated. Change here only.
5
+ // used by recvVendor. This path is UNPROVEN and reads back all zeros.
6
+ //
7
+ // Hardware sweep 2026-07-19 (Tiny 2, camera awake) — do not re-chase this:
8
+ // - sel 0x02 after a vendor SET: all zeros (the long-standing symptom).
9
+ // - sel 0x06: returns the flat STATUS BLOCK verbatim (starts 0x25, fails
10
+ // parseFrame "bad magic") — it is NOT a per-command reply. Pointing
11
+ // recvVendor at 6 was tried and reverted; it only masked the problem.
12
+ // - sels 1,3,5,9,11,15,16: zeros. The `aa 29` reply frames visible in USB
13
+ // captures are not reachable through this xu_get surface at all.
14
+ // Preset/state read-back instead lives on dedicated FLAT selectors (12 = list,
15
+ // 13 = entry cursor), like STATUS_SELECTOR below — not via framed replies.
7
16
  const RESPONSE_SELECTOR = 0x02;
8
17
  const DEFAULT_REPLY_LEN = 60;
9
18
  // The camera exposes a flat status block on this XU selector; GET_CUR reads it
@@ -57,6 +66,24 @@ export class WindowsTransport {
57
66
  async procAmpRange(property) {
58
67
  return this.helper.procAmpRange(property);
59
68
  }
69
+ async gimbalSet(yawDeg, pitchDeg, rollDeg = 0) {
70
+ await this.sendVendor(encodePtzMoveAngle(yawDeg, pitchDeg, rollDeg).buildFrame(this.nextSeq()));
71
+ }
72
+ async gimbalSpeed(yaw, pitch, roll, autoStopMs) {
73
+ // Firmware velocity-yaw is inverted relative to position-yaw — negate yaw so
74
+ // the contract is consistent: +yaw pans to camera's left.
75
+ await this.sendVendor(encodePtzMoveSpeed(-yaw, pitch, roll).buildFrame(this.nextSeq()));
76
+ if (autoStopMs > 0) {
77
+ await new Promise((r) => setTimeout(r, autoStopMs));
78
+ await this.sendVendor(encodePtzMoveSpeed(0, 0, 0).buildFrame(this.nextSeq()));
79
+ }
80
+ }
81
+ async gimbalRecenter() {
82
+ await this.sendVendor(encodeRecenter().buildFrame(this.nextSeq()));
83
+ }
84
+ async readSerial() {
85
+ return readSerialVia(this);
86
+ }
60
87
  nextSeq() {
61
88
  this.seq = this.seq >= 0xffff ? 1 : this.seq + 1;
62
89
  return this.seq;
@@ -1 +1 @@
1
- {"version":3,"file":"windows.js","sourceRoot":"","sources":["../../src/transport/windows.ts"],"names":[],"mappings":"AAGA,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAChC,6EAA6E;AAC7E,gFAAgF;AAChF,4EAA4E;AAC5E,gFAAgF;AAChF,4CAA4C;AAC5C,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAC/B,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,+EAA+E;AAC/E,2EAA2E;AAC3E,+EAA+E;AAC/E,MAAM,eAAe,GAAG,IAAI,CAAC;AAC7B,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAE5B,MAAM,OAAO,gBAAgB;IAGP;IAFZ,GAAG,GAAG,CAAC,CAAC;IAEhB,YAAoB,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE7C,KAAK,CAAC,UAAU,CAAC,KAAa;QAC5B,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,KAAa,EAAE,MAAM,GAAG,iBAAiB;QACxD,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,MAAM,GAAG,gBAAgB;QACxC,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,QAAgB,EAAE,IAAY;QACxC,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,QAAgB,EAAE,MAAc;QAC7C,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,SAAS;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,KAAa;QACzB,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,IAAkB;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAAgB,EAAE,KAAa,EAAE,KAAa;QAC7D,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,QAAgB;QACjC,OAAO,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAAgB;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAAgB,EAAE,KAAa,EAAE,KAAa;QAC7D,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,QAAgB;QACjC,OAAO,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC5C,CAAC;IAED,OAAO;QACL,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;QACjD,OAAO,IAAI,CAAC,GAAG,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAC5B,CAAC;CACF"}
1
+ {"version":3,"file":"windows.js","sourceRoot":"","sources":["../../src/transport/windows.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obsbot-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Cross-platform MCP server for OBSBOT Tiny 2 (UVC) camera control",
5
5
  "license": "MIT",
6
6
  "author": "Michael Jordan",
@@ -35,7 +35,8 @@
35
35
  "scripts": {
36
36
  "build": "tsc -p tsconfig.json",
37
37
  "test": "vitest run",
38
- "test:watch": "vitest"
38
+ "test:watch": "vitest",
39
+ "integration": "node scripts/integration/run.mjs"
39
40
  },
40
41
  "dependencies": {
41
42
  "@modelcontextprotocol/sdk": "^1.0.0",
@@ -1,22 +0,0 @@
1
- import { DeviceManager } from "./manager.js";
2
- import { ObsbotTransport } from "../transport/transport.js";
3
- /**
4
- * Owns the device connection lifecycle: lazily opens the first OBSBOT, caches
5
- * the transport, and supports invalidation + re-open for self-healing across a
6
- * mid-session disconnect. A re-open after a prior open is flagged as a
7
- * "reconnect" so callers can surface `reconnected: true` and re-read state
8
- * (the device power-cycles on unplug — prior assumptions are void).
9
- */
10
- export declare class DeviceSession {
11
- private mgr;
12
- private transport?;
13
- private opened;
14
- private reconnected;
15
- constructor(mgr: DeviceManager);
16
- /** Return the cached transport, opening (or re-opening) the device if needed. */
17
- get(): Promise<ObsbotTransport>;
18
- /** Drop the cached transport (e.g. after a device error) so the next get() re-opens. */
19
- invalidate(): void;
20
- /** True once if the transport was re-opened after an invalidate; clears on read. */
21
- takeReconnected(): boolean;
22
- }
@@ -1,37 +0,0 @@
1
- /**
2
- * Owns the device connection lifecycle: lazily opens the first OBSBOT, caches
3
- * the transport, and supports invalidation + re-open for self-healing across a
4
- * mid-session disconnect. A re-open after a prior open is flagged as a
5
- * "reconnect" so callers can surface `reconnected: true` and re-read state
6
- * (the device power-cycles on unplug — prior assumptions are void).
7
- */
8
- export class DeviceSession {
9
- mgr;
10
- transport;
11
- opened = false;
12
- reconnected = false;
13
- constructor(mgr) {
14
- this.mgr = mgr;
15
- }
16
- /** Return the cached transport, opening (or re-opening) the device if needed. */
17
- async get() {
18
- if (!this.transport) {
19
- this.transport = await this.mgr.openFirstObsbot();
20
- if (this.opened)
21
- this.reconnected = true; // this is a RE-open, not the first
22
- this.opened = true;
23
- }
24
- return this.transport;
25
- }
26
- /** Drop the cached transport (e.g. after a device error) so the next get() re-opens. */
27
- invalidate() {
28
- this.transport = undefined;
29
- }
30
- /** True once if the transport was re-opened after an invalidate; clears on read. */
31
- takeReconnected() {
32
- const r = this.reconnected;
33
- this.reconnected = false;
34
- return r;
35
- }
36
- }
37
- //# sourceMappingURL=session.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/device/session.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,OAAO,aAAa;IAKJ;IAJZ,SAAS,CAAmB;IAC5B,MAAM,GAAG,KAAK,CAAC;IACf,WAAW,GAAG,KAAK,CAAC;IAE5B,YAAoB,GAAkB;QAAlB,QAAG,GAAH,GAAG,CAAe;IAAG,CAAC;IAE1C,iFAAiF;IACjF,KAAK,CAAC,GAAG;QACP,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,IAAI,CAAC,SAAS,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC;YAClD,IAAI,IAAI,CAAC,MAAM;gBAAE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,mCAAmC;YAC7E,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED,wFAAwF;IACxF,UAAU;QACR,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED,oFAAoF;IACpF,eAAe;QACb,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC;QAC3B,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;QACzB,OAAO,CAAC,CAAC;IACX,CAAC;CACF"}