sgbaird commited on
Commit
3a9961d
·
verified ·
1 Parent(s): 9f45bc4

try with a claude edit

Browse files

https://claude.site/artifacts/08fd681d-335d-406c-9121-1c4bb623671a

Files changed (1) hide show
  1. app.py +544 -301
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
- def __init__(self):
15
- self.queue = deque()
16
- self.current_user = None
17
- self.session_start_time = None
18
- self.lock = threading.Lock()
19
-
20
- def enqueue_user(self, user_id):
21
- with self.lock:
22
- if user_id not in self.queue and user_id != self.current_user:
23
- self.queue.append(user_id)
24
-
25
- def dequeue_user(self):
26
- with self.lock:
27
- if self.queue:
28
- return self.queue.popleft()
29
- return None
30
-
31
- def get_queue_info(self, user_id):
32
- with self.lock:
33
- if user_id == self.current_user:
34
- remaining_time = max(0, SESSION_TIME - (time.time() - self.session_start_time))
35
- return 0, remaining_time
36
- elif user_id in self.queue:
37
- position = list(self.queue).index(user_id) + 1
38
- if self.session_start_time:
39
- wait_time = (position - 1) * SESSION_TIME + max(0, SESSION_TIME - (time.time() - self.session_start_time))
40
- else:
41
- wait_time = position * SESSION_TIME
42
- return position, wait_time
43
- else:
44
- return None, None
45
-
46
- def start_session(self, user_id):
47
- with self.lock:
48
- if self.current_user is None:
49
- self.current_user = user_id
50
- self.session_start_time = time.time()
51
- return True
52
- return False
53
-
54
- def end_session(self):
55
- with self.lock:
56
- if self.current_user and time.time() - self.session_start_time >= SESSION_TIME:
57
- self.current_user = None
58
- self.session_start_time = None
59
- return True
60
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  queue_system = QueueSystem()
63
 
64
  def queue_size():
65
- return f"There are {len(queue_system.queue)} people in the queue."
66
-
67
- # Background timer thread to end session after SESSION_TIME
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  def background_timer():
69
- while True:
70
- time.sleep(1)
71
- if queue_system.end_session():
72
- next_user = queue_system.dequeue_user()
73
- if next_user:
74
- queue_system.start_session(next_user)
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- background-color: #161624;
85
- padding: 16px;
86
- border-radius: 8px;
87
  }
88
  #nogaprow {
89
- gap: 0px !important;
90
  }
91
  #nogapcol {
92
- padding: 0px !important;
93
- border: none !important;
94
- box-shadow: none !important;
 
 
 
 
 
 
 
 
95
  }
96
  """
97
 
98
  """
99
- checks the user position on the queue and returns a message along with
100
  whether the command should be executed.
101
  """
102
  def authenticate_user(user_id):
103
- if queue_system.current_user is None:
104
- queue_system.start_session(user_id)
105
- queue_system.enqueue_user(user_id)
106
- position, wait_time = queue_system.get_queue_info(user_id)
107
-
108
- if position == 0:
109
- remaining_time_msg = f"Your turn!\nTime remaining: {wait_time:.2f} seconds."
110
- return True, remaining_time_msg
111
- elif position is not None:
112
- if position == 1:
113
- wait_msg = f"You are next!\nWait time: {wait_time:.2f} seconds."
114
- else:
115
- wait_msg = f"There are {position - 1} people ahead of you in the queue.\nWait time: {wait_time:.2f} seconds."
116
- return False, wait_msg
117
- else:
118
- return False, "Error: You are not in the queue."
 
 
 
119
 
120
  def enter_queue(user_id):
121
- _, msg = authenticate_user(user_id)
122
- return msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  def query_angles(user_id):
125
- to_execute, queue_status_msg = authenticate_user(user_id)
126
- if to_execute:
127
- resp = client.get_angles()
128
- resp["command"] = "query/angles"
129
- return json.dumps(resp, indent=4), queue_status_msg
130
- else:
131
- return None, queue_status_msg
 
 
 
 
 
132
 
133
  def query_coords(user_id):
134
- to_execute, queue_status_msg = authenticate_user(user_id)
135
- if to_execute:
136
- resp = client.get_coords()
137
- resp["command"] = "query/coords"
138
- return json.dumps(resp, indent=4), queue_status_msg
139
- else:
140
- return None, queue_status_msg
 
 
 
 
 
141
 
142
  def query_gripper(user_id):
143
- to_execute, queue_status_msg = authenticate_user(user_id)
144
- if to_execute:
145
- resp = client.get_gripper_value()
146
- resp["command"] = "query/gripper"
147
- return json.dumps(resp, indent=4), queue_status_msg
148
- else:
149
- return None, queue_status_msg
 
 
 
 
 
150
 
151
  def query_camera(user_id):
152
- to_execute, queue_status_msg = authenticate_user(user_id)
153
- if to_execute:
154
- resp = client.get_camera()
155
- resp["command"] = "query/camera"
156
- if not resp["success"]:
157
- return json.dumps(resp, indent=4), None, queue_status_msg
158
- img = resp.pop("image")
159
- return json.dumps(resp, indent=4), gr.Image(visible=True, value=img), queue_status_msg
160
- else:
161
- return None, None, queue_status_msg
 
 
 
 
 
162
 
163
  def control_angles(user_id, angle0, angle1, angle2, angle3, angle4, angle5, movement_speed):
164
- to_execute, queue_status_msg = authenticate_user(user_id)
165
- if to_execute:
166
- resp = client.send_angles([angle0, angle1, angle2, angle3, angle4, angle5], movement_speed)
167
- resp["command"] = "control/angles"
168
- return json.dumps(resp, indent=4), queue_status_msg
169
- else:
170
- return None, queue_status_msg
 
 
 
 
 
171
 
172
  def control_coords(user_id, x, y, z, roll, pitch, yaw, movement_speed):
173
- to_execute, queue_status_msg = authenticate_user(user_id)
174
- if to_execute:
175
- resp = client.send_coords([x, y, z, roll, pitch, yaw], movement_speed)
176
- resp["command"] = "control/coords"
177
- return json.dumps(resp, indent=4), queue_status_msg
178
- else:
179
- return None, queue_status_msg
 
 
 
 
 
180
 
181
  def control_gripper(user_id, gripper_value, movement_speed):
182
- to_execute, queue_status_msg = authenticate_user(user_id)
183
- if to_execute:
184
- resp = client.send_gripper_value(gripper_value, movement_speed)
185
- resp["command"] = "control/gripper"
186
- return json.dumps(resp, indent=4), queue_status_msg
187
- else:
188
- return None, queue_status_msg
 
 
 
 
 
189
 
190
  def set_coords_to_current(user_id):
191
- to_execute, queue_status_msg = authenticate_user(user_id)
192
- if to_execute:
193
- resp, _ = query_coords(user_id)
194
- resp = json.loads(resp)
195
- if not resp["success"]:
196
- return None, None, None, None, None, None, queue_status_msg
197
- return resp["coords"] + [queue_status_msg]
198
- else:
199
- return None, None, None, None, None, None, queue_status_msg
 
 
 
 
200
 
201
  def set_angles_to_current(user_id):
202
- to_execute, queue_status_msg = authenticate_user(user_id)
203
- if to_execute:
204
- resp, _ = query_angles(user_id)
205
- resp = json.loads(resp)
206
- if not resp["success"]:
207
- return None, None, None, None, None, None, queue_status_msg
208
- return resp["angles"] + [queue_status_msg]
209
- else:
210
- return None, None, None, None, None, None, queue_status_msg
 
 
 
 
211
 
212
  def reset():
213
- return 0, 0, 0, 0, 0, 0, 50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
  with gr.Blocks(css=CSS) as app:
216
- gr.Markdown("# MyCobot 280pi MQTT Control Demo")
217
- gr.HTML('''
218
- <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">
219
- <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>
220
- </a>
221
- ''')
222
- 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 it's current status.")
223
-
224
- with gr.Row():
225
- with gr.Column():
226
- 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!")
227
- enter_queue_button = gr.Button("Join queue")
228
- with gr.Column():
229
- status_text = gr.Textbox(label="Queue status", value="", interactive=False, lines=5)
230
-
231
- with gr.Row():
232
- # QUERY PANEL
233
- with gr.Column(elem_id="col"):
234
- gr.Markdown("## Query")
235
- 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.")
236
- angle_query_button = gr.Button("Query Angles")
237
- coord_query_button = gr.Button("Query Coordinates")
238
- gripper_query_button = gr.Button("Query Gripper state")
239
- camera_query_button = gr.Button("Query Camera")
240
-
241
- # GRIPPER PANEL
242
- with gr.Column(elem_id="col"):
243
- gr.Markdown("## Gripper Control")
244
- gr.Markdown("Use this panel to control the gripper of the cobot.")
245
- gripper_value = gr.Slider(minimum=0.0, maximum=100.0, step=1.0, label="Gripper value")
246
- speed_gripper = gr.Slider(value=50.0, minimum=0.0, maximum=100.0, step=1.0, label="Movement speed")
247
- gripper_control_button = gr.Button("Send gripper command")
248
-
249
- with gr.Row():
250
- # ANGLE PANEL
251
- with gr.Column(elem_id="col"):
252
- gr.Markdown("## Angle Control")
253
- 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.")
254
- angle_set_button = gr.Button("Set to current angles")
255
- reset_angle_button = gr.Button("Reset angles")
256
- with gr.Row(elem_id="nogaprow"):
257
- with gr.Column(elem_id="nogapcol"):
258
- angle1 = gr.Slider(value=0.0, label="Angle 1", step=1.0, minimum=-168, maximum=168)
259
- angle3 = gr.Slider(value=0.0, label="Angle 3", step=1.0, minimum=-135, maximum=135)
260
- angle5 = gr.Slider(value=0.0, label="Angle 5", step=1.0, minimum=-150, maximum=150)
261
- with gr.Column(elem_id="nogapcol"):
262
- angle2 = gr.Slider(value=0.0, label="Angle 2", step=1.0, minimum=-145, maximum=145)
263
- angle4 = gr.Slider(value=0.0, label="Angle 4", step=1.0, minimum=-165, maximum=165)
264
- angle6 = gr.Slider(value=0.0, label="Angle 6", step=1.0, minimum=-180, maximum=180)
265
- speed_angles = gr.Slider(value=50.0, minimum=0.0, maximum=100.0, step=1.0, label="Movement speed")
266
- angle_control_button = gr.Button("Send angle command")
267
-
268
- # COORD PANEL
269
- with gr.Column(elem_id="col"):
270
- gr.Markdown("## Coordinate Control")
271
- 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).")
272
- coord_set_button = gr.Button("Set to current coords")
273
- reset_coords_button = gr.Button("Reset coordinates")
274
- with gr.Row(elem_id="nogaprow"):
275
- with gr.Column(elem_id="nogapcol"):
276
- xcoord = gr.Slider(value=0.0, label="X coordinate", step=1.0, minimum=-350, maximum=350)
277
- ycoord = gr.Slider(value=0.0, label="Y coordinate", step=1.0, minimum=-350, maximum=350)
278
- zcoord = gr.Slider(value=0.0, label="Z coordinate", step=1.0, minimum=-70, maximum=523)
279
- with gr.Column(elem_id="nogapcol"):
280
- roll = gr.Slider(value=0.0, label="Roll", step=1.0, minimum=-180, maximum=180)
281
- pitch = gr.Slider(value=0.0, label="Pitch", step=1.0, minimum=-180, maximum=180)
282
- yaw = gr.Slider(value=0.0, label="Yaw", step=1.0, minimum=-180, maximum=180)
283
- speed_coords = gr.Slider(value=50.0, minimum=0.0, maximum=100.0, step=1.0, label="Movement speed")
284
- coord_control_button = gr.Button("Send coordinate command")
285
-
286
- response = gr.Textbox(label="Response")
287
- response_image = gr.Image(visible=False)
288
-
289
- # Queue-aware event handling
290
- angle_query_button.click(
291
- query_angles,
292
- inputs = [user_id],
293
- outputs = [response, status_text]
294
- )
295
- coord_query_button.click(
296
- query_coords,
297
- inputs = [user_id],
298
- outputs = [response, status_text]
299
- )
300
- gripper_query_button.click(
301
- query_gripper,
302
- inputs = [user_id],
303
- outputs = [response, status_text]
304
- )
305
- camera_query_button.click(
306
- query_camera,
307
- inputs = [user_id],
308
- outputs = [response, response_image, status_text]
309
- )
310
- gripper_control_button.click(
311
- control_gripper,
312
- inputs = [user_id, gripper_value, speed_gripper],
313
- outputs = [response, status_text]
314
- )
315
- angle_control_button.click(
316
- control_angles,
317
- inputs = [user_id, angle1, angle2, angle3, angle4, angle5, angle6, speed_angles],
318
- outputs = [response, status_text]
319
- )
320
- coord_control_button.click(
321
- control_coords,
322
- inputs = [user_id, xcoord, ycoord, zcoord, roll, pitch, yaw, speed_coords],
323
- outputs = [response, status_text]
324
- )
325
- coord_set_button.click(
326
- set_coords_to_current,
327
- inputs = [user_id],
328
- outputs = [xcoord, ycoord, zcoord, roll, pitch, yaw, status_text]
329
- )
330
- angle_set_button.click(
331
- set_angles_to_current,
332
- inputs = [user_id],
333
- outputs = [angle1, angle2, angle3, angle4, angle5, angle6, status_text]
334
- )
335
- reset_angle_button.click(
336
- reset,
337
- outputs = [angle1, angle2, angle3, angle4, angle5, angle6, speed_angles]
338
- )
339
- reset_coords_button.click(
340
- reset,
341
- outputs = [xcoord, ycoord, zcoord, roll, pitch, yaw, speed_coords]
342
- )
343
- enter_queue_button.click(
344
- enter_queue,
345
- inputs = [user_id],
346
- outputs = [status_text]
347
- )
348
-
349
- app.load(
350
- namesgenerator.get_random_name,
351
- outputs=[user_id]
352
- )
353
-
354
- app.load(
355
- queue_size,
356
- outputs=[status_text]
357
- )
358
-
359
- app.queue(default_concurrency_limit=1, max_size=100)
360
- app.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()