RickDakan commited on
Commit
d7104f9
·
1 Parent(s): 1f39fd4

fix: attach robot video via host handle (track arrives before connectToHost resolves)

Browse files
Files changed (1) hide show
  1. app.js +80 -2
app.js CHANGED
@@ -57,8 +57,17 @@ window.dispatchEvent(new Event("reachymini:ready"));
57
 
58
  const params = new URLSearchParams(window.location.search);
59
  const isEmbed = params.get("embedded") === "1" || params.get("embed") === "1";
 
 
 
60
 
61
- if (isEmbed) {
 
 
 
 
 
 
62
  document.body.classList.add("embed");
63
  runEmbed().catch((err) => {
64
  console.error("[howdy-friend] boot failed", err);
@@ -113,6 +122,75 @@ async function runEmbed() {
113
  handle.onLeave(() => app.shutdown(detachVideo));
114
  }
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  // ---------------------------------------------------------------------------
117
  // Core app: tracking loop, wave detection, reactions
118
  // ---------------------------------------------------------------------------
@@ -223,7 +301,7 @@ class HowdyApp {
223
  const ex = (cx - 0.5) * 2; // -1..1
224
  const ey = (cy - 0.5) * 2;
225
  this.servo(ex, ey, dt);
226
- setStatus("Tracking", `yaw ${this.headYaw.toFixed(0)}° body ${this.bodyYaw.toFixed(0)}°`, "tracking");
227
  } else if ((now - this.lastFaceAt) / 1000 > TRACK.noFaceResetSec) {
228
  this.easeHome(dt);
229
  setStatus("Looking for faces", "");
 
57
 
58
  const params = new URLSearchParams(window.location.search);
59
  const isEmbed = params.get("embedded") === "1" || params.get("embed") === "1";
60
+ // Dev mode (?dev=1): no robot, no OAuth. Uses the laptop webcam as the video source and a fake
61
+ // robot that logs commands instead of sending them. For tuning vision + control without hardware.
62
+ const isDev = params.get("dev") === "1";
63
 
64
+ if (isDev) {
65
+ document.body.classList.add("embed");
66
+ runDev().catch((err) => {
67
+ console.error("[howdy-friend] dev boot failed", err);
68
+ log(`Dev boot failed: ${err?.message ?? err}`);
69
+ });
70
+ } else if (isEmbed) {
71
  document.body.classList.add("embed");
72
  runEmbed().catch((err) => {
73
  console.error("[howdy-friend] boot failed", err);
 
122
  handle.onLeave(() => app.shutdown(detachVideo));
123
  }
124
 
125
+ // ---------------------------------------------------------------------------
126
+ // Dev mode: webcam + fake robot
127
+ // ---------------------------------------------------------------------------
128
+
129
+ async function runDev() {
130
+ const ui = bindUi();
131
+ setStatus("Dev mode: opening webcam", "");
132
+ log("DEV MODE: no robot. Commands are logged, not sent.");
133
+
134
+ // Production never calls getUserMedia (it would grab the wrong camera). Dev mode is the exception.
135
+ const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: false });
136
+ ui.video.srcObject = stream;
137
+ await ui.video.play();
138
+ const detachVideo = () => stream.getTracks().forEach((t) => t.stop());
139
+
140
+ setStatus("Loading vision models", "");
141
+ const vision = await loadVision();
142
+ log("Vision models ready");
143
+
144
+ const clips = new ClipCache();
145
+ clips.preload(WAVE_CLIPS).then((n) => log(`${n}/${WAVE_CLIPS.length} reaction clips ready`));
146
+
147
+ const reachy = new FakeReachy();
148
+ const app = new HowdyApp({ reachy, vision, clips, ui });
149
+ app.start();
150
+ window.addEventListener("beforeunload", () => app.shutdown(detachVideo));
151
+ }
152
+
153
+ // Mimics the subset of the SDK used by HowdyApp. Logs a pose summary at most twice a second so the
154
+ // on-screen log stays readable; every call still goes to the browser console.
155
+ class FakeReachy {
156
+ constructor() { this.lastPoseLogAt = 0; this.audio = null; }
157
+
158
+ setTarget(t) {
159
+ console.debug("[fake] setTarget", t);
160
+ if (!t.head) return;
161
+ const now = performance.now();
162
+ if (now - this.lastPoseLogAt < 500) return;
163
+ this.lastPoseLogAt = now;
164
+ const rpy = matrixToRpyDeg(t.head);
165
+ const body = typeof t.body_yaw === "number" ? t.body_yaw / DEG : 0;
166
+ log(`cmd head yaw ${rpy.yaw.toFixed(1)}° pitch ${rpy.pitch.toFixed(1)}° | body ${body.toFixed(1)}°`);
167
+ }
168
+
169
+ gotoTarget(t) { log(`gotoTarget (${t.duration ?? "?"}s)`); this.setTarget(t); }
170
+ setMotorMode(m) { console.debug("[fake] setMotorMode", m); }
171
+ cancelMove() { this.stopAudio(); }
172
+ attachVideo() { return () => {}; }
173
+
174
+ async playMove(motion, opts = {}) {
175
+ const dur = motion.time?.[motion.time.length - 1] ?? 1;
176
+ const first = motion.set_target_data[0], last = motion.set_target_data[motion.set_target_data.length - 1];
177
+ const f = first?.head ? matrixToRpyDeg(first.head) : null, l = last?.head ? matrixToRpyDeg(last.head) : null;
178
+ log(`playMove "${opts.description ?? ""}" ${dur.toFixed(2)}s, ${motion.set_target_data.length} frames` +
179
+ (f && l ? `, yaw ${f.yaw.toFixed(0)}°→${l.yaw.toFixed(0)}°` : "") + (opts.audioBlob ? ", with audio" : ""));
180
+ if (opts.audioBlob) {
181
+ this.stopAudio();
182
+ this.audio = new Audio(URL.createObjectURL(opts.audioBlob));
183
+ this.audio.play().catch((e) => log(`audio blocked: ${e.message}`));
184
+ }
185
+ await new Promise((r) => setTimeout(r, dur * 1000));
186
+ return {};
187
+ }
188
+
189
+ stopAudio() {
190
+ if (this.audio) { try { this.audio.pause(); } catch (_) {} this.audio = null; }
191
+ }
192
+ }
193
+
194
  // ---------------------------------------------------------------------------
195
  // Core app: tracking loop, wave detection, reactions
196
  // ---------------------------------------------------------------------------
 
301
  const ex = (cx - 0.5) * 2; // -1..1
302
  const ey = (cy - 0.5) * 2;
303
  this.servo(ex, ey, dt);
304
+ setStatus("Tracking", `yaw ${this.headYaw.toFixed(0)}° pitch ${this.headPitch.toFixed(0)}° body ${this.bodyYaw.toFixed(0)}°`, "tracking");
305
  } else if ((now - this.lastFaceAt) / 1000 > TRACK.noFaceResetSec) {
306
  this.easeHome(dt);
307
  setStatus("Looking for faces", "");