Georg commited on
Commit
f6af091
·
1 Parent(s): 9078db7

Add homing functionality to mujoco_server.py and implement test script

Browse files

- Introduced a state-based homing mechanism for UR5 robots, allowing for smoother joint movements towards home positions.
- Added a new `homing_state` dictionary to track the current state of the homing process, including joint errors and jogging status.
- Implemented logic to handle joint overshoot and stuck conditions during the homing sequence.
- Created a `test_home.py` script to simulate continuous home commands, testing the homing functionality and ensuring proper behavior during operation.
- Updated WebSocket message handling to support starting and stopping the homing process based on user input.

Files changed (2) hide show
  1. mujoco_server.py +163 -36
  2. test_home.py +97 -0
mujoco_server.py CHANGED
@@ -122,6 +122,20 @@ last_teleop_command: dict[str, Any] = {
122
  }
123
  teleop_lock = threading.Lock()
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  # WebSocket clients
126
  ws_clients = set()
127
  ws_clients_lock = threading.Lock()
@@ -990,10 +1004,9 @@ def handle_ws_message(ws, data):
990
  _schedule_robot_switch(robot, scene)
991
 
992
  elif msg_type == 'home':
993
- # Stepwise homing for UR5: move robot toward home via joint jogging
994
- # Each home message jogs the joint furthest from home
995
- # If messages stop arriving, robot stops (jogging stops automatically)
996
- with mujoco_lock:
997
  if env is not None and current_robot in ("ur5", "ur5_t_push"):
998
  home_pose = getattr(env, "home_pose", None)
999
  get_joints = getattr(env, "get_joint_positions", None)
@@ -1007,35 +1020,134 @@ def handle_ws_message(ws, data):
1007
  target_positions = list(home_pose)
1008
 
1009
  current_positions = get_joints()
1010
-
1011
- # Stop any existing jog
1012
- stop_jog()
1013
-
1014
- # Find the joint that's furthest from home and jog it
1015
- # We jog one joint at a time for smoother motion
1016
  tolerance = 0.01 # rad (~0.57 degrees)
1017
- max_error = 0.0
1018
- max_error_joint = -1
1019
 
1020
- for joint_idx in range(min(len(target_positions), len(current_positions))):
1021
- current = current_positions[joint_idx]
1022
- target = target_positions[joint_idx]
1023
- error = abs(target - current)
1024
-
1025
- if error > tolerance and error > max_error:
1026
- max_error = error
1027
- max_error_joint = joint_idx
1028
-
1029
- # Jog the joint with the largest error
1030
- if max_error_joint >= 0:
1031
- current = current_positions[max_error_joint]
1032
- target = target_positions[max_error_joint]
1033
- error = target - current
1034
-
1035
- direction = '+' if error > 0 else '-'
1036
- # Use velocity proportional to error, but capped
1037
- velocity = min(0.3, max(0.05, abs(error))) # rad/s, 0.05 to 0.3 rad/s
1038
- start_jog('joint', joint=max_error_joint + 1, direction=direction, velocity=velocity)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1039
 
1040
  elif env is not None:
1041
  # Instant homing for non-UR5 robots
@@ -1048,6 +1160,18 @@ def handle_ws_message(ws, data):
1048
  positions = list(home_pose)
1049
  set_joints(positions)
1050
 
 
 
 
 
 
 
 
 
 
 
 
 
1051
  elif msg_type == 'camera':
1052
  payload = data.get('data', {})
1053
  a = cam.azimuth * np.pi / 180.0
@@ -2126,7 +2250,7 @@ def index():
2126
 
2127
  <div class="rl-notifications" id="rl_notifications_list"></div>
2128
 
2129
- <div class="hint" id="hint_box">
2130
  Drag: Rotate Camera<br>
2131
  Scroll: Zoom
2132
  </div>
@@ -3042,12 +3166,11 @@ def index():
3042
  console.log('Starting Nova jogging homing...');
3043
  startNovaJoggingHome();
3044
  } else {
3045
- // Local mode: send home message
3046
- console.log('Starting local homing, sending home message...');
3047
  send('home');
3048
- console.log('Sent first home message');
3049
 
3050
- // Then send repeatedly while button is held (every 100ms)
3051
  if (homingInterval) clearInterval(homingInterval);
3052
  homingInterval = setInterval(() => {
3053
  send('home');
@@ -3067,10 +3190,14 @@ def index():
3067
  homingInterval = null;
3068
  }
3069
  } else {
 
3070
  if (homingInterval) {
3071
  clearInterval(homingInterval);
3072
  homingInterval = null;
3073
  }
 
 
 
3074
  }
3075
  }
3076
 
 
122
  }
123
  teleop_lock = threading.Lock()
124
 
125
+ # Homing state
126
+ homing_state = {
127
+ "active": False,
128
+ "current_joint": None,
129
+ "joint_errors": [],
130
+ "current_velocity": 0.2,
131
+ "previous_error": None,
132
+ "previous_abs_error": None,
133
+ "stuck_count": 0,
134
+ "is_jogging": False,
135
+ "current_direction": None,
136
+ }
137
+ homing_lock = threading.Lock()
138
+
139
  # WebSocket clients
140
  ws_clients = set()
141
  ws_clients_lock = threading.Lock()
 
1004
  _schedule_robot_switch(robot, scene)
1005
 
1006
  elif msg_type == 'home':
1007
+ # State-based homing: processes one step each time a 'home' message is received
1008
+ # UI sends home messages continuously while button is pressed
1009
+ with mujoco_lock, homing_lock:
 
1010
  if env is not None and current_robot in ("ur5", "ur5_t_push"):
1011
  home_pose = getattr(env, "home_pose", None)
1012
  get_joints = getattr(env, "get_joint_positions", None)
 
1020
  target_positions = list(home_pose)
1021
 
1022
  current_positions = get_joints()
 
 
 
 
 
 
1023
  tolerance = 0.01 # rad (~0.57 degrees)
 
 
1024
 
1025
+ # Initialize homing state if not active
1026
+ if not homing_state["active"]:
1027
+ # Compute differences for all joints
1028
+ joint_errors = []
1029
+ for joint_idx in range(min(len(target_positions), len(current_positions))):
1030
+ current = current_positions[joint_idx]
1031
+ target = target_positions[joint_idx]
1032
+ error = target - current
1033
+ # Wrap angle to [-pi, pi]
1034
+ error = (error + np.pi) % (2 * np.pi) - np.pi
1035
+ abs_error = abs(error)
1036
+
1037
+ if abs_error > tolerance:
1038
+ joint_errors.append({
1039
+ 'index': joint_idx,
1040
+ 'error': error,
1041
+ 'abs_error': abs_error
1042
+ })
1043
+
1044
+ if joint_errors:
1045
+ # Sort by absolute error (largest first)
1046
+ joint_errors.sort(key=lambda x: x['abs_error'], reverse=True)
1047
+ homing_state["active"] = True
1048
+ homing_state["joint_errors"] = joint_errors
1049
+ homing_state["current_joint"] = joint_errors[0]['index']
1050
+ homing_state["current_velocity"] = 0.15
1051
+ homing_state["previous_error"] = None
1052
+ homing_state["previous_abs_error"] = None
1053
+ homing_state["stuck_count"] = 0
1054
+ homing_state["is_jogging"] = False
1055
+ homing_state["current_direction"] = None
1056
+ print(f"🏠 Starting homing sequence for {len(joint_errors)} joints")
1057
+ print(f"→ Homing joint {homing_state['current_joint'] + 1}...")
1058
+ else:
1059
+ print("✓ Already at home position")
1060
+ return
1061
+
1062
+ # Process current joint
1063
+ joint_idx = homing_state["current_joint"]
1064
+ current_pos = current_positions[joint_idx]
1065
+ target_pos = target_positions[joint_idx]
1066
+
1067
+ # Compute shortest angular distance
1068
+ error = target_pos - current_pos
1069
+ error = (error + np.pi) % (2 * np.pi) - np.pi
1070
+ abs_error = abs(error)
1071
+
1072
+ # Check if current joint reached target
1073
+ if abs_error <= tolerance:
1074
+ if homing_state["is_jogging"]:
1075
+ stop_jog()
1076
+ homing_state["is_jogging"] = False
1077
+ print(f"✓ Joint {joint_idx + 1} reached target (error: {abs_error:.4f} rad)")
1078
+
1079
+ # Move to next joint
1080
+ homing_state["joint_errors"].pop(0)
1081
+ if homing_state["joint_errors"]:
1082
+ homing_state["current_joint"] = homing_state["joint_errors"][0]['index']
1083
+ homing_state["current_velocity"] = 0.15
1084
+ homing_state["previous_error"] = None
1085
+ homing_state["previous_abs_error"] = None
1086
+ homing_state["stuck_count"] = 0
1087
+ homing_state["is_jogging"] = False
1088
+ homing_state["current_direction"] = None
1089
+ print(f"→ Homing joint {homing_state['current_joint'] + 1}...")
1090
+ else:
1091
+ # All joints homed
1092
+ print("✓ Homing sequence complete!")
1093
+ homing_state["active"] = False
1094
+ return
1095
+
1096
+ # Check if stuck
1097
+ if homing_state["previous_abs_error"] is not None:
1098
+ if abs(abs_error - homing_state["previous_abs_error"]) < 0.0001:
1099
+ homing_state["stuck_count"] += 1
1100
+ if homing_state["stuck_count"] > 20: # Stuck for ~2 seconds
1101
+ if abs_error <= tolerance * 3:
1102
+ print(f"⚠ Joint {joint_idx + 1} stuck at {abs_error:.4f}, close enough")
1103
+ if homing_state["is_jogging"]:
1104
+ stop_jog()
1105
+ homing_state["is_jogging"] = False
1106
+ # Move to next joint
1107
+ homing_state["joint_errors"].pop(0)
1108
+ if homing_state["joint_errors"]:
1109
+ homing_state["current_joint"] = homing_state["joint_errors"][0]['index']
1110
+ homing_state["current_velocity"] = 0.15
1111
+ homing_state["previous_error"] = None
1112
+ homing_state["previous_abs_error"] = None
1113
+ homing_state["stuck_count"] = 0
1114
+ homing_state["is_jogging"] = False
1115
+ homing_state["current_direction"] = None
1116
+ print(f"→ Homing joint {homing_state['current_joint'] + 1}...")
1117
+ else:
1118
+ homing_state["active"] = False
1119
+ return
1120
+ else:
1121
+ homing_state["stuck_count"] = 0
1122
+
1123
+ # Check for overshoot
1124
+ if homing_state["previous_error"] is not None:
1125
+ prev_err = homing_state["previous_error"]
1126
+ if (prev_err > 0 and error < 0) or (prev_err < 0 and error > 0):
1127
+ print(f"↩ Joint {joint_idx + 1} overshot! {prev_err:.4f} → {error:.4f}")
1128
+ homing_state["current_velocity"] = homing_state["current_velocity"] / 2.0
1129
+ homing_state["current_velocity"] = max(0.005, homing_state["current_velocity"])
1130
+ print(f" Velocity halved to {homing_state['current_velocity']:.3f}")
1131
+ if homing_state["is_jogging"]:
1132
+ stop_jog()
1133
+ homing_state["is_jogging"] = False
1134
+
1135
+ # Calculate velocity and direction
1136
+ direction = '+' if error > 0 else '-'
1137
+ desired_velocity = min(homing_state["current_velocity"], max(0.005, abs_error * 1.0))
1138
+
1139
+ # Start/restart jogging if needed
1140
+ if homing_state["current_direction"] != direction or not homing_state["is_jogging"]:
1141
+ if homing_state["is_jogging"]:
1142
+ stop_jog()
1143
+
1144
+ start_jog('joint', joint=joint_idx + 1, direction=direction, velocity=desired_velocity)
1145
+ homing_state["is_jogging"] = True
1146
+ homing_state["current_direction"] = direction
1147
+ print(f" J{joint_idx + 1}: err={error:.4f}, dir={direction}, vel={desired_velocity:.3f}")
1148
+
1149
+ homing_state["previous_error"] = error
1150
+ homing_state["previous_abs_error"] = abs_error
1151
 
1152
  elif env is not None:
1153
  # Instant homing for non-UR5 robots
 
1160
  positions = list(home_pose)
1161
  set_joints(positions)
1162
 
1163
+ elif msg_type == 'stop_home':
1164
+ # Stop homing when button is released
1165
+ with mujoco_lock, homing_lock:
1166
+ if homing_state["active"]:
1167
+ if env is not None and current_robot in ("ur5", "ur5_t_push"):
1168
+ stop_jog = getattr(env, "stop_jog", None)
1169
+ if callable(stop_jog) and homing_state["is_jogging"]:
1170
+ stop_jog()
1171
+ print("⏸ Homing stopped by user")
1172
+ homing_state["active"] = False
1173
+ homing_state["is_jogging"] = False
1174
+
1175
  elif msg_type == 'camera':
1176
  payload = data.get('data', {})
1177
  a = cam.azimuth * np.pi / 180.0
 
2250
 
2251
  <div class="rl-notifications" id="rl_notifications_list"></div>
2252
 
2253
+ <div class="hint" id="hint_box" style="display: none;">
2254
  Drag: Rotate Camera<br>
2255
  Scroll: Zoom
2256
  </div>
 
3166
  console.log('Starting Nova jogging homing...');
3167
  startNovaJoggingHome();
3168
  } else {
3169
+ // Local mode: send home messages continuously while button is pressed
3170
+ console.log('Starting local homing...');
3171
  send('home');
 
3172
 
3173
+ // Send home message every 100ms while button is pressed
3174
  if (homingInterval) clearInterval(homingInterval);
3175
  homingInterval = setInterval(() => {
3176
  send('home');
 
3190
  homingInterval = null;
3191
  }
3192
  } else {
3193
+ // Stop the home message interval
3194
  if (homingInterval) {
3195
  clearInterval(homingInterval);
3196
  homingInterval = null;
3197
  }
3198
+ // Send stop_home message to backend
3199
+ send('stop_home');
3200
+ console.log('Sent stop_home message');
3201
  }
3202
  }
3203
 
test_home.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test the home function by sending continuous home commands.
4
+ Simulates the UI button press behavior.
5
+ """
6
+
7
+ import time
8
+ import json
9
+ from websocket import create_connection
10
+ import threading
11
+
12
+ WS_URL = "ws://localhost:3004/nova-sim/api/v1/ws"
13
+
14
+ print("=" * 80)
15
+ print("HOME FUNCTION TEST")
16
+ print("=" * 80)
17
+
18
+ # Connect to WebSocket
19
+ print("\n1. Connecting to WebSocket...")
20
+ ws = create_connection(WS_URL)
21
+ print(" ✓ Connected")
22
+
23
+ # Switch to UR5
24
+ print("\n2. Switching to UR5...")
25
+ ws.send(json.dumps({"type": "switch_robot", "data": {"robot": "ur5", "scene": None}}))
26
+ time.sleep(2)
27
+ print(" ✓ Switched to UR5")
28
+
29
+ # Jog some joints away from home position
30
+ print("\n3. Moving joints away from home...")
31
+ for joint, direction, duration in [(1, "+", 2), (3, "-", 2), (6, "+", 3)]:
32
+ ws.send(json.dumps({
33
+ "type": "start_jog",
34
+ "data": {"jog_type": "joint", "params": {"joint": joint, "direction": direction, "velocity": 0.4}}
35
+ }))
36
+ time.sleep(duration)
37
+ ws.send(json.dumps({"type": "stop_jog"}))
38
+ time.sleep(0.5)
39
+ print(" ✓ Joints moved away from home")
40
+
41
+ # Send continuous home messages (simulating button press)
42
+ print("\n4. Starting HOME sequence (sending messages every 100ms)...")
43
+ print(" Press Ctrl+C to stop or wait 60 seconds")
44
+ print("=" * 80)
45
+ print("\n📺 WATCH SERVER CONSOLE FOR:")
46
+ print(" 🏠 Starting homing sequence for X joints")
47
+ print(" → Homing joint X...")
48
+ print(" J1: err=..., dir=..., vel=...")
49
+ print(" ✓ Joint X reached target")
50
+ print(" ✓ Homing sequence complete!")
51
+ print("")
52
+
53
+ stop_homing = False
54
+ message_count = 0
55
+
56
+ def send_home_messages():
57
+ """Send home messages every 100ms while running"""
58
+ global message_count
59
+ while not stop_homing:
60
+ ws.send(json.dumps({"type": "home"}))
61
+ message_count += 1
62
+ time.sleep(0.1)
63
+
64
+ # Start sending home messages in background
65
+ home_thread = threading.Thread(target=send_home_messages, daemon=True)
66
+ home_thread.start()
67
+
68
+ try:
69
+ # Wait for homing to complete (up to 60 seconds)
70
+ for i in range(12):
71
+ time.sleep(5)
72
+ print(f" {(i+1)*5}s - sent {message_count} home messages")
73
+
74
+ print("\n Timeout reached (60 seconds)")
75
+
76
+ except KeyboardInterrupt:
77
+ print("\n\n Interrupted by user")
78
+
79
+ # Stop homing
80
+ print("\n5. Stopping homing...")
81
+ stop_homing = True
82
+ time.sleep(0.2)
83
+ ws.send(json.dumps({"type": "stop_home"}))
84
+ print(f" ✓ Sent stop_home message (total: {message_count} home messages)")
85
+
86
+ # Close connection
87
+ ws.close()
88
+
89
+ print("\n" + "=" * 80)
90
+ print("TEST COMPLETE")
91
+ print("=" * 80)
92
+ print("\nCheck the server console to verify:")
93
+ print(" ✓ Sequential homing (one joint after another)")
94
+ print(" ✓ Joints stopped when error <= 0.01 rad")
95
+ print(" ✓ Overshoot detection and velocity halving (if applicable)")
96
+ print(" ✓ 'Homing sequence complete!' was printed")
97
+ print("=" * 80)