@pollen-robotics/reachy-mini-sdk 1.10.0-dev.0.main.b50d97e → 1.10.0-dev.0.main.c8367cd

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 (128) hide show
  1. package/APP_CREATION_GUIDE.md +98 -0
  2. package/dist/lib/central-listener.d.ts +71 -0
  3. package/dist/lib/central-listener.d.ts.map +1 -0
  4. package/dist/lib/central-listener.js +277 -0
  5. package/dist/lib/central-listener.js.map +1 -0
  6. package/dist/lib/central-listener.test.d.ts +2 -0
  7. package/dist/lib/central-listener.test.d.ts.map +1 -0
  8. package/dist/lib/central-listener.test.js +378 -0
  9. package/dist/lib/central-listener.test.js.map +1 -0
  10. package/dist/lib/dc-watchdog.test.d.ts +2 -0
  11. package/dist/lib/dc-watchdog.test.d.ts.map +1 -0
  12. package/dist/lib/dc-watchdog.test.js +154 -0
  13. package/dist/lib/dc-watchdog.test.js.map +1 -0
  14. package/dist/lib/ensure-awake.test.d.ts +2 -0
  15. package/dist/lib/ensure-awake.test.d.ts.map +1 -0
  16. package/dist/lib/ensure-awake.test.js +117 -0
  17. package/dist/lib/ensure-awake.test.js.map +1 -0
  18. package/dist/lib/ice-grace.test.d.ts +2 -0
  19. package/dist/lib/ice-grace.test.d.ts.map +1 -0
  20. package/dist/lib/ice-grace.test.js +211 -0
  21. package/dist/lib/ice-grace.test.js.map +1 -0
  22. package/dist/lib/imu.test.d.ts +2 -0
  23. package/dist/lib/imu.test.d.ts.map +1 -0
  24. package/dist/lib/imu.test.js +78 -0
  25. package/dist/lib/imu.test.js.map +1 -0
  26. package/dist/lib/logger.d.ts +38 -0
  27. package/dist/lib/logger.d.ts.map +1 -0
  28. package/dist/lib/logger.js +81 -0
  29. package/dist/lib/logger.js.map +1 -0
  30. package/dist/lib/pending-replies.d.ts +127 -0
  31. package/dist/lib/pending-replies.d.ts.map +1 -0
  32. package/dist/lib/pending-replies.js +255 -0
  33. package/dist/lib/pending-replies.js.map +1 -0
  34. package/dist/lib/pending-replies.test.d.ts +2 -0
  35. package/dist/lib/pending-replies.test.d.ts.map +1 -0
  36. package/dist/lib/pending-replies.test.js +217 -0
  37. package/dist/lib/pending-replies.test.js.map +1 -0
  38. package/dist/lib/reachy-mini.d.ts +202 -88
  39. package/dist/lib/reachy-mini.d.ts.map +1 -1
  40. package/dist/lib/reachy-mini.js +651 -566
  41. package/dist/lib/reachy-mini.js.map +1 -1
  42. package/dist/lib/redial.test.d.ts +2 -0
  43. package/dist/lib/redial.test.d.ts.map +1 -0
  44. package/dist/lib/redial.test.js +206 -0
  45. package/dist/lib/redial.test.js.map +1 -0
  46. package/dist/lib/request.test.d.ts +2 -0
  47. package/dist/lib/request.test.d.ts.map +1 -0
  48. package/dist/lib/request.test.js +90 -0
  49. package/dist/lib/request.test.js.map +1 -0
  50. package/dist/lib/session-supervisor.d.ts +201 -0
  51. package/dist/lib/session-supervisor.d.ts.map +1 -0
  52. package/dist/lib/session-supervisor.js +520 -0
  53. package/dist/lib/session-supervisor.js.map +1 -0
  54. package/dist/lib/token-store.d.ts +50 -0
  55. package/dist/lib/token-store.d.ts.map +1 -0
  56. package/dist/lib/token-store.js +120 -0
  57. package/dist/lib/token-store.js.map +1 -0
  58. package/dist/lib/token-store.test.d.ts +2 -0
  59. package/dist/lib/token-store.test.d.ts.map +1 -0
  60. package/dist/lib/token-store.test.js +82 -0
  61. package/dist/lib/token-store.test.js.map +1 -0
  62. package/dist/lib/types.d.ts +280 -6
  63. package/dist/lib/types.d.ts.map +1 -1
  64. package/dist/lib/url-helpers.d.ts.map +1 -1
  65. package/dist/lib/url-helpers.js +3 -1
  66. package/dist/lib/url-helpers.js.map +1 -1
  67. package/dist/lib/version.d.ts +2 -0
  68. package/dist/lib/version.d.ts.map +1 -0
  69. package/dist/lib/version.js +5 -0
  70. package/dist/lib/version.js.map +1 -0
  71. package/dist/reachy-mini-sdk.d.ts +15 -1
  72. package/dist/reachy-mini-sdk.d.ts.map +1 -1
  73. package/dist/reachy-mini-sdk.js +12 -0
  74. package/dist/reachy-mini-sdk.js.map +1 -1
  75. package/host/dist/ReachyHost.d.ts.map +1 -1
  76. package/host/dist/chunks/centralRest-uXNMidlL.js +2315 -0
  77. package/host/dist/chunks/index-Beu4GNzi.js +821 -0
  78. package/host/dist/chunks/{mountHost-DjE5zE9R.js → mountHost-BTTF4Uu9.js} +24933 -27488
  79. package/host/dist/components/ConnectingView.d.ts.map +1 -1
  80. package/host/dist/components/DaemonUpdateGate.d.ts +65 -0
  81. package/host/dist/components/DaemonUpdateGate.d.ts.map +1 -0
  82. package/host/dist/components/ErrorView.d.ts.map +1 -1
  83. package/host/dist/components/HostErrorBoundary.d.ts +30 -0
  84. package/host/dist/components/HostErrorBoundary.d.ts.map +1 -0
  85. package/host/dist/components/LeavingView.d.ts.map +1 -1
  86. package/host/dist/components/PickerView.d.ts +16 -14
  87. package/host/dist/components/PickerView.d.ts.map +1 -1
  88. package/host/dist/components/PostOAuthSplash.d.ts +14 -9
  89. package/host/dist/components/PostOAuthSplash.d.ts.map +1 -1
  90. package/host/dist/components/ReachyHostShell.d.ts.map +1 -1
  91. package/host/dist/components/SignInView.d.ts.map +1 -1
  92. package/host/dist/components/TopBar.d.ts +5 -6
  93. package/host/dist/components/TopBar.d.ts.map +1 -1
  94. package/host/dist/components/WelcomeBackOverlay.d.ts.map +1 -1
  95. package/host/dist/embed/index.d.ts +68 -5
  96. package/host/dist/embed/index.d.ts.map +1 -1
  97. package/host/dist/entry/auto.js +2 -2
  98. package/host/dist/entry/embed.d.ts +5 -4
  99. package/host/dist/entry/embed.d.ts.map +1 -1
  100. package/host/dist/entry/embed.js +5 -4
  101. package/host/dist/hooks/useHostBridge.d.ts +35 -1
  102. package/host/dist/hooks/useHostBridge.d.ts.map +1 -1
  103. package/host/dist/hooks/useLatestDaemonVersion.d.ts +7 -0
  104. package/host/dist/hooks/useLatestDaemonVersion.d.ts.map +1 -0
  105. package/host/dist/hooks/useOAuth.d.ts +18 -0
  106. package/host/dist/hooks/useOAuth.d.ts.map +1 -1
  107. package/host/dist/hooks/useRobots.d.ts +0 -2
  108. package/host/dist/hooks/useRobots.d.ts.map +1 -1
  109. package/host/dist/index.d.ts +2 -2
  110. package/host/dist/index.d.ts.map +1 -1
  111. package/host/dist/index.js +11 -10
  112. package/host/dist/lib/daemonRelease.d.ts +84 -0
  113. package/host/dist/lib/daemonRelease.d.ts.map +1 -0
  114. package/host/dist/lib/protocol.d.ts +141 -7
  115. package/host/dist/lib/protocol.d.ts.map +1 -1
  116. package/host/dist/lib/rebootWatch.d.ts +51 -0
  117. package/host/dist/lib/rebootWatch.d.ts.map +1 -0
  118. package/host/dist/lib/sdk-types.d.ts +1 -1
  119. package/host/dist/lib/sdk-types.d.ts.map +1 -1
  120. package/host/dist/lib/sdkStaleness.d.ts +78 -0
  121. package/host/dist/lib/sdkStaleness.d.ts.map +1 -0
  122. package/host/dist/lib/settings.d.ts +16 -5
  123. package/host/dist/lib/settings.d.ts.map +1 -1
  124. package/package.json +11 -4
  125. package/host/dist/chunks/index-lMs2sCXG.js +0 -439
  126. package/host/dist/chunks/reachy-mini-CdsdQLIU.js +0 -1523
  127. package/host/dist/lib/centralListener.d.ts +0 -73
  128. package/host/dist/lib/centralListener.d.ts.map +0 -1
@@ -0,0 +1,2315 @@
1
+ var re = Object.defineProperty, se = (s, e, t) => e in s ? re(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t, A = (s, e, t) => (se(s, typeof e != "symbol" ? e + "" : e, t), t), Z = "https://huggingface.co";
2
+ async function D(s, e) {
3
+ var t, n;
4
+ const r = new ie(s.url, s.status, (t = s.headers.get("X-Request-Id")) != null ? t : void 0);
5
+ r.message = `Api error with status ${r.statusCode}`;
6
+ const i = [`URL: ${r.url}`, r.requestId ? `Request ID: ${r.requestId}` : void 0].filter(Boolean).join(". ");
7
+ if ((n = s.headers.get("Content-Type")) != null && n.startsWith("application/json")) {
8
+ const o = await s.json();
9
+ r.message = o.error || o.message || r.message, r.data = o;
10
+ } else
11
+ r.data = { message: await s.text() };
12
+ throw r.message += `. ${i}`, r;
13
+ }
14
+ var ie = class extends Error {
15
+ constructor(s, e, t, n) {
16
+ super(n), A(this, "statusCode"), A(this, "url"), A(this, "requestId"), A(this, "data"), this.statusCode = e, this.requestId = t, this.url = s;
17
+ }
18
+ };
19
+ new Promise((s) => {
20
+ });
21
+ function oe(s) {
22
+ if (globalThis.Buffer)
23
+ return globalThis.Buffer.from(s).toString("base64");
24
+ {
25
+ const e = [];
26
+ return s.forEach((t) => {
27
+ e.push(String.fromCharCode(t));
28
+ }), globalThis.btoa(e.join(""));
29
+ }
30
+ }
31
+ async function B(s) {
32
+ var e, t;
33
+ if (typeof window > "u")
34
+ throw new Error("oauthHandleRedirect is only available in the browser");
35
+ const n = new URLSearchParams(window.location.search), [r, i] = [n.get("error"), n.get("error_description")];
36
+ if (r)
37
+ throw new Error(`${r}: ${i}`);
38
+ const o = n.get("code"), a = localStorage.getItem("huggingface.co:oauth:nonce");
39
+ if (!o)
40
+ throw new Error("Missing oauth code from query parameters in redirected URL");
41
+ if (!a)
42
+ throw new Error("Missing oauth nonce from localStorage");
43
+ const c = localStorage.getItem("huggingface.co:oauth:code_verifier");
44
+ if (!c)
45
+ throw new Error("Missing oauth code_verifier from localStorage");
46
+ const d = n.get("state");
47
+ if (!d)
48
+ throw new Error("Missing oauth state from query parameters in redirected URL");
49
+ let u;
50
+ try {
51
+ u = JSON.parse(d);
52
+ } catch {
53
+ throw new Error("Invalid oauth state in redirected URL, unable to parse JSON: " + d);
54
+ }
55
+ if (u.nonce !== a)
56
+ throw new Error("Invalid oauth state in redirected URL");
57
+ const l = Z, m = `${new URL(l).origin}/.well-known/openid-configuration`, g = await fetch(m, {
58
+ headers: {
59
+ Accept: "application/json"
60
+ }
61
+ });
62
+ if (!g.ok)
63
+ throw await D(g);
64
+ const f = await g.json(), v = await fetch(f.token_endpoint, {
65
+ method: "POST",
66
+ headers: {
67
+ "Content-Type": "application/x-www-form-urlencoded"
68
+ },
69
+ body: new URLSearchParams({
70
+ grant_type: "authorization_code",
71
+ code: o,
72
+ redirect_uri: u.redirectUri,
73
+ code_verifier: c
74
+ }).toString()
75
+ });
76
+ if (localStorage.removeItem("huggingface.co:oauth:code_verifier"), localStorage.removeItem("huggingface.co:oauth:nonce"), !v.ok)
77
+ throw await D(v);
78
+ const w = await v.json(), b = new Date(Date.now() + w.expires_in * 1e3), E = await fetch(f.userinfo_endpoint, {
79
+ headers: {
80
+ Authorization: `Bearer ${w.access_token}`
81
+ }
82
+ });
83
+ if (!E.ok)
84
+ throw await D(E);
85
+ const h = await E.json();
86
+ return {
87
+ accessToken: w.access_token,
88
+ accessTokenExpiresAt: b,
89
+ userInfo: {
90
+ id: h.sub,
91
+ name: h.name,
92
+ fullname: h.preferred_username,
93
+ email: h.email,
94
+ emailVerified: h.email_verified,
95
+ avatarUrl: h.picture,
96
+ websiteUrl: h.website,
97
+ isPro: h.isPro,
98
+ orgs: (t = (e = h.orgs) == null ? void 0 : e.map((p) => ({
99
+ id: p.sub,
100
+ name: p.name,
101
+ fullname: p.name,
102
+ isEnterprise: p.isEnterprise,
103
+ canPay: p.canPay,
104
+ avatarUrl: p.picture,
105
+ roleInOrg: p.roleInOrg
106
+ }))) != null ? t : []
107
+ },
108
+ state: u.state,
109
+ scope: w.scope
110
+ };
111
+ }
112
+ async function ae(s) {
113
+ if (typeof window > "u")
114
+ throw new Error("oauthHandleRedirect is only available in the browser");
115
+ const e = new URLSearchParams(window.location.search);
116
+ return e.has("error") ? B() : e.has("code") ? localStorage.getItem("huggingface.co:oauth:nonce") ? B() : (console.warn(
117
+ "Missing oauth nonce from localStorage. This can happen when the user refreshes the page after logging in, without changing the URL."
118
+ ), !1) : !1;
119
+ }
120
+ async function ce(s) {
121
+ var e, t;
122
+ if (typeof window > "u")
123
+ throw new Error("oauthLogin is only available in the browser");
124
+ const n = s?.hubUrl || Z, r = `${new URL(n).origin}/.well-known/openid-configuration`, i = await fetch(r, {
125
+ headers: {
126
+ Accept: "application/json"
127
+ }
128
+ });
129
+ if (!i.ok)
130
+ throw await D(i);
131
+ const o = await i.json(), a = globalThis.crypto.randomUUID(), c = globalThis.crypto.randomUUID() + globalThis.crypto.randomUUID();
132
+ localStorage.setItem("huggingface.co:oauth:nonce", a), localStorage.setItem("huggingface.co:oauth:code_verifier", c);
133
+ const d = s?.redirectUrl || window.location.href, u = JSON.stringify({
134
+ nonce: a,
135
+ redirectUri: d,
136
+ state: s?.state
137
+ }), l = (t = (e = window?.huggingface) == null ? void 0 : e.variables) != null ? t : null, m = s?.clientId || l?.OAUTH_CLIENT_ID;
138
+ if (!m)
139
+ throw l ? new Error("Missing clientId, please add hf_oauth: true to the README.md's metadata in your static Space") : new Error("Missing clientId");
140
+ const g = oe(
141
+ new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(c)))
142
+ ).replace(/[+]/g, "-").replace(/[/]/g, "_").replace(/=/g, "");
143
+ return `${o.authorization_endpoint}?${new URLSearchParams({
144
+ client_id: m,
145
+ scope: s?.scopes || l?.OAUTH_SCOPES || "openid profile",
146
+ response_type: "code",
147
+ redirect_uri: d,
148
+ state: u,
149
+ code_challenge: g,
150
+ code_challenge_method: "S256"
151
+ }).toString()}`;
152
+ }
153
+ const S = {
154
+ debug: 0,
155
+ info: 1,
156
+ warn: 2,
157
+ error: 3,
158
+ silent: 4
159
+ }, le = "reachy-log";
160
+ function de() {
161
+ try {
162
+ const s = globalThis.localStorage?.getItem(le);
163
+ if (s && s in S) return s;
164
+ } catch {
165
+ }
166
+ return "info";
167
+ }
168
+ let C = de();
169
+ function O(s) {
170
+ const e = `[reachy:${s}]`;
171
+ return {
172
+ debug: (...t) => {
173
+ S[C] <= S.debug && console.debug(e, ...t);
174
+ },
175
+ info: (...t) => {
176
+ S[C] <= S.info && console.info(e, ...t);
177
+ },
178
+ warn: (...t) => {
179
+ S[C] <= S.warn && console.warn(e, ...t);
180
+ },
181
+ error: (...t) => {
182
+ S[C] <= S.error && console.error(e, ...t);
183
+ }
184
+ };
185
+ }
186
+ function I(s) {
187
+ return s * Math.PI / 180;
188
+ }
189
+ function ue(s, e, t) {
190
+ const n = I(s), r = I(e), i = I(t), o = Math.cos(i), a = Math.sin(i), c = Math.cos(r), d = Math.sin(r), u = Math.cos(n), l = Math.sin(n);
191
+ return [
192
+ [o * c, o * d * l - a * u, o * d * u + a * l, 0],
193
+ [a * c, a * d * l + o * u, a * d * u - o * l, 0],
194
+ [-d, c * l, c * u, 0],
195
+ [0, 0, 0, 1]
196
+ ];
197
+ }
198
+ const ee = 4e3;
199
+ class P extends Error {
200
+ }
201
+ class he {
202
+ _replySlots = /* @__PURE__ */ new Map();
203
+ _rpcCounter = 0;
204
+ _pendingRpc = /* @__PURE__ */ new Map();
205
+ _pendingMotion = {
206
+ wake_up: [],
207
+ goto_sleep: []
208
+ };
209
+ _broadcastWaiters = [];
210
+ /* ─── Reply slots ────────────────────────────────────────────────── */
211
+ /**
212
+ * Register a slot waiter, run `send`, and await the matching daemon
213
+ * response. If a previous request on the same slot is still pending
214
+ * when a new one comes in, the older promise is resolved to `null`
215
+ * so its caller doesn't hang forever.
216
+ */
217
+ slotRoundtrip(e, t) {
218
+ return new Promise((n) => {
219
+ const r = this._replySlots.get(e);
220
+ r && r(null);
221
+ let i;
222
+ const o = (a) => {
223
+ i !== void 0 && (clearTimeout(i), i = void 0), this._replySlots.get(e) === o && this._replySlots.delete(e), n(a);
224
+ };
225
+ this._replySlots.set(e, o), i = setTimeout(() => o(null), ee), t();
226
+ });
227
+ }
228
+ /**
229
+ * Deliver a daemon reply to the pending waiter for `slot`, if any.
230
+ * Returns `true` when a waiter consumed the value — some message-handler
231
+ * branches use that to decide whether the message was theirs to swallow.
232
+ */
233
+ settleReplySlot(e, t) {
234
+ const n = this._replySlots.get(e);
235
+ return n ? (n(t), !0) : !1;
236
+ }
237
+ /* ─── JSON-RPC ───────────────────────────────────────────────────── */
238
+ /**
239
+ * One JSON-RPC round-trip: mint the correlation id, hand it to
240
+ * `send` (which reports `false` when the channel isn't open — the
241
+ * promise then rejects immediately), and await the matching
242
+ * response; rejects on timeout. Same shape as `slotRoundtrip`.
243
+ */
244
+ rpcRoundtrip(e, t, n) {
245
+ const r = `rpc-${++this._rpcCounter}`;
246
+ return n(r) ? new Promise((i, o) => {
247
+ const a = setTimeout(() => {
248
+ this._pendingRpc.delete(r), o(new Error(`rpcCall(${e}) timed out after ${t}ms`));
249
+ }, t);
250
+ this._pendingRpc.set(r, { resolve: i, reject: o, timer: a });
251
+ }) : Promise.reject(new Error(`rpcCall(${e}): data channel not open`));
252
+ }
253
+ /**
254
+ * Route a JSON-RPC RESPONSE (id + result/error) to its waiter.
255
+ * Returns `true` when the message was response-shaped — even if the
256
+ * waiter already timed out — so the caller can stop processing it;
257
+ * `false` for notifications and anything else.
258
+ */
259
+ settleRpcResponse(e) {
260
+ if (!("id" in e) || e.id == null || !("result" in e || "error" in e))
261
+ return !1;
262
+ const t = this._pendingRpc.get(e.id);
263
+ if (!t) return !0;
264
+ if (this._pendingRpc.delete(e.id), clearTimeout(t.timer), "error" in e && e.error) {
265
+ const n = e.error, r = new Error(n.message ?? "rpc error");
266
+ r.reason = n.data?.reason, t.reject(r);
267
+ } else
268
+ t.resolve(e.result);
269
+ return !0;
270
+ }
271
+ /* ─── Motion completions (wake_up / goto_sleep) ──────────────────── */
272
+ /** FIFO waiter for one `wake_up` / `goto_sleep` ack; rejects on timeout. */
273
+ awaitMotion(e, t) {
274
+ return new Promise((n, r) => {
275
+ const i = {
276
+ resolve: n,
277
+ reject: r,
278
+ timer: setTimeout(() => {
279
+ const o = this._pendingMotion[e], a = o.indexOf(i);
280
+ a !== -1 && o.splice(a, 1), r(new Error(`${e} timed out after ${t}ms`));
281
+ }, t)
282
+ };
283
+ this._pendingMotion[e].push(i);
284
+ });
285
+ }
286
+ /**
287
+ * Deliver a `wake_up` / `goto_sleep` reply to the oldest waiter.
288
+ * Returns `true` when a waiter consumed it; `false` when nothing was
289
+ * pending or the reply is neither a completion nor an error (the
290
+ * message then falls through to the caller's other branches).
291
+ */
292
+ settleMotion(e, t) {
293
+ const n = this._pendingMotion[e];
294
+ if (t.completed === !0 && n.length > 0) {
295
+ const r = n.shift();
296
+ return clearTimeout(r.timer), r.resolve(), !0;
297
+ }
298
+ if (t.error && n.length > 0) {
299
+ const r = n.shift();
300
+ return clearTimeout(r.timer), r.reject(new Error(`${e}: ${t.error}`)), !0;
301
+ }
302
+ return !1;
303
+ }
304
+ /* ─── Broadcast waiters ──────────────────────────────────────────── */
305
+ /** Await the first broadcast matching `predicate`; rejects on timeout. */
306
+ awaitBroadcast(e, { timeoutMs: t = 5e3, debugLabel: n = "" } = {}) {
307
+ return new Promise((r, i) => {
308
+ const o = {
309
+ predicate: e,
310
+ resolve: r,
311
+ reject: i,
312
+ timer: setTimeout(() => {
313
+ const a = this._broadcastWaiters.indexOf(o);
314
+ a !== -1 && this._broadcastWaiters.splice(a, 1), i(new P(`broadcast timeout (${t} ms): ${n}`));
315
+ }, t)
316
+ };
317
+ this._broadcastWaiters.push(o);
318
+ });
319
+ }
320
+ /**
321
+ * Offer a broadcast to the waiters; the first (most recent) matching
322
+ * waiter consumes it. Returns `true` when one did.
323
+ */
324
+ matchBroadcast(e) {
325
+ for (let t = this._broadcastWaiters.length - 1; t >= 0; t--) {
326
+ const n = this._broadcastWaiters[t];
327
+ if (n.predicate(e))
328
+ return this._broadcastWaiters.splice(t, 1), clearTimeout(n.timer), n.resolve(e), !0;
329
+ }
330
+ return !1;
331
+ }
332
+ /* ─── Teardown ───────────────────────────────────────────────────── */
333
+ /**
334
+ * Settle the whole ledger against a dead channel: reply slots
335
+ * resolve `null` (the "no answer" value all slot callers handle;
336
+ * public wrappers coerce where needed, e.g. `applyAudioConfig` maps
337
+ * it to `false`), everything else rejects with `err`. Called from
338
+ * every session-teardown path.
339
+ */
340
+ settleAll(e) {
341
+ for (const t of [...this._replySlots.values()])
342
+ t(null);
343
+ this._replySlots.clear();
344
+ for (const t of this._pendingRpc.values())
345
+ clearTimeout(t.timer), t.reject(e);
346
+ this._pendingRpc.clear();
347
+ for (const t of Object.keys(this._pendingMotion)) {
348
+ const n = this._pendingMotion[t];
349
+ for (; n.length; ) {
350
+ const r = n.shift();
351
+ clearTimeout(r.timer), r.reject(e);
352
+ }
353
+ }
354
+ for (const t of this._broadcastWaiters)
355
+ clearTimeout(t.timer), t.reject(e);
356
+ this._broadcastWaiters = [];
357
+ }
358
+ }
359
+ const M = O("session"), q = 3e3, G = 1e3, _e = 6e4, V = [0, 2e3, 4e3, 8e3, 8e3], pe = 15e3, J = 1e3, fe = 2500, me = 8e3;
360
+ class ge {
361
+ _deps;
362
+ // ICE-blip debounce. All handler slots are scoped to the lifetime
363
+ // of a live session (armed from the pc's state handler, cleared by
364
+ // every teardown path via `clearIceGrace`).
365
+ _iceGraceTimer = null;
366
+ _iceGraceReason = null;
367
+ _pendingVisibilityHandler = null;
368
+ // Network awareness (installed in startSession, removed on teardown).
369
+ _onlineHandler = null;
370
+ _offlineHandler = null;
371
+ _connectionChangeHandler = null;
372
+ // Automatic re-dial. Mutable via `setAutoReconnect()` so flows that
373
+ // EXPECT a transport death (the daemon self-update reboot) can stand
374
+ // the machinery down.
375
+ _autoReconnect;
376
+ _redialing = !1;
377
+ _redialWake = null;
378
+ // Data-channel silence watchdog.
379
+ _dcWatchdogId = null;
380
+ _lastDcInboundAt = 0;
381
+ _dcWatchdogLastTickAt = 0;
382
+ _dcSilenceNudged = !1;
383
+ constructor(e, t) {
384
+ this._deps = e, this._autoReconnect = t.autoReconnect;
385
+ }
386
+ /** True while the re-dial loop is running. */
387
+ get redialing() {
388
+ return this._redialing;
389
+ }
390
+ /**
391
+ * Enable/disable the automatic session re-dial at runtime.
392
+ * Disabling also aborts any re-dial already in flight.
393
+ */
394
+ setAutoReconnect(e) {
395
+ this._autoReconnect = e, e || this.cancelRedial();
396
+ }
397
+ /** Fatal errors carry the classic webrtc `error` payload shape. */
398
+ _emitError(e) {
399
+ this._deps.emit("error", { source: "webrtc", error: e });
400
+ }
401
+ /* ─── ICE-blip debounce + network awareness ──────────────────────
402
+ *
403
+ * Both halves below are intentionally generic (they don't know
404
+ * about motion, audio, or the FSM): they just smooth out
405
+ * browser-level events so the consumer's own state machine doesn't
406
+ * get torn down by routine WiFi/4G/screen-off noise.
407
+ */
408
+ /** ICE healed back to `connected`/`completed`. */
409
+ onIceHealed() {
410
+ this.clearIceGrace();
411
+ }
412
+ /**
413
+ * ICE hit `disconnected` — TRANSIENT per spec, debounce before
414
+ * escalating. If the tab is hidden, JS timers are throttled and
415
+ * would fire unpredictably late, so defer the grace window to the
416
+ * next foreground frame.
417
+ */
418
+ onIceDisconnected() {
419
+ typeof document < "u" && document.hidden ? this._armIceGraceOnVisibility() : this._scheduleIceGrace(q, "disconnected");
420
+ }
421
+ /**
422
+ * ICE hit `failed` — terminal per spec, but in practice we've seen
423
+ * `failed → connected` on rapid AP roams / BT route changes on iOS.
424
+ * Give the ICE agent a short window to surprise us before rejecting
425
+ * the session.
426
+ */
427
+ onIceFailed() {
428
+ this._scheduleIceGrace(G, "failed");
429
+ }
430
+ /**
431
+ * Cancel any pending ICE grace timer and visibility handler. Called
432
+ * on a healed `connected`/`completed` transition AND from the
433
+ * lifecycle teardown paths so a callback can't fire after the pc
434
+ * is closed.
435
+ */
436
+ clearIceGrace() {
437
+ this._iceGraceTimer !== null && (clearTimeout(this._iceGraceTimer), this._iceGraceTimer = null), this._iceGraceReason = null, this._pendingVisibilityHandler && typeof document < "u" && document.removeEventListener("visibilitychange", this._pendingVisibilityHandler), this._pendingVisibilityHandler = null;
438
+ }
439
+ /**
440
+ * Start a grace window. After `ms`, re-check the live ICE state:
441
+ * - If we healed back to `connected`/`completed`, the timer was
442
+ * already cancelled in the pc's state handler, so we never get
443
+ * here.
444
+ * - If we're still in the originally-observed bad state (or
445
+ * worse), surface the error and reject any pending session
446
+ * promise. The original code path is preserved verbatim so
447
+ * downstream consumers see the same `error` payload shape.
448
+ */
449
+ _scheduleIceGrace(e, t) {
450
+ if (this._iceGraceTimer !== null) {
451
+ if (this._iceGraceReason === t) return;
452
+ clearTimeout(this._iceGraceTimer);
453
+ }
454
+ this._iceGraceReason = t, this._iceGraceTimer = setTimeout(() => {
455
+ this._iceGraceTimer = null;
456
+ const n = this._iceGraceReason;
457
+ this._iceGraceReason = null;
458
+ const r = this._deps.iceState();
459
+ if (!(r === "connected" || r === "completed")) {
460
+ if (n === "disconnected" && r === "disconnected") {
461
+ if (this.maybeBeginRedial(`ICE stuck in 'disconnected' for > ${e}ms`)) return;
462
+ this._emitError(new Error(`ICE stuck in 'disconnected' for > ${e}ms`));
463
+ return;
464
+ }
465
+ if (n === "failed" || r === "failed") {
466
+ const i = new Error("ICE connection failed");
467
+ if (this._deps.rejectPendingSession(i)) {
468
+ this._redialing || this._emitError(i);
469
+ return;
470
+ }
471
+ if (this.maybeBeginRedial("ICE connection failed")) return;
472
+ this._emitError(i);
473
+ }
474
+ }
475
+ }, e);
476
+ }
477
+ /**
478
+ * `disconnected` while the tab is hidden. JS timers are throttled
479
+ * in background tabs (Chrome clamps to ~1 Hz, Safari can pause
480
+ * altogether), so a foreground grace timer would either miss the
481
+ * window or fire long after the connection healed. Wait for the
482
+ * tab to come back, then re-evaluate.
483
+ */
484
+ _armIceGraceOnVisibility() {
485
+ if (this._pendingVisibilityHandler) return;
486
+ const e = Date.now(), t = () => {
487
+ if (typeof document < "u" && document.hidden || (document.removeEventListener("visibilitychange", t), this._pendingVisibilityHandler = null, !this._deps.hasPc())) return;
488
+ const n = this._deps.iceState();
489
+ if (!(n === "connected" || n === "completed")) {
490
+ if (Date.now() - e > _e) {
491
+ const r = new Error("Session expired while tab was backgrounded");
492
+ if (this._deps.rejectPendingSession(r)) {
493
+ this._redialing || this._emitError(r);
494
+ return;
495
+ }
496
+ if (this.maybeBeginRedial("Session expired while tab was backgrounded")) return;
497
+ this._emitError(r);
498
+ return;
499
+ }
500
+ if (n === "failed") {
501
+ this._scheduleIceGrace(G, "failed");
502
+ return;
503
+ }
504
+ this._scheduleIceGrace(q, "disconnected");
505
+ }
506
+ };
507
+ document.addEventListener("visibilitychange", t), this._pendingVisibilityHandler = t;
508
+ }
509
+ /**
510
+ * Install browser-level network listeners and forward them as
511
+ * public `networkOnline` / `networkOffline` / `networkChange`
512
+ * events on the instance. Idempotent: called from `startSession()`,
513
+ * removed by `uninstallNetworkListeners` on teardown. Reachable
514
+ * only when there's a live `window` (defensive guard for SSR /
515
+ * test environments).
516
+ *
517
+ * `online` / `offline` are semantically about CONNECTIVITY:
518
+ * "does the OS think we can reach the internet". They flip
519
+ * symmetrically.
520
+ *
521
+ * `connection.change` (NetworkInformation API, Chrome / Android
522
+ * WebView only) is semantically about the TRANSPORT: it fires
523
+ * on Wi-Fi → 4G swaps, AP roams, etc. without necessarily going
524
+ * through `offline`. We forward it as its own `networkChange`
525
+ * event rather than aliasing it onto `networkOnline`, so
526
+ * consumers don't have to guess whether they're seeing a real
527
+ * connectivity recovery or a silent transport swap.
528
+ */
529
+ installNetworkListeners() {
530
+ if (this._onlineHandler || typeof window > "u") return;
531
+ const e = () => this._deps.emit("networkOnline", {}), t = () => this._deps.emit("networkOffline", {});
532
+ window.addEventListener("online", e), window.addEventListener("offline", t), this._onlineHandler = e, this._offlineHandler = t;
533
+ const n = navigator.connection;
534
+ if (n && typeof n.addEventListener == "function") {
535
+ const r = () => this._deps.emit("networkChange", {
536
+ effectiveType: n.effectiveType,
537
+ downlink: n.downlink,
538
+ rtt: n.rtt,
539
+ saveData: n.saveData
540
+ });
541
+ n.addEventListener("change", r), this._connectionChangeHandler = r;
542
+ }
543
+ }
544
+ /** Counterpart to `installNetworkListeners`. */
545
+ uninstallNetworkListeners() {
546
+ typeof window < "u" && (this._onlineHandler && window.removeEventListener("online", this._onlineHandler), this._offlineHandler && window.removeEventListener("offline", this._offlineHandler));
547
+ const e = navigator.connection;
548
+ e && this._connectionChangeHandler && typeof e.removeEventListener == "function" && e.removeEventListener("change", this._connectionChangeHandler), this._onlineHandler = null, this._offlineHandler = null, this._connectionChangeHandler = null;
549
+ }
550
+ /* ─── Automatic session re-dial ─────────────────────────────────── */
551
+ /**
552
+ * Escalation funnel for dead-transport signals. Returns `true` when
553
+ * the failure is being handled by a re-dial (callers should then skip
554
+ * their fatal `error` emit). Only fires for an ESTABLISHED session:
555
+ * mid-setup failures keep rejecting the startSession() promise so the
556
+ * original caller stays in charge of retries.
557
+ */
558
+ maybeBeginRedial(e) {
559
+ if (!this._autoReconnect) return !1;
560
+ if (this._redialing) return !0;
561
+ if (this._deps.isMidSetup()) return !1;
562
+ const t = this._deps.selectedRobotId();
563
+ return !t || !this._deps.hasPc() ? !1 : (this._redialing = !0, M.info(`auto-reconnect: ${e} - re-dialing ${t}`), this._runRedialLoop(t, e), !0);
564
+ }
565
+ /**
566
+ * The re-dial loop. One teardown, then up to REDIAL_BACKOFF_MS.length
567
+ * attempts, each preceded by its backoff slot. Every attempt re-runs
568
+ * the normal startSession() flow (and connect() first if the SSE feed
569
+ * died with the network), so a success is indistinguishable from a
570
+ * fresh session: `streaming` re-fires, videoTrack re-attaches, the
571
+ * pose subscription is re-asserted by the session-ready path.
572
+ *
573
+ * Exits early — without emitting anything further — when the user
574
+ * calls stopSession()/disconnect() or dials another robot, all of
575
+ * which flip `_redialing` off via `cancelRedial`.
576
+ */
577
+ async _runRedialLoop(e, t) {
578
+ const n = V.length;
579
+ this._deps.teardownForRedial();
580
+ for (let i = 1; i <= n; i++) {
581
+ if (!this._redialing || (this._deps.emit("sessionReconnecting", { attempt: i, maxAttempts: n, cause: t }), await this._redialSleep(V[i - 1]), !this._redialing)) return;
582
+ try {
583
+ if (this._deps.isSignalingDown() && await this._deps.reconnectSignaling(), !this._redialing || (await we(
584
+ this._deps.dial(e),
585
+ pe,
586
+ "auto-reconnect dial timed out"
587
+ ), !this._redialing)) return;
588
+ this._redialing = !1, M.info(`auto-reconnect: recovered on attempt ${i}`), this._deps.emit("sessionReconnected", { attempt: i });
589
+ return;
590
+ } catch (o) {
591
+ M.warn(
592
+ `auto-reconnect attempt ${i}/${n} failed:`,
593
+ o
594
+ ), this._redialing && this._deps.teardownForRedial();
595
+ }
596
+ }
597
+ if (!this._redialing) return;
598
+ this._redialing = !1;
599
+ const r = `Auto-reconnect gave up after ${n} attempts (${t})`;
600
+ M.warn(r), this._deps.emit("sessionStopped", { reason: "reconnect_failed", message: r }), this._emitError(new Error(r));
601
+ }
602
+ /**
603
+ * Cancellable backoff sleep — `cancelRedial` wakes it immediately.
604
+ * A timer firing after a cancel is harmless: the loop re-checks
605
+ * `_redialing` right after every sleep.
606
+ */
607
+ _redialSleep(e) {
608
+ return e <= 0 ? Promise.resolve() : new Promise((t) => {
609
+ this._redialWake = t, setTimeout(() => {
610
+ this._redialWake === t && (this._redialWake = null), t();
611
+ }, e);
612
+ });
613
+ }
614
+ /**
615
+ * Abort a pending re-dial. Called from stopSession(), disconnect()
616
+ * and an external startSession() — every path where the user (or the
617
+ * consumer app) takes over the session lifecycle.
618
+ */
619
+ cancelRedial() {
620
+ if (!(!this._redialing && !this._redialWake) && (this._redialing = !1, this._redialWake)) {
621
+ const e = this._redialWake;
622
+ this._redialWake = null, e();
623
+ }
624
+ }
625
+ /* ─── Data-channel silence watchdog ─────────────────────────────── */
626
+ /** (Re)start the watchdog. Called when a session reaches ready. */
627
+ startDcWatchdog() {
628
+ this.stopDcWatchdog();
629
+ const e = Date.now();
630
+ this._lastDcInboundAt = e, this._dcWatchdogLastTickAt = e, this._dcSilenceNudged = !1, this._dcWatchdogId = setInterval(() => this._dcWatchdogTick(), J);
631
+ }
632
+ stopDcWatchdog() {
633
+ this._dcWatchdogId !== null && (clearInterval(this._dcWatchdogId), this._dcWatchdogId = null);
634
+ }
635
+ /**
636
+ * Liveness stamp: every inbound data-channel message — control
637
+ * replies, broadcasts, pose frames — proves the transport is alive.
638
+ */
639
+ stampDcInbound() {
640
+ this._lastDcInboundAt = Date.now(), this._dcSilenceNudged = !1;
641
+ }
642
+ _dcWatchdogTick() {
643
+ const e = Date.now(), t = e - this._dcWatchdogLastTickAt;
644
+ if (this._dcWatchdogLastTickAt = e, !this._deps.isStreaming()) return;
645
+ if (t > J * 3) {
646
+ this._lastDcInboundAt = e, this._dcSilenceNudged = !1;
647
+ return;
648
+ }
649
+ if (typeof document < "u" && document.hidden) {
650
+ this._lastDcInboundAt = e, this._dcSilenceNudged = !1;
651
+ return;
652
+ }
653
+ const n = e - this._lastDcInboundAt;
654
+ if (n < fe) return;
655
+ if (!this._dcSilenceNudged) {
656
+ this._dcSilenceNudged = !0, this._deps.nudgeState();
657
+ return;
658
+ }
659
+ if (n < me) return;
660
+ this._dcSilenceNudged = !1;
661
+ const r = `No data-channel traffic for ${n}ms`;
662
+ this.maybeBeginRedial(r) || (this.stopDcWatchdog(), this._emitError(new Error(r)));
663
+ }
664
+ }
665
+ function we(s, e, t) {
666
+ return Promise.race([
667
+ s,
668
+ new Promise((n, r) => setTimeout(() => r(new Error(t)), e))
669
+ ]);
670
+ }
671
+ const z = "0.0.0-managed-by-ci", be = O("sdk");
672
+ function ye() {
673
+ if (typeof window > "u" || !window.location.hash) return;
674
+ const s = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
675
+ let e;
676
+ try {
677
+ e = new URLSearchParams(s);
678
+ } catch {
679
+ return;
680
+ }
681
+ const t = e.get("hf_token");
682
+ if (!t) return;
683
+ const n = e.get("hf_username") || "user", r = e.get("hf_token_expires"), i = r && !Number.isNaN(new Date(r).getTime()) ? r : new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3).toISOString();
684
+ try {
685
+ sessionStorage.setItem("hf_token", t), sessionStorage.setItem("hf_username", n), sessionStorage.setItem("hf_token_expires", i);
686
+ } catch (c) {
687
+ be.warn("could not persist pre-seeded HF credentials:", c);
688
+ }
689
+ e.delete("hf_token"), e.delete("hf_username"), e.delete("hf_token_expires");
690
+ const o = e.toString(), a = window.location.pathname + window.location.search + (o ? "#" + o : "");
691
+ try {
692
+ window.history.replaceState(null, "", a);
693
+ } catch {
694
+ }
695
+ }
696
+ function Se() {
697
+ if (typeof window > "u") return null;
698
+ if (window.location.hash) {
699
+ const s = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
700
+ try {
701
+ const t = new URLSearchParams(s).get("robot_peer_id");
702
+ if (t) return t;
703
+ } catch {
704
+ }
705
+ }
706
+ if (window.location.search)
707
+ try {
708
+ const e = new URLSearchParams(window.location.search).get("robot_peer_id");
709
+ if (e) return e;
710
+ } catch {
711
+ }
712
+ return null;
713
+ }
714
+ function ve(s) {
715
+ const e = s.split(`\r
716
+ `);
717
+ let t = !1;
718
+ for (const n of e)
719
+ if (n.startsWith("m=audio") ? t = !0 : n.startsWith("m=") && (t = !1), t && n === "a=sendrecv") return !0;
720
+ return !1;
721
+ }
722
+ const j = "hf_token", F = "hf_username", x = "hf_token_expires", L = "hf_token_last_seen", Re = 1440 * 60 * 1e3;
723
+ function Ie(s) {
724
+ try {
725
+ sessionStorage.setItem(j, s.token), sessionStorage.setItem(F, s.username), sessionStorage.setItem(x, s.expires), sessionStorage.setItem(L, (/* @__PURE__ */ new Date()).toISOString());
726
+ } catch {
727
+ }
728
+ }
729
+ function ke(s = Date.now()) {
730
+ try {
731
+ const e = sessionStorage.getItem(j), t = sessionStorage.getItem(F), n = sessionStorage.getItem(x);
732
+ if (!e || !n) return null;
733
+ if (new Date(n).getTime() <= s)
734
+ return $(), null;
735
+ const r = sessionStorage.getItem(L);
736
+ if (r !== null) {
737
+ const i = new Date(r).getTime();
738
+ if (!Number.isFinite(i) || s - i > Re)
739
+ return $(), null;
740
+ }
741
+ return sessionStorage.setItem(L, new Date(s).toISOString()), { token: e, username: t ?? "", expires: n };
742
+ } catch {
743
+ return null;
744
+ }
745
+ }
746
+ function $() {
747
+ try {
748
+ sessionStorage.removeItem(j), sessionStorage.removeItem(F), sessionStorage.removeItem(x), sessionStorage.removeItem(L);
749
+ } catch {
750
+ }
751
+ }
752
+ const Ee = /* @__PURE__ */ new Set([
753
+ "login_required",
754
+ "consent_required",
755
+ "interaction_required",
756
+ "access_denied"
757
+ ]);
758
+ function Te() {
759
+ try {
760
+ const s = new URL(window.location.href), e = s.searchParams.get("error");
761
+ return e === null || !Ee.has(e) ? null : (s.searchParams.delete("error"), s.searchParams.delete("error_description"), s.searchParams.delete("state"), window.history.replaceState(null, "", s.toString()), e);
762
+ } catch {
763
+ return null;
764
+ }
765
+ }
766
+ const y = 12 * 1024, U = 1 * 1024 * 1024, Ae = 512 * 1024;
767
+ function Ce() {
768
+ return typeof CompressionStream < "u";
769
+ }
770
+ function K() {
771
+ return "u" + Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
772
+ }
773
+ function N(s) {
774
+ let e = "";
775
+ for (let n = 0; n < s.length; n += 32768)
776
+ e += String.fromCharCode.apply(
777
+ null,
778
+ Array.from(s.subarray(n, n + 32768))
779
+ );
780
+ return btoa(e);
781
+ }
782
+ async function Me(s) {
783
+ const e = new TextEncoder().encode(s), t = await new Response(
784
+ new Blob([e]).stream().pipeThrough(new CompressionStream("gzip"))
785
+ ).arrayBuffer();
786
+ return N(new Uint8Array(t));
787
+ }
788
+ function Y(s) {
789
+ const e = Math.round(Number(s) || 0);
790
+ return Math.max(0, Math.min(100, e));
791
+ }
792
+ function X(s) {
793
+ switch ((s.type || "").toLowerCase().split(";")[0]?.trim() ?? "") {
794
+ case "audio/ogg":
795
+ case "application/ogg":
796
+ return "ogg-base64";
797
+ case "audio/opus":
798
+ return "opus-base64";
799
+ case "audio/mpeg":
800
+ case "audio/mp3":
801
+ return "mp3-base64";
802
+ case "audio/flac":
803
+ case "audio/x-flac":
804
+ return "flac-base64";
805
+ case "audio/mp4":
806
+ case "audio/m4a":
807
+ case "audio/x-m4a":
808
+ return "m4a-base64";
809
+ case "audio/aac":
810
+ return "aac-base64";
811
+ default:
812
+ return "wav-base64";
813
+ }
814
+ }
815
+ const _ = O("sdk"), Q = 750, De = 5e3;
816
+ class He extends EventTarget {
817
+ // ─── Config ──────────────────────────────────────────────────────────
818
+ _signalingUrl;
819
+ _clientId;
820
+ _appName;
821
+ _videoJitterBufferTargetMs;
822
+ _autoStartFromUrl;
823
+ _autoStartAttempted;
824
+ // ─── Public-ish state mirrors ────────────────────────────────────────
825
+ _state = "disconnected";
826
+ _robots = [];
827
+ _robotState = {};
828
+ // Highest `seq` seen on the unordered `pose` channel. Frames that arrive
829
+ // out of order (older seq) are dropped so a late packet can't rewind the
830
+ // live mirror. `null` until the first pose frame.
831
+ _lastPoseSeq = null;
832
+ // When the last pushed pose frame was applied, used to park the periodic
833
+ // `get_state` poll while the stream is live (see POSE_STREAM_FRESH_MS).
834
+ _lastPoseFrameAt = 0;
835
+ // Local refcount of pose-stream consumers (the 3D mirror, the wizard's
836
+ // move-end watcher, ...). The daemon's subscription is a per-peer boolean
837
+ // (not refcounted), so we only send `unsubscribe_pose` once the LAST local
838
+ // consumer releases - otherwise one consumer's cleanup would kill the
839
+ // stream for the others.
840
+ _poseSubRefs = 0;
841
+ _preselectedRobotId;
842
+ // ─── Auth ────────────────────────────────────────────────────────────
843
+ _token = null;
844
+ _username = null;
845
+ _tokenExpires = null;
846
+ // ─── Signaling ───────────────────────────────────────────────────────
847
+ _sseAbortController = null;
848
+ // ─── WebRTC ──────────────────────────────────────────────────────────
849
+ _pc = null;
850
+ _dc = null;
851
+ _sessionId = null;
852
+ _selectedRobotId = null;
853
+ _pendingRemoteIce = [];
854
+ // ─── Audio ───────────────────────────────────────────────────────────
855
+ _micStream = null;
856
+ _micMuted = !0;
857
+ _audioMuted = !0;
858
+ _micSupported = !1;
859
+ // ─── Timers ──────────────────────────────────────────────────────────
860
+ _latencyMonitorId = null;
861
+ _stateRefreshInterval = null;
862
+ // ─── Pending replies (reply slots / JSON-RPC / motion / broadcast) ───
863
+ // All request/response waiters on the data channel live in one
864
+ // ledger so every teardown path settles them in a single call.
865
+ _pending = new he();
866
+ // ─── Log subscribers ─────────────────────────────────────────────────
867
+ _logSubscribers = /* @__PURE__ */ new Set();
868
+ _updateProgressSubscribers = /* @__PURE__ */ new Set();
869
+ // ─── JSON-RPC notification listeners (one-way events, no id) ─────────
870
+ // onNotification() subscribes to events the robot/app pushes
871
+ // (conversation.phase/turn/transcript, ...).
872
+ _rpcListeners = /* @__PURE__ */ new Map();
873
+ // ─── Active upload ids for no-arg cancels ────────────────────────────
874
+ _activeMoveUploadId = null;
875
+ _activeAudioUploadId = null;
876
+ // ─── Session promise plumbing ────────────────────────────────────────
877
+ _sessionResolve = null;
878
+ _sessionReject = null;
879
+ _iceConnected = !1;
880
+ _dcOpen = !1;
881
+ // ─── Resilience ──────────────────────────────────────────────────────
882
+ // ICE-blip debounce, network awareness, automatic session re-dial and
883
+ // the data-channel silence watchdog all live in the supervisor; the
884
+ // class forwards pc/browser events to it and implements its deps as
885
+ // closures over the private state below (see the constructor).
886
+ _supervisor;
887
+ // ─── Video element ───────────────────────────────────────────────────
888
+ _videoElement = null;
889
+ constructor(e = {}) {
890
+ super(), this._signalingUrl = e.signalingUrl || "https://pollen-robotics-reachy-mini-central.hf.space", this._clientId = e.clientId || null, this._appName = e.appName || "unknown", this._videoJitterBufferTargetMs = e.videoJitterBufferTargetMs ?? 0, this._autoStartFromUrl = e.autoStartFromUrl === !0, this._autoStartAttempted = !1, this._preselectedRobotId = Se(), this._supervisor = new ge({
891
+ iceState: () => this._pc?.iceConnectionState ?? null,
892
+ hasPc: () => !!this._pc,
893
+ isStreaming: () => this._state === "streaming",
894
+ isMidSetup: () => !!(this._sessionResolve || this._sessionReject),
895
+ isSignalingDown: () => this._state === "disconnected",
896
+ selectedRobotId: () => this._selectedRobotId,
897
+ rejectPendingSession: (t) => {
898
+ if (!this._sessionReject) return !1;
899
+ const n = this._sessionReject;
900
+ return this._sessionResolve = null, this._sessionReject = null, n(t), !0;
901
+ },
902
+ reconnectSignaling: () => this.connect(),
903
+ // The private dial skips the public startSession()'s
904
+ // cancelRedial, so the loop's own attempts don't cancel it.
905
+ dial: (t) => this._startSessionInternal(t),
906
+ teardownForRedial: () => this._teardownForRedial(),
907
+ nudgeState: () => {
908
+ this.requestState();
909
+ },
910
+ emit: (t, n) => this._emit(t, n)
911
+ }, { autoReconnect: e.autoReconnect !== !1 });
912
+ }
913
+ // ─── Read-only properties ────────────────────────────────────────────
914
+ get state() {
915
+ return this._state;
916
+ }
917
+ get robots() {
918
+ return this._robots;
919
+ }
920
+ get robotState() {
921
+ return this._robotState;
922
+ }
923
+ get username() {
924
+ return this._username;
925
+ }
926
+ get isAuthenticated() {
927
+ return !!this._token;
928
+ }
929
+ get micSupported() {
930
+ return this._micSupported;
931
+ }
932
+ get micMuted() {
933
+ return this._micMuted;
934
+ }
935
+ get audioMuted() {
936
+ return this._audioMuted;
937
+ }
938
+ get preselectedRobotId() {
939
+ return this._preselectedRobotId;
940
+ }
941
+ get isEmbedded() {
942
+ return this._preselectedRobotId !== null;
943
+ }
944
+ /**
945
+ * Live RTCPeerConnection, or `null` between sessions. Read-only escape
946
+ * hatch for stats sampling (`getStats()`); mutating it is unsupported.
947
+ * Auto-reconnect re-dials REPLACE this object, so re-read it on every
948
+ * use — never capture it across ticks.
949
+ */
950
+ get peerConnection() {
951
+ return this._pc;
952
+ }
953
+ /**
954
+ * Build version of this SDK (npm package `version`), injected from
955
+ * package.json at build time. This is the JS SDK's OWN version and is
956
+ * distinct from `getVersion()`, which asks the DAEMON its version over
957
+ * the data channel. `0.0.0-managed-by-ci` means an unreleased/branch
958
+ * build (npm releases carry a real semver).
959
+ */
960
+ get sdkVersion() {
961
+ return z;
962
+ }
963
+ /** Same value as the instance `sdkVersion`, reachable without an
964
+ * instance: `ReachyMini.version`. */
965
+ static get version() {
966
+ return z;
967
+ }
968
+ /**
969
+ * Internal: try to honour the `autoStartFromUrl` constructor
970
+ * option. Called from the signaling-message handler after every
971
+ * `robotsChanged` emit, so a robot that comes online after the
972
+ * SDK is already `connected` still triggers the auto-start.
973
+ */
974
+ _maybeAutoStart() {
975
+ if (!this._autoStartFromUrl || this._autoStartAttempted || !this._preselectedRobotId || this._state !== "connected" || !this._robots.find((n) => n.id === this._preselectedRobotId)) return;
976
+ this._autoStartAttempted = !0;
977
+ const t = this._preselectedRobotId;
978
+ setTimeout(() => {
979
+ this._state === "connected" && this.startSession(t).catch((n) => {
980
+ _.warn("autoStartFromUrl: startSession rejected:", n);
981
+ });
982
+ }, 0);
983
+ }
984
+ // ─── Auth ────────────────────────────────────────────────────────────
985
+ async authenticate() {
986
+ try {
987
+ ye();
988
+ const e = Te();
989
+ e && _.info("silent sign-in declined:", e);
990
+ const t = await ae();
991
+ if (t)
992
+ return this._username = t.userInfo.preferred_username || t.userInfo.name || null, this._token = t.accessToken, this._tokenExpires = t.accessTokenExpiresAt, Ie({
993
+ token: this._token,
994
+ username: this._username ?? "",
995
+ expires: typeof this._tokenExpires == "string" ? this._tokenExpires : this._tokenExpires.toISOString()
996
+ }), !0;
997
+ const n = ke();
998
+ return n && n.username ? (this._token = n.token, this._username = n.username, this._tokenExpires = n.expires, !0) : !1;
999
+ } catch (e) {
1000
+ return _.error("authenticate failed:", e), !1;
1001
+ }
1002
+ }
1003
+ async login(e) {
1004
+ const t = {};
1005
+ this._clientId && (t.clientId = this._clientId);
1006
+ let n = await ce(t);
1007
+ if (e?.prompt) {
1008
+ const r = new URL(n);
1009
+ r.searchParams.set("prompt", e.prompt), n = r.toString();
1010
+ }
1011
+ window.location.href = n;
1012
+ }
1013
+ logout() {
1014
+ $(), this._username = null, this._tokenExpires = null, this.disconnect();
1015
+ }
1016
+ // ─── Lifecycle ───────────────────────────────────────────────────────
1017
+ async connect(e) {
1018
+ if (this._state !== "disconnected") throw new Error("Already connected");
1019
+ if (e && (this._token = e), !this._token) throw new Error("No token — call authenticate() first or pass a token");
1020
+ this._sseAbortController = new AbortController();
1021
+ let t;
1022
+ try {
1023
+ t = await fetch(
1024
+ `${this._signalingUrl}/events`,
1025
+ {
1026
+ signal: this._sseAbortController.signal,
1027
+ headers: { Authorization: `Bearer ${this._token}` }
1028
+ }
1029
+ );
1030
+ } catch (n) {
1031
+ throw this._sseAbortController = null, n;
1032
+ }
1033
+ if (!t.ok)
1034
+ throw this._sseAbortController = null, new Error(`HTTP ${t.status}`);
1035
+ return new Promise((n, r) => {
1036
+ let i = !1;
1037
+ const o = t.body.getReader(), a = new TextDecoder();
1038
+ let c = "";
1039
+ (async () => {
1040
+ try {
1041
+ for (; ; ) {
1042
+ const { done: u, value: l } = await o.read();
1043
+ if (u) break;
1044
+ c += a.decode(l, { stream: !0 });
1045
+ const m = c.split(`
1046
+ `);
1047
+ c = m.pop() ?? "";
1048
+ for (const g of m)
1049
+ if (g.startsWith("data:"))
1050
+ try {
1051
+ const f = JSON.parse(g.slice(5).trim());
1052
+ !i && f.type === "welcome" && f.peerId && (i = !0, this._state = "connected", await this._sendToServer({
1053
+ type: "setPeerStatus",
1054
+ roles: ["listener"],
1055
+ meta: { name: this._appName }
1056
+ }), this._emit("connected", { peerId: f.peerId }), n()), this._handleSignalingMessage(f);
1057
+ } catch {
1058
+ }
1059
+ }
1060
+ } catch (u) {
1061
+ if (u.name !== "AbortError" && this._emit("error", { source: "signaling", error: u }), !i) {
1062
+ r(u);
1063
+ return;
1064
+ }
1065
+ }
1066
+ this._state !== "disconnected" && (this._state = "disconnected", this._emit("disconnected", { reason: "SSE closed" })), i || r(new Error("Connection closed before welcome"));
1067
+ })();
1068
+ });
1069
+ }
1070
+ async autoConnect(e = {}) {
1071
+ const {
1072
+ token: t,
1073
+ pickRobot: n,
1074
+ autoPickIfSingle: r = !0,
1075
+ filterBusy: i = !0,
1076
+ wakeOnConnect: o = !0
1077
+ } = e;
1078
+ if (this._state === "streaming") {
1079
+ const c = this._robots?.find((d) => d.id === this._selectedRobotId);
1080
+ return {
1081
+ robotId: this._selectedRobotId,
1082
+ robotName: c?.meta?.name ?? null,
1083
+ isEmbedded: this.isEmbedded,
1084
+ alreadyStreaming: !0
1085
+ };
1086
+ }
1087
+ const a = this._autoStartFromUrl;
1088
+ this._autoStartFromUrl = !1;
1089
+ try {
1090
+ if (t)
1091
+ this._token = t;
1092
+ else if (!this._token && !await this.authenticate())
1093
+ throw new Error("Not authenticated — call login() or pass a token");
1094
+ this._state === "disconnected" && await this.connect();
1095
+ let c, d = null;
1096
+ if (this.isEmbedded) {
1097
+ c = this._preselectedRobotId;
1098
+ try {
1099
+ await this._waitForRobotInList(c, 5e3);
1100
+ } catch {
1101
+ }
1102
+ d = this._robots?.find((l) => l.id === c)?.meta?.name ?? null;
1103
+ } else {
1104
+ const u = await this._fetchOwnedRobots({ filterBusy: i });
1105
+ if (u.length === 0)
1106
+ throw new Error("No reachable robots");
1107
+ if (r && u.length === 1 && !u[0].busy)
1108
+ c = u[0].id, d = u[0].name;
1109
+ else if (n) {
1110
+ const l = await n(u);
1111
+ if (!l) throw new Error("Robot selection cancelled");
1112
+ c = l, d = u.find((m) => m.id === l)?.name ?? null;
1113
+ } else
1114
+ throw new Error(
1115
+ "Multiple robots available — pass a pickRobot callback to autoConnect()"
1116
+ );
1117
+ }
1118
+ if (await this.startSession(c), o && typeof this.ensureAwake == "function")
1119
+ try {
1120
+ await this.ensureAwake();
1121
+ } catch (u) {
1122
+ _.warn("autoConnect: ensureAwake failed:", u);
1123
+ }
1124
+ return { robotId: c, robotName: d, isEmbedded: this.isEmbedded };
1125
+ } finally {
1126
+ this._autoStartFromUrl = a;
1127
+ }
1128
+ }
1129
+ async _fetchOwnedRobots({ filterBusy: e = !0 } = {}) {
1130
+ try {
1131
+ const t = await fetch(`${this._signalingUrl}/api/robot-status`, {
1132
+ headers: { Authorization: `Bearer ${this._token}` }
1133
+ });
1134
+ if (!t.ok) throw new Error(`HTTP ${t.status}`);
1135
+ const n = await t.json(), r = /* @__PURE__ */ new Map();
1136
+ for (const i of n.robots || []) {
1137
+ if (e && i.busy) continue;
1138
+ const o = i.meta?.install_id ?? i.meta?.hardware_id ?? i.peerId;
1139
+ r.set(o, {
1140
+ id: i.peerId,
1141
+ name: i.robotName ?? i.meta?.name ?? null,
1142
+ busy: !!i.busy,
1143
+ activeApp: i.activeApp ?? null,
1144
+ meta: i.meta ?? {},
1145
+ lastSeenAgeSeconds: i.last_seen_age_seconds ?? null
1146
+ });
1147
+ }
1148
+ return Array.from(r.values()).sort(
1149
+ (i, o) => (i.lastSeenAgeSeconds ?? 1 / 0) - (o.lastSeenAgeSeconds ?? 1 / 0)
1150
+ );
1151
+ } catch (t) {
1152
+ return _.warn("/api/robot-status unavailable, using SSE list:", t), (this._robots || []).map((n) => ({
1153
+ id: n.id,
1154
+ name: n.meta?.name ?? null,
1155
+ busy: !1,
1156
+ activeApp: null,
1157
+ meta: n.meta ?? {},
1158
+ lastSeenAgeSeconds: null
1159
+ }));
1160
+ }
1161
+ }
1162
+ _waitForRobotInList(e, t) {
1163
+ return this._robots?.find((n) => n.id === e) ? Promise.resolve() : new Promise((n, r) => {
1164
+ const i = () => {
1165
+ this._robots?.find((a) => a.id === e) && (this.removeEventListener("robotsChanged", i), clearTimeout(o), n());
1166
+ }, o = setTimeout(() => {
1167
+ this.removeEventListener("robotsChanged", i), r(new Error(`Timeout waiting for robot ${e} in list`));
1168
+ }, t);
1169
+ this.addEventListener("robotsChanged", i);
1170
+ });
1171
+ }
1172
+ async startSession(e) {
1173
+ return this._supervisor.cancelRedial(), this._startSessionInternal(e);
1174
+ }
1175
+ async _startSessionInternal(e) {
1176
+ if (this._state !== "connected") throw new Error("Not connected");
1177
+ this._selectedRobotId = e, this._iceConnected = !1, this._dcOpen = !1, this._micSupported = !1, this._pendingRemoteIce = [];
1178
+ try {
1179
+ const t = window, n = t.AudioContext ?? t.webkitAudioContext;
1180
+ if (!n) throw new Error("AudioContext not supported");
1181
+ const r = new n(), i = r.createMediaStreamDestination(), o = r.createOscillator(), a = r.createGain();
1182
+ a.gain.value = 0, o.connect(a).connect(i), o.start();
1183
+ const c = i.stream;
1184
+ c.getAudioTracks().forEach((d) => {
1185
+ d.enabled = !1;
1186
+ }), this._micStream = c, this._micMuted = !0;
1187
+ } catch (t) {
1188
+ _.warn("audio sender placeholder setup failed:", t), this._micStream = null;
1189
+ }
1190
+ return this._pc = new RTCPeerConnection({
1191
+ iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
1192
+ }), this._supervisor.installNetworkListeners(), new Promise((t, n) => {
1193
+ this._sessionResolve = t, this._sessionReject = n, this._pc.ontrack = (r) => {
1194
+ if (r.track.kind === "video") {
1195
+ const i = this._videoJitterBufferTargetMs;
1196
+ try {
1197
+ r.receiver.jitterBufferTarget = i;
1198
+ } catch {
1199
+ }
1200
+ try {
1201
+ r.receiver.playoutDelayHint = i / 1e3;
1202
+ } catch {
1203
+ }
1204
+ this._emit("videoTrack", { track: r.track, stream: r.streams[0] });
1205
+ }
1206
+ }, this._pc.onicecandidate = async (r) => {
1207
+ r.candidate && this._sessionId && await this._sendToServer({
1208
+ type: "peer",
1209
+ sessionId: this._sessionId,
1210
+ ice: {
1211
+ candidate: r.candidate.candidate,
1212
+ sdpMLineIndex: r.candidate.sdpMLineIndex,
1213
+ sdpMid: r.candidate.sdpMid
1214
+ }
1215
+ });
1216
+ }, this._pc.oniceconnectionstatechange = () => {
1217
+ const r = this._pc?.iceConnectionState;
1218
+ if (r) {
1219
+ if (this._emit("iceStateChange", { state: r }), r === "connected" || r === "completed") {
1220
+ this._supervisor.onIceHealed(), this._iceConnected = !0, this._checkSessionReady();
1221
+ return;
1222
+ }
1223
+ if (r === "disconnected") {
1224
+ this._supervisor.onIceDisconnected();
1225
+ return;
1226
+ }
1227
+ if (r === "failed") {
1228
+ this._supervisor.onIceFailed();
1229
+ return;
1230
+ }
1231
+ }
1232
+ }, this._pc.ondatachannel = (r) => {
1233
+ const i = r.channel;
1234
+ if (i.label === "pose") {
1235
+ this._lastPoseSeq = null, i.onmessage = (o) => {
1236
+ const a = JSON.parse(o.data);
1237
+ if (typeof a.seq == "number") {
1238
+ if (this._lastPoseSeq !== null && a.seq <= this._lastPoseSeq) return;
1239
+ this._lastPoseSeq = a.seq;
1240
+ }
1241
+ this._lastPoseFrameAt = Date.now(), this._handleRobotMessage(a, !0);
1242
+ };
1243
+ return;
1244
+ }
1245
+ this._dc = i, this._dc.onopen = () => {
1246
+ this._dcOpen = !0, this._checkSessionReady();
1247
+ }, this._dc.onmessage = (o) => this._handleRobotMessage(JSON.parse(o.data));
1248
+ }, this._sendToServer({ type: "startSession", peerId: e }).then((r) => {
1249
+ if (r?.type === "sessionRejected") {
1250
+ this._failSessionRejected(r);
1251
+ return;
1252
+ }
1253
+ r?.sessionId && (this._sessionId = r.sessionId);
1254
+ });
1255
+ });
1256
+ }
1257
+ /**
1258
+ * Common transport teardown shared by every session-ending path
1259
+ * (stopSession, disconnect, the auto-redial, a session rejection):
1260
+ * settle the pending-reply ledger and the startSession() resolvers
1261
+ * with `settleErr`, stand the resilience plumbing down, stop the
1262
+ * session-scoped timers, release the mic placeholder and close the
1263
+ * pc/dc. Returns the session id (already cleared on the instance)
1264
+ * so each caller decides whether and how to send `endSession`.
1265
+ * What else differs — events, subscriber wipes, where `_state`
1266
+ * lands — stays with the callers.
1267
+ */
1268
+ _closeTransport(e) {
1269
+ if (this._pending.settleAll(e), this._sessionReject) {
1270
+ const n = this._sessionReject;
1271
+ this._sessionResolve = null, this._sessionReject = null, n(e);
1272
+ }
1273
+ this._sessionResolve = null, this._supervisor.clearIceGrace(), this._supervisor.uninstallNetworkListeners(), this._supervisor.stopDcWatchdog(), this._stateRefreshInterval && (clearInterval(this._stateRefreshInterval), this._stateRefreshInterval = null), this._latencyMonitorId && (clearInterval(this._latencyMonitorId), this._latencyMonitorId = null), this._micStream && (this._micStream.getTracks().forEach((n) => n.stop()), this._micStream = null), this._micMuted = !0, this._micSupported = !1, this._pc && (this._pc.close(), this._pc = null), this._dc && (this._dc.close(), this._dc = null), this._iceConnected = !1, this._dcOpen = !1;
1274
+ const t = this._sessionId;
1275
+ return this._sessionId = null, t;
1276
+ }
1277
+ _failSessionRejected(e) {
1278
+ const t = new Error(
1279
+ e.reason === "robot_busy" ? `Robot is busy: "${e.activeApp || "another app"}" is already connected` : `Session rejected: ${e.reason || "unknown reason"}`
1280
+ );
1281
+ t.reason = e.reason ?? null, t.activeApp = e.activeApp ?? null, this._closeTransport(t), this._supervisor.redialing || this._emit("sessionRejected", { reason: e.reason, activeApp: e.activeApp });
1282
+ }
1283
+ async stopSession() {
1284
+ this._supervisor.cancelRedial(), this._logSubscribers.clear(), this._updateProgressSubscribers.clear();
1285
+ const e = this._closeTransport(new Error("Session stopped"));
1286
+ e && await this._sendToServer({ type: "endSession", sessionId: e }), this._state === "streaming" && (this._state = "connected", this._emit("sessionStopped", { reason: "user" }));
1287
+ }
1288
+ disconnect() {
1289
+ this._supervisor.cancelRedial(), this._sseAbortController && (this._sseAbortController.abort(), this._sseAbortController = null), this._logSubscribers.clear(), this._updateProgressSubscribers.clear();
1290
+ const e = this._closeTransport(new Error("Disconnected"));
1291
+ e && this._token && this._sendToServer({ type: "endSession", sessionId: e }), this._robots = [], this._state = "disconnected", this._emit("disconnected", { reason: "user" });
1292
+ }
1293
+ // ─── Resilience: supervisor delegation ───────────────────────────────
1294
+ // The policy lives in `session-supervisor.ts`; only the transport
1295
+ // teardown below stays here because it manipulates the class's own
1296
+ // private state.
1297
+ /**
1298
+ * Enable/disable the automatic session re-dial at runtime.
1299
+ * Disabling also aborts any re-dial already in flight. Used by
1300
+ * flows that EXPECT the transport to die — e.g. the daemon
1301
+ * self-update, whose `systemctl restart` teardown must surface as
1302
+ * `sessionStopped` immediately (it's the "install done, rebooting"
1303
+ * signal), not get absorbed by ~22 s of doomed reconnect attempts
1304
+ * against a robot that is rebooting anyway.
1305
+ *
1306
+ * Sharp edge: cancelling an in-flight re-dial leaves the session
1307
+ * torn down without emitting `sessionStopped` or `error` — the
1308
+ * caller owns the terminal event (see `ReachyMiniInstance`).
1309
+ */
1310
+ setAutoReconnect(e) {
1311
+ this._supervisor.setAutoReconnect(e);
1312
+ }
1313
+ /**
1314
+ * Transport-only teardown: everything stopSession() does EXCEPT the
1315
+ * user-facing state flip (`sessionStopped` is not emitted, `_state`
1316
+ * falls back to 'connected' so the re-dial can start a session).
1317
+ * Pending promises that ride the dead channel are settled just like
1318
+ * stopSession() settles them — callers see the same failure shape a
1319
+ * manual stop would produce; that includes a timed-out dial
1320
+ * attempt's own startSession() promise (its resolvers were left
1321
+ * armed, and a late signaling reply could otherwise settle them
1322
+ * against a closed pc).
1323
+ */
1324
+ _teardownForRedial() {
1325
+ const e = this._closeTransport(new Error("Session reconnecting"));
1326
+ e && this._token && this._sendToServer({ type: "endSession", sessionId: e }), this._state === "streaming" && (this._state = "connected");
1327
+ }
1328
+ // ─── Commands ────────────────────────────────────────────────────────
1329
+ /**
1330
+ * Atomic raw-units pose update over the data channel. Channels you
1331
+ * omit are held at their last commanded value (per-axis, independent).
1332
+ *
1333
+ * **Head pose is in the WORLD frame.** The daemon's IK splits the
1334
+ * requested head world-yaw between body rotation and the stewart
1335
+ * platform, subject to the mechanical limit
1336
+ * `|head_yaw_world − body_yaw| ≤ 65°`.
1337
+ *
1338
+ * **If you want the head to FOLLOW the body** (tank-style rotation):
1339
+ * a `setTarget({ body_yaw })` on its own does NOT rotate the head —
1340
+ * the head's commanded world yaw is unchanged, so its gaze stays
1341
+ * pinned in world frame while the body turns under it. To make the
1342
+ * head turn with the body, include a `head` matrix in the SAME call
1343
+ * with the body-yaw delta added to the head RPY's yaw:
1344
+ *
1345
+ * ```ts
1346
+ * // Body-yaw drag handler: tank-couple the head so it follows.
1347
+ * const delta = newBodyDeg - lastCommandedBodyDeg;
1348
+ * const nextHeadYaw = lastCommandedHeadYawDeg + delta;
1349
+ * robot.setTarget({
1350
+ * head: rpyToMatrix(headRoll, headPitch, nextHeadYaw).flat(),
1351
+ * body_yaw: degToRad(newBodyDeg),
1352
+ * });
1353
+ * lastCommandedHeadYawDeg = nextHeadYaw;
1354
+ * lastCommandedBodyDeg = newBodyDeg;
1355
+ * ```
1356
+ *
1357
+ * **Baseline must be the last COMMANDED value, not telemetry.** For
1358
+ * continuous-input controllers (slider drag, joystick), do not use
1359
+ * `state.head` from the `state` event as the baseline for incremental
1360
+ * commands — telemetry lags one WebRTC round-trip, so cumulative
1361
+ * deltas computed against it stall (every iteration in a rapid drag
1362
+ * adds the same `delta` to the same stale baseline → the head fails
1363
+ * to keep up). Track the last-commanded RPY in your own buffer.
1364
+ *
1365
+ * @param head Flat row-major 4×4 matrix (16 finite numbers) in
1366
+ * the world frame. Omit to hold the previous head target.
1367
+ * @param antennas `[rightRad, leftRad]` (radians). Omit to hold.
1368
+ * @param body_yaw Signed radians. Omit to hold.
1369
+ * @returns `true` if the command was queued on the data channel,
1370
+ * `false` if the channel is not open.
1371
+ */
1372
+ setTarget({ head: e, antennas: t, body_yaw: n } = {}) {
1373
+ const r = { type: "set_full_target" };
1374
+ if (e !== void 0) {
1375
+ if (!Array.isArray(e) || e.length !== 16 || !e.every((i) => Number.isFinite(i)))
1376
+ throw new TypeError(
1377
+ `setTarget: head must be a 16-element flat row-major 4×4 matrix of finite numbers; got ${Array.isArray(e) ? `Array(${e.length})` : typeof e}`
1378
+ );
1379
+ r.head = e;
1380
+ }
1381
+ if (t !== void 0) {
1382
+ if (!Array.isArray(t) || t.length !== 2 || !t.every((i) => Number.isFinite(i)))
1383
+ throw new TypeError(
1384
+ `setTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t) ? `Array(${t.length})` : typeof t}`
1385
+ );
1386
+ r.antennas = t;
1387
+ }
1388
+ if (n !== void 0) {
1389
+ if (!Number.isFinite(n))
1390
+ throw new TypeError(
1391
+ `setTarget: body_yaw must be a finite number (radians); got ${n}`
1392
+ );
1393
+ r.body_yaw = n;
1394
+ }
1395
+ return this._sendCommand(r);
1396
+ }
1397
+ gotoTarget({ head: e, antennas: t, body_yaw: n, duration: r }) {
1398
+ const i = { type: "goto_target" };
1399
+ if (e !== void 0) {
1400
+ if (!Array.isArray(e) || e.length !== 16 || !e.every((o) => Number.isFinite(o)))
1401
+ throw new TypeError(
1402
+ `gotoTarget: head must be a 16-element flat row-major 4×4 matrix of finite numbers; got ${Array.isArray(e) ? `Array(${e.length})` : typeof e}`
1403
+ );
1404
+ i.head = e;
1405
+ }
1406
+ if (t !== void 0) {
1407
+ if (!Array.isArray(t) || t.length !== 2 || !t.every((o) => Number.isFinite(o)))
1408
+ throw new TypeError(
1409
+ `gotoTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t) ? `Array(${t.length})` : typeof t}`
1410
+ );
1411
+ i.antennas = t;
1412
+ }
1413
+ if (n !== void 0) {
1414
+ if (!Number.isFinite(n))
1415
+ throw new TypeError(
1416
+ `gotoTarget: body_yaw must be a finite number (radians); got ${n}`
1417
+ );
1418
+ i.body_yaw = n;
1419
+ }
1420
+ if (!Number.isFinite(r) || r <= 0)
1421
+ throw new TypeError(
1422
+ `gotoTarget: duration must be a positive finite number (seconds); got ${r}`
1423
+ );
1424
+ return i.duration = r, this._sendCommand(i);
1425
+ }
1426
+ setHeadRpyDeg(e, t, n) {
1427
+ return this.setTarget({ head: ue(e, t, n).flat() });
1428
+ }
1429
+ setAntennasDeg(e, t) {
1430
+ return this.setTarget({ antennas: [I(e), I(t)] });
1431
+ }
1432
+ setBodyYawDeg(e) {
1433
+ return this.setTarget({ body_yaw: I(e) });
1434
+ }
1435
+ playSound(e) {
1436
+ return this._sendCommand({ type: "play_sound", file: e });
1437
+ }
1438
+ playRecordedMove(e, {
1439
+ dataset: t,
1440
+ initialGotoDuration: n
1441
+ } = {}) {
1442
+ return this._sendCommand({
1443
+ type: "play_recorded_move",
1444
+ move_name: e,
1445
+ ...t ? { dataset_name: t } : {},
1446
+ ...n && n > 0 ? { initial_goto_duration: n } : {}
1447
+ });
1448
+ }
1449
+ /**
1450
+ * Stop whatever move is currently playing on the daemon (recorded move,
1451
+ * uploaded move, goto). Fire-and-forget and idempotent: the daemon acks
1452
+ * ok with `stopped: false` when nothing was running. Returns `false` if
1453
+ * the data channel is not open.
1454
+ */
1455
+ stopMove() {
1456
+ return this._sendCommand({ type: "stop_move" });
1457
+ }
1458
+ preloadDataset(e) {
1459
+ return this._sendCommand({ type: "preload_dataset", dataset_name: e });
1460
+ }
1461
+ /**
1462
+ * Like `preloadDataset()`, but resolves once the daemon acks the preload
1463
+ * (`{command: "preload_dataset", ...}` on the data channel), i.e. when the
1464
+ * dataset is actually in the local HF cache. Resolves `true` on success,
1465
+ * `false` when the daemon reports a download failure, and `null` on the
1466
+ * fail-open timeout (download slower than `timeoutMs`, or a daemon that
1467
+ * predates the command and never replies) - callers should proceed in all
1468
+ * three cases, `playRecordedMove` still downloads on demand. Rejects when
1469
+ * the data channel isn't open or the session tears down mid-flight.
1470
+ */
1471
+ preloadDatasetAndWait(e, { timeoutMs: t = 12e4 } = {}) {
1472
+ return this.preloadDataset(e) ? this._pending.awaitBroadcast(
1473
+ (n) => n.command === "preload_dataset" && n.dataset_name === e,
1474
+ { timeoutMs: t, debugLabel: `preload_dataset(${e})` }
1475
+ ).then((n) => n.status === "ok").catch((n) => {
1476
+ if (n instanceof P) return null;
1477
+ throw n;
1478
+ }) : Promise.reject(new Error("Data channel not open"));
1479
+ }
1480
+ clearIncomingAudio() {
1481
+ return this._sendCommand({ type: "clear_incoming_audio" });
1482
+ }
1483
+ startHeadTracking(e = 1) {
1484
+ if (!Number.isFinite(e))
1485
+ throw new TypeError(`startHeadTracking: weight must be a finite number; got ${e}`);
1486
+ const t = Math.min(Math.max(e, 0), 1);
1487
+ return this._sendCommand({
1488
+ type: "set_head_tracking",
1489
+ enabled: !0,
1490
+ weight: t
1491
+ });
1492
+ }
1493
+ stopHeadTracking() {
1494
+ return this._sendCommand({ type: "set_head_tracking", enabled: !1 });
1495
+ }
1496
+ getTrackedFace() {
1497
+ return this._slotRoundtrip("tracked_face", { type: "get_tracked_face" });
1498
+ }
1499
+ /**
1500
+ * Trigger a PyPI update of the daemon over the data channel. Remote
1501
+ * counterpart of `POST /update/start`. The daemon acks then restarts
1502
+ * itself once the install finishes, which tears this session down -
1503
+ * the caller is expected to reconnect afterwards.
1504
+ *
1505
+ * Pass `onProgress` to receive `update_progress` events (one per log
1506
+ * line of the update job). A *successful* update restarts the daemon
1507
+ * before a `done` event can arrive, so treat the session teardown +
1508
+ * a successful reconnect as the success signal; `onProgress` will fire
1509
+ * with `status: 'failed'` if the install errors before the restart.
1510
+ *
1511
+ * Returns `false` if the data channel isn't open.
1512
+ */
1513
+ startDaemonUpdate({ preRelease: e = !1, onProgress: t } = {}) {
1514
+ return t && this._updateProgressSubscribers.add(t), this._sendCommand({ type: "start_update", pre_release: e });
1515
+ }
1516
+ setMotorMode(e) {
1517
+ return this._sendCommand({ type: "set_motor_mode", mode: e });
1518
+ }
1519
+ setMotorTorque(e, t = null) {
1520
+ return this._sendCommand({ type: "set_torque", on: e, ids: t });
1521
+ }
1522
+ wakeUp({ timeoutMs: e = 8e3 } = {}) {
1523
+ return this._sendCommand({ type: "set_motor_mode", mode: "enabled" }), this._sendCommandAwaitCompletion("wake_up", e);
1524
+ }
1525
+ gotoSleep({ timeoutMs: e = 8e3 } = {}) {
1526
+ return this._sendCommandAwaitCompletion("goto_sleep", e);
1527
+ }
1528
+ _sendCommandAwaitCompletion(e, t) {
1529
+ return this._sendCommand({ type: e }) ? this._pending.awaitMotion(e, t) : Promise.reject(new Error(`${e}: data channel not open`));
1530
+ }
1531
+ isAwake() {
1532
+ const e = this._robotState?.motor_mode;
1533
+ return e === "enabled" || e === "gravity_compensation";
1534
+ }
1535
+ async ensureAwake(e = 1e3) {
1536
+ if (this._robotState?.motor_mode === void 0 && await new Promise((n) => {
1537
+ const r = () => {
1538
+ this.removeEventListener("state", r), clearTimeout(i), n();
1539
+ }, i = setTimeout(r, e);
1540
+ this.addEventListener("state", r), this.requestState();
1541
+ }), this._robotState?.motor_mode === "gravity_compensation")
1542
+ return _.info("ensureAwake: gravity compensation inherited, flipping to enabled (no emote)"), this.setMotorMode("enabled"), this.requestState(), !0;
1543
+ if (this.isAwake())
1544
+ return _.info("ensureAwake: already awake, nothing to do"), !0;
1545
+ _.info(`ensureAwake: robot asleep (motor_mode=${this._robotState?.motor_mode ?? "unknown"}), playing wake-up`);
1546
+ const t = Date.now();
1547
+ try {
1548
+ await this.wakeUp({ timeoutMs: De }), _.info(`ensureAwake: wake-up trajectory done in ${Date.now() - t}ms`);
1549
+ } catch (n) {
1550
+ _.warn(`ensureAwake: wake-up not confirmed after ${Date.now() - t}ms (continuing):`, n);
1551
+ }
1552
+ return !0;
1553
+ }
1554
+ /**
1555
+ * Query the daemon version. Resolves `null` when the daemon predates
1556
+ * `get_version` (fail-open on the shared slot timeout) or the reply is
1557
+ * superseded by session teardown.
1558
+ */
1559
+ getVersion() {
1560
+ return this._slotRoundtrip("version", { type: "get_version" });
1561
+ }
1562
+ getHardwareId() {
1563
+ return this._slotRoundtrip("hardware_id", { type: "get_hardware_id" });
1564
+ }
1565
+ /**
1566
+ * One-shot IMU reading (BMI088, wireless version only). Resolves `null`
1567
+ * when the robot has no IMU (Lite, simulation) or the daemon predates
1568
+ * the `get_imu` command (fail-open on the shared slot timeout).
1569
+ */
1570
+ getImu() {
1571
+ return this._slotRoundtrip("imu", { type: "get_imu" });
1572
+ }
1573
+ getVolume() {
1574
+ return this._slotRoundtrip("volume", { type: "get_volume" });
1575
+ }
1576
+ setVolume(e) {
1577
+ return this._slotRoundtrip("volume", { type: "set_volume", volume: Y(e) });
1578
+ }
1579
+ getMicrophoneVolume() {
1580
+ return this._slotRoundtrip("mic_volume", { type: "get_microphone_volume" });
1581
+ }
1582
+ setMicrophoneVolume(e) {
1583
+ return this._slotRoundtrip("mic_volume", { type: "set_microphone_volume", volume: Y(e) });
1584
+ }
1585
+ /**
1586
+ * Query whether the first wake-up setup wizard has been completed.
1587
+ * Robot-wide, persisted on the robot. Resolves `false` when pending,
1588
+ * `true` when done, or `null` when the channel isn't open / the daemon
1589
+ * predates the `get_first_wake_up` command (callers should fail-open
1590
+ * and skip the wizard on `null`).
1591
+ */
1592
+ getFirstWakeUp() {
1593
+ return !this._dc || this._dc.readyState !== "open" ? Promise.resolve(null) : this._slotRoundtrip("first_wake_up", { type: "get_first_wake_up" });
1594
+ }
1595
+ /**
1596
+ * Persist the first wake-up wizard completion flag on the robot.
1597
+ * Resolves with the stored value (or `null` on channel-closed).
1598
+ */
1599
+ setFirstWakeUp(e) {
1600
+ return !this._dc || this._dc.readyState !== "open" ? Promise.resolve(null) : this._slotRoundtrip("first_wake_up", { type: "set_first_wake_up", is_completed: e });
1601
+ }
1602
+ /**
1603
+ * Query the persisted robot display name. Resolves the stored name,
1604
+ * `null` when none is set / the channel isn't open / the daemon predates
1605
+ * the `get_robot_name` command.
1606
+ */
1607
+ getRobotName() {
1608
+ return this._slotRoundtrip("robot_name", { type: "get_robot_name" });
1609
+ }
1610
+ /**
1611
+ * Set and persist the robot display name on the robot. Resolves with the
1612
+ * stored (trimmed) name, or `null` on error / channel-closed. Applied live
1613
+ * by the daemon (status + central relay + mDNS), so it takes effect right
1614
+ * away without a restart; the persisted name also overrides --robot-name
1615
+ * on the next start.
1616
+ */
1617
+ setRobotName(e) {
1618
+ return this._slotRoundtrip("robot_name", { type: "set_robot_name", name: e });
1619
+ }
1620
+ /**
1621
+ * Sign this robot out of Hugging Face: asks the daemon to delete its
1622
+ * stored HF token, which de-registers the robot from the central
1623
+ * signaling relay (it disappears from its owner's robot list until it
1624
+ * is set up again). Works over the WebRTC data channel, so it reaches
1625
+ * the robot remotely (no LAN HTTP path required).
1626
+ *
1627
+ * Resolves `true` when the daemon acked success, `false` on a daemon
1628
+ * error, or `null` when no ack arrives before the timeout (e.g. a
1629
+ * daemon that predates the `delete_hf_token` command silently drops
1630
+ * it). Rejects if the data channel isn't open. Note the sign-out
1631
+ * drops the central relay, so the session may tear down right after
1632
+ * the ack - callers should treat a post-call session drop as expected,
1633
+ * and a successful sign-out may surface as `null` if teardown races
1634
+ * ahead of the ack.
1635
+ */
1636
+ signOut() {
1637
+ return this._slotRoundtrip("delete_hf_token", { type: "delete_hf_token" });
1638
+ }
1639
+ applyAudioConfig(e, { verify: t = !0 } = {}) {
1640
+ return this._slotRoundtrip("apply_audio_config", { type: "apply_audio_config", config: e, verify: t }).then((n) => n === !0);
1641
+ }
1642
+ readAudioParameter(e) {
1643
+ return this._slotRoundtrip("read_audio_parameter", { type: "read_audio_parameter", name: e });
1644
+ }
1645
+ /**
1646
+ * Internal: send a command and await the matching daemon response in a
1647
+ * named single-resolver slot (see `PendingReplies.slotRoundtrip`).
1648
+ * Every caller has a strict request/response shape where a single
1649
+ * in-flight call per slot is sufficient.
1650
+ */
1651
+ _slotRoundtrip(e, t) {
1652
+ return !this._dc || this._dc.readyState !== "open" ? Promise.reject(new Error("Data channel not open")) : this._pending.slotRoundtrip(e, () => {
1653
+ this._sendCommand(t);
1654
+ });
1655
+ }
1656
+ sendRaw(e) {
1657
+ return this._sendCommand(e);
1658
+ }
1659
+ /**
1660
+ * Generic command round-trip for daemon commands the SDK has no typed
1661
+ * wrapper for (yet). Escape hatch so an app can use a newer daemon
1662
+ * feature without waiting for an SDK release: sends `command` and
1663
+ * resolves with the first robot message whose `command` field equals
1664
+ * the sent `type` - the daemon's reply convention - or with `null` on
1665
+ * the fail-open timeout (daemon predates the command, or the command
1666
+ * is fire-and-forget and never replies).
1667
+ *
1668
+ * Rejects when the data channel isn't open or the session tears down
1669
+ * mid-flight, mirroring the typed wrappers.
1670
+ *
1671
+ * Replies the SDK already consumes internally (`get_imu`,
1672
+ * `get_volume`, ...) are swallowed by their own handlers and never
1673
+ * reach this matcher - use the typed wrappers for those. Pass `match`
1674
+ * for replies that don't follow the `command` echo convention.
1675
+ */
1676
+ request(e, { timeoutMs: t = ee, match: n } = {}) {
1677
+ if (!this._sendCommand(e))
1678
+ return Promise.reject(new Error("Data channel not open"));
1679
+ const r = n ?? ((i) => i.command === e.type);
1680
+ return this._pending.awaitBroadcast(r, { timeoutMs: t, debugLabel: `request(${e.type})` }).catch((i) => {
1681
+ if (i instanceof P) return null;
1682
+ throw i;
1683
+ });
1684
+ }
1685
+ subscribeLogs({ onLine: e, onError: t }) {
1686
+ if (typeof e != "function")
1687
+ throw new TypeError("subscribeLogs: onLine callback is required");
1688
+ const n = { onLine: e, onError: t }, r = this._logSubscribers.size === 0;
1689
+ this._logSubscribers.add(n), r && this._sendCommand({ type: "subscribe_logs" });
1690
+ let i = !1;
1691
+ return () => {
1692
+ i || (i = !0, this._logSubscribers.delete(n), this._logSubscribers.size === 0 && this._sendCommand({ type: "unsubscribe_logs" }));
1693
+ };
1694
+ }
1695
+ requestState() {
1696
+ return this._sendCommand({ type: "get_state" });
1697
+ }
1698
+ /**
1699
+ * Ask the daemon to *push* the robot state (~30 Hz) over the dedicated
1700
+ * unreliable/unordered `pose` data channel instead of polling get_state.
1701
+ * Fires `state` events as frames arrive. No-op against an older daemon (no
1702
+ * pose channel) - fall back to `requestState()` polling there.
1703
+ *
1704
+ * Refcounted: pair every `subscribePose()` with exactly one
1705
+ * `unsubscribePose()`. Multiple consumers share a single daemon-side
1706
+ * subscription; the daemon only stops pushing once the last one releases.
1707
+ * If the channel isn't open yet (or the session later reconnects), the
1708
+ * subscription is (re-)asserted from `_checkSessionReady`.
1709
+ */
1710
+ subscribePose() {
1711
+ return this._poseSubRefs++, this._sendCommand({ type: "subscribe_pose" });
1712
+ }
1713
+ /** Release one pose-stream consumer; sends `unsubscribe_pose` on the last. */
1714
+ unsubscribePose() {
1715
+ return this._poseSubRefs > 0 && this._poseSubRefs--, this._poseSubRefs > 0 ? !0 : this._sendCommand({ type: "unsubscribe_pose" });
1716
+ }
1717
+ // ─── Audio ───────────────────────────────────────────────────────────
1718
+ setAudioMuted(e) {
1719
+ this._audioMuted = e, this._videoElement && (this._videoElement.muted = e);
1720
+ }
1721
+ setMicMuted(e) {
1722
+ this._micMuted = e, this._micStream && this._micStream.getAudioTracks().forEach((t) => {
1723
+ t.enabled = !e;
1724
+ });
1725
+ }
1726
+ // ─── Video helper ────────────────────────────────────────────────────
1727
+ attachVideo(e) {
1728
+ this._videoElement = e, e.muted = this._audioMuted;
1729
+ const t = (r) => {
1730
+ const i = r;
1731
+ e.srcObject = i.detail.stream, e.playsInline = !0, "requestVideoFrameCallback" in e && this._startLatencyMonitor(e);
1732
+ }, n = () => {
1733
+ e.srcObject = null;
1734
+ };
1735
+ return this.addEventListener("videoTrack", t), this.addEventListener("sessionStopped", n), () => {
1736
+ this.removeEventListener("videoTrack", t), this.removeEventListener("sessionStopped", n), this._latencyMonitorId && (clearInterval(this._latencyMonitorId), this._latencyMonitorId = null), e.srcObject = null, this._videoElement = null;
1737
+ };
1738
+ }
1739
+ // ─── Daemon-side recorded-move playback ──────────────────────────────
1740
+ async playMove(e, {
1741
+ audioBlob: t = null,
1742
+ audioLeadMs: n = -100,
1743
+ description: r = "move",
1744
+ encoding: i = "gzip+base64",
1745
+ playFrequency: o = 100,
1746
+ initialGotoDuration: a = 0,
1747
+ startTimeoutMs: c = 8e3,
1748
+ onProgress: d = () => {
1749
+ },
1750
+ onStarted: u = () => {
1751
+ }
1752
+ } = {}) {
1753
+ if (!this._dc || this._dc.readyState !== "open")
1754
+ throw new Error("data channel not open");
1755
+ if (!e?.time?.length || !e?.set_target_data?.length)
1756
+ throw new Error("playMove: motion must have time + set_target_data");
1757
+ const l = K();
1758
+ this._activeMoveUploadId = l;
1759
+ const m = {
1760
+ description: r,
1761
+ time: e.time,
1762
+ set_target_data: e.set_target_data
1763
+ }, g = JSON.stringify(m);
1764
+ let f, v;
1765
+ i === "gzip+base64" && Ce() ? (f = await Me(g), v = "gzip+base64") : (f = g, v = "json");
1766
+ const w = Math.ceil(f.length / y) || 1;
1767
+ d({
1768
+ phase: "starting",
1769
+ sent: 0,
1770
+ total: w,
1771
+ bytes: f.length,
1772
+ encoding: v
1773
+ }), this._sendCommand({
1774
+ type: "upload_move_start",
1775
+ upload_id: l,
1776
+ total_chunks: w,
1777
+ description: r,
1778
+ encoding: v
1779
+ });
1780
+ for (let h = 0; h < w; h++) {
1781
+ this._dc.bufferedAmount > U && await this._awaitDataChannelDrain();
1782
+ const p = h * y;
1783
+ this._sendCommand({
1784
+ type: "upload_move_chunk",
1785
+ upload_id: l,
1786
+ chunk_index: h,
1787
+ chunk: f.slice(p, p + y)
1788
+ }), d({ phase: "upload", sent: h + 1, total: w });
1789
+ }
1790
+ if (this._sendCommand({ type: "upload_move_finish", upload_id: l }), d({ phase: "uploaded", sent: w, total: w }), t) {
1791
+ const h = new Uint8Array(await t.arrayBuffer()), p = N(h), R = Math.ceil(p.length / y) || 1;
1792
+ d({
1793
+ phase: "audio-starting",
1794
+ sent: 0,
1795
+ total: R,
1796
+ bytes: p.length
1797
+ }), this._sendCommand({
1798
+ type: "upload_audio_start",
1799
+ upload_id: l,
1800
+ total_chunks: R,
1801
+ encoding: X(t),
1802
+ description: r
1803
+ });
1804
+ for (let T = 0; T < R; T++) {
1805
+ this._dc.bufferedAmount > U && await this._awaitDataChannelDrain();
1806
+ const W = T * y;
1807
+ this._sendCommand({
1808
+ type: "upload_audio_chunk",
1809
+ upload_id: l,
1810
+ chunk_index: T,
1811
+ chunk: p.slice(W, W + y)
1812
+ }), d({ phase: "audio-upload", sent: T + 1, total: R });
1813
+ }
1814
+ this._sendCommand({ type: "upload_audio_finish", upload_id: l }), d({ phase: "audio-uploaded", sent: R, total: R });
1815
+ }
1816
+ this._sendCommand({
1817
+ type: "play_uploaded_move",
1818
+ upload_id: l,
1819
+ play_frequency: o,
1820
+ initial_goto_duration: a,
1821
+ audio_lead_ms: n
1822
+ });
1823
+ let b;
1824
+ try {
1825
+ b = await this._waitForBroadcast(
1826
+ (h) => h?.type === "play_uploaded_move" && h?.upload_id === l && (h.started === !0 || typeof h.error == "string"),
1827
+ { timeoutMs: c, debugLabel: "play_uploaded_move started" }
1828
+ );
1829
+ } catch (h) {
1830
+ throw new Error(
1831
+ `Daemon did not respond to play_uploaded_move (requires the reachy_mini daemon with feature/daemon-side-move-upload). Underlying: ${h.message}`
1832
+ );
1833
+ }
1834
+ if (typeof b.error == "string")
1835
+ throw new Error(`play_uploaded_move: ${b.error}`);
1836
+ try {
1837
+ u({
1838
+ duration_s: b.duration_s,
1839
+ has_audio: b.has_audio === !0
1840
+ });
1841
+ } catch (h) {
1842
+ _.warn("playMove.onStarted threw:", h);
1843
+ }
1844
+ d({ phase: "playing", duration_s: b.duration_s });
1845
+ const E = await this._waitForBroadcast(
1846
+ (h) => h?.type === "play_uploaded_move" && h?.upload_id === l && (h.finished === !0 || h.cancelled === !0 || typeof h.error == "string"),
1847
+ {
1848
+ timeoutMs: (b.duration_s + 30) * 1e3,
1849
+ debugLabel: "play_uploaded_move final"
1850
+ }
1851
+ );
1852
+ return this._activeMoveUploadId === l && (this._activeMoveUploadId = null), E;
1853
+ }
1854
+ cancelMove(e = null) {
1855
+ const t = e ?? this._activeMoveUploadId;
1856
+ return t ? this._sendCommand({ type: "cancel_move", upload_id: t }) : !1;
1857
+ }
1858
+ async uploadAudio(e, { description: t = "audio", onProgress: n = () => {
1859
+ } } = {}) {
1860
+ if (!this._dc || this._dc.readyState !== "open")
1861
+ throw new Error("data channel not open");
1862
+ if (!(e instanceof Blob))
1863
+ throw new TypeError("uploadAudio: expected a Blob");
1864
+ const r = K(), i = new Uint8Array(await e.arrayBuffer()), o = N(i), a = Math.ceil(o.length / y) || 1;
1865
+ n({ phase: "audio-starting", sent: 0, total: a, bytes: o.length }), this._sendCommand({
1866
+ type: "upload_audio_start",
1867
+ upload_id: r,
1868
+ total_chunks: a,
1869
+ encoding: X(e),
1870
+ description: t
1871
+ });
1872
+ for (let c = 0; c < a; c++) {
1873
+ this._dc.bufferedAmount > U && await this._awaitDataChannelDrain();
1874
+ const d = c * y;
1875
+ this._sendCommand({
1876
+ type: "upload_audio_chunk",
1877
+ upload_id: r,
1878
+ chunk_index: c,
1879
+ chunk: o.slice(d, d + y)
1880
+ }), n({ phase: "audio-upload", sent: c + 1, total: a });
1881
+ }
1882
+ return this._sendCommand({ type: "upload_audio_finish", upload_id: r }), n({ phase: "audio-uploaded", sent: a, total: a }), r;
1883
+ }
1884
+ async playUploadedAudio(e, { timeoutMs: t = 8e3 } = {}) {
1885
+ if (!this._dc || this._dc.readyState !== "open")
1886
+ throw new Error("data channel not open");
1887
+ const n = this._waitForBroadcast(
1888
+ (i) => i?.type === "play_uploaded_audio" && i?.upload_id === e && (i.started === !0 || typeof i.error == "string"),
1889
+ { timeoutMs: t, debugLabel: "play_uploaded_audio started" }
1890
+ );
1891
+ this._sendCommand({ type: "play_uploaded_audio", upload_id: e });
1892
+ const r = await n;
1893
+ if (typeof r.error == "string") throw new Error(r.error);
1894
+ return this._activeAudioUploadId = e, r;
1895
+ }
1896
+ cancelAudio(e = null) {
1897
+ const t = e ?? this._activeAudioUploadId;
1898
+ return t ? (this._activeAudioUploadId === t && (this._activeAudioUploadId = null), this._sendCommand({ type: "cancel_audio", upload_id: t })) : !1;
1899
+ }
1900
+ // ─── Private ─────────────────────────────────────────────────────────
1901
+ _emit(e, t) {
1902
+ this.dispatchEvent(new CustomEvent(e, { detail: t }));
1903
+ }
1904
+ _waitForBroadcast(e, t = {}) {
1905
+ return this._pending.awaitBroadcast(e, t);
1906
+ }
1907
+ async _awaitDataChannelDrain() {
1908
+ for (; this._dc && this._dc.bufferedAmount > Ae; )
1909
+ if (await new Promise((e) => setTimeout(e, 30)), !this._dc || this._dc.readyState !== "open")
1910
+ throw new Error("data channel closed mid-upload");
1911
+ }
1912
+ async _sendToServer(e) {
1913
+ if (!this._token) throw new Error("No token — authenticate() first");
1914
+ try {
1915
+ const t = await fetch(`${this._signalingUrl}/send`, {
1916
+ method: "POST",
1917
+ headers: {
1918
+ "Content-Type": "application/json",
1919
+ Authorization: `Bearer ${this._token}`
1920
+ },
1921
+ body: JSON.stringify(e)
1922
+ });
1923
+ if (!t.ok) {
1924
+ let n = "";
1925
+ try {
1926
+ n = await t.text();
1927
+ } catch {
1928
+ }
1929
+ return _.warn(
1930
+ `/send rejected (${t.status}) for type=${e?.type}; body=${n || "<empty>"}`
1931
+ ), null;
1932
+ }
1933
+ return await t.json();
1934
+ } catch (t) {
1935
+ return _.error("signaling /send failed:", t), null;
1936
+ }
1937
+ }
1938
+ _sendCommand(e) {
1939
+ return !this._dc || this._dc.readyState !== "open" ? !1 : (this._dc.send(JSON.stringify(e)), !0);
1940
+ }
1941
+ /**
1942
+ * Call a JSON-RPC method on the robot/app over the DataChannel and await
1943
+ * its result. This is the one way to drive an on-robot app (start/stop it
1944
+ * via `apps.*`, or drive a running app via its own namespace, e.g.
1945
+ * `conversation.say`). Rejects on the JSON-RPC error, a closed channel, or
1946
+ * timeout.
1947
+ */
1948
+ rpcCall(e, t = {}, n = {}) {
1949
+ const r = n.timeoutMs ?? 2e4;
1950
+ return this._pending.rpcRoundtrip(
1951
+ e,
1952
+ r,
1953
+ (i) => this._sendCommand({ jsonrpc: "2.0", id: i, method: e, params: t })
1954
+ );
1955
+ }
1956
+ /**
1957
+ * Subscribe to a JSON-RPC notification (one-way event) pushed by the
1958
+ * robot/app, e.g. `conversation.turn`. Returns an unsubscribe function.
1959
+ */
1960
+ onNotification(e, t) {
1961
+ let n = this._rpcListeners.get(e);
1962
+ return n || (n = /* @__PURE__ */ new Set(), this._rpcListeners.set(e, n)), n.add(t), () => {
1963
+ this._rpcListeners.get(e)?.delete(t);
1964
+ };
1965
+ }
1966
+ _handleRpcMessage(e) {
1967
+ if (!this._pending.settleRpcResponse(e) && typeof e.method == "string") {
1968
+ const t = e.params ?? {};
1969
+ for (const n of this._rpcListeners.get(e.method) ?? [])
1970
+ try {
1971
+ n(t);
1972
+ } catch (r) {
1973
+ _.error(`onNotification(${e.method}) threw:`, r);
1974
+ }
1975
+ }
1976
+ }
1977
+ _checkSessionReady() {
1978
+ this._iceConnected && this._dcOpen && this._sessionResolve && (this._state = "streaming", this.requestState(), this._poseSubRefs > 0 && this._sendCommand({ type: "subscribe_pose" }), this._lastPoseFrameAt = 0, this._stateRefreshInterval = setInterval(() => {
1979
+ Date.now() - this._lastPoseFrameAt < Q || this.requestState();
1980
+ }, 500), this._supervisor.startDcWatchdog(), this._emit("streaming", { sessionId: this._sessionId, robotId: this._selectedRobotId }), this._sessionResolve(), this._sessionResolve = null, this._sessionReject = null);
1981
+ }
1982
+ async _handleSignalingMessage(e) {
1983
+ switch (e.type) {
1984
+ case "welcome":
1985
+ break;
1986
+ case "list":
1987
+ this._robots = e.producers || [], this._emit("robotsChanged", { robots: this._robots }), this._maybeAutoStart();
1988
+ break;
1989
+ case "peerStatusChanged": {
1990
+ const t = await this._sendToServer({ type: "list" });
1991
+ t?.producers && (this._robots = t.producers, this._emit("robotsChanged", { robots: this._robots }), this._maybeAutoStart());
1992
+ break;
1993
+ }
1994
+ case "sessionStarted":
1995
+ this._sessionId = e.sessionId ?? null;
1996
+ break;
1997
+ case "sessionRejected":
1998
+ this._failSessionRejected(e);
1999
+ break;
2000
+ case "endSession":
2001
+ this._handleEndSession(e);
2002
+ break;
2003
+ case "peer":
2004
+ this._handlePeerMessage(e);
2005
+ break;
2006
+ }
2007
+ }
2008
+ _handleEndSession(e) {
2009
+ const t = e.reason, n = t === "robot_busy_local_app" ? "Robot is busy: a local Python app is running" : t === "local_app_started" ? "Disconnected: a local Python app started on the robot" : t === "robot_busy_local" ? "Robot is busy: another session is already active" : null;
2010
+ if (this._sessionReject) {
2011
+ const r = new Error(
2012
+ n || `Session ended before it could start: ${t || "unknown reason"}`
2013
+ );
2014
+ r.reason = t ?? null, this._supervisor.redialing || this._emit("sessionRejected", { reason: t, activeApp: null }), this._supervisor.clearIceGrace(), this._supervisor.uninstallNetworkListeners(), this._pc && (this._pc.close(), this._pc = null), this._micStream && (this._micStream.getTracks().forEach((o) => o.stop()), this._micStream = null), this._iceConnected = !1, this._dcOpen = !1, this._micMuted = !0, this._micSupported = !1;
2015
+ const i = this._sessionReject;
2016
+ this._sessionResolve = null, this._sessionReject = null, i(r);
2017
+ return;
2018
+ }
2019
+ this._state === "streaming" && (this._emit("sessionStopped", {
2020
+ reason: t || "remote_end",
2021
+ message: n
2022
+ }), this.stopSession().catch(() => {
2023
+ }));
2024
+ }
2025
+ async _handlePeerMessage(e) {
2026
+ if (this._pc)
2027
+ try {
2028
+ if (e.sdp) {
2029
+ const t = e.sdp;
2030
+ if (t.type === "offer") {
2031
+ const r = ve(t.sdp);
2032
+ if (this._micSupported = r, this._emit("micSupported", { supported: r }), r && this._micStream)
2033
+ for (const o of this._micStream.getAudioTracks())
2034
+ this._pc.addTrack(o, this._micStream);
2035
+ await this._pc.setRemoteDescription(new RTCSessionDescription(t));
2036
+ const i = await this._pc.createAnswer();
2037
+ await this._pc.setLocalDescription(i), await this._sendToServer({
2038
+ type: "peer",
2039
+ sessionId: this._sessionId,
2040
+ sdp: { type: "answer", sdp: i.sdp }
2041
+ });
2042
+ } else
2043
+ await this._pc.setRemoteDescription(new RTCSessionDescription(t));
2044
+ const n = this._pendingRemoteIce;
2045
+ if (n && n.length) {
2046
+ this._pendingRemoteIce = [];
2047
+ for (const r of n)
2048
+ try {
2049
+ await this._pc.addIceCandidate(new RTCIceCandidate(r));
2050
+ } catch (i) {
2051
+ _.warn("buffered ICE candidate rejected:", i);
2052
+ }
2053
+ }
2054
+ }
2055
+ if (e.ice) {
2056
+ if (!e.ice.candidate) return;
2057
+ this._pc.remoteDescription ? await this._pc.addIceCandidate(new RTCIceCandidate(e.ice)) : (this._pendingRemoteIce || (this._pendingRemoteIce = []), this._pendingRemoteIce.push(e.ice));
2058
+ }
2059
+ } catch (t) {
2060
+ _.error("webrtc signaling failed:", t), this._emit("error", { source: "webrtc", error: t });
2061
+ }
2062
+ }
2063
+ /**
2064
+ * @param fromPoseStream Frame came from the pushed `pose` channel rather
2065
+ * than the reliable control channel. Only those may refresh the state
2066
+ * mirror while the stream is live (see the `data.state` branch).
2067
+ */
2068
+ _handleRobotMessage(e, t = !1) {
2069
+ if (this._supervisor.stampDcInbound(), e.jsonrpc === "2.0") {
2070
+ this._handleRpcMessage(e);
2071
+ return;
2072
+ }
2073
+ if (!("version" in e && this._pending.settleReplySlot("version", e.version ?? null)) && !("hardware_id" in e && this._pending.settleReplySlot("hardware_id", e.hardware_id ?? null))) {
2074
+ if (e.command === "get_volume" || e.command === "set_volume") {
2075
+ this._pending.settleReplySlot("volume", e.status === "error" ? null : e.volume);
2076
+ return;
2077
+ }
2078
+ if (e.command === "get_microphone_volume" || e.command === "set_microphone_volume") {
2079
+ this._pending.settleReplySlot("mic_volume", e.status === "error" ? null : e.volume);
2080
+ return;
2081
+ }
2082
+ if (e.command === "get_first_wake_up" || e.command === "set_first_wake_up") {
2083
+ this._pending.settleReplySlot(
2084
+ "first_wake_up",
2085
+ e.status === "error" ? null : !!e.is_completed
2086
+ );
2087
+ return;
2088
+ }
2089
+ if (e.command === "get_robot_name" || e.command === "set_robot_name") {
2090
+ this._pending.settleReplySlot(
2091
+ "robot_name",
2092
+ e.status === "error" ? null : e.name ?? null
2093
+ );
2094
+ return;
2095
+ }
2096
+ if (e.command === "delete_hf_token") {
2097
+ this._pending.settleReplySlot("delete_hf_token", e.status !== "error");
2098
+ return;
2099
+ }
2100
+ if (e.command === "apply_audio_config") {
2101
+ this._pending.settleReplySlot("apply_audio_config", e.error ? !1 : !!e.applied);
2102
+ return;
2103
+ }
2104
+ if (e.command === "read_audio_parameter") {
2105
+ this._pending.settleReplySlot(
2106
+ "read_audio_parameter",
2107
+ e.error ? null : e.values ?? null
2108
+ );
2109
+ return;
2110
+ }
2111
+ if (e.command === "get_tracked_face") {
2112
+ this._pending.settleReplySlot("tracked_face", e.face_target ?? null);
2113
+ return;
2114
+ }
2115
+ if (e.command === "get_imu") {
2116
+ this._pending.settleReplySlot("imu", e.imu ?? null);
2117
+ return;
2118
+ }
2119
+ if (!((e.command === "wake_up" || e.command === "goto_sleep") && this._pending.settleMotion(e.command, e))) {
2120
+ if (e.type === "log_line") {
2121
+ for (const n of this._logSubscribers)
2122
+ try {
2123
+ n.onLine({ timestamp: e.timestamp, line: e.line });
2124
+ } catch (r) {
2125
+ _.error("subscribeLogs onLine threw:", r);
2126
+ }
2127
+ return;
2128
+ }
2129
+ if (e.type === "log_stream_error") {
2130
+ for (const n of this._logSubscribers)
2131
+ if (typeof n.onError == "function")
2132
+ try {
2133
+ n.onError(e.error);
2134
+ } catch (r) {
2135
+ _.error("subscribeLogs onError threw:", r);
2136
+ }
2137
+ return;
2138
+ }
2139
+ if (e.command === "start_update") {
2140
+ if (typeof e.error == "string") {
2141
+ const n = { status: "failed", error: e.error };
2142
+ for (const r of this._updateProgressSubscribers)
2143
+ try {
2144
+ r(n);
2145
+ } catch (i) {
2146
+ _.error("startDaemonUpdate onProgress threw:", i);
2147
+ }
2148
+ }
2149
+ return;
2150
+ }
2151
+ if (e.type === "update_progress") {
2152
+ const n = {
2153
+ status: e.status,
2154
+ line: typeof e.line == "string" ? e.line : void 0,
2155
+ error: typeof e.error == "string" ? e.error : void 0
2156
+ };
2157
+ for (const r of this._updateProgressSubscribers)
2158
+ try {
2159
+ r(n);
2160
+ } catch (i) {
2161
+ _.error("startDaemonUpdate onProgress threw:", i);
2162
+ }
2163
+ return;
2164
+ }
2165
+ if (e.state && (t || Date.now() - this._lastPoseFrameAt >= Q)) {
2166
+ const n = e.state;
2167
+ n.head_pose && (this._robotState.head = n.head_pose.flat()), n.antennas && (this._robotState.antennas = [n.antennas[0], n.antennas[1]]), n.head_joint_positions && (this._robotState.head_joint_positions = n.head_joint_positions), typeof n.body_yaw == "number" && (this._robotState.body_yaw = n.body_yaw), n.motor_mode && (this._robotState.motor_mode = n.motor_mode), typeof n.is_move_running == "boolean" && (this._robotState.is_move_running = n.is_move_running), n.face_target && (this._robotState.face_target = n.face_target), "doa" in n && (this._robotState.doa = n.doa ?? void 0), this._emit("state", { ...this._robotState });
2168
+ }
2169
+ e.error && this._emit("error", { source: "robot", error: e.error }), this._pending.matchBroadcast(e);
2170
+ }
2171
+ }
2172
+ }
2173
+ /** Snap video playback to live edge if buffered lag exceeds 0.5 s. */
2174
+ _startLatencyMonitor(e) {
2175
+ this._latencyMonitorId && clearInterval(this._latencyMonitorId), this._latencyMonitorId = setInterval(() => {
2176
+ if (!e.srcObject || e.paused) return;
2177
+ const t = e.buffered;
2178
+ if (t.length > 0) {
2179
+ const n = t.end(t.length - 1), r = n - e.currentTime;
2180
+ r > 0.5 && (_.debug(`video latency correction: was ${r.toFixed(2)}s behind`), e.currentTime = n - 0.1);
2181
+ }
2182
+ }, 2e3);
2183
+ }
2184
+ }
2185
+ const Le = "pollen-robotics/reachy_mini", Ue = `https://api.github.com/repos/${Le}/releases/latest`, te = "reachy.daemonLatestRelease.v1", Pe = 360 * 60 * 1e3, ne = "1.8.2";
2186
+ function k(s) {
2187
+ if (typeof s != "string") return null;
2188
+ const e = s.trim().replace(/^v/i, "").match(/^(\d+)\.(\d+)\.(\d+)/);
2189
+ return e ? {
2190
+ major: Number(e[1]),
2191
+ minor: Number(e[2]),
2192
+ patch: Number(e[3])
2193
+ } : null;
2194
+ }
2195
+ function H(s, e) {
2196
+ return s.major - e.major || s.minor - e.minor || s.patch - e.patch;
2197
+ }
2198
+ function $e(s, e) {
2199
+ const t = k(s), n = k(e);
2200
+ return !t || !n ? !1 : H(t, n) < 0;
2201
+ }
2202
+ function We(s, e) {
2203
+ const t = k(s);
2204
+ if (!t) return null;
2205
+ const n = k(ne);
2206
+ return n && H(t, n) < 0 ? "block" : $e(s, e) ? "notice" : null;
2207
+ }
2208
+ function Be(s) {
2209
+ const e = k(s), t = k(ne);
2210
+ return !!e && !!t && H(e, t) >= 0;
2211
+ }
2212
+ function Ne() {
2213
+ try {
2214
+ const s = localStorage.getItem(te);
2215
+ if (!s) return null;
2216
+ const e = JSON.parse(s);
2217
+ return typeof e.at != "number" ? null : {
2218
+ value: typeof e.value == "string" ? e.value : null,
2219
+ at: e.at
2220
+ };
2221
+ } catch {
2222
+ return null;
2223
+ }
2224
+ }
2225
+ async function qe() {
2226
+ const s = Ne();
2227
+ if (s && Date.now() - s.at < Pe) return s.value;
2228
+ try {
2229
+ const e = await fetch(Ue, {
2230
+ headers: { Accept: "application/vnd.github+json" }
2231
+ });
2232
+ if (!e.ok)
2233
+ return s?.value ?? null;
2234
+ const t = await e.json(), n = typeof t.tag_name == "string" ? t.tag_name : null, r = n ? n.replace(/^v/i, "") : null;
2235
+ try {
2236
+ localStorage.setItem(te, JSON.stringify({ value: r, at: Date.now() }));
2237
+ } catch {
2238
+ }
2239
+ return r;
2240
+ } catch {
2241
+ return s?.value ?? null;
2242
+ }
2243
+ }
2244
+ const Oe = 8e3;
2245
+ function je(s) {
2246
+ if (!s) return null;
2247
+ const e = s.id ?? s.peerId ?? s.peer_id;
2248
+ return typeof e == "string" && e.length > 0 ? e : null;
2249
+ }
2250
+ function Fe(s) {
2251
+ const e = je(s);
2252
+ if (!e) return null;
2253
+ const t = s.meta?.name ?? s.name, n = typeof s.meta?.transport == "string" && s.meta.transport.length > 0 ? s.meta.transport : null, r = typeof s.meta?.hardware_id == "string" && s.meta.hardware_id.length > 0 ? s.meta.hardware_id : null;
2254
+ return {
2255
+ id: e,
2256
+ meta: t ? { name: t } : void 0,
2257
+ busy: s.busy === !0,
2258
+ activeApp: typeof s.activeApp == "string" && s.activeApp.trim().length > 0 ? s.activeApp : null,
2259
+ transport: n,
2260
+ hardwareId: r
2261
+ };
2262
+ }
2263
+ async function Ge(s) {
2264
+ const { signalingUrl: e, hfToken: t } = s;
2265
+ if (!t) return { ok: !1, robots: [], reason: "No HF token" };
2266
+ if (!e)
2267
+ return { ok: !1, robots: [], reason: "No signaling URL" };
2268
+ const n = `${e.replace(/\/$/, "")}/api/robot-status`, r = new AbortController(), i = setTimeout(() => r.abort(), Oe), o = s.signal ? xe([r.signal, s.signal]) : r.signal;
2269
+ try {
2270
+ const a = await fetch(n, {
2271
+ method: "GET",
2272
+ headers: {
2273
+ Authorization: `Bearer ${t}`,
2274
+ Accept: "application/json"
2275
+ },
2276
+ signal: o
2277
+ });
2278
+ if (a.status === 401 || a.status === 403)
2279
+ return { ok: !1, robots: [], reason: "Token rejected by Hugging Face" };
2280
+ if (!a.ok)
2281
+ return {
2282
+ ok: !1,
2283
+ robots: [],
2284
+ reason: `Central returned HTTP ${a.status}`
2285
+ };
2286
+ const c = await a.json();
2287
+ return { ok: !0, robots: (Array.isArray(c.robots) ? c.robots : []).map(Fe).filter((l) => l != null) };
2288
+ } catch (a) {
2289
+ if (a instanceof DOMException && a.name === "AbortError")
2290
+ return { ok: !1, robots: [], reason: "Central request timed out" };
2291
+ const c = a instanceof Error ? a.message : String(a);
2292
+ return { ok: !1, robots: [], reason: `Network error: ${c}` };
2293
+ } finally {
2294
+ clearTimeout(i);
2295
+ }
2296
+ }
2297
+ function xe(s) {
2298
+ const e = new AbortController();
2299
+ for (const t of s) {
2300
+ if (t.aborted)
2301
+ return e.abort(), e.signal;
2302
+ t.addEventListener("abort", () => e.abort(), { once: !0 });
2303
+ }
2304
+ return e.signal;
2305
+ }
2306
+ export {
2307
+ ne as M,
2308
+ He as R,
2309
+ Ge as a,
2310
+ O as c,
2311
+ We as d,
2312
+ qe as f,
2313
+ k as p,
2314
+ Be as s
2315
+ };