Georg commited on
Commit ·
e9e2294
1
Parent(s): 2425670
Implement blocking homing endpoint and refactor homing logic in mujoco_server.py
Browse files- Introduced a new blocking API endpoint (`/homing`) for UR5 that pauses jogging commands while executing homing, enhancing control during the homing process.
- Refactored the homing logic to remove the need for continuous home messages, improving efficiency and reducing potential drift.
- Updated the frontend to utilize the new blocking endpoint, simplifying the user interaction for homing.
- Removed deprecated home and stop_home message types from protocol_types.py to streamline the codebase.
- Enhanced tests to validate the new blocking homing functionality and ensure proper behavior during homing operations.
- Updated README.md to document the new homing endpoint and its usage.
- README.md +9 -10
- frontend/index.html +33 -21
- mujoco_server.py +249 -164
- protocol_types.py +0 -12
- tests/test_api.py +7 -11
- tests/test_client.py +13 -3
- tests/test_homing.py +19 -81
README.md
CHANGED
|
@@ -422,6 +422,7 @@ Nova-Sim provides a minimal HTTP API for static information:
|
|
| 422 |
| `/video_feed` | `GET` | MJPEG video stream of the main camera |
|
| 423 |
| `/camera/<name>/video_feed` | `GET` | MJPEG video stream of auxiliary cameras (added via `/cameras`) |
|
| 424 |
| `/cameras` | `POST` | Register a new auxiliary camera for the current robot/scene |
|
|
|
|
| 425 |
|
| 426 |
**Example `/env` response:**
|
| 427 |
```json
|
|
@@ -478,6 +479,14 @@ Notes:
|
|
| 478 |
- Cameras are scoped to the current robot + scene and show up in `/env` under `camera_feeds`.
|
| 479 |
- The UI listens for a `state` message containing `camera_event` and refreshes tiles automatically.
|
| 480 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
### Client → Server WebSocket Messages
|
| 482 |
|
| 483 |
**`action`** - Send velocity actions to all robots:
|
|
@@ -511,16 +520,6 @@ For robot arms (UR5):
|
|
| 511 |
- `robot`: Required. One of: `"g1"`, `"spot"`, `"ur5"`, `"ur5_t_push"`
|
| 512 |
- `scene`: Optional scene name (see `/metadata` for available scenes)
|
| 513 |
|
| 514 |
-
**`home`** - Move robot to home position (UR5 only):
|
| 515 |
-
```json
|
| 516 |
-
{"type": "home"}
|
| 517 |
-
```
|
| 518 |
-
|
| 519 |
-
**`stop_home`** - Stop homing sequence (UR5 only):
|
| 520 |
-
```json
|
| 521 |
-
{"type": "stop_home"}
|
| 522 |
-
```
|
| 523 |
-
|
| 524 |
**`teleop_action`** - Send teleoperation action (primarily for UR5 keyboard control):
|
| 525 |
```json
|
| 526 |
{"type": "teleop_action", "data": {"vx": 0.01, "vy": 0.0, "vz": 0.0}}
|
|
|
|
| 422 |
| `/video_feed` | `GET` | MJPEG video stream of the main camera |
|
| 423 |
| `/camera/<name>/video_feed` | `GET` | MJPEG video stream of auxiliary cameras (added via `/cameras`) |
|
| 424 |
| `/cameras` | `POST` | Register a new auxiliary camera for the current robot/scene |
|
| 425 |
+
| `/homing` | `GET` | **Blocking** homing for UR5 via Nova jogging (pauses joggers while executing) |
|
| 426 |
|
| 427 |
**Example `/env` response:**
|
| 428 |
```json
|
|
|
|
| 479 |
- Cameras are scoped to the current robot + scene and show up in `/env` under `camera_feeds`.
|
| 480 |
- The UI listens for a `state` message containing `camera_event` and refreshes tiles automatically.
|
| 481 |
|
| 482 |
+
**Blocking homing (`GET /homing`)**
|
| 483 |
+
```
|
| 484 |
+
/nova-sim/api/v1/homing?timeout_s=30&tolerance=0.01&poll_interval_s=0.1
|
| 485 |
+
```
|
| 486 |
+
Notes:
|
| 487 |
+
- This endpoint is **blocking**: it returns only after the robot reaches `home_pose` or a timeout occurs.
|
| 488 |
+
- While homing is active, jogging commands are paused (incoming jog/teleop actions are ignored).
|
| 489 |
+
|
| 490 |
### Client → Server WebSocket Messages
|
| 491 |
|
| 492 |
**`action`** - Send velocity actions to all robots:
|
|
|
|
| 520 |
- `robot`: Required. One of: `"g1"`, `"spot"`, `"ur5"`, `"ur5_t_push"`
|
| 521 |
- `scene`: Optional scene name (see `/metadata` for available scenes)
|
| 522 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
**`teleop_action`** - Send teleoperation action (primarily for UR5 keyboard control):
|
| 524 |
```json
|
| 525 |
{"type": "teleop_action", "data": {"vx": 0.01, "vy": 0.0, "vz": 0.0}}
|
frontend/index.html
CHANGED
|
@@ -1078,9 +1078,7 @@
|
|
| 1078 |
|
| 1079 |
<button class="danger" style="width: 100%; margin-top: 15px;" onclick="resetEnv()">Reset
|
| 1080 |
Environment</button>
|
| 1081 |
-
<button id="homeBtn" style="width: 100%; margin-top: 10px; display: none;"
|
| 1082 |
-
onmouseup="stopHoming()" onmouseleave="stopHoming()" ontouchstart="startHoming()"
|
| 1083 |
-
ontouchend="stopHoming()">🏠 Move to Home</button>
|
| 1084 |
</div>
|
| 1085 |
</div>
|
| 1086 |
|
|
@@ -2129,10 +2127,18 @@
|
|
| 2129 |
}
|
| 2130 |
|
| 2131 |
// Homing functionality for UR5
|
| 2132 |
-
let
|
| 2133 |
-
let homingJoints = []; // Joints being homed
|
| 2134 |
|
| 2135 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2136 |
if (currentRobot !== 'ur5' && currentRobot !== 'ur5_t_push') {
|
| 2137 |
return;
|
| 2138 |
}
|
|
@@ -2142,24 +2148,30 @@
|
|
| 2142 |
return;
|
| 2143 |
}
|
| 2144 |
|
| 2145 |
-
|
| 2146 |
-
|
|
|
|
| 2147 |
|
| 2148 |
-
|
| 2149 |
-
|
| 2150 |
-
homingInterval = setInterval(() => {
|
| 2151 |
-
send('home', {});
|
| 2152 |
-
}, 100);
|
| 2153 |
-
}
|
| 2154 |
|
| 2155 |
-
|
| 2156 |
-
|
| 2157 |
-
|
| 2158 |
-
|
| 2159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2160 |
}
|
| 2161 |
-
// Send stop_home message to backend
|
| 2162 |
-
send('stop_home', {});
|
| 2163 |
}
|
| 2164 |
|
| 2165 |
function setCmd(vx, vy, vyaw) {
|
|
|
|
| 1078 |
|
| 1079 |
<button class="danger" style="width: 100%; margin-top: 15px;" onclick="resetEnv()">Reset
|
| 1080 |
Environment</button>
|
| 1081 |
+
<button id="homeBtn" style="width: 100%; margin-top: 10px; display: none;" onclick="startHoming()">🏠 Move to Home</button>
|
|
|
|
|
|
|
| 1082 |
</div>
|
| 1083 |
</div>
|
| 1084 |
|
|
|
|
| 2127 |
}
|
| 2128 |
|
| 2129 |
// Homing functionality for UR5
|
| 2130 |
+
let homingInProgress = false;
|
|
|
|
| 2131 |
|
| 2132 |
+
function setHomeButtonState(inProgress) {
|
| 2133 |
+
const homeBtn = document.getElementById('homeBtn');
|
| 2134 |
+
if (!homeBtn) {
|
| 2135 |
+
return;
|
| 2136 |
+
}
|
| 2137 |
+
homeBtn.disabled = inProgress;
|
| 2138 |
+
homeBtn.textContent = inProgress ? '🏠 Homing...' : '🏠 Move to Home';
|
| 2139 |
+
}
|
| 2140 |
+
|
| 2141 |
+
async function startHoming() {
|
| 2142 |
if (currentRobot !== 'ur5' && currentRobot !== 'ur5_t_push') {
|
| 2143 |
return;
|
| 2144 |
}
|
|
|
|
| 2148 |
return;
|
| 2149 |
}
|
| 2150 |
|
| 2151 |
+
if (homingInProgress) {
|
| 2152 |
+
return;
|
| 2153 |
+
}
|
| 2154 |
|
| 2155 |
+
homingInProgress = true;
|
| 2156 |
+
setHomeButtonState(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2157 |
|
| 2158 |
+
const params = new URLSearchParams({
|
| 2159 |
+
timeout_s: '30',
|
| 2160 |
+
tolerance: '0.01',
|
| 2161 |
+
poll_interval_s: '0.1'
|
| 2162 |
+
});
|
| 2163 |
+
try {
|
| 2164 |
+
const response = await fetch(`${API_PREFIX}/homing?${params.toString()}`);
|
| 2165 |
+
const payload = await response.json();
|
| 2166 |
+
if (!response.ok) {
|
| 2167 |
+
console.warn('Homing failed:', payload);
|
| 2168 |
+
}
|
| 2169 |
+
} catch (err) {
|
| 2170 |
+
console.warn('Homing request failed:', err);
|
| 2171 |
+
} finally {
|
| 2172 |
+
homingInProgress = false;
|
| 2173 |
+
setHomeButtonState(false);
|
| 2174 |
}
|
|
|
|
|
|
|
| 2175 |
}
|
| 2176 |
|
| 2177 |
function setCmd(vx, vy, vyaw) {
|
mujoco_server.py
CHANGED
|
@@ -145,6 +145,8 @@ homing_state = {
|
|
| 145 |
"saved_velocity": None, # Store velocity to restore after homing
|
| 146 |
}
|
| 147 |
homing_lock = threading.Lock()
|
|
|
|
|
|
|
| 148 |
|
| 149 |
# WebSocket clients
|
| 150 |
ws_clients = set()
|
|
@@ -237,8 +239,6 @@ AVAILABLE_ACTIONS = [
|
|
| 237 |
"camera_follow",
|
| 238 |
"toggle_target_visibility",
|
| 239 |
"teleop_action",
|
| 240 |
-
"home",
|
| 241 |
-
"stop_home",
|
| 242 |
"arm_target",
|
| 243 |
"gripper",
|
| 244 |
"control_mode",
|
|
@@ -1207,19 +1207,6 @@ def simulation_loop():
|
|
| 1207 |
if env is None:
|
| 1208 |
return
|
| 1209 |
|
| 1210 |
-
# Check for homing timeout (prevent drift when home messages stop)
|
| 1211 |
-
with homing_lock:
|
| 1212 |
-
if homing_state.get("is_jogging", False):
|
| 1213 |
-
last_home_time = homing_state.get("last_home_time", 0)
|
| 1214 |
-
timeout = 0.5 # 500ms timeout - if no home message for this long, stop
|
| 1215 |
-
if time.time() - last_home_time > timeout:
|
| 1216 |
-
stop_jog = getattr(env, "stop_jog", None)
|
| 1217 |
-
if callable(stop_jog):
|
| 1218 |
-
stop_jog()
|
| 1219 |
-
homing_state["is_jogging"] = False
|
| 1220 |
-
homing_state["active"] = False
|
| 1221 |
-
print("[Homing] Stopped jogging due to timeout (no home messages)")
|
| 1222 |
-
|
| 1223 |
sim_dt = env.model.opt.timestep
|
| 1224 |
|
| 1225 |
# Run physics steps
|
|
@@ -1408,6 +1395,137 @@ def generate_overlay_frames(name: str):
|
|
| 1408 |
time.sleep(0.04)
|
| 1409 |
|
| 1410 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1411 |
def handle_ws_message(ws, data):
|
| 1412 |
"""Handle incoming WebSocket message."""
|
| 1413 |
global needs_robot_switch, camera_follow, t_target_visible, last_teleop_action
|
|
@@ -1443,6 +1561,13 @@ def handle_ws_message(ws, data):
|
|
| 1443 |
j6 = payload.get('j6', 0.0)
|
| 1444 |
gripper = payload.get('gripper', None)
|
| 1445 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1446 |
with mujoco_lock:
|
| 1447 |
if env is not None:
|
| 1448 |
# For UR5: translate velocity actions to jogging commands
|
|
@@ -1510,155 +1635,6 @@ def handle_ws_message(ws, data):
|
|
| 1510 |
scene = payload.get('scene')
|
| 1511 |
_schedule_robot_switch(robot, scene)
|
| 1512 |
|
| 1513 |
-
elif msg_type == 'home':
|
| 1514 |
-
# Multi-axis homing with Nova API v2
|
| 1515 |
-
# UI sends home messages continuously while button is pressed
|
| 1516 |
-
# When messages stop, jogging should stop to prevent drift
|
| 1517 |
-
with mujoco_lock, homing_lock:
|
| 1518 |
-
# Update last home message timestamp
|
| 1519 |
-
homing_state["last_home_time"] = time.time()
|
| 1520 |
-
if env is not None and current_robot in ("ur5", "ur5_t_push"):
|
| 1521 |
-
home_pose = getattr(env, "home_pose", None)
|
| 1522 |
-
get_joints = getattr(env, "get_joint_positions", None)
|
| 1523 |
-
start_multi_joint_jog = getattr(env, "start_multi_joint_jog", None)
|
| 1524 |
-
stop_jog = getattr(env, "stop_jog", None)
|
| 1525 |
-
|
| 1526 |
-
if home_pose is not None and callable(get_joints) and callable(start_multi_joint_jog) and callable(stop_jog):
|
| 1527 |
-
if hasattr(home_pose, "tolist"):
|
| 1528 |
-
target_positions = home_pose.tolist()
|
| 1529 |
-
else:
|
| 1530 |
-
target_positions = list(home_pose)
|
| 1531 |
-
|
| 1532 |
-
current_positions = get_joints()
|
| 1533 |
-
tolerance = 0.01 # rad (~0.57 degrees)
|
| 1534 |
-
|
| 1535 |
-
# Calculate errors for all joints
|
| 1536 |
-
errors = []
|
| 1537 |
-
for joint_idx in range(min(len(target_positions), len(current_positions))):
|
| 1538 |
-
current = current_positions[joint_idx]
|
| 1539 |
-
target = target_positions[joint_idx]
|
| 1540 |
-
# Compute shortest angular distance (no wrapping needed for UR5)
|
| 1541 |
-
# UR5 joints don't wrap around - they have limited ranges
|
| 1542 |
-
error = target - current
|
| 1543 |
-
errors.append(error)
|
| 1544 |
-
|
| 1545 |
-
# Check if any joint needs homing
|
| 1546 |
-
max_abs_error = max(abs(e) for e in errors)
|
| 1547 |
-
if max_abs_error < tolerance:
|
| 1548 |
-
# All joints at home - stop
|
| 1549 |
-
if homing_state.get("is_jogging", False):
|
| 1550 |
-
stop_jog()
|
| 1551 |
-
homing_state["is_jogging"] = False
|
| 1552 |
-
homing_state["active"] = False
|
| 1553 |
-
homing_state.pop("previous_errors", None)
|
| 1554 |
-
print("[Homing] Completed - all joints at target")
|
| 1555 |
-
return
|
| 1556 |
-
|
| 1557 |
-
# Initialize previous errors tracking if needed
|
| 1558 |
-
if "previous_errors" not in homing_state:
|
| 1559 |
-
homing_state["previous_errors"] = [0.0] * len(errors)
|
| 1560 |
-
|
| 1561 |
-
# Calculate proportional velocities for all joints
|
| 1562 |
-
base_velocity = 0.15 # rad/s - slower to prevent overshoot
|
| 1563 |
-
velocities = []
|
| 1564 |
-
|
| 1565 |
-
# Tighter tolerance for stopping individual joints
|
| 1566 |
-
stop_tolerance = tolerance * 0.5 # Half the main tolerance
|
| 1567 |
-
|
| 1568 |
-
for joint_idx, error in enumerate(errors):
|
| 1569 |
-
abs_error = abs(error)
|
| 1570 |
-
|
| 1571 |
-
# Check for overshoot (sign change)
|
| 1572 |
-
prev_error = homing_state["previous_errors"][joint_idx]
|
| 1573 |
-
if prev_error != 0 and ((prev_error > 0 and error < 0) or (prev_error < 0 and error > 0)):
|
| 1574 |
-
# Overshoot detected - stop this joint completely
|
| 1575 |
-
velocities.append(0.0)
|
| 1576 |
-
continue
|
| 1577 |
-
|
| 1578 |
-
if abs_error < stop_tolerance:
|
| 1579 |
-
# Close enough - stop this joint
|
| 1580 |
-
velocities.append(0.0)
|
| 1581 |
-
else:
|
| 1582 |
-
# Proportional control: scale velocity by distance relative to max error
|
| 1583 |
-
# This ensures all joints reach home at approximately the same time
|
| 1584 |
-
if max_abs_error > 0:
|
| 1585 |
-
velocity = base_velocity * (abs_error / max_abs_error)
|
| 1586 |
-
else:
|
| 1587 |
-
velocity = 0.0
|
| 1588 |
-
|
| 1589 |
-
# Apply additional damping when very close to target
|
| 1590 |
-
# Gradually reduce velocity as we approach the target
|
| 1591 |
-
damping_zone = tolerance * 3.0 # Start damping at 3x tolerance
|
| 1592 |
-
if abs_error < damping_zone:
|
| 1593 |
-
# Quadratic damping: velocity reduces rapidly near target
|
| 1594 |
-
damping_factor = (abs_error / damping_zone) ** 2
|
| 1595 |
-
velocity *= damping_factor
|
| 1596 |
-
|
| 1597 |
-
# Minimum velocity threshold - below this, just stop
|
| 1598 |
-
min_velocity = 0.02 # rad/s
|
| 1599 |
-
if velocity < min_velocity:
|
| 1600 |
-
velocities.append(0.0)
|
| 1601 |
-
else:
|
| 1602 |
-
# Apply velocity in correct direction
|
| 1603 |
-
if error > 0:
|
| 1604 |
-
velocities.append(velocity)
|
| 1605 |
-
else:
|
| 1606 |
-
velocities.append(-velocity)
|
| 1607 |
-
|
| 1608 |
-
# Store current errors for overshoot detection
|
| 1609 |
-
homing_state["previous_errors"] = errors
|
| 1610 |
-
|
| 1611 |
-
# Check if all velocities are zero (all joints at target or stopped due to overshoot)
|
| 1612 |
-
if all(abs(v) < 0.001 for v in velocities):
|
| 1613 |
-
# All joints stopped - end homing
|
| 1614 |
-
if homing_state.get("is_jogging", False):
|
| 1615 |
-
stop_jog()
|
| 1616 |
-
homing_state["is_jogging"] = False
|
| 1617 |
-
homing_state["active"] = False
|
| 1618 |
-
homing_state.pop("previous_errors", None)
|
| 1619 |
-
homing_state.pop("current_velocities", None)
|
| 1620 |
-
print("[Homing] All joints reached target or stopped")
|
| 1621 |
-
return
|
| 1622 |
-
|
| 1623 |
-
# Start multi-axis jogging with calculated velocities
|
| 1624 |
-
current_velocities = homing_state.get("current_velocities")
|
| 1625 |
-
|
| 1626 |
-
# Only restart if velocities changed significantly (avoid unnecessary restarts)
|
| 1627 |
-
velocities_changed = current_velocities is None or any(
|
| 1628 |
-
abs(v1 - v2) > 0.01 for v1, v2 in zip(velocities, current_velocities)
|
| 1629 |
-
)
|
| 1630 |
-
|
| 1631 |
-
if velocities_changed:
|
| 1632 |
-
if homing_state.get("is_jogging", False):
|
| 1633 |
-
stop_jog()
|
| 1634 |
-
|
| 1635 |
-
start_multi_joint_jog(velocities)
|
| 1636 |
-
homing_state["is_jogging"] = True
|
| 1637 |
-
homing_state["current_velocities"] = velocities
|
| 1638 |
-
homing_state["active"] = True
|
| 1639 |
-
|
| 1640 |
-
elif env is not None:
|
| 1641 |
-
# Instant homing for non-UR5 robots
|
| 1642 |
-
home_pose = getattr(env, "home_pose", None)
|
| 1643 |
-
set_joints = getattr(env, "set_joint_positions", None)
|
| 1644 |
-
if home_pose is not None and callable(set_joints):
|
| 1645 |
-
if hasattr(home_pose, "tolist"):
|
| 1646 |
-
positions = home_pose.tolist()
|
| 1647 |
-
else:
|
| 1648 |
-
positions = list(home_pose)
|
| 1649 |
-
set_joints(positions)
|
| 1650 |
-
|
| 1651 |
-
elif msg_type == 'stop_home':
|
| 1652 |
-
# Stop homing when button is released
|
| 1653 |
-
with mujoco_lock, homing_lock:
|
| 1654 |
-
if homing_state["active"]:
|
| 1655 |
-
if env is not None and current_robot in ("ur5", "ur5_t_push"):
|
| 1656 |
-
stop_jog = getattr(env, "stop_jog", None)
|
| 1657 |
-
if callable(stop_jog) and homing_state["is_jogging"]:
|
| 1658 |
-
stop_jog()
|
| 1659 |
-
homing_state["active"] = False
|
| 1660 |
-
homing_state["is_jogging"] = False
|
| 1661 |
-
|
| 1662 |
elif msg_type == 'camera':
|
| 1663 |
payload = data.get('data', {})
|
| 1664 |
a = cam.azimuth * np.pi / 180.0
|
|
@@ -1696,6 +1672,9 @@ def handle_ws_message(ws, data):
|
|
| 1696 |
vz = float(payload.get('vz', payload.get('dz', 0.0)))
|
| 1697 |
timestamp = time.time()
|
| 1698 |
|
|
|
|
|
|
|
|
|
|
| 1699 |
with mujoco_lock:
|
| 1700 |
if env is not None and current_robot in ("ur5", "ur5_t_push"):
|
| 1701 |
# Check if all velocities are zero - if so, stop any active jogging
|
|
@@ -2313,6 +2292,112 @@ def get_env_info():
|
|
| 2313 |
return jsonify(response)
|
| 2314 |
|
| 2315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2316 |
|
| 2317 |
|
| 2318 |
if __name__ == '__main__':
|
|
|
|
| 145 |
"saved_velocity": None, # Store velocity to restore after homing
|
| 146 |
}
|
| 147 |
homing_lock = threading.Lock()
|
| 148 |
+
jogging_pause_lock = threading.Lock()
|
| 149 |
+
jogging_paused = False
|
| 150 |
|
| 151 |
# WebSocket clients
|
| 152 |
ws_clients = set()
|
|
|
|
| 239 |
"camera_follow",
|
| 240 |
"toggle_target_visibility",
|
| 241 |
"teleop_action",
|
|
|
|
|
|
|
| 242 |
"arm_target",
|
| 243 |
"gripper",
|
| 244 |
"control_mode",
|
|
|
|
| 1207 |
if env is None:
|
| 1208 |
return
|
| 1209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1210 |
sim_dt = env.model.opt.timestep
|
| 1211 |
|
| 1212 |
# Run physics steps
|
|
|
|
| 1395 |
time.sleep(0.04)
|
| 1396 |
|
| 1397 |
|
| 1398 |
+
def _set_jogging_paused(paused: bool) -> bool:
|
| 1399 |
+
"""Pause or resume jogging commands; returns the previous pause state."""
|
| 1400 |
+
global jogging_paused
|
| 1401 |
+
with jogging_pause_lock:
|
| 1402 |
+
previous = jogging_paused
|
| 1403 |
+
jogging_paused = paused
|
| 1404 |
+
return previous
|
| 1405 |
+
|
| 1406 |
+
|
| 1407 |
+
def _is_jogging_paused() -> bool:
|
| 1408 |
+
with jogging_pause_lock:
|
| 1409 |
+
return jogging_paused
|
| 1410 |
+
|
| 1411 |
+
|
| 1412 |
+
def _step_multi_axis_homing_locked(tolerance: float = 0.01) -> dict:
|
| 1413 |
+
"""Run one homing control step (expects mujoco_lock + homing_lock held)."""
|
| 1414 |
+
homing_state["last_home_time"] = time.time()
|
| 1415 |
+
|
| 1416 |
+
if env is None or current_robot not in ("ur5", "ur5_t_push"):
|
| 1417 |
+
return {"status": "unsupported"}
|
| 1418 |
+
|
| 1419 |
+
home_pose = getattr(env, "home_pose", None)
|
| 1420 |
+
get_joints = getattr(env, "get_joint_positions", None)
|
| 1421 |
+
start_multi_joint_jog = getattr(env, "start_multi_joint_jog", None)
|
| 1422 |
+
stop_jog = getattr(env, "stop_jog", None)
|
| 1423 |
+
|
| 1424 |
+
if home_pose is None or not callable(get_joints) or not callable(start_multi_joint_jog) or not callable(stop_jog):
|
| 1425 |
+
return {"status": "unsupported"}
|
| 1426 |
+
|
| 1427 |
+
if hasattr(home_pose, "tolist"):
|
| 1428 |
+
target_positions = home_pose.tolist()
|
| 1429 |
+
else:
|
| 1430 |
+
target_positions = list(home_pose)
|
| 1431 |
+
|
| 1432 |
+
current_positions = get_joints()
|
| 1433 |
+
|
| 1434 |
+
# Calculate errors for all joints
|
| 1435 |
+
errors = []
|
| 1436 |
+
for joint_idx in range(min(len(target_positions), len(current_positions))):
|
| 1437 |
+
current = current_positions[joint_idx]
|
| 1438 |
+
target = target_positions[joint_idx]
|
| 1439 |
+
error = target - current
|
| 1440 |
+
errors.append(error)
|
| 1441 |
+
|
| 1442 |
+
max_abs_error = max(abs(e) for e in errors) if errors else 0.0
|
| 1443 |
+
if max_abs_error < tolerance:
|
| 1444 |
+
if homing_state.get("is_jogging", False):
|
| 1445 |
+
stop_jog()
|
| 1446 |
+
homing_state["is_jogging"] = False
|
| 1447 |
+
homing_state["active"] = False
|
| 1448 |
+
homing_state.pop("previous_errors", None)
|
| 1449 |
+
homing_state.pop("current_velocities", None)
|
| 1450 |
+
print("[Homing] Completed - all joints at target")
|
| 1451 |
+
return {
|
| 1452 |
+
"status": "done",
|
| 1453 |
+
"errors": errors,
|
| 1454 |
+
"max_abs_error": max_abs_error,
|
| 1455 |
+
}
|
| 1456 |
+
|
| 1457 |
+
if "previous_errors" not in homing_state:
|
| 1458 |
+
homing_state["previous_errors"] = [0.0] * len(errors)
|
| 1459 |
+
|
| 1460 |
+
base_velocity = 0.15 # rad/s - slower to prevent overshoot
|
| 1461 |
+
velocities = []
|
| 1462 |
+
stop_tolerance = tolerance * 0.5
|
| 1463 |
+
|
| 1464 |
+
for joint_idx, error in enumerate(errors):
|
| 1465 |
+
abs_error = abs(error)
|
| 1466 |
+
|
| 1467 |
+
prev_error = homing_state["previous_errors"][joint_idx]
|
| 1468 |
+
if prev_error != 0 and ((prev_error > 0 and error < 0) or (prev_error < 0 and error > 0)):
|
| 1469 |
+
velocities.append(0.0)
|
| 1470 |
+
continue
|
| 1471 |
+
|
| 1472 |
+
if abs_error < stop_tolerance:
|
| 1473 |
+
velocities.append(0.0)
|
| 1474 |
+
else:
|
| 1475 |
+
if max_abs_error > 0:
|
| 1476 |
+
velocity = base_velocity * (abs_error / max_abs_error)
|
| 1477 |
+
else:
|
| 1478 |
+
velocity = 0.0
|
| 1479 |
+
|
| 1480 |
+
damping_zone = tolerance * 3.0
|
| 1481 |
+
if abs_error < damping_zone:
|
| 1482 |
+
damping_factor = (abs_error / damping_zone) ** 2
|
| 1483 |
+
velocity *= damping_factor
|
| 1484 |
+
|
| 1485 |
+
min_velocity = 0.02
|
| 1486 |
+
if velocity < min_velocity:
|
| 1487 |
+
velocities.append(0.0)
|
| 1488 |
+
else:
|
| 1489 |
+
velocities.append(velocity if error > 0 else -velocity)
|
| 1490 |
+
|
| 1491 |
+
homing_state["previous_errors"] = errors
|
| 1492 |
+
|
| 1493 |
+
if all(abs(v) < 0.001 for v in velocities):
|
| 1494 |
+
if homing_state.get("is_jogging", False):
|
| 1495 |
+
stop_jog()
|
| 1496 |
+
homing_state["is_jogging"] = False
|
| 1497 |
+
homing_state["active"] = False
|
| 1498 |
+
homing_state.pop("previous_errors", None)
|
| 1499 |
+
homing_state.pop("current_velocities", None)
|
| 1500 |
+
print("[Homing] All joints reached target or stopped")
|
| 1501 |
+
return {
|
| 1502 |
+
"status": "done",
|
| 1503 |
+
"errors": errors,
|
| 1504 |
+
"max_abs_error": max_abs_error,
|
| 1505 |
+
}
|
| 1506 |
+
|
| 1507 |
+
current_velocities = homing_state.get("current_velocities")
|
| 1508 |
+
velocities_changed = current_velocities is None or any(
|
| 1509 |
+
abs(v1 - v2) > 0.01 for v1, v2 in zip(velocities, current_velocities)
|
| 1510 |
+
)
|
| 1511 |
+
|
| 1512 |
+
if velocities_changed:
|
| 1513 |
+
if homing_state.get("is_jogging", False):
|
| 1514 |
+
stop_jog()
|
| 1515 |
+
|
| 1516 |
+
start_multi_joint_jog(velocities)
|
| 1517 |
+
homing_state["is_jogging"] = True
|
| 1518 |
+
homing_state["current_velocities"] = velocities
|
| 1519 |
+
homing_state["active"] = True
|
| 1520 |
+
|
| 1521 |
+
return {
|
| 1522 |
+
"status": "running",
|
| 1523 |
+
"errors": errors,
|
| 1524 |
+
"max_abs_error": max_abs_error,
|
| 1525 |
+
"velocities": velocities,
|
| 1526 |
+
}
|
| 1527 |
+
|
| 1528 |
+
|
| 1529 |
def handle_ws_message(ws, data):
|
| 1530 |
"""Handle incoming WebSocket message."""
|
| 1531 |
global needs_robot_switch, camera_follow, t_target_visible, last_teleop_action
|
|
|
|
| 1561 |
j6 = payload.get('j6', 0.0)
|
| 1562 |
gripper = payload.get('gripper', None)
|
| 1563 |
|
| 1564 |
+
if current_robot in ("ur5", "ur5_t_push") and _is_jogging_paused():
|
| 1565 |
+
if gripper is not None:
|
| 1566 |
+
with mujoco_lock:
|
| 1567 |
+
if env is not None:
|
| 1568 |
+
env.set_gripper(float(gripper))
|
| 1569 |
+
return
|
| 1570 |
+
|
| 1571 |
with mujoco_lock:
|
| 1572 |
if env is not None:
|
| 1573 |
# For UR5: translate velocity actions to jogging commands
|
|
|
|
| 1635 |
scene = payload.get('scene')
|
| 1636 |
_schedule_robot_switch(robot, scene)
|
| 1637 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1638 |
elif msg_type == 'camera':
|
| 1639 |
payload = data.get('data', {})
|
| 1640 |
a = cam.azimuth * np.pi / 180.0
|
|
|
|
| 1672 |
vz = float(payload.get('vz', payload.get('dz', 0.0)))
|
| 1673 |
timestamp = time.time()
|
| 1674 |
|
| 1675 |
+
if current_robot in ("ur5", "ur5_t_push") and _is_jogging_paused():
|
| 1676 |
+
return
|
| 1677 |
+
|
| 1678 |
with mujoco_lock:
|
| 1679 |
if env is not None and current_robot in ("ur5", "ur5_t_push"):
|
| 1680 |
# Check if all velocities are zero - if so, stop any active jogging
|
|
|
|
| 2292 |
return jsonify(response)
|
| 2293 |
|
| 2294 |
|
| 2295 |
+
@app.route(f'{API_PREFIX}/homing', methods=['GET'])
|
| 2296 |
+
@_env_request
|
| 2297 |
+
def home_blocking():
|
| 2298 |
+
timeout_s = float(request.args.get("timeout_s", 30.0))
|
| 2299 |
+
tolerance = float(request.args.get("tolerance", 0.01))
|
| 2300 |
+
poll_interval = float(request.args.get("poll_interval_s", 0.1))
|
| 2301 |
+
|
| 2302 |
+
if timeout_s <= 0:
|
| 2303 |
+
return jsonify({"error": "timeout_s must be > 0"}), 400
|
| 2304 |
+
if tolerance <= 0:
|
| 2305 |
+
return jsonify({"error": "tolerance must be > 0"}), 400
|
| 2306 |
+
if poll_interval <= 0:
|
| 2307 |
+
return jsonify({"error": "poll_interval_s must be > 0"}), 400
|
| 2308 |
+
|
| 2309 |
+
with mujoco_lock:
|
| 2310 |
+
if env is None:
|
| 2311 |
+
return jsonify({"error": "no environment initialized"}), 503
|
| 2312 |
+
|
| 2313 |
+
if current_robot not in ("ur5", "ur5_t_push"):
|
| 2314 |
+
with mujoco_lock:
|
| 2315 |
+
home_pose = getattr(env, "home_pose", None)
|
| 2316 |
+
set_joints = getattr(env, "set_joint_positions", None)
|
| 2317 |
+
if home_pose is None or not callable(set_joints):
|
| 2318 |
+
return jsonify({"error": "homing not supported for current robot"}), 400
|
| 2319 |
+
positions = home_pose.tolist() if hasattr(home_pose, "tolist") else list(home_pose)
|
| 2320 |
+
set_joints(positions)
|
| 2321 |
+
return jsonify({"status": "completed", "robot": current_robot, "homed": True})
|
| 2322 |
+
|
| 2323 |
+
with mujoco_lock, homing_lock:
|
| 2324 |
+
if homing_state.get("active"):
|
| 2325 |
+
return jsonify({"error": "homing already active"}), 409
|
| 2326 |
+
home_pose = getattr(env, "home_pose", None)
|
| 2327 |
+
get_joints = getattr(env, "get_joint_positions", None)
|
| 2328 |
+
start_multi_joint_jog = getattr(env, "start_multi_joint_jog", None)
|
| 2329 |
+
stop_jog = getattr(env, "stop_jog", None)
|
| 2330 |
+
if home_pose is None or not callable(get_joints) or not callable(start_multi_joint_jog) or not callable(stop_jog):
|
| 2331 |
+
return jsonify({"error": "Nova homing not supported for current robot"}), 400
|
| 2332 |
+
homing_state["active"] = True
|
| 2333 |
+
|
| 2334 |
+
previous_pause = _set_jogging_paused(True)
|
| 2335 |
+
start_time = time.time()
|
| 2336 |
+
status = "running"
|
| 2337 |
+
result_snapshot = {}
|
| 2338 |
+
|
| 2339 |
+
try:
|
| 2340 |
+
with mujoco_lock:
|
| 2341 |
+
stop_jog = getattr(env, "stop_jog", None)
|
| 2342 |
+
if callable(stop_jog):
|
| 2343 |
+
stop_jog()
|
| 2344 |
+
|
| 2345 |
+
while True:
|
| 2346 |
+
with mujoco_lock, homing_lock:
|
| 2347 |
+
result_snapshot = _step_multi_axis_homing_locked(tolerance=tolerance)
|
| 2348 |
+
|
| 2349 |
+
if result_snapshot.get("status") == "done":
|
| 2350 |
+
status = "completed"
|
| 2351 |
+
break
|
| 2352 |
+
|
| 2353 |
+
if time.time() - start_time >= timeout_s:
|
| 2354 |
+
status = "timeout"
|
| 2355 |
+
break
|
| 2356 |
+
|
| 2357 |
+
time.sleep(poll_interval)
|
| 2358 |
+
finally:
|
| 2359 |
+
with mujoco_lock, homing_lock:
|
| 2360 |
+
stop_jog = getattr(env, "stop_jog", None)
|
| 2361 |
+
if callable(stop_jog) and homing_state.get("is_jogging", False):
|
| 2362 |
+
stop_jog()
|
| 2363 |
+
homing_state["active"] = False
|
| 2364 |
+
homing_state["is_jogging"] = False
|
| 2365 |
+
homing_state.pop("previous_errors", None)
|
| 2366 |
+
homing_state.pop("current_velocities", None)
|
| 2367 |
+
_set_jogging_paused(previous_pause)
|
| 2368 |
+
|
| 2369 |
+
with mujoco_lock:
|
| 2370 |
+
home_pose = getattr(env, "home_pose", None)
|
| 2371 |
+
get_joints = getattr(env, "get_joint_positions", None)
|
| 2372 |
+
final_positions = get_joints() if callable(get_joints) else []
|
| 2373 |
+
|
| 2374 |
+
home_pose_list = (
|
| 2375 |
+
home_pose.tolist()
|
| 2376 |
+
if home_pose is not None and hasattr(home_pose, "tolist")
|
| 2377 |
+
else (list(home_pose) if home_pose is not None else None)
|
| 2378 |
+
)
|
| 2379 |
+
|
| 2380 |
+
final_errors = None
|
| 2381 |
+
max_abs_error = None
|
| 2382 |
+
if home_pose_list is not None and final_positions is not None and len(final_positions) > 0:
|
| 2383 |
+
final_errors = [float(t - c) for t, c in zip(home_pose_list, final_positions)]
|
| 2384 |
+
max_abs_error = max(abs(e) for e in final_errors)
|
| 2385 |
+
|
| 2386 |
+
final_positions_list = [float(v) for v in final_positions] if final_positions is not None and len(final_positions) > 0 else None
|
| 2387 |
+
response = {
|
| 2388 |
+
"status": status,
|
| 2389 |
+
"robot": current_robot,
|
| 2390 |
+
"homed": status == "completed",
|
| 2391 |
+
"duration_s": time.time() - start_time,
|
| 2392 |
+
"home_pose": home_pose_list,
|
| 2393 |
+
"final_positions": final_positions_list,
|
| 2394 |
+
"final_errors": final_errors,
|
| 2395 |
+
"max_abs_error": max_abs_error,
|
| 2396 |
+
"jogging_paused": _is_jogging_paused(),
|
| 2397 |
+
}
|
| 2398 |
+
return jsonify(response)
|
| 2399 |
+
|
| 2400 |
+
|
| 2401 |
|
| 2402 |
|
| 2403 |
if __name__ == '__main__':
|
protocol_types.py
CHANGED
|
@@ -110,16 +110,6 @@ class SwitchRobotMessage(TypedDict):
|
|
| 110 |
data: SwitchRobotData
|
| 111 |
|
| 112 |
|
| 113 |
-
class HomeMessage(TypedDict):
|
| 114 |
-
"""Home robot message (UR5 only)."""
|
| 115 |
-
type: Literal["home"]
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
class StopHomeMessage(TypedDict):
|
| 119 |
-
"""Stop homing sequence message (UR5 only)."""
|
| 120 |
-
type: Literal["stop_home"]
|
| 121 |
-
|
| 122 |
-
|
| 123 |
class CameraRotateData(TypedDict):
|
| 124 |
"""Camera rotation data."""
|
| 125 |
action: Literal["rotate"]
|
|
@@ -459,8 +449,6 @@ ClientMessage = Union[
|
|
| 459 |
TeleopActionMessage,
|
| 460 |
ResetMessage,
|
| 461 |
SwitchRobotMessage,
|
| 462 |
-
HomeMessage,
|
| 463 |
-
StopHomeMessage,
|
| 464 |
CameraMessage,
|
| 465 |
CameraFollowMessage,
|
| 466 |
ArmTargetMessage,
|
|
|
|
| 110 |
data: SwitchRobotData
|
| 111 |
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
class CameraRotateData(TypedDict):
|
| 114 |
"""Camera rotation data."""
|
| 115 |
action: Literal["rotate"]
|
|
|
|
| 449 |
TeleopActionMessage,
|
| 450 |
ResetMessage,
|
| 451 |
SwitchRobotMessage,
|
|
|
|
|
|
|
| 452 |
CameraMessage,
|
| 453 |
CameraFollowMessage,
|
| 454 |
ArmTargetMessage,
|
tests/test_api.py
CHANGED
|
@@ -178,17 +178,13 @@ class TestWebSocketMessages:
|
|
| 178 |
print(f"\nInitial joints: {initial_joints}")
|
| 179 |
print(f"Home pose: {home_pose}")
|
| 180 |
|
| 181 |
-
#
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
try:
|
| 189 |
-
ws.recv(timeout=0.05)
|
| 190 |
-
except Exception:
|
| 191 |
-
pass
|
| 192 |
|
| 193 |
# Wait for final state messages
|
| 194 |
final_state = None
|
|
|
|
| 178 |
print(f"\nInitial joints: {initial_joints}")
|
| 179 |
print(f"Home pose: {home_pose}")
|
| 180 |
|
| 181 |
+
# Call blocking homing endpoint
|
| 182 |
+
homing_resp = requests.get(
|
| 183 |
+
f"{BASE_URL}/homing",
|
| 184 |
+
params={"timeout_s": 10, "tolerance": 0.05, "poll_interval_s": 0.1},
|
| 185 |
+
timeout=15,
|
| 186 |
+
)
|
| 187 |
+
homing_resp.raise_for_status()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
# Wait for final state messages
|
| 190 |
final_state = None
|
tests/test_client.py
CHANGED
|
@@ -54,9 +54,19 @@ class NovaSimTestClient:
|
|
| 54 |
self.ws.send(json.dumps(msg))
|
| 55 |
self._receive_state()
|
| 56 |
|
| 57 |
-
def
|
| 58 |
-
"""
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
def get_joint_positions(self):
|
| 62 |
"""Get current joint positions from latest state."""
|
|
|
|
| 54 |
self.ws.send(json.dumps(msg))
|
| 55 |
self._receive_state()
|
| 56 |
|
| 57 |
+
def home_blocking(self, timeout_s: float = 30.0, tolerance: float = 0.01, poll_interval_s: float = 0.1):
|
| 58 |
+
"""Invoke the blocking homing endpoint."""
|
| 59 |
+
resp = requests.get(
|
| 60 |
+
f"{self.base_url}/homing",
|
| 61 |
+
params={
|
| 62 |
+
"timeout_s": timeout_s,
|
| 63 |
+
"tolerance": tolerance,
|
| 64 |
+
"poll_interval_s": poll_interval_s,
|
| 65 |
+
},
|
| 66 |
+
timeout=max(5.0, timeout_s + 5.0),
|
| 67 |
+
)
|
| 68 |
+
resp.raise_for_status()
|
| 69 |
+
return resp.json()
|
| 70 |
|
| 71 |
def get_joint_positions(self):
|
| 72 |
"""Get current joint positions from latest state."""
|
tests/test_homing.py
CHANGED
|
@@ -18,7 +18,7 @@ def test_homing():
|
|
| 18 |
print("=" * 70)
|
| 19 |
print("\nThis test verifies:")
|
| 20 |
print(" 1. Robot position can be read from state stream")
|
| 21 |
-
print(" 2.
|
| 22 |
print(" 3. All joints move simultaneously toward home_pose")
|
| 23 |
print(" 4. Robot reaches home within acceptable tolerance")
|
| 24 |
print(" 5. Multi-axis jogging is faster than sequential")
|
|
@@ -51,33 +51,16 @@ def test_homing():
|
|
| 51 |
print(f" ℹ Robot already near home position")
|
| 52 |
print(f" Note: In digital twin mode, robot position comes from real hardware")
|
| 53 |
|
| 54 |
-
# Start homing
|
| 55 |
print(f"\n[3] Starting multi-axis homing (Nova API v2)...")
|
| 56 |
print(" All joints will move simultaneously toward home")
|
| 57 |
-
print("
|
| 58 |
|
| 59 |
home_start_time = time.time()
|
| 60 |
-
message_count = 0
|
| 61 |
joints_before = initial_joints.copy()
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
client.send_home()
|
| 66 |
-
message_count += 1
|
| 67 |
-
time.sleep(0.1)
|
| 68 |
-
|
| 69 |
-
# Check progress every second
|
| 70 |
-
if message_count % 10 == 0:
|
| 71 |
-
current_joints = client.get_joint_positions()
|
| 72 |
-
current_errors = [abs(actual - target) for actual, target in zip(current_joints, expected_home)]
|
| 73 |
-
current_max_error = max(current_errors)
|
| 74 |
-
elapsed = time.time() - home_start_time
|
| 75 |
-
print(f" t={elapsed:.1f}s: max_error={math.degrees(current_max_error):.2f}° ({message_count} messages)")
|
| 76 |
-
|
| 77 |
-
# Check if reached home
|
| 78 |
-
if current_max_error < 0.05:
|
| 79 |
-
print(f"\n✓ Homing completed in {elapsed:.2f}s!")
|
| 80 |
-
break
|
| 81 |
|
| 82 |
homing_time = time.time() - home_start_time
|
| 83 |
|
|
@@ -162,12 +145,11 @@ def test_homing():
|
|
| 162 |
|
| 163 |
|
| 164 |
def test_drift_prevention():
|
| 165 |
-
"""Test that robot
|
| 166 |
print("\n" + "=" * 70)
|
| 167 |
print("Drift Prevention Test")
|
| 168 |
print("=" * 70)
|
| 169 |
-
print("\nThis test verifies that the robot stops moving
|
| 170 |
-
print("messages stop, preventing drift.\n")
|
| 171 |
|
| 172 |
client = NovaSimTestClient("http://localhost:3004/nova-sim/api/v1")
|
| 173 |
|
|
@@ -181,20 +163,14 @@ def test_drift_prevention():
|
|
| 181 |
print(f"\n[1] Initial joint positions:")
|
| 182 |
print(f" [{', '.join(f'{j:7.4f}' for j in initial_joints)}]")
|
| 183 |
|
| 184 |
-
#
|
| 185 |
-
print(f"\n[2]
|
| 186 |
-
|
| 187 |
-
rate_hz = 10
|
| 188 |
-
num_messages = int(duration * rate_hz)
|
| 189 |
-
|
| 190 |
-
for i in range(num_messages):
|
| 191 |
-
client.send_home()
|
| 192 |
-
time.sleep(1.0 / rate_hz)
|
| 193 |
|
| 194 |
# Get position right after stopping
|
| 195 |
time.sleep(0.1)
|
| 196 |
position_after_homing = client.get_joint_positions()
|
| 197 |
-
print(f"\n[3] Position immediately after
|
| 198 |
print(f" [{', '.join(f'{j:7.4f}' for j in position_after_homing)}]")
|
| 199 |
|
| 200 |
# Wait 2 seconds and check if robot drifted
|
|
@@ -202,7 +178,7 @@ def test_drift_prevention():
|
|
| 202 |
time.sleep(2.0)
|
| 203 |
|
| 204 |
final_position = client.get_joint_positions()
|
| 205 |
-
print(f"\n[5] Position 2 seconds after
|
| 206 |
print(f" [{', '.join(f'{j:7.4f}' for j in final_position)}]")
|
| 207 |
|
| 208 |
# Calculate drift
|
|
@@ -228,9 +204,8 @@ def test_drift_prevention():
|
|
| 228 |
print(f"✓ NO DRIFT - Robot stopped properly (drift < {drift_threshold:.4f} rad)")
|
| 229 |
success = True
|
| 230 |
else:
|
| 231 |
-
print(f"✗ DRIFT DETECTED - Robot kept moving after
|
| 232 |
-
print(f"
|
| 233 |
-
print(f" Check the timeout mechanism in the simulation loop.")
|
| 234 |
success = False
|
| 235 |
print("=" * 70)
|
| 236 |
|
|
@@ -295,42 +270,9 @@ def test_overshoot_detection():
|
|
| 295 |
print(f" [{', '.join(f'{j:7.4f}' for j in moved_joints)}]")
|
| 296 |
print(f" Joint 1 error from home: {abs(moved_joints[0] - home_joints[0]):.4f} rad")
|
| 297 |
|
| 298 |
-
# Now home
|
| 299 |
-
print(f"\n[4] Starting homing
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
duration = 15.0
|
| 303 |
-
rate_hz = 10
|
| 304 |
-
num_messages = int(duration * rate_hz)
|
| 305 |
-
|
| 306 |
-
prev_j1_error = None
|
| 307 |
-
overshoot_count = 0
|
| 308 |
-
|
| 309 |
-
for i in range(num_messages):
|
| 310 |
-
# Send home message
|
| 311 |
-
client.send_home()
|
| 312 |
-
time.sleep(1.0 / rate_hz)
|
| 313 |
-
|
| 314 |
-
# Check position every second
|
| 315 |
-
if (i + 1) % 10 == 0:
|
| 316 |
-
current_joints = client.get_joint_positions()
|
| 317 |
-
j1_error = current_joints[0] - home_joints[0]
|
| 318 |
-
|
| 319 |
-
# Check for overshoot (sign change)
|
| 320 |
-
if prev_j1_error is not None:
|
| 321 |
-
if (prev_j1_error > 0 and j1_error < 0) or (prev_j1_error < 0 and j1_error > 0):
|
| 322 |
-
overshoot_count += 1
|
| 323 |
-
print(f" t={(i+1)/10:.1f}s: ⚠️ OVERSHOOT #{overshoot_count} detected!")
|
| 324 |
-
print(f" prev_error={prev_j1_error:+.4f} -> current_error={j1_error:+.4f}")
|
| 325 |
-
|
| 326 |
-
# Check if close to home
|
| 327 |
-
if abs(j1_error) < 0.01:
|
| 328 |
-
print(f" t={(i+1)/10:.1f}s: ✓ Reached home (error={abs(j1_error):.4f} rad)")
|
| 329 |
-
break
|
| 330 |
-
else:
|
| 331 |
-
print(f" t={(i+1)/10:.1f}s: j1_error={j1_error:+.4f} rad ({i+1} messages)")
|
| 332 |
-
|
| 333 |
-
prev_j1_error = j1_error
|
| 334 |
|
| 335 |
# Get final position
|
| 336 |
time.sleep(0.5)
|
|
@@ -349,7 +291,6 @@ def test_overshoot_detection():
|
|
| 349 |
print(f" {status} Joint {i+1}: error={error:.4f} rad ({math.degrees(error):.2f}°)")
|
| 350 |
|
| 351 |
print(f"\n[7] Results:")
|
| 352 |
-
print(f" Total overshoots detected: {overshoot_count}")
|
| 353 |
print(f" Max final error: {max_error:.4f} rad ({math.degrees(max_error):.2f}°)")
|
| 354 |
|
| 355 |
print("\n" + "=" * 70)
|
|
@@ -360,10 +301,7 @@ def test_overshoot_detection():
|
|
| 360 |
print(f"⚠ Homing incomplete (max error = {max_error:.4f} rad)")
|
| 361 |
success = False
|
| 362 |
|
| 363 |
-
|
| 364 |
-
print(f"ℹ Overshoot detection working ({overshoot_count} overshoots detected and corrected)")
|
| 365 |
-
else:
|
| 366 |
-
print(f"✓ No overshoots needed (smooth convergence)")
|
| 367 |
print("=" * 70)
|
| 368 |
|
| 369 |
client.close()
|
|
|
|
| 18 |
print("=" * 70)
|
| 19 |
print("\nThis test verifies:")
|
| 20 |
print(" 1. Robot position can be read from state stream")
|
| 21 |
+
print(" 2. Calling the blocking homing endpoint triggers multi-axis homing")
|
| 22 |
print(" 3. All joints move simultaneously toward home_pose")
|
| 23 |
print(" 4. Robot reaches home within acceptable tolerance")
|
| 24 |
print(" 5. Multi-axis jogging is faster than sequential")
|
|
|
|
| 51 |
print(f" ℹ Robot already near home position")
|
| 52 |
print(f" Note: In digital twin mode, robot position comes from real hardware")
|
| 53 |
|
| 54 |
+
# Start homing using the blocking endpoint
|
| 55 |
print(f"\n[3] Starting multi-axis homing (Nova API v2)...")
|
| 56 |
print(" All joints will move simultaneously toward home")
|
| 57 |
+
print(" Calling /homing with a 30 second timeout...")
|
| 58 |
|
| 59 |
home_start_time = time.time()
|
|
|
|
| 60 |
joints_before = initial_joints.copy()
|
| 61 |
+
home_result = client.home_blocking(timeout_s=30.0, tolerance=0.01, poll_interval_s=0.1)
|
| 62 |
+
elapsed = time.time() - home_start_time
|
| 63 |
+
print(f" Homing result: {home_result.get('status')} in {elapsed:.2f}s")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
homing_time = time.time() - home_start_time
|
| 66 |
|
|
|
|
| 145 |
|
| 146 |
|
| 147 |
def test_drift_prevention():
|
| 148 |
+
"""Test that robot doesn't drift after blocking homing completes."""
|
| 149 |
print("\n" + "=" * 70)
|
| 150 |
print("Drift Prevention Test")
|
| 151 |
print("=" * 70)
|
| 152 |
+
print("\nThis test verifies that the robot stops moving after blocking homing.\n")
|
|
|
|
| 153 |
|
| 154 |
client = NovaSimTestClient("http://localhost:3004/nova-sim/api/v1")
|
| 155 |
|
|
|
|
| 163 |
print(f"\n[1] Initial joint positions:")
|
| 164 |
print(f" [{', '.join(f'{j:7.4f}' for j in initial_joints)}]")
|
| 165 |
|
| 166 |
+
# Run homing
|
| 167 |
+
print(f"\n[2] Calling blocking homing endpoint...")
|
| 168 |
+
client.home_blocking(timeout_s=10.0, tolerance=0.05, poll_interval_s=0.1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
# Get position right after stopping
|
| 171 |
time.sleep(0.1)
|
| 172 |
position_after_homing = client.get_joint_positions()
|
| 173 |
+
print(f"\n[3] Position immediately after homing:")
|
| 174 |
print(f" [{', '.join(f'{j:7.4f}' for j in position_after_homing)}]")
|
| 175 |
|
| 176 |
# Wait 2 seconds and check if robot drifted
|
|
|
|
| 178 |
time.sleep(2.0)
|
| 179 |
|
| 180 |
final_position = client.get_joint_positions()
|
| 181 |
+
print(f"\n[5] Position 2 seconds after homing:")
|
| 182 |
print(f" [{', '.join(f'{j:7.4f}' for j in final_position)}]")
|
| 183 |
|
| 184 |
# Calculate drift
|
|
|
|
| 204 |
print(f"✓ NO DRIFT - Robot stopped properly (drift < {drift_threshold:.4f} rad)")
|
| 205 |
success = True
|
| 206 |
else:
|
| 207 |
+
print(f"✗ DRIFT DETECTED - Robot kept moving after homing!")
|
| 208 |
+
print(f" Check server logs for jogging stop behavior.")
|
|
|
|
| 209 |
success = False
|
| 210 |
print("=" * 70)
|
| 211 |
|
|
|
|
| 270 |
print(f" [{', '.join(f'{j:7.4f}' for j in moved_joints)}]")
|
| 271 |
print(f" Joint 1 error from home: {abs(moved_joints[0] - home_joints[0]):.4f} rad")
|
| 272 |
|
| 273 |
+
# Now home using the blocking endpoint
|
| 274 |
+
print(f"\n[4] Starting homing...")
|
| 275 |
+
client.home_blocking(timeout_s=15.0, tolerance=0.01, poll_interval_s=0.1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
# Get final position
|
| 278 |
time.sleep(0.5)
|
|
|
|
| 291 |
print(f" {status} Joint {i+1}: error={error:.4f} rad ({math.degrees(error):.2f}°)")
|
| 292 |
|
| 293 |
print(f"\n[7] Results:")
|
|
|
|
| 294 |
print(f" Max final error: {max_error:.4f} rad ({math.degrees(max_error):.2f}°)")
|
| 295 |
|
| 296 |
print("\n" + "=" * 70)
|
|
|
|
| 301 |
print(f"⚠ Homing incomplete (max error = {max_error:.4f} rad)")
|
| 302 |
success = False
|
| 303 |
|
| 304 |
+
print(f"✓ Homing completed via blocking endpoint")
|
|
|
|
|
|
|
|
|
|
| 305 |
print("=" * 70)
|
| 306 |
|
| 307 |
client.close()
|