try with a claude edit
Browse fileshttps://claude.site/artifacts/08fd681d-335d-406c-9121-1c4bb623671a
app.py
CHANGED
@@ -6,355 +6,598 @@ import threading
|
|
6 |
import time
|
7 |
from collections import deque
|
8 |
import namesgenerator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
# Queue system setup
|
11 |
SESSION_TIME = 120
|
|
|
12 |
|
13 |
class QueueSystem:
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
|
62 |
queue_system = QueueSystem()
|
63 |
|
64 |
def queue_size():
|
65 |
-
|
66 |
-
|
67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
def background_timer():
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
|
76 |
timer_thread = threading.Thread(target=background_timer, daemon=True)
|
77 |
timer_thread.start()
|
78 |
|
79 |
-
user, pwd, host, endpoint, port = get_credentials(False)
|
80 |
-
client = CobotController(user, pwd, host, port, endpoint)
|
81 |
-
|
82 |
CSS = """
|
83 |
#col {
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
}
|
88 |
#nogaprow {
|
89 |
-
|
90 |
}
|
91 |
#nogapcol {
|
92 |
-
|
93 |
-
|
94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
}
|
96 |
"""
|
97 |
|
98 |
"""
|
99 |
-
|
100 |
whether the command should be executed.
|
101 |
"""
|
102 |
def authenticate_user(user_id):
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
|
|
|
|
|
|
119 |
|
120 |
def enter_queue(user_id):
|
121 |
-
|
122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
|
124 |
def query_angles(user_id):
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
|
|
|
|
|
|
|
|
|
|
132 |
|
133 |
def query_coords(user_id):
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
|
|
|
|
|
|
|
|
|
|
141 |
|
142 |
def query_gripper(user_id):
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
|
|
|
|
|
|
|
|
|
|
150 |
|
151 |
def query_camera(user_id):
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
|
|
|
|
|
|
|
|
|
|
162 |
|
163 |
def control_angles(user_id, angle0, angle1, angle2, angle3, angle4, angle5, movement_speed):
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
|
|
|
|
|
|
|
|
|
|
171 |
|
172 |
def control_coords(user_id, x, y, z, roll, pitch, yaw, movement_speed):
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
|
|
|
|
|
|
|
|
|
|
180 |
|
181 |
def control_gripper(user_id, gripper_value, movement_speed):
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
|
|
|
|
|
|
|
|
|
|
189 |
|
190 |
def set_coords_to_current(user_id):
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
|
|
|
|
|
|
|
|
200 |
|
201 |
def set_angles_to_current(user_id):
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
|
|
|
|
|
|
|
|
211 |
|
212 |
def reset():
|
213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
214 |
|
215 |
with gr.Blocks(css=CSS) as app:
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
import time
|
7 |
from collections import deque
|
8 |
import namesgenerator
|
9 |
+
import logging
|
10 |
+
import traceback
|
11 |
+
|
12 |
+
# Setup logging
|
13 |
+
logging.basicConfig(
|
14 |
+
level=logging.INFO,
|
15 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
16 |
+
handlers=[
|
17 |
+
logging.FileHandler("cobot_controller.log"),
|
18 |
+
logging.StreamHandler()
|
19 |
+
]
|
20 |
+
)
|
21 |
+
logger = logging.getLogger("cobot_controller")
|
22 |
|
23 |
# Queue system setup
|
24 |
SESSION_TIME = 120
|
25 |
+
MQTT_RECONNECT_TIMEOUT = 5 # seconds to wait before reconnecting
|
26 |
|
27 |
class QueueSystem:
|
28 |
+
def __init__(self):
|
29 |
+
self.queue = deque()
|
30 |
+
self.current_user = None
|
31 |
+
self.session_start_time = None
|
32 |
+
self.lock = threading.Lock()
|
33 |
+
self.active_users = set() # Track all users who have interacted with the system
|
34 |
+
|
35 |
+
def enqueue_user(self, user_id):
|
36 |
+
with self.lock:
|
37 |
+
if not user_id:
|
38 |
+
return False
|
39 |
+
|
40 |
+
# Add user to active users set
|
41 |
+
self.active_users.add(user_id)
|
42 |
+
|
43 |
+
if user_id not in self.queue and user_id != self.current_user:
|
44 |
+
self.queue.append(user_id)
|
45 |
+
logger.info(f"User {user_id} added to queue. Queue length: {len(self.queue)}")
|
46 |
+
return True
|
47 |
+
return False
|
48 |
+
|
49 |
+
def dequeue_user(self):
|
50 |
+
with self.lock:
|
51 |
+
if self.queue:
|
52 |
+
next_user = self.queue.popleft()
|
53 |
+
logger.info(f"User {next_user} dequeued. Queue length: {len(self.queue)}")
|
54 |
+
return next_user
|
55 |
+
return None
|
56 |
+
|
57 |
+
def get_queue_info(self, user_id):
|
58 |
+
with self.lock:
|
59 |
+
if not user_id:
|
60 |
+
return None, None
|
61 |
+
|
62 |
+
if user_id == self.current_user:
|
63 |
+
remaining_time = max(0, SESSION_TIME - (time.time() - self.session_start_time))
|
64 |
+
return 0, remaining_time
|
65 |
+
elif user_id in self.queue:
|
66 |
+
position = list(self.queue).index(user_id) + 1
|
67 |
+
if self.session_start_time:
|
68 |
+
wait_time = (position - 1) * SESSION_TIME + max(0, SESSION_TIME - (time.time() - self.session_start_time))
|
69 |
+
else:
|
70 |
+
wait_time = position * SESSION_TIME
|
71 |
+
return position, wait_time
|
72 |
+
else:
|
73 |
+
return None, None
|
74 |
+
|
75 |
+
def start_session(self, user_id):
|
76 |
+
with self.lock:
|
77 |
+
if not user_id:
|
78 |
+
return False
|
79 |
+
|
80 |
+
if self.current_user is None:
|
81 |
+
self.current_user = user_id
|
82 |
+
self.session_start_time = time.time()
|
83 |
+
logger.info(f"Session started for user {user_id}")
|
84 |
+
return True
|
85 |
+
return False
|
86 |
+
|
87 |
+
def end_session(self):
|
88 |
+
with self.lock:
|
89 |
+
if self.current_user and time.time() - self.session_start_time >= SESSION_TIME:
|
90 |
+
logger.info(f"Session ended for user {self.current_user}")
|
91 |
+
self.current_user = None
|
92 |
+
self.session_start_time = None
|
93 |
+
return True
|
94 |
+
return False
|
95 |
+
|
96 |
+
def force_end_session(self):
|
97 |
+
"""Force end the current session regardless of time remaining"""
|
98 |
+
with self.lock:
|
99 |
+
if self.current_user:
|
100 |
+
logger.info(f"Session forcefully ended for user {self.current_user}")
|
101 |
+
self.current_user = None
|
102 |
+
self.session_start_time = None
|
103 |
+
return True
|
104 |
+
return False
|
105 |
+
|
106 |
+
def get_queue_size(self):
|
107 |
+
with self.lock:
|
108 |
+
return len(self.queue)
|
109 |
|
110 |
queue_system = QueueSystem()
|
111 |
|
112 |
def queue_size():
|
113 |
+
size = queue_system.get_queue_size()
|
114 |
+
return f"There are {size} people in the queue."
|
115 |
+
|
116 |
+
# Initialize the client with a connection manager
|
117 |
+
def get_client():
|
118 |
+
"""Factory function to create and return a client with fresh credentials"""
|
119 |
+
try:
|
120 |
+
user, pwd, host, endpoint, port = get_credentials(False)
|
121 |
+
client = CobotController(user, pwd, host, port, endpoint)
|
122 |
+
logger.info("Created new MQTT client connection")
|
123 |
+
return client
|
124 |
+
except Exception as e:
|
125 |
+
logger.error(f"Failed to create MQTT client: {str(e)}")
|
126 |
+
logger.error(traceback.format_exc())
|
127 |
+
return None
|
128 |
+
|
129 |
+
# Create initial client
|
130 |
+
client = get_client()
|
131 |
+
|
132 |
+
# Function to refresh client connection
|
133 |
+
def refresh_client():
|
134 |
+
global client
|
135 |
+
logger.info("Refreshing MQTT client connection")
|
136 |
+
# Close existing connection if possible
|
137 |
+
try:
|
138 |
+
if client:
|
139 |
+
client.disconnect()
|
140 |
+
except:
|
141 |
+
pass
|
142 |
+
|
143 |
+
# Create new connection
|
144 |
+
client = get_client()
|
145 |
+
return client is not None
|
146 |
+
|
147 |
+
# Background timer thread to end session after SESSION_TIME and manage connections
|
148 |
def background_timer():
|
149 |
+
global client
|
150 |
+
connection_failures = 0
|
151 |
+
|
152 |
+
while True:
|
153 |
+
time.sleep(1)
|
154 |
+
|
155 |
+
# Check connection health periodically
|
156 |
+
if connection_failures > 5:
|
157 |
+
if refresh_client():
|
158 |
+
connection_failures = 0
|
159 |
+
else:
|
160 |
+
connection_failures += 1
|
161 |
+
time.sleep(MQTT_RECONNECT_TIMEOUT)
|
162 |
+
|
163 |
+
# Handle queue
|
164 |
+
if queue_system.end_session():
|
165 |
+
next_user = queue_system.dequeue_user()
|
166 |
+
if next_user:
|
167 |
+
queue_system.start_session(next_user)
|
168 |
|
169 |
timer_thread = threading.Thread(target=background_timer, daemon=True)
|
170 |
timer_thread.start()
|
171 |
|
|
|
|
|
|
|
172 |
CSS = """
|
173 |
#col {
|
174 |
+
background-color: #161624;
|
175 |
+
padding: 16px;
|
176 |
+
border-radius: 8px;
|
177 |
}
|
178 |
#nogaprow {
|
179 |
+
gap: 0px !important;
|
180 |
}
|
181 |
#nogapcol {
|
182 |
+
padding: 0px !important;
|
183 |
+
border: none !important;
|
184 |
+
box-shadow: none !important;
|
185 |
+
}
|
186 |
+
.status-error {
|
187 |
+
color: #ff5555;
|
188 |
+
font-weight: bold;
|
189 |
+
}
|
190 |
+
.status-success {
|
191 |
+
color: #55ff55;
|
192 |
+
font-weight: bold;
|
193 |
}
|
194 |
"""
|
195 |
|
196 |
"""
|
197 |
+
Checks the user position on the queue and returns a message along with
|
198 |
whether the command should be executed.
|
199 |
"""
|
200 |
def authenticate_user(user_id):
|
201 |
+
if not user_id:
|
202 |
+
return False, "Error: Please enter a valid user ID"
|
203 |
+
|
204 |
+
if queue_system.current_user is None:
|
205 |
+
queue_system.start_session(user_id)
|
206 |
+
queue_system.enqueue_user(user_id)
|
207 |
+
position, wait_time = queue_system.get_queue_info(user_id)
|
208 |
+
|
209 |
+
if position == 0:
|
210 |
+
remaining_time_msg = f"Your turn!\nTime remaining: {wait_time:.2f} seconds."
|
211 |
+
return True, remaining_time_msg
|
212 |
+
elif position is not None:
|
213 |
+
if position == 1:
|
214 |
+
wait_msg = f"You are next!\nWait time: {wait_time:.2f} seconds."
|
215 |
+
else:
|
216 |
+
wait_msg = f"There are {position - 1} people ahead of you in the queue.\nWait time: {wait_time:.2f} seconds."
|
217 |
+
return False, wait_msg
|
218 |
+
else:
|
219 |
+
return False, "Error: You are not in the queue."
|
220 |
|
221 |
def enter_queue(user_id):
|
222 |
+
if not user_id:
|
223 |
+
return "Error: Please enter a valid user ID"
|
224 |
+
|
225 |
+
queue_system.enqueue_user(user_id)
|
226 |
+
_, msg = authenticate_user(user_id)
|
227 |
+
return msg
|
228 |
+
|
229 |
+
def execute_with_retry(func, *args):
|
230 |
+
"""Execute a client function with retry logic"""
|
231 |
+
global client
|
232 |
+
max_retries = 3
|
233 |
+
|
234 |
+
for attempt in range(max_retries):
|
235 |
+
try:
|
236 |
+
if client is None:
|
237 |
+
refresh_client()
|
238 |
+
if client is None:
|
239 |
+
return {"success": False, "message": "Failed to connect to robot"}
|
240 |
+
|
241 |
+
result = func(*args)
|
242 |
+
# If we get here, the function executed without error
|
243 |
+
return result
|
244 |
+
|
245 |
+
except Exception as e:
|
246 |
+
logger.error(f"Error on attempt {attempt+1}/{max_retries}: {str(e)}")
|
247 |
+
logger.error(traceback.format_exc())
|
248 |
+
|
249 |
+
# Try to refresh the client connection
|
250 |
+
refresh_client()
|
251 |
+
|
252 |
+
# If this was our last retry, return failure
|
253 |
+
if attempt == max_retries - 1:
|
254 |
+
return {"success": False, "message": f"Command failed after {max_retries} attempts: {str(e)}"}
|
255 |
+
|
256 |
+
# Wait before retrying
|
257 |
+
time.sleep(1)
|
258 |
|
259 |
def query_angles(user_id):
|
260 |
+
to_execute, queue_status_msg = authenticate_user(user_id)
|
261 |
+
if to_execute:
|
262 |
+
try:
|
263 |
+
resp = execute_with_retry(client.get_angles)
|
264 |
+
resp["command"] = "query/angles"
|
265 |
+
return json.dumps(resp, indent=4), queue_status_msg
|
266 |
+
except Exception as e:
|
267 |
+
logger.error(f"Error in query_angles: {str(e)}")
|
268 |
+
error_msg = {"success": False, "message": f"Error: {str(e)}", "command": "query/angles"}
|
269 |
+
return json.dumps(error_msg, indent=4), queue_status_msg
|
270 |
+
else:
|
271 |
+
return None, queue_status_msg
|
272 |
|
273 |
def query_coords(user_id):
|
274 |
+
to_execute, queue_status_msg = authenticate_user(user_id)
|
275 |
+
if to_execute:
|
276 |
+
try:
|
277 |
+
resp = execute_with_retry(client.get_coords)
|
278 |
+
resp["command"] = "query/coords"
|
279 |
+
return json.dumps(resp, indent=4), queue_status_msg
|
280 |
+
except Exception as e:
|
281 |
+
logger.error(f"Error in query_coords: {str(e)}")
|
282 |
+
error_msg = {"success": False, "message": f"Error: {str(e)}", "command": "query/coords"}
|
283 |
+
return json.dumps(error_msg, indent=4), queue_status_msg
|
284 |
+
else:
|
285 |
+
return None, queue_status_msg
|
286 |
|
287 |
def query_gripper(user_id):
|
288 |
+
to_execute, queue_status_msg = authenticate_user(user_id)
|
289 |
+
if to_execute:
|
290 |
+
try:
|
291 |
+
resp = execute_with_retry(client.get_gripper_value)
|
292 |
+
resp["command"] = "query/gripper"
|
293 |
+
return json.dumps(resp, indent=4), queue_status_msg
|
294 |
+
except Exception as e:
|
295 |
+
logger.error(f"Error in query_gripper: {str(e)}")
|
296 |
+
error_msg = {"success": False, "message": f"Error: {str(e)}", "command": "query/gripper"}
|
297 |
+
return json.dumps(error_msg, indent=4), queue_status_msg
|
298 |
+
else:
|
299 |
+
return None, queue_status_msg
|
300 |
|
301 |
def query_camera(user_id):
|
302 |
+
to_execute, queue_status_msg = authenticate_user(user_id)
|
303 |
+
if to_execute:
|
304 |
+
try:
|
305 |
+
resp = execute_with_retry(client.get_camera)
|
306 |
+
resp["command"] = "query/camera"
|
307 |
+
if not resp["success"]:
|
308 |
+
return json.dumps(resp, indent=4), None, queue_status_msg
|
309 |
+
img = resp.pop("image")
|
310 |
+
return json.dumps(resp, indent=4), gr.Image(visible=True, value=img), queue_status_msg
|
311 |
+
except Exception as e:
|
312 |
+
logger.error(f"Error in query_camera: {str(e)}")
|
313 |
+
error_msg = {"success": False, "message": f"Error: {str(e)}", "command": "query/camera"}
|
314 |
+
return json.dumps(error_msg, indent=4), None, queue_status_msg
|
315 |
+
else:
|
316 |
+
return None, None, queue_status_msg
|
317 |
|
318 |
def control_angles(user_id, angle0, angle1, angle2, angle3, angle4, angle5, movement_speed):
|
319 |
+
to_execute, queue_status_msg = authenticate_user(user_id)
|
320 |
+
if to_execute:
|
321 |
+
try:
|
322 |
+
resp = execute_with_retry(client.send_angles, [angle0, angle1, angle2, angle3, angle4, angle5], movement_speed)
|
323 |
+
resp["command"] = "control/angles"
|
324 |
+
return json.dumps(resp, indent=4), queue_status_msg
|
325 |
+
except Exception as e:
|
326 |
+
logger.error(f"Error in control_angles: {str(e)}")
|
327 |
+
error_msg = {"success": False, "message": f"Error: {str(e)}", "command": "control/angles"}
|
328 |
+
return json.dumps(error_msg, indent=4), queue_status_msg
|
329 |
+
else:
|
330 |
+
return None, queue_status_msg
|
331 |
|
332 |
def control_coords(user_id, x, y, z, roll, pitch, yaw, movement_speed):
|
333 |
+
to_execute, queue_status_msg = authenticate_user(user_id)
|
334 |
+
if to_execute:
|
335 |
+
try:
|
336 |
+
resp = execute_with_retry(client.send_coords, [x, y, z, roll, pitch, yaw], movement_speed)
|
337 |
+
resp["command"] = "control/coords"
|
338 |
+
return json.dumps(resp, indent=4), queue_status_msg
|
339 |
+
except Exception as e:
|
340 |
+
logger.error(f"Error in control_coords: {str(e)}")
|
341 |
+
error_msg = {"success": False, "message": f"Error: {str(e)}", "command": "control/coords"}
|
342 |
+
return json.dumps(error_msg, indent=4), queue_status_msg
|
343 |
+
else:
|
344 |
+
return None, queue_status_msg
|
345 |
|
346 |
def control_gripper(user_id, gripper_value, movement_speed):
|
347 |
+
to_execute, queue_status_msg = authenticate_user(user_id)
|
348 |
+
if to_execute:
|
349 |
+
try:
|
350 |
+
resp = execute_with_retry(client.send_gripper_value, gripper_value, movement_speed)
|
351 |
+
resp["command"] = "control/gripper"
|
352 |
+
return json.dumps(resp, indent=4), queue_status_msg
|
353 |
+
except Exception as e:
|
354 |
+
logger.error(f"Error in control_gripper: {str(e)}")
|
355 |
+
error_msg = {"success": False, "message": f"Error: {str(e)}", "command": "control/gripper"}
|
356 |
+
return json.dumps(error_msg, indent=4), queue_status_msg
|
357 |
+
else:
|
358 |
+
return None, queue_status_msg
|
359 |
|
360 |
def set_coords_to_current(user_id):
|
361 |
+
to_execute, queue_status_msg = authenticate_user(user_id)
|
362 |
+
if to_execute:
|
363 |
+
try:
|
364 |
+
resp, _ = query_coords(user_id)
|
365 |
+
resp = json.loads(resp)
|
366 |
+
if not resp["success"]:
|
367 |
+
return None, None, None, None, None, None, queue_status_msg
|
368 |
+
return resp["coords"] + [queue_status_msg]
|
369 |
+
except Exception as e:
|
370 |
+
logger.error(f"Error in set_coords_to_current: {str(e)}")
|
371 |
+
return None, None, None, None, None, None, f"Error: {str(e)}\n{queue_status_msg}"
|
372 |
+
else:
|
373 |
+
return None, None, None, None, None, None, queue_status_msg
|
374 |
|
375 |
def set_angles_to_current(user_id):
|
376 |
+
to_execute, queue_status_msg = authenticate_user(user_id)
|
377 |
+
if to_execute:
|
378 |
+
try:
|
379 |
+
resp, _ = query_angles(user_id)
|
380 |
+
resp = json.loads(resp)
|
381 |
+
if not resp["success"]:
|
382 |
+
return None, None, None, None, None, None, queue_status_msg
|
383 |
+
return resp["angles"] + [queue_status_msg]
|
384 |
+
except Exception as e:
|
385 |
+
logger.error(f"Error in set_angles_to_current: {str(e)}")
|
386 |
+
return None, None, None, None, None, None, f"Error: {str(e)}\n{queue_status_msg}"
|
387 |
+
else:
|
388 |
+
return None, None, None, None, None, None, queue_status_msg
|
389 |
|
390 |
def reset():
|
391 |
+
return 0, 0, 0, 0, 0, 0, 50
|
392 |
+
|
393 |
+
def force_refresh_connection(user_id):
|
394 |
+
"""Force refresh the MQTT connection"""
|
395 |
+
to_execute, queue_status_msg = authenticate_user(user_id)
|
396 |
+
if to_execute:
|
397 |
+
success = refresh_client()
|
398 |
+
if success:
|
399 |
+
return "Connection refreshed successfully.", queue_status_msg
|
400 |
+
else:
|
401 |
+
return "Failed to refresh connection.", queue_status_msg
|
402 |
+
else:
|
403 |
+
return None, queue_status_msg
|
404 |
+
|
405 |
+
def skip_current_user(admin_password, user_id):
|
406 |
+
"""Admin function to skip the current user and move to the next in queue"""
|
407 |
+
# Very simple admin password check
|
408 |
+
if admin_password != "admin123": # Change this to a secure password
|
409 |
+
return "Invalid admin password"
|
410 |
+
|
411 |
+
if queue_system.force_end_session():
|
412 |
+
next_user = queue_system.dequeue_user()
|
413 |
+
if next_user:
|
414 |
+
queue_system.start_session(next_user)
|
415 |
+
return f"Skipped current user. New user {next_user} is now active."
|
416 |
+
else:
|
417 |
+
return "Skipped current user. No users in queue."
|
418 |
+
else:
|
419 |
+
return "No active user to skip."
|
420 |
|
421 |
with gr.Blocks(css=CSS) as app:
|
422 |
+
gr.Markdown("# MyCobot 280pi MQTT Control Demo")
|
423 |
+
gr.HTML('''
|
424 |
+
<a href="https://colab.research.google.com/github/AccelerationConsortium/ac-training-lab/blob/cobot-usage-docs/src/ac_training_lab/cobot280pi/gradio-client-demo.ipynb" target="_blank">
|
425 |
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="117" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="117" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h30v20H0z"/><path fill="#007ec6" d="M30 0h87v20H30z"/><path fill="url(#b)" d="M0 0h117v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><svg x="4px" y="0px" width="22px" height="20px" viewBox="-2 0 28 24" style="background-color: #fff;border-radius: 1px;"><path style="fill:#e8710a;" d="M1.977,16.77c-2.667-2.277-2.605-7.079,0-9.357C2.919,8.057,3.522,9.075,4.49,9.691c-1.152,1.6-1.146,3.201-0.004,4.803C3.522,15.111,2.918,16.126,1.977,16.77z"/><path style="fill:#f9ab00;" d="M12.257,17.114c-1.767-1.633-2.485-3.658-2.118-6.02c0.451-2.91,2.139-4.893,4.946-5.678c2.565-0.718,4.964-0.217,6.878,1.819c-0.884,0.743-1.707,1.547-2.434,2.446C18.488,8.827,17.319,8.435,16,8.856c-2.404,0.767-3.046,3.241-1.494,5.644c-0.241,0.275-0.493,0.541-0.721,0.826C13.295,15.939,12.511,16.3,12.257,17.114z"/><path style="fill:#e8710a;" d="M19.529,9.682c0.727-0.899,1.55-1.703,2.434-2.446c2.703,2.783,2.701,7.031-0.005,9.764c-2.648,2.674-6.936,2.725-9.701,0.115c0.254-0.814,1.038-1.175,1.528-1.788c0.228-0.285,0.48-0.552,0.721-0.826c1.053,0.916,2.254,1.268,3.6,0.83C20.502,14.551,21.151,11.927,19.529,9.682z"/><path style="fill:#f9ab00;" d="M4.49,9.691C3.522,9.075,2.919,8.057,1.977,7.413c2.209-2.398,5.721-2.942,8.476-1.355c0.555,0.32,0.719,0.606,0.285,1.128c-0.157,0.188-0.258,0.422-0.391,0.631c-0.299,0.47-0.509,1.067-0.929,1.371C8.933,9.539,8.523,8.847,8.021,8.746C6.673,8.475,5.509,8.787,4.49,9.691z"/><path style="fill:#f9ab00;" d="M1.977,16.77c0.941-0.644,1.545-1.659,2.509-2.277c1.373,1.152,2.85,1.433,4.45,0.499c0.332-0.194,0.503-0.088,0.673,0.19c0.386,0.635,0.753,1.285,1.181,1.89c0.34,0.48,0.222,0.715-0.253,1.006C7.84,19.73,4.205,19.188,1.977,16.77z"/></svg><text x="245" y="140" transform="scale(.1)" textLength="30"> </text><text x="725" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="770">Open in Colab</text><text x="725" y="140" transform="scale(.1)" textLength="770">Open in Colab</text></g> </svg>
|
426 |
+
</a>
|
427 |
+
''')
|
428 |
+
gr.Markdown("This is a demo that uses the MQTT protocol to communicate with the MyCobot 280pi over the internet. You can remotely send commands to the cobot and query its current status.")
|
429 |
+
|
430 |
+
with gr.Accordion("System Status", open=True):
|
431 |
+
connection_status = gr.Textbox(label="Connection Status", value="Connected", interactive=False)
|
432 |
+
refresh_connection_btn = gr.Button("Refresh Connection")
|
433 |
+
|
434 |
+
with gr.Row():
|
435 |
+
with gr.Column():
|
436 |
+
user_id = gr.Textbox(label="User ID", info="Enter a unique user id of your choice or take note of the automatically generated user id. This user id will be placed into a queue to give you access to the cobot, so make sure you remember it!")
|
437 |
+
enter_queue_button = gr.Button("Join queue")
|
438 |
+
with gr.Column():
|
439 |
+
status_text = gr.Textbox(label="Queue status", value="", interactive=False, lines=5)
|
440 |
+
|
441 |
+
with gr.Accordion("Admin Controls", open=False):
|
442 |
+
with gr.Row():
|
443 |
+
admin_password = gr.Textbox(label="Admin Password", type="password")
|
444 |
+
skip_user_button = gr.Button("Skip Current User")
|
445 |
+
admin_result = gr.Textbox(label="Admin Result", interactive=False)
|
446 |
+
|
447 |
+
with gr.Row():
|
448 |
+
# QUERY PANEL
|
449 |
+
with gr.Column(elem_id="col"):
|
450 |
+
gr.Markdown("## Query")
|
451 |
+
gr.Markdown("Use buttons on this panel to query the current status of the cobot, including information like joint angles, coordinates, gripper state and what the onboard camera sees.")
|
452 |
+
angle_query_button = gr.Button("Query Angles")
|
453 |
+
coord_query_button = gr.Button("Query Coordinates")
|
454 |
+
gripper_query_button = gr.Button("Query Gripper state")
|
455 |
+
camera_query_button = gr.Button("Query Camera")
|
456 |
+
|
457 |
+
# GRIPPER PANEL
|
458 |
+
with gr.Column(elem_id="col"):
|
459 |
+
gr.Markdown("## Gripper Control")
|
460 |
+
gr.Markdown("Use this panel to control the gripper of the cobot.")
|
461 |
+
gripper_value = gr.Slider(minimum=0.0, maximum=100.0, step=1.0, label="Gripper value")
|
462 |
+
speed_gripper = gr.Slider(value=50.0, minimum=0.0, maximum=100.0, step=1.0, label="Movement speed")
|
463 |
+
gripper_control_button = gr.Button("Send gripper command")
|
464 |
+
|
465 |
+
with gr.Row():
|
466 |
+
# ANGLE PANEL
|
467 |
+
with gr.Column(elem_id="col"):
|
468 |
+
gr.Markdown("## Angle Control")
|
469 |
+
gr.Markdown("Use this panel to control the joint angles of the cobot. Each angle corresponds to one of the 6 joints on the cobot.")
|
470 |
+
angle_set_button = gr.Button("Set to current angles")
|
471 |
+
reset_angle_button = gr.Button("Reset angles")
|
472 |
+
with gr.Row(elem_id="nogaprow"):
|
473 |
+
with gr.Column(elem_id="nogapcol"):
|
474 |
+
angle1 = gr.Slider(value=0.0, label="Angle 1", step=1.0, minimum=-168, maximum=168)
|
475 |
+
angle3 = gr.Slider(value=0.0, label="Angle 3", step=1.0, minimum=-135, maximum=135)
|
476 |
+
angle5 = gr.Slider(value=0.0, label="Angle 5", step=1.0, minimum=-150, maximum=150)
|
477 |
+
with gr.Column(elem_id="nogapcol"):
|
478 |
+
angle2 = gr.Slider(value=0.0, label="Angle 2", step=1.0, minimum=-145, maximum=145)
|
479 |
+
angle4 = gr.Slider(value=0.0, label="Angle 4", step=1.0, minimum=-165, maximum=165)
|
480 |
+
angle6 = gr.Slider(value=0.0, label="Angle 6", step=1.0, minimum=-180, maximum=180)
|
481 |
+
speed_angles = gr.Slider(value=50.0, minimum=0.0, maximum=100.0, step=1.0, label="Movement speed")
|
482 |
+
angle_control_button = gr.Button("Send angle command")
|
483 |
+
|
484 |
+
# COORD PANEL
|
485 |
+
with gr.Column(elem_id="col"):
|
486 |
+
gr.Markdown("## Coordinate Control")
|
487 |
+
gr.Markdown("Use this panel to control the joint coordinates of the cobot head. The angles are in [6-DoF format](https://en.wikipedia.org/wiki/Six_degrees_of_freedom).")
|
488 |
+
coord_set_button = gr.Button("Set to current coords")
|
489 |
+
reset_coords_button = gr.Button("Reset coordinates")
|
490 |
+
with gr.Row(elem_id="nogaprow"):
|
491 |
+
with gr.Column(elem_id="nogapcol"):
|
492 |
+
xcoord = gr.Slider(value=0.0, label="X coordinate", step=1.0, minimum=-350, maximum=350)
|
493 |
+
ycoord = gr.Slider(value=0.0, label="Y coordinate", step=1.0, minimum=-350, maximum=350)
|
494 |
+
zcoord = gr.Slider(value=0.0, label="Z coordinate", step=1.0, minimum=-70, maximum=523)
|
495 |
+
with gr.Column(elem_id="nogapcol"):
|
496 |
+
roll = gr.Slider(value=0.0, label="Roll", step=1.0, minimum=-180, maximum=180)
|
497 |
+
pitch = gr.Slider(value=0.0, label="Pitch", step=1.0, minimum=-180, maximum=180)
|
498 |
+
yaw = gr.Slider(value=0.0, label="Yaw", step=1.0, minimum=-180, maximum=180)
|
499 |
+
speed_coords = gr.Slider(value=50.0, minimum=0.0, maximum=100.0, step=1.0, label="Movement speed")
|
500 |
+
coord_control_button = gr.Button("Send coordinate command")
|
501 |
+
|
502 |
+
response = gr.Textbox(label="Response")
|
503 |
+
response_image = gr.Image(visible=False)
|
504 |
+
|
505 |
+
# Queue-aware event handling
|
506 |
+
refresh_connection_btn.click(
|
507 |
+
force_refresh_connection,
|
508 |
+
inputs=[user_id],
|
509 |
+
outputs=[response, status_text]
|
510 |
+
)
|
511 |
+
|
512 |
+
angle_query_button.click(
|
513 |
+
query_angles,
|
514 |
+
inputs = [user_id],
|
515 |
+
outputs = [response, status_text]
|
516 |
+
)
|
517 |
+
coord_query_button.click(
|
518 |
+
query_coords,
|
519 |
+
inputs = [user_id],
|
520 |
+
outputs = [response, status_text]
|
521 |
+
)
|
522 |
+
gripper_query_button.click(
|
523 |
+
query_gripper,
|
524 |
+
inputs = [user_id],
|
525 |
+
outputs = [response, status_text]
|
526 |
+
)
|
527 |
+
camera_query_button.click(
|
528 |
+
query_camera,
|
529 |
+
inputs = [user_id],
|
530 |
+
outputs = [response, response_image, status_text]
|
531 |
+
)
|
532 |
+
gripper_control_button.click(
|
533 |
+
control_gripper,
|
534 |
+
inputs = [user_id, gripper_value, speed_gripper],
|
535 |
+
outputs = [response, status_text]
|
536 |
+
)
|
537 |
+
angle_control_button.click(
|
538 |
+
control_angles,
|
539 |
+
inputs = [user_id, angle1, angle2, angle3, angle4, angle5, angle6, speed_angles],
|
540 |
+
outputs = [response, status_text]
|
541 |
+
)
|
542 |
+
coord_control_button.click(
|
543 |
+
control_coords,
|
544 |
+
inputs = [user_id, xcoord, ycoord, zcoord, roll, pitch, yaw, speed_coords],
|
545 |
+
outputs = [response, status_text]
|
546 |
+
)
|
547 |
+
coord_set_button.click(
|
548 |
+
set_coords_to_current,
|
549 |
+
inputs = [user_id],
|
550 |
+
outputs = [xcoord, ycoord, zcoord, roll, pitch, yaw, status_text]
|
551 |
+
)
|
552 |
+
angle_set_button.click(
|
553 |
+
set_angles_to_current,
|
554 |
+
inputs = [user_id],
|
555 |
+
outputs = [angle1, angle2, angle3, angle4, angle5, angle6, status_text]
|
556 |
+
)
|
557 |
+
reset_angle_button.click(
|
558 |
+
reset,
|
559 |
+
outputs = [angle1, angle2, angle3, angle4, angle5, angle6, speed_angles]
|
560 |
+
)
|
561 |
+
reset_coords_button.click(
|
562 |
+
reset,
|
563 |
+
outputs = [xcoord, ycoord, zcoord, roll, pitch, yaw, speed_coords]
|
564 |
+
)
|
565 |
+
enter_queue_button.click(
|
566 |
+
enter_queue,
|
567 |
+
inputs = [user_id],
|
568 |
+
outputs = [status_text]
|
569 |
+
)
|
570 |
+
|
571 |
+
skip_user_button.click(
|
572 |
+
skip_current_user,
|
573 |
+
inputs = [admin_password, user_id],
|
574 |
+
outputs = [admin_result]
|
575 |
+
)
|
576 |
+
|
577 |
+
app.load(
|
578 |
+
namesgenerator.get_random_name,
|
579 |
+
outputs=[user_id]
|
580 |
+
)
|
581 |
+
|
582 |
+
app.load(
|
583 |
+
queue_size,
|
584 |
+
outputs=[status_text]
|
585 |
+
)
|
586 |
+
|
587 |
+
# Set up health monitoring and automatic connection refresh
|
588 |
+
def update_connection_status():
|
589 |
+
global client
|
590 |
+
if client is None:
|
591 |
+
return "Disconnected - Click 'Refresh Connection'"
|
592 |
+
try:
|
593 |
+
# Try a lightweight operation to test connection
|
594 |
+
test = client.get_angles()
|
595 |
+
if test["success"]:
|
596 |
+
return "Connected"
|
597 |
+
else:
|
598 |
+
return "Connection issues - Click 'Refresh Connection'"
|
599 |
+
except:
|
600 |
+
return "Disconnected - Click 'Refresh Connection'"
|
601 |
+
|
602 |
+
app.queue(default_concurrency_limit=1, max_size=100, api_open=False)
|
603 |
+
app.launch()
|