Neil-YL commited on
Commit
18a3854
·
verified ·
1 Parent(s): 3af9058

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +355 -127
app.py CHANGED
@@ -5,21 +5,23 @@ import time
5
  import paho.mqtt.client as mqtt
6
  import json
7
  import secrets
8
- from well_status_utils import find_unused_wells, update_used_wells
9
 
10
 
11
- # Task Queue
 
 
12
  task_queue = Queue()
13
  result_queue = Queue()
14
  current_task = None
 
 
15
 
16
- # MQTT Configuration
17
  MQTT_BROKER = "248cc294c37642359297f75b7b023374.s2.eu.hivemq.cloud"
18
  MQTT_PORT = 8883
19
  MQTT_USERNAME = "sgbaird"
20
  MQTT_PASSWORD = "D.Pq5gYtejYbU#L"
21
 
22
-
23
  OT2_SERIAL = "OT2CEP20240218R04"
24
  PICO_ID = "e66130100f895134"
25
 
@@ -29,177 +31,403 @@ SENSOR_COMMAND_TOPIC = f"command/picow/{PICO_ID}/as7341/read"
29
  SENSOR_DATA_TOPIC = f"color-mixing/picow/{PICO_ID}/as7341"
30
 
31
 
32
- # MQTT Client
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  mqtt_client = mqtt.Client()
34
- mqtt_client.tls_set(tls_version=mqtt.ssl.PROTOCOL_TLS_CLIENT) # Enable TLS
35
  mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
36
 
37
- # MQTT Handlers
38
  def on_connect(client, userdata, flags, rc):
39
- print("Connected to MQTT Broker with result code", rc)
40
- client.subscribe([(OT2_STATUS_TOPIC,2),(SENSOR_DATA_TOPIC,2)])
41
 
42
  def on_message(client, userdata, msg):
43
- global current_task
44
- global sensor_results
45
-
46
  try:
47
  payload = json.loads(msg.payload.decode("utf-8"))
48
  if msg.topic == OT2_STATUS_TOPIC:
49
  handle_sensor_status(payload)
50
-
51
  elif msg.topic == SENSOR_DATA_TOPIC:
52
- print("Sensor: Data received.")
53
- print(payload)
54
  sensor_results = payload
55
  mqtt_client.publish(
56
  OT2_COMMAND_TOPIC,
57
- json.dumps({"command": {"sensor_status": "read"}, "experiment_id": payload["experiment_id"],"session_id": payload["session_id"]}),
 
 
 
 
58
  )
59
- print("Sending sensor to charging position.")
60
-
61
  except Exception as e:
62
- print("Error processing MQTT message:", e)
63
 
64
  mqtt_client.on_connect = on_connect
65
  mqtt_client.on_message = on_message
66
  mqtt_client.connect(MQTT_BROKER, MQTT_PORT)
67
  mqtt_client.loop_start()
68
 
69
- # Task processing logic
70
- def add_to_queue(student_id, R, Y, B):
71
- global current_task
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- # Ensure total volume is 300 µL
74
- if R + Y + B != 300:
75
- raise gr.Error("The total R, Y, and B volume must be exactly 300 µL.")
76
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- experiment_id = secrets.token_hex(4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  try:
81
- # Find an available well on the plate
82
  empty_wells = find_unused_wells()
83
  if not empty_wells:
84
- raise ValueError("No available wells for the experiment.")
85
  selected_well = empty_wells[0]
86
- # Execute OT-2 protocol to mix the RYB proportions
87
- update_used_wells([selected_well]) # Mark the well as used
88
 
89
  except Exception as e:
90
  yield {
91
- "Status": "Error",
92
- "Message": f"Experiment execution failed: {e}"
93
  }
94
- return
95
 
96
- # Add task to queue
97
  task = {
98
  "R": R,
99
  "Y": Y,
100
  "B": B,
101
- "well":selected_well,
102
  "session_id": student_id,
103
  "experiment_id": experiment_id,
104
  "status": "queued",
105
  }
106
- task_queue.put(task)
 
 
 
 
 
 
107
 
108
- # Monitor queue position
109
- while task in list(task_queue.queue):
110
- queue_position = list(task_queue.queue).index(task) + 1
111
- time.sleep(1) # Simulate periodic updates
112
- yield {
113
- "Status": "Queued",
114
- "Message": f"Your experiment is queued. Current position: {queue_position}",
115
- "Student ID": student_id,
116
- "RYB Volumes (µL)": {"R": R, "Y": Y, "B": B},
117
- "well":selected_well,
118
- }
119
 
120
- # When processing starts
121
- yield {"Status": "In Progress", "Message": "Experiment is running..."}
122
-
123
- while True:
124
- result = result_queue.get() # This will block until the result is available
125
- if result["Experiment ID"] == experiment_id:
126
- yield result # Return the updated result to the Gradio interface
127
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
- # Start Task Processor Thread
130
- def task_processor():
131
- global current_task
132
- while True:
133
- if not current_task and not task_queue.empty():
134
- current_task = task_queue.get()
135
- current_task["status"] = "processing"
136
- mqtt_client.publish(
137
- OT2_COMMAND_TOPIC,
138
- json.dumps({
139
- "command": {"R": current_task["R"], "Y": current_task["Y"], "B": current_task["B"], "well": current_task["well"]}, "experiment_id": current_task["experiment_id"],
140
- "session_id": current_task["session_id"]
141
- }),
142
- )
143
- time.sleep(1)
144
 
145
- def handle_sensor_status(payload):
146
- global current_task
147
- global sensor_results
148
- if "in_place" in json.dumps(payload):
149
- print("OT-2: Sensor in place. Sending read command to sensor.")
150
- mqtt_client.publish(SENSOR_COMMAND_TOPIC,
151
- json.dumps({
152
- "command": {
153
- "R": current_task["R"],
154
- "Y": current_task["Y"],
155
- "B": current_task["B"],
156
- "well": current_task["well"]
157
- },
158
- "experiment_id": current_task["experiment_id"],
159
- "session_id": current_task["session_id"]
160
- })
161
- )
162
 
163
- elif payload["status"]["sensor_status"] == "charging":
164
- print("OT-2: Sensor returned to charging position.")
165
- # Push result to result_queue
166
- result_queue.put({
167
- "Message": "Experiment completed!",
168
- "Student ID": current_task["session_id"],
169
- "Command": {
170
- "R": current_task["R"],
171
- "Y": current_task["Y"],
172
- "B": current_task["B"],
173
- "well": current_task["well"],
174
- },
175
- "Sensor Data": sensor_results["sensor_data"],
176
- "Experiment ID": current_task["experiment_id"]
177
-
178
- })
179
 
180
- current_task = None
 
 
 
 
 
 
 
 
 
 
181
 
182
-
 
 
 
 
 
183
 
184
- processor_thread = threading.Thread(target=task_processor, daemon=True)
185
- processor_thread.start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
- # Define the Gradio interface
188
- inputs = [
189
- gr.Textbox(label="Student ID", placeholder="Enter your unique ID"),
190
- gr.Slider(1, 300, step=1, label="Red (R) Volume (µL)"),
191
- gr.Slider(1, 300, step=1, label="Yellow (Y) Volume (µL)"),
192
- gr.Slider(1, 300, step=1, label="Blue (B) Volume (µL)")
193
- ]
194
- outputs = gr.JSON()
195
-
196
- app = gr.Interface(
197
- fn=add_to_queue,
198
- inputs=inputs,
199
- outputs=outputs,
200
- title="OT-2 Liquid Color Matching Experiment Queue",
201
- description="Enter R, Y, and B volumes (in µL). Ensure the total volume is exactly 300 µL.",
202
- flagging_mode="never"
203
- )
204
-
205
- app.launch()
 
5
  import paho.mqtt.client as mqtt
6
  import json
7
  import secrets
8
+ from DB_utls import find_unused_wells, update_used_wells, save_result, get_student_quota, decrement_student_quota
9
 
10
 
11
+ # NOTE: New global dict to store tasks keyed by (student_id, experiment_id)
12
+ tasks_dict = {}
13
+
14
  task_queue = Queue()
15
  result_queue = Queue()
16
  current_task = None
17
+ sensor_results = None
18
+ queue_counter = task_queue.qsize()
19
 
 
20
  MQTT_BROKER = "248cc294c37642359297f75b7b023374.s2.eu.hivemq.cloud"
21
  MQTT_PORT = 8883
22
  MQTT_USERNAME = "sgbaird"
23
  MQTT_PASSWORD = "D.Pq5gYtejYbU#L"
24
 
 
25
  OT2_SERIAL = "OT2CEP20240218R04"
26
  PICO_ID = "e66130100f895134"
27
 
 
31
  SENSOR_DATA_TOPIC = f"color-mixing/picow/{PICO_ID}/as7341"
32
 
33
 
34
+ def check_student_quota(student_id):
35
+ """Check student's remaining experiment quota"""
36
+ student_quota = get_student_quota(student_id)
37
+ return student_quota
38
+
39
+ def validate_ryb_input(R, Y, B):
40
+ """Validate RYB input volumes"""
41
+ total = R + Y + B
42
+ if total > 300:
43
+ return {
44
+ "is_valid": False,
45
+ "message": f"Total volume cannot exceed 300 µL. Current total: {total} µL."
46
+ }
47
+ return {
48
+ "is_valid": True,
49
+ "message": f"Current total: {total} µL."
50
+ }
51
+
52
+
53
  mqtt_client = mqtt.Client()
54
+ mqtt_client.tls_set(tls_version=mqtt.ssl.PROTOCOL_TLS_CLIENT)
55
  mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
56
 
 
57
  def on_connect(client, userdata, flags, rc):
58
+ print(f"Connected to MQTT Broker with result code {rc}")
59
+ client.subscribe([(OT2_STATUS_TOPIC, 2), (SENSOR_DATA_TOPIC, 2)])
60
 
61
  def on_message(client, userdata, msg):
62
+ global current_task, sensor_results
 
 
63
  try:
64
  payload = json.loads(msg.payload.decode("utf-8"))
65
  if msg.topic == OT2_STATUS_TOPIC:
66
  handle_sensor_status(payload)
 
67
  elif msg.topic == SENSOR_DATA_TOPIC:
68
+ print("Sensor data received")
 
69
  sensor_results = payload
70
  mqtt_client.publish(
71
  OT2_COMMAND_TOPIC,
72
+ json.dumps({
73
+ "command": {"sensor_status": "read"},
74
+ "experiment_id": payload["experiment_id"],
75
+ "session_id": payload["session_id"]
76
+ }),
77
  )
 
 
78
  except Exception as e:
79
+ print(f"Error processing MQTT message: {e}")
80
 
81
  mqtt_client.on_connect = on_connect
82
  mqtt_client.on_message = on_message
83
  mqtt_client.connect(MQTT_BROKER, MQTT_PORT)
84
  mqtt_client.loop_start()
85
 
86
+ def handle_sensor_status(payload):
87
+ global current_task, sensor_results
88
+ if "in_place" in json.dumps(payload):
89
+ mqtt_client.publish(
90
+ SENSOR_COMMAND_TOPIC,
91
+ json.dumps({
92
+ "command": {
93
+ "R": current_task["R"],
94
+ "Y": current_task["Y"],
95
+ "B": current_task["B"],
96
+ "well": current_task["well"]
97
+ },
98
+ "experiment_id": current_task["experiment_id"],
99
+ "session_id": current_task["session_id"]
100
+ })
101
+ )
102
+ elif payload["status"]["sensor_status"] == "charging":
103
+
104
+ experiment_result = {
105
+ "Status": "Complete",
106
+ "Message": "Experiment completed successfully!",
107
+ "Student ID": current_task["session_id"],
108
+ "Command": {
109
+ "R": current_task["R"],
110
+ "Y": current_task["Y"],
111
+ "B": current_task["B"],
112
+ "well": current_task["well"],
113
+ },
114
+ "Sensor Data": sensor_results["sensor_data"],
115
+ "Experiment ID": current_task["experiment_id"]
116
+ }
117
+ # Store full result in result queue
118
+ result_queue.put(experiment_result)
119
 
120
+ # Create a version of experiment_result without "Status" and "Message" for database storage
121
+ db_data = {key: experiment_result[key] for key in experiment_result if key not in ["Status", "Message"]}
122
+
123
+ save_result(db_data)
124
+
125
+ current_task = None
126
+
127
+
128
+ def task_processor():
129
+ """
130
+ Background thread that processes tasks one by one.
131
+ """
132
+ global current_task, queue_counter
133
+ task_start_time = None
134
+ TIMEOUT_SECONDS = 165 # 2min45s
135
+
136
+ while True:
137
+ if current_task:
138
+ # Check for timeout
139
+ if task_start_time and (time.time() - task_start_time > TIMEOUT_SECONDS):
140
+ print("sending timeout message to OT-2")
141
+ mqtt_client.publish(
142
+ OT2_COMMAND_TOPIC,
143
+ json.dumps({
144
+ "command": {"sensor_status": "sensor_timeout"},
145
+ "experiment_id": current_task["experiment_id"],
146
+ "session_id": current_task["session_id"]
147
+ }),
148
+ )
149
+ result_queue.put({
150
+ "Status": "Error",
151
+ "Message": "Experiment timed out",
152
+ "Student ID": current_task["session_id"],
153
+ "Command": {
154
+ "R": current_task["R"],
155
+ "Y": current_task["Y"],
156
+ "B": current_task["B"],
157
+ "well": current_task["well"],
158
+ },
159
+ "Experiment ID": current_task["experiment_id"]
160
+ })
161
+ current_task = None
162
+ task_start_time = None
163
+ continue
164
+
165
+ if not current_task and not task_queue.empty():
166
+ # Fetch a new task from the queue
167
+ student_id, experiment_id = task_queue.get() # NOTE: We'll store (student_id, experiment_id) instead of task
168
+ queue_counter -= 1
169
+ task_start_time = time.time()
170
+
171
+ # NOTE: We retrieve the actual task from tasks_dict
172
+ current_task = tasks_dict[(student_id, experiment_id)]
173
+
174
+ print(f"[DEBUG] Task processor - Getting new task. Queue counter: {queue_counter}")
175
+
176
+ print(f"[DEBUG] Task start time: {task_start_time}")
177
+
178
+ # Mark status as "processing"
179
+ current_task["status"] = "processing"
180
+
181
+ mqtt_client.publish(
182
+ OT2_COMMAND_TOPIC,
183
+ json.dumps({
184
+ "command": {
185
+ "R": current_task["R"],
186
+ "Y": current_task["Y"],
187
+ "B": current_task["B"],
188
+ "well": current_task["well"]
189
+ },
190
+ "experiment_id": current_task["experiment_id"],
191
+ "session_id": current_task["session_id"]
192
+ }),
193
+ )
194
+
195
+ time.sleep(1)
196
+
197
+
198
+ processor_thread = threading.Thread(target=task_processor, daemon=True)
199
+ processor_thread.start()
200
+
201
+
202
+ def verify_student_id(student_id):
203
+ """Verify student ID and check quota"""
204
+ global queue_counter
205
+ if not student_id:
206
+ return [
207
+ gr.update(interactive=False, value=1),
208
+ gr.update(interactive=False, value=1),
209
+ gr.update(interactive=False, value=1),
210
+ "Please enter a Student ID",
211
+ gr.update(interactive=False)
212
+ ]
213
+
214
+ quota_remaining = check_student_quota(student_id)
215
 
216
+ print(f"[DEBUG] Updating status: Queue counter: {queue_counter}")
217
+
218
+ if quota_remaining <= 0:
219
+ return [
220
+ gr.update(interactive=False, value=1),
221
+ gr.update(interactive=False, value=1),
222
+ gr.update(interactive=False, value=1),
223
+ "No experiments remaining. Please contact administrator.",
224
+ gr.update(interactive=False)
225
+ ]
226
+
227
+ return [
228
+ gr.update(interactive=True, value=1),
229
+ gr.update(interactive=True, value=1),
230
+ gr.update(interactive=True, value=1),
231
+ f"Student ID verified. Available experiments: {quota_remaining}\nCurrent queue length: {queue_counter} experiment(s)",
232
+ gr.update(interactive=True)
233
+ ]
234
+
235
+ def update_status_with_queue(R, Y, B):
236
+ """Check if RYB inputs are valid and return updated queue info"""
237
+ global queue_counter
238
+ validation_result = validate_ryb_input(R, Y, B)
239
+ total = R + Y + B
240
+ return [
241
+ f"{validation_result['message']}\nCurrent queue length: {queue_counter} experiment(s)",
242
+ gr.update(interactive=(total <= 300))
243
+ ]
244
+
245
+ def update_queue_display():
246
+ """Refresh queue info for the UI"""
247
+ global current_task, queue_counter
248
+ try:
249
+ print(f"[DEBUG] Updating queue display - Counter: {queue_counter}")
250
+ print(f"[DEBUG] Current task: {current_task}")
251
+
252
+ if current_task:
253
+ status = f"""### Current Queue Status
254
+ - Active experiment: Yes
255
+ - Queue length: {queue_counter} experiment(s)"""
256
+ else:
257
+ status = f"""### Current Queue Status
258
+ - Active experiment: No
259
+ - Queue length: {queue_counter} experiment(s)
260
+ - Total experiments: {queue_counter}"""
261
+ return status
262
+ except Exception as e:
263
+ print(f"[DEBUG] Error in update_queue_display: {str(e)}")
264
+ return f"Error getting queue status: {str(e)}"
265
 
266
+
267
+ def add_to_queue(student_id, R, Y, B):
268
+ global queue_counter
269
+ print(f"[DEBUG] Before adding - Queue counter: {queue_counter}")
270
+
271
+ # Validate RYB inputs
272
+ validation_result = validate_ryb_input(R, Y, B)
273
+ if not validation_result["is_valid"]:
274
+ yield {
275
+ "Status": "Error",
276
+ "Message": validation_result["message"]
277
+ }
278
+ return
279
+
280
+ # Check quota
281
+ quota_remaining = check_student_quota(student_id)
282
+ if quota_remaining <= 0:
283
+ yield {
284
+ "Status": "Error",
285
+ "Message": "No experiments remaining"
286
+ }
287
+ return
288
+
289
+ # Select well
290
+ experiment_id = secrets.token_hex(4)
291
  try:
 
292
  empty_wells = find_unused_wells()
293
  if not empty_wells:
294
+ raise ValueError("No available wells")
295
  selected_well = empty_wells[0]
296
+
 
297
 
298
  except Exception as e:
299
  yield {
300
+ "Status": "Error",
301
+ "Message": str(e)
302
  }
303
+ return
304
 
305
+ # NOTE: Create the task and store it in tasks_dict
306
  task = {
307
  "R": R,
308
  "Y": Y,
309
  "B": B,
310
+ "well": selected_well,
311
  "session_id": student_id,
312
  "experiment_id": experiment_id,
313
  "status": "queued",
314
  }
315
+ tasks_dict[(student_id, experiment_id)] = task # Keep track globally
316
+
317
+ # Put only (student_id, experiment_id) in the Queue
318
+ task_queue.put((student_id, experiment_id))
319
+ queue_counter += 1
320
+ update_used_wells([selected_well])
321
+ decrement_student_quota(student_id)
322
 
323
+ print(f"[DEBUG] After adding - Queue counter: {queue_counter}")
324
+ print(f"DEBUG: Current queue content: {[t for t in list(task_queue.queue)]}")
325
+ print(f"[DEBUG] Task added: {task}")
 
 
 
 
 
 
 
 
326
 
327
+ # First yield: "Queued"
328
+ yield {
329
+ "Status": "Queued",
330
+ "Position": queue_counter,
331
+ "Student ID": student_id,
332
+ "Experiment ID": experiment_id,
333
+ "Well": selected_well,
334
+ "Volumes": {"R": R, "Y": Y, "B": B}
335
+ }
336
+
337
+ # NOTE: Wait until the task's status becomes 'processing'
338
+ # This ensures we only yield "Running" when the backend actually starts the job.
339
+ while tasks_dict[(student_id, experiment_id)]["status"] == "queued":
340
+ time.sleep(15)
341
+
342
+ # Second yield: "Running" (happens only after status is 'processing')
343
+ yield {
344
+ "Status": "Running",
345
+ "Student ID": student_id,
346
+ "Experiment ID": experiment_id,
347
+ "Well": selected_well,
348
+ "Volumes": {"R": R, "Y": Y, "B": B}
349
+ }
350
 
351
+ # Finally, wait for the result
352
+ result = result_queue.get()
353
+ yield result
 
 
 
 
 
 
 
 
 
 
 
 
354
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
 
356
+ with gr.Blocks(title="OT-2 Liquid Color Matching Experiment Queue") as demo:
357
+ gr.Markdown("## OT-2 Liquid Color Matching Experiment Queue")
358
+ gr.Markdown("Enter R, Y, and B volumes (in µL). Total volume must be exactly 300 µL.")
359
+
360
+ with gr.Row():
361
+ with gr.Column(scale=2):
362
+ with gr.Row():
363
+ student_id_input = gr.Textbox(
364
+ label="Student ID",
365
+ placeholder="Enter your unique ID"
366
+ )
367
+ verify_id_btn = gr.Button("Verify ID")
 
 
 
 
368
 
369
+ r_slider = gr.Slider(1, 300, step=1, label="Red (R) Volume (µL)", interactive=False)
370
+ y_slider = gr.Slider(1, 300, step=1, label="Yellow (Y) Volume (µL)", interactive=False)
371
+ b_slider = gr.Slider(1, 300, step=1, label="Blue (B) Volume (µL)", interactive=False)
372
+ status_output = gr.Textbox(label="Status")
373
+ submit_btn = gr.Button("Submit Experiment", interactive=False)
374
+ result_output = gr.JSON(label="Experiment Status")
375
+
376
+ with gr.Column(scale=1):
377
+ gr.Markdown("### Queue Status")
378
+ queue_status = gr.Markdown("Loading queue status...")
379
+ update_status_btn = gr.Button("Refresh Queue Status")
380
 
381
+ verify_id_btn.click(
382
+ verify_student_id,
383
+ inputs=[student_id_input],
384
+ outputs=[r_slider, y_slider, b_slider, status_output, submit_btn],
385
+ api_name="verify_student_id"
386
+ )
387
 
388
+ r_slider.change(
389
+ update_status_with_queue,
390
+ inputs=[r_slider, y_slider, b_slider],
391
+ outputs=[status_output, submit_btn]
392
+ )
393
+ y_slider.change(
394
+ update_status_with_queue,
395
+ inputs=[r_slider, y_slider, b_slider],
396
+ outputs=[status_output, submit_btn]
397
+ )
398
+ b_slider.change(
399
+ update_status_with_queue,
400
+ inputs=[r_slider, y_slider, b_slider],
401
+ outputs=[status_output, submit_btn]
402
+ )
403
+
404
+ # NOTE: concurrency_limit=3 is preserved; no changes here
405
+ submit_btn.click(
406
+ add_to_queue,
407
+ inputs=[student_id_input, r_slider, y_slider, b_slider],
408
+ outputs=result_output,
409
+ api_name="submit",
410
+ concurrency_limit=3
411
+ ).then(
412
+ update_queue_display,
413
+ None,
414
+ queue_status
415
+ )
416
+
417
+ update_status_btn.click(
418
+ update_queue_display,
419
+ None,
420
+ queue_status
421
+ )
422
+
423
+ demo.load(
424
+ update_queue_display,
425
+ None,
426
+ queue_status
427
+ )
428
+
429
+ # NOTE: Left as-is, you have not used demo.queue(...) except for concurrency_limit on the .click
430
+ demo.queue # No changes here
431
 
432
+ if __name__ == "__main__":
433
+ demo.launch()