@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.
- package/APP_CREATION_GUIDE.md +98 -0
- package/dist/lib/central-listener.d.ts +71 -0
- package/dist/lib/central-listener.d.ts.map +1 -0
- package/dist/lib/central-listener.js +277 -0
- package/dist/lib/central-listener.js.map +1 -0
- package/dist/lib/central-listener.test.d.ts +2 -0
- package/dist/lib/central-listener.test.d.ts.map +1 -0
- package/dist/lib/central-listener.test.js +378 -0
- package/dist/lib/central-listener.test.js.map +1 -0
- package/dist/lib/dc-watchdog.test.d.ts +2 -0
- package/dist/lib/dc-watchdog.test.d.ts.map +1 -0
- package/dist/lib/dc-watchdog.test.js +154 -0
- package/dist/lib/dc-watchdog.test.js.map +1 -0
- package/dist/lib/ensure-awake.test.d.ts +2 -0
- package/dist/lib/ensure-awake.test.d.ts.map +1 -0
- package/dist/lib/ensure-awake.test.js +117 -0
- package/dist/lib/ensure-awake.test.js.map +1 -0
- package/dist/lib/ice-grace.test.d.ts +2 -0
- package/dist/lib/ice-grace.test.d.ts.map +1 -0
- package/dist/lib/ice-grace.test.js +211 -0
- package/dist/lib/ice-grace.test.js.map +1 -0
- package/dist/lib/imu.test.d.ts +2 -0
- package/dist/lib/imu.test.d.ts.map +1 -0
- package/dist/lib/imu.test.js +78 -0
- package/dist/lib/imu.test.js.map +1 -0
- package/dist/lib/logger.d.ts +38 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +81 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/pending-replies.d.ts +127 -0
- package/dist/lib/pending-replies.d.ts.map +1 -0
- package/dist/lib/pending-replies.js +255 -0
- package/dist/lib/pending-replies.js.map +1 -0
- package/dist/lib/pending-replies.test.d.ts +2 -0
- package/dist/lib/pending-replies.test.d.ts.map +1 -0
- package/dist/lib/pending-replies.test.js +217 -0
- package/dist/lib/pending-replies.test.js.map +1 -0
- package/dist/lib/reachy-mini.d.ts +202 -88
- package/dist/lib/reachy-mini.d.ts.map +1 -1
- package/dist/lib/reachy-mini.js +651 -566
- package/dist/lib/reachy-mini.js.map +1 -1
- package/dist/lib/redial.test.d.ts +2 -0
- package/dist/lib/redial.test.d.ts.map +1 -0
- package/dist/lib/redial.test.js +206 -0
- package/dist/lib/redial.test.js.map +1 -0
- package/dist/lib/request.test.d.ts +2 -0
- package/dist/lib/request.test.d.ts.map +1 -0
- package/dist/lib/request.test.js +90 -0
- package/dist/lib/request.test.js.map +1 -0
- package/dist/lib/session-supervisor.d.ts +201 -0
- package/dist/lib/session-supervisor.d.ts.map +1 -0
- package/dist/lib/session-supervisor.js +520 -0
- package/dist/lib/session-supervisor.js.map +1 -0
- package/dist/lib/token-store.d.ts +50 -0
- package/dist/lib/token-store.d.ts.map +1 -0
- package/dist/lib/token-store.js +120 -0
- package/dist/lib/token-store.js.map +1 -0
- package/dist/lib/token-store.test.d.ts +2 -0
- package/dist/lib/token-store.test.d.ts.map +1 -0
- package/dist/lib/token-store.test.js +82 -0
- package/dist/lib/token-store.test.js.map +1 -0
- package/dist/lib/types.d.ts +280 -6
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/url-helpers.d.ts.map +1 -1
- package/dist/lib/url-helpers.js +3 -1
- package/dist/lib/url-helpers.js.map +1 -1
- package/dist/lib/version.d.ts +2 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +5 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/reachy-mini-sdk.d.ts +15 -1
- package/dist/reachy-mini-sdk.d.ts.map +1 -1
- package/dist/reachy-mini-sdk.js +12 -0
- package/dist/reachy-mini-sdk.js.map +1 -1
- package/host/dist/ReachyHost.d.ts.map +1 -1
- package/host/dist/chunks/centralRest-uXNMidlL.js +2315 -0
- package/host/dist/chunks/index-Beu4GNzi.js +821 -0
- package/host/dist/chunks/{mountHost-DjE5zE9R.js → mountHost-BTTF4Uu9.js} +24933 -27488
- package/host/dist/components/ConnectingView.d.ts.map +1 -1
- package/host/dist/components/DaemonUpdateGate.d.ts +65 -0
- package/host/dist/components/DaemonUpdateGate.d.ts.map +1 -0
- package/host/dist/components/ErrorView.d.ts.map +1 -1
- package/host/dist/components/HostErrorBoundary.d.ts +30 -0
- package/host/dist/components/HostErrorBoundary.d.ts.map +1 -0
- package/host/dist/components/LeavingView.d.ts.map +1 -1
- package/host/dist/components/PickerView.d.ts +16 -14
- package/host/dist/components/PickerView.d.ts.map +1 -1
- package/host/dist/components/PostOAuthSplash.d.ts +14 -9
- package/host/dist/components/PostOAuthSplash.d.ts.map +1 -1
- package/host/dist/components/ReachyHostShell.d.ts.map +1 -1
- package/host/dist/components/SignInView.d.ts.map +1 -1
- package/host/dist/components/TopBar.d.ts +5 -6
- package/host/dist/components/TopBar.d.ts.map +1 -1
- package/host/dist/components/WelcomeBackOverlay.d.ts.map +1 -1
- package/host/dist/embed/index.d.ts +68 -5
- package/host/dist/embed/index.d.ts.map +1 -1
- package/host/dist/entry/auto.js +2 -2
- package/host/dist/entry/embed.d.ts +5 -4
- package/host/dist/entry/embed.d.ts.map +1 -1
- package/host/dist/entry/embed.js +5 -4
- package/host/dist/hooks/useHostBridge.d.ts +35 -1
- package/host/dist/hooks/useHostBridge.d.ts.map +1 -1
- package/host/dist/hooks/useLatestDaemonVersion.d.ts +7 -0
- package/host/dist/hooks/useLatestDaemonVersion.d.ts.map +1 -0
- package/host/dist/hooks/useOAuth.d.ts +18 -0
- package/host/dist/hooks/useOAuth.d.ts.map +1 -1
- package/host/dist/hooks/useRobots.d.ts +0 -2
- package/host/dist/hooks/useRobots.d.ts.map +1 -1
- package/host/dist/index.d.ts +2 -2
- package/host/dist/index.d.ts.map +1 -1
- package/host/dist/index.js +11 -10
- package/host/dist/lib/daemonRelease.d.ts +84 -0
- package/host/dist/lib/daemonRelease.d.ts.map +1 -0
- package/host/dist/lib/protocol.d.ts +141 -7
- package/host/dist/lib/protocol.d.ts.map +1 -1
- package/host/dist/lib/rebootWatch.d.ts +51 -0
- package/host/dist/lib/rebootWatch.d.ts.map +1 -0
- package/host/dist/lib/sdk-types.d.ts +1 -1
- package/host/dist/lib/sdk-types.d.ts.map +1 -1
- package/host/dist/lib/sdkStaleness.d.ts +78 -0
- package/host/dist/lib/sdkStaleness.d.ts.map +1 -0
- package/host/dist/lib/settings.d.ts +16 -5
- package/host/dist/lib/settings.d.ts.map +1 -1
- package/package.json +11 -4
- package/host/dist/chunks/index-lMs2sCXG.js +0 -439
- package/host/dist/chunks/reachy-mini-CdsdQLIU.js +0 -1523
- package/host/dist/lib/centralListener.d.ts +0 -73
- 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
|
+
};
|