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.
- mujoco_server.py +163 -36
- 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 |
-
#
|
| 994 |
-
#
|
| 995 |
-
|
| 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 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 3046 |
-
console.log('Starting local homing
|
| 3047 |
send('home');
|
| 3048 |
-
console.log('Sent first home message');
|
| 3049 |
|
| 3050 |
-
//
|
| 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)
|