Spaces:
Running
Running
Add fallback for Spaces deployment with duration limitation
Browse files- README.md +3 -0
- frontend/app.js +34 -26
README.md
CHANGED
|
@@ -67,6 +67,9 @@ uv run python app.py
|
|
| 67 |
## Current Limitations
|
| 68 |
|
| 69 |
- Sound and picture actions are not yet implemented in execution
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
## Deployment to HuggingFace Spaces
|
| 72 |
|
|
|
|
| 67 |
## Current Limitations
|
| 68 |
|
| 69 |
- Sound and picture actions are not yet implemented in execution
|
| 70 |
+
- **On HuggingFace Spaces**: Due to browser security policies (HTTPS → HTTP localhost), movement durations may not be fully respected. For accurate timing, run the demo locally.
|
| 71 |
+
- Spaces version uses WebSocket fallback which moves faster than specified durations
|
| 72 |
+
- Local version uses REST API `/goto` endpoint with proper duration support
|
| 73 |
|
| 74 |
## Deployment to HuggingFace Spaces
|
| 75 |
|
frontend/app.js
CHANGED
|
@@ -355,27 +355,21 @@ function matrixToEulerXYZ(rotMatrix) {
|
|
| 355 |
}
|
| 356 |
}
|
| 357 |
|
| 358 |
-
// Execute a single IR action (send to robot
|
| 359 |
async function executeIRAction(action) {
|
| 360 |
if (!state.robotConnected) {
|
| 361 |
throw new Error('Robot not connected');
|
| 362 |
}
|
| 363 |
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
interpolation: action.interpolation || 'minjerk'
|
| 367 |
-
};
|
| 368 |
-
|
| 369 |
if (action.head_pose) {
|
| 370 |
-
// Extract translation from 4x4 matrix
|
| 371 |
const x = action.head_pose[0][3];
|
| 372 |
const y = action.head_pose[1][3];
|
| 373 |
const z = action.head_pose[2][3];
|
| 374 |
-
|
| 375 |
-
// Extract rotation (euler angles) from 3x3 rotation matrix portion
|
| 376 |
const euler = matrixToEulerXYZ(action.head_pose);
|
| 377 |
|
| 378 |
-
|
| 379 |
x: x,
|
| 380 |
y: y,
|
| 381 |
z: z,
|
|
@@ -385,16 +379,18 @@ async function executeIRAction(action) {
|
|
| 385 |
};
|
| 386 |
}
|
| 387 |
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
|
| 392 |
-
|
| 393 |
-
gotoRequest.
|
| 394 |
-
|
| 395 |
|
| 396 |
-
// Send POST request to /goto endpoint with duration
|
| 397 |
-
try {
|
| 398 |
const response = await fetch('http://localhost:8000/api/move/goto', {
|
| 399 |
method: 'POST',
|
| 400 |
headers: { 'Content-Type': 'application/json' },
|
|
@@ -402,18 +398,30 @@ async function executeIRAction(action) {
|
|
| 402 |
});
|
| 403 |
|
| 404 |
if (!response.ok) {
|
| 405 |
-
|
| 406 |
-
throw new Error(error.detail || 'Failed to execute movement');
|
| 407 |
}
|
| 408 |
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
// Wait for the movement to complete (duration + small buffer)
|
| 412 |
await sleep(action.duration * 1000 + 100);
|
| 413 |
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
}
|
| 418 |
}
|
| 419 |
|
|
|
|
| 355 |
}
|
| 356 |
}
|
| 357 |
|
| 358 |
+
// Execute a single IR action (send to robot)
|
| 359 |
async function executeIRAction(action) {
|
| 360 |
if (!state.robotConnected) {
|
| 361 |
throw new Error('Robot not connected');
|
| 362 |
}
|
| 363 |
|
| 364 |
+
// Extract pose data
|
| 365 |
+
let head_pose = null;
|
|
|
|
|
|
|
|
|
|
| 366 |
if (action.head_pose) {
|
|
|
|
| 367 |
const x = action.head_pose[0][3];
|
| 368 |
const y = action.head_pose[1][3];
|
| 369 |
const z = action.head_pose[2][3];
|
|
|
|
|
|
|
| 370 |
const euler = matrixToEulerXYZ(action.head_pose);
|
| 371 |
|
| 372 |
+
head_pose = {
|
| 373 |
x: x,
|
| 374 |
y: y,
|
| 375 |
z: z,
|
|
|
|
| 379 |
};
|
| 380 |
}
|
| 381 |
|
| 382 |
+
// Try using /goto endpoint (works locally, respects duration)
|
| 383 |
+
// Falls back to WebSocket if fetch fails (e.g., on HuggingFace Spaces HTTPS -> HTTP localhost)
|
| 384 |
+
try {
|
| 385 |
+
const gotoRequest = {
|
| 386 |
+
duration: action.duration,
|
| 387 |
+
interpolation: action.interpolation || 'minjerk'
|
| 388 |
+
};
|
| 389 |
|
| 390 |
+
if (head_pose) gotoRequest.head_pose = head_pose;
|
| 391 |
+
if (action.antennas) gotoRequest.antennas = action.antennas;
|
| 392 |
+
if (action.body_yaw !== null) gotoRequest.body_yaw = action.body_yaw;
|
| 393 |
|
|
|
|
|
|
|
| 394 |
const response = await fetch('http://localhost:8000/api/move/goto', {
|
| 395 |
method: 'POST',
|
| 396 |
headers: { 'Content-Type': 'application/json' },
|
|
|
|
| 398 |
});
|
| 399 |
|
| 400 |
if (!response.ok) {
|
| 401 |
+
throw new Error('goto endpoint failed');
|
|
|
|
| 402 |
}
|
| 403 |
|
| 404 |
+
// Wait for movement to complete
|
|
|
|
|
|
|
| 405 |
await sleep(action.duration * 1000 + 100);
|
| 406 |
|
| 407 |
+
} catch (fetchError) {
|
| 408 |
+
// Fallback to WebSocket (for Spaces deployment)
|
| 409 |
+
// Note: This won't respect duration perfectly - movements will be fast
|
| 410 |
+
console.warn('Using WebSocket fallback (duration may not be accurate):', fetchError.message);
|
| 411 |
+
|
| 412 |
+
if (!state.robotWs || state.robotWs.readyState !== WebSocket.OPEN) {
|
| 413 |
+
throw new Error('Robot WebSocket not connected');
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
const wsMessage = {};
|
| 417 |
+
if (head_pose) wsMessage.target_head_pose = head_pose;
|
| 418 |
+
if (action.antennas) wsMessage.target_antennas = action.antennas;
|
| 419 |
+
if (action.body_yaw !== null) wsMessage.target_body_yaw = action.body_yaw;
|
| 420 |
+
|
| 421 |
+
state.robotWs.send(JSON.stringify(wsMessage));
|
| 422 |
+
|
| 423 |
+
// Wait for duration even though robot may move faster
|
| 424 |
+
await sleep(action.duration * 1000);
|
| 425 |
}
|
| 426 |
}
|
| 427 |
|