sgbaird commited on
Commit
5c56e7a
·
1 Parent(s): c525ad7

add app_ot2.py as a reference

Browse files
Files changed (1) hide show
  1. app_ot2.py +196 -192
app_ot2.py CHANGED
@@ -5,7 +5,13 @@ import time
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
  import os
10
 
11
  # NOTE: New global dict to store tasks keyed by (student_id, experiment_id)
@@ -18,7 +24,6 @@ sensor_results = None
18
  queue_counter = task_queue.qsize()
19
 
20
 
21
-
22
  MQTT_BROKER = os.getenv("MQTT_BROKER")
23
  MQTT_PORT = int(os.getenv("MQTT_PORT"))
24
  MQTT_USERNAME = os.getenv("MQTT_USERNAME")
@@ -37,30 +42,30 @@ SENSOR_DATA_TOPIC = f"color-mixing/picow/{PICO_ID}/as7341"
37
  def check_student_quota(student_id):
38
  """Check student's remaining experiment quota"""
39
  student_quota = get_student_quota(student_id)
40
- return student_quota
 
41
 
42
  def validate_ryb_input(R, Y, B):
43
  """Validate RYB input volumes"""
44
  total = R + Y + B
45
  if total > 300:
46
  return {
47
- "is_valid": False,
48
- "message": f"Total volume cannot exceed 300 µL. Current total: {total} µL."
49
  }
50
- return {
51
- "is_valid": True,
52
- "message": f"Current total: {total} µL."
53
- }
54
 
55
 
56
  mqtt_client = mqtt.Client()
57
  mqtt_client.tls_set(tls_version=mqtt.ssl.PROTOCOL_TLS_CLIENT)
58
  mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
59
 
 
60
  def on_connect(client, userdata, flags, rc):
61
  print(f"Connected to MQTT Broker with result code {rc}")
62
  client.subscribe([(OT2_STATUS_TOPIC, 2), (SENSOR_DATA_TOPIC, 2)])
63
 
 
64
  def on_message(client, userdata, msg):
65
  global current_task, sensor_results
66
  try:
@@ -72,56 +77,66 @@ def on_message(client, userdata, msg):
72
  sensor_results = payload
73
  mqtt_client.publish(
74
  OT2_COMMAND_TOPIC,
75
- json.dumps({
76
- "command": {"sensor_status": "read"},
77
- "experiment_id": payload["experiment_id"],
78
- "session_id": payload["session_id"]
79
- }),
 
 
80
  )
81
  except Exception as e:
82
  print(f"Error processing MQTT message: {e}")
83
 
 
84
  mqtt_client.on_connect = on_connect
85
  mqtt_client.on_message = on_message
86
  mqtt_client.connect(MQTT_BROKER, MQTT_PORT)
87
  mqtt_client.loop_start()
88
 
 
89
  def handle_sensor_status(payload):
90
  global current_task, sensor_results
91
  if "in_place" in json.dumps(payload):
92
  mqtt_client.publish(
93
  SENSOR_COMMAND_TOPIC,
94
- json.dumps({
95
- "command": {
96
- "R": current_task["R"],
97
- "Y": current_task["Y"],
98
- "B": current_task["B"],
99
- "well": current_task["well"]
100
- },
101
- "experiment_id": current_task["experiment_id"],
102
- "session_id": current_task["session_id"]
103
- })
 
 
104
  )
105
  elif payload["status"]["sensor_status"] == "charging":
106
-
107
  experiment_result = {
108
- "Status": "Complete",
109
- "Message": "Experiment completed successfully!",
110
- "Student ID": current_task["session_id"],
111
- "Command": {
112
- "R": current_task["R"],
113
- "Y": current_task["Y"],
114
- "B": current_task["B"],
115
- "well": current_task["well"],
116
- },
117
- "Sensor Data": sensor_results["sensor_data"],
118
- "Experiment ID": current_task["experiment_id"]
119
- }
120
- # Store full result in result queue
121
  result_queue.put(experiment_result)
122
 
123
- # Create a version of experiment_result without "Status" and "Message" for database storage
124
- db_data = {key: experiment_result[key] for key in experiment_result if key not in ["Status", "Message"]}
 
 
 
 
125
 
126
  save_result(db_data)
127
 
@@ -135,7 +150,7 @@ def task_processor():
135
  global current_task, queue_counter
136
  task_start_time = None
137
  TIMEOUT_SECONDS = 165 # 2min45s
138
-
139
  while True:
140
  if current_task:
141
  # Check for timeout
@@ -143,54 +158,62 @@ def task_processor():
143
  print("sending timeout message to OT-2")
144
  mqtt_client.publish(
145
  OT2_COMMAND_TOPIC,
146
- json.dumps({
147
- "command": {"sensor_status": "sensor_timeout"},
148
- "experiment_id": current_task["experiment_id"],
149
- "session_id": current_task["session_id"]
150
- }),
151
- )
152
- result_queue.put({
153
- "Status": "Error",
154
- "Message": "Experiment timed out",
155
- "Student ID": current_task["session_id"],
156
- "Command": {
157
- "R": current_task["R"],
158
- "Y": current_task["Y"],
159
- "B": current_task["B"],
160
- "well": current_task["well"],
161
- },
162
- "Experiment ID": current_task["experiment_id"]
163
- })
 
 
 
 
164
  current_task = None
165
  task_start_time = None
166
  continue
167
 
168
  if not current_task and not task_queue.empty():
169
  # Fetch a new task from the queue
170
- student_id, experiment_id = task_queue.get() # NOTE: We'll store (student_id, experiment_id) instead of task
 
 
171
  queue_counter -= 1
172
  task_start_time = time.time()
173
 
174
  # NOTE: We retrieve the actual task from tasks_dict
175
  current_task = tasks_dict[(student_id, experiment_id)]
176
-
177
  # Mark status as "processing"
178
  current_task["status"] = "processing"
179
-
180
  mqtt_client.publish(
181
  OT2_COMMAND_TOPIC,
182
- json.dumps({
183
- "command": {
184
- "R": current_task["R"],
185
- "Y": current_task["Y"],
186
- "B": current_task["B"],
187
- "well": current_task["well"]
188
- },
189
- "experiment_id": current_task["experiment_id"],
190
- "session_id": current_task["session_id"]
191
- }),
 
 
192
  )
193
-
194
  time.sleep(1)
195
 
196
 
@@ -207,29 +230,29 @@ def verify_student_id(student_id):
207
  gr.update(interactive=False, value=0),
208
  gr.update(interactive=False, value=0),
209
  "Please enter a Student ID",
210
- gr.update(interactive=False)
211
  ]
212
-
213
  quota_remaining = check_student_quota(student_id)
214
-
215
-
216
  if quota_remaining <= 0:
217
  return [
218
  gr.update(interactive=False, value=0),
219
  gr.update(interactive=False, value=0),
220
  gr.update(interactive=False, value=0),
221
  "No experiments remaining. Please contact administrator.",
222
- gr.update(interactive=False)
223
  ]
224
-
225
  return [
226
  gr.update(interactive=True, value=0),
227
  gr.update(interactive=True, value=0),
228
  gr.update(interactive=True, value=0),
229
  f"Student ID verified. Available experiments: {quota_remaining}\nCurrent queue length: {queue_counter} experiment(s)",
230
- gr.update(interactive=True)
231
  ]
232
 
 
233
  def update_status_with_queue(R, Y, B):
234
  """Check if RYB inputs are valid and return updated queue info"""
235
  global queue_counter
@@ -237,15 +260,16 @@ def update_status_with_queue(R, Y, B):
237
  total = R + Y + B
238
  return [
239
  f"{validation_result['message']}\nCurrent queue length: {queue_counter} experiment(s)",
240
- gr.update(interactive=(total <= 300))
241
  ]
242
 
 
243
  def update_queue_display():
244
  """Refresh queue info for the UI"""
245
  global current_task, queue_counter
246
  num_available_wells = len(find_unused_wells())
247
  try:
248
- print(f"[DEBUG] Updating queue display - Counter: {queue_counter}")
249
  if current_task:
250
  status = f"""### Current Queue Status
251
  - Active experiment: Yes
@@ -265,33 +289,26 @@ def update_queue_display():
265
 
266
  def add_to_queue(student_id, R, Y, B):
267
  global queue_counter
268
-
269
  if student_id == "debug":
270
  yield {
271
- "Status": "Error",
272
- "Message": "Debug ID cannot submit to real experiment queue. Please use your student id to submit experiment."
273
  }
274
  return
275
 
276
-
277
  # Validate RYB inputs
278
  validation_result = validate_ryb_input(R, Y, B)
279
  if not validation_result["is_valid"]:
280
- yield {
281
- "Status": "Error",
282
- "Message": validation_result["message"]
283
- }
284
  return
285
-
286
  # Check quota
287
  quota_remaining = check_student_quota(student_id)
288
  if quota_remaining <= 0:
289
- yield {
290
- "Status": "Error",
291
- "Message": "No experiments remaining"
292
- }
293
  return
294
-
295
  # Select well
296
  experiment_id = secrets.token_hex(4)
297
  try:
@@ -299,15 +316,11 @@ def add_to_queue(student_id, R, Y, B):
299
  if not empty_wells:
300
  raise ValueError("No available wells")
301
  selected_well = empty_wells[0]
302
-
303
 
304
  except Exception as e:
305
- yield {
306
- "Status": "Error",
307
- "Message": str(e)
308
- }
309
  return
310
-
311
  # NOTE: Create the task and store it in tasks_dict
312
  task = {
313
  "R": R,
@@ -319,16 +332,15 @@ def add_to_queue(student_id, R, Y, B):
319
  "status": "queued",
320
  }
321
  tasks_dict[(student_id, experiment_id)] = task # Keep track globally
322
-
323
  # Put only (student_id, experiment_id) in the Queue
324
  task_queue.put((student_id, experiment_id))
325
  queue_counter += 1
326
  update_used_wells([selected_well])
327
  decrement_student_quota(student_id)
328
 
329
-
330
  print(f"Task added: {task}")
331
-
332
  # First yield: "Queued"
333
  yield {
334
  "Status": "Queued",
@@ -336,106 +348,109 @@ def add_to_queue(student_id, R, Y, B):
336
  "Student ID": student_id,
337
  "Experiment ID": experiment_id,
338
  "Well": selected_well,
339
- "Volumes": {"R": R, "Y": Y, "B": B}
340
  }
341
-
342
  # NOTE: Wait until the task's status becomes 'processing'
343
  # This ensures we only yield "Running" when the backend actually starts the job.
344
  while tasks_dict[(student_id, experiment_id)]["status"] == "queued":
345
  time.sleep(20)
346
-
347
  # Second yield: "Running" (happens only after status is 'processing')
348
  yield {
349
  "Status": "Running",
350
  "Student ID": student_id,
351
  "Experiment ID": experiment_id,
352
  "Well": selected_well,
353
- "Volumes": {"R": R, "Y": Y, "B": B}
354
  }
355
 
356
  # Finally, wait for the result
357
  result = result_queue.get()
358
  yield result
359
 
 
360
  def debug_experiment(student_id, R, Y, B):
361
  if student_id != "debug":
362
  return {"Status": "Error", "Message": "Invalid debug request"}
363
-
364
  experiment_id = "debug-" + secrets.token_hex(4)
365
 
366
  yield {
367
- "Status": "Queued",
368
- "Position": "debug",
369
- "Student ID": student_id,
370
- "Experiment ID": experiment_id,
371
- "Well": "DEBUG-A1",
372
- "Volumes": {"R": R, "Y": Y, "B": B}
373
- }
374
-
375
  time.sleep(1)
376
 
377
  yield {
378
- "Status": "Running",
379
- "Student ID": student_id,
380
- "Experiment ID": experiment_id,
381
- "Well": "DEBUG-A1",
382
- "Volumes": {"R": R, "Y": Y, "B": B}
383
- }
384
-
385
  time.sleep(1)
386
  result_debug = {
387
- "Status": "Complete",
388
- "Message": "Debug mode - simulated result (no actual experiment performed)",
389
- "Student ID": student_id,
390
- "Command": {
391
- "R": R,
392
- "Y": Y,
393
- "B": B,
394
- "well": "DEBUG-A1"
395
- },
396
- "Sensor Data": {
397
- "ch583": 2800,
398
- "ch670": 3000,
399
- "ch510": 1700,
400
- "ch410": 240,
401
- "ch620": 3900,
402
- "ch470": 1000,
403
- "ch550": 2400,
404
- "ch440": 900
405
- },
406
- "Experiment ID": experiment_id
407
- }
408
 
409
  yield result_debug
410
-
411
 
412
  with gr.Blocks(title="OT-2 Liquid Color Matching Experiment Queue") as demo:
413
  gr.Markdown("## OT-2 Liquid Color Matching Experiment Queue")
414
- gr.Markdown("Enter R, Y, and B volumes (in µL). Total volume must not exceed 300 µL.(a volume of exactly 300 µL is recommended)")
415
-
 
 
416
  with gr.Row():
417
  with gr.Column(scale=2):
418
  with gr.Row():
419
  student_id_input = gr.Textbox(
420
- label="Student ID",
421
- placeholder="Enter your unique ID"
422
  )
423
  verify_id_btn = gr.Button("Verify ID")
424
-
425
- r_slider = gr.Slider(0, 300, step=1, label="Red (R) Volume (µL)", interactive=False)
426
- y_slider = gr.Slider(0, 300, step=1, label="Yellow (Y) Volume (µL)", interactive=False)
427
- b_slider = gr.Slider(0, 300, step=1, label="Blue (B) Volume (µL)", interactive=False)
 
 
 
 
 
 
428
  status_output = gr.Textbox(label="Status")
429
  submit_btn = gr.Button("Submit Experiment", interactive=False)
430
  result_output = gr.JSON(label="Experiment Status")
431
-
432
  with gr.Column(scale=1):
433
  gr.Markdown("### Queue Status")
434
  queue_status = gr.Markdown("Loading queue status...")
435
  update_status_btn = gr.Button("Refresh Queue Status")
436
  gr.Markdown("### YouTube Livestream")
437
- #src="https://www.youtube.com/embed/live_stream?channel=UCHBzCfYpGwoqygH9YNh9A6g"
438
- iframe_html = '''
439
  <div style="position: relative; width: 100%; padding-top: 56.25%;">
440
  <iframe
441
  style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
@@ -447,68 +462,57 @@ with gr.Blocks(title="OT-2 Liquid Color Matching Experiment Queue") as demo:
447
  allowfullscreen>
448
  </iframe>
449
  </div>
450
- '''
451
  gr.HTML(iframe_html)
452
 
453
  verify_id_btn.click(
454
  verify_student_id,
455
  inputs=[student_id_input],
456
  outputs=[r_slider, y_slider, b_slider, status_output, submit_btn],
457
- api_name="verify_student_id"
458
  )
459
 
460
  r_slider.change(
461
- update_status_with_queue,
462
- inputs=[r_slider, y_slider, b_slider],
463
- outputs=[status_output, submit_btn]
464
  )
465
  y_slider.change(
466
- update_status_with_queue,
467
- inputs=[r_slider, y_slider, b_slider],
468
- outputs=[status_output, submit_btn]
469
  )
470
  b_slider.change(
471
- update_status_with_queue,
472
- inputs=[r_slider, y_slider, b_slider],
473
- outputs=[status_output, submit_btn]
474
  )
475
-
476
  # NOTE: concurrency_limit=3 is preserved; no changes here
477
  submit_btn.click(
478
  add_to_queue,
479
  inputs=[student_id_input, r_slider, y_slider, b_slider],
480
  outputs=result_output,
481
  api_name="submit",
482
- concurrency_limit=8
483
- ).then(
484
- update_queue_display,
485
- None,
486
- queue_status
487
- )
488
 
489
  update_status_btn.click(
490
- update_queue_display,
491
- None,
492
- queue_status,
493
- api_name="update_queue_display"
494
  )
495
 
496
- demo.load(
497
- update_queue_display,
498
- None,
499
- queue_status
500
- )
501
 
502
- debug_btn = gr.Button("Debug Submit", visible=False)
503
  debug_btn.click(
504
- debug_experiment,
505
- inputs=[student_id_input, r_slider, y_slider, b_slider],
506
- outputs=result_output,
507
- api_name="debug"
508
  )
509
 
510
 
511
- demo.queue
512
 
513
  if __name__ == "__main__":
514
- demo.launch()
 
5
  import paho.mqtt.client as mqtt
6
  import json
7
  import secrets
8
+ from DB_utils import (
9
+ find_unused_wells,
10
+ update_used_wells,
11
+ save_result,
12
+ get_student_quota,
13
+ decrement_student_quota,
14
+ )
15
  import os
16
 
17
  # NOTE: New global dict to store tasks keyed by (student_id, experiment_id)
 
24
  queue_counter = task_queue.qsize()
25
 
26
 
 
27
  MQTT_BROKER = os.getenv("MQTT_BROKER")
28
  MQTT_PORT = int(os.getenv("MQTT_PORT"))
29
  MQTT_USERNAME = os.getenv("MQTT_USERNAME")
 
42
  def check_student_quota(student_id):
43
  """Check student's remaining experiment quota"""
44
  student_quota = get_student_quota(student_id)
45
+ return student_quota
46
+
47
 
48
  def validate_ryb_input(R, Y, B):
49
  """Validate RYB input volumes"""
50
  total = R + Y + B
51
  if total > 300:
52
  return {
53
+ "is_valid": False,
54
+ "message": f"Total volume cannot exceed 300 µL. Current total: {total} µL.",
55
  }
56
+ return {"is_valid": True, "message": f"Current total: {total} µL."}
 
 
 
57
 
58
 
59
  mqtt_client = mqtt.Client()
60
  mqtt_client.tls_set(tls_version=mqtt.ssl.PROTOCOL_TLS_CLIENT)
61
  mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
62
 
63
+
64
  def on_connect(client, userdata, flags, rc):
65
  print(f"Connected to MQTT Broker with result code {rc}")
66
  client.subscribe([(OT2_STATUS_TOPIC, 2), (SENSOR_DATA_TOPIC, 2)])
67
 
68
+
69
  def on_message(client, userdata, msg):
70
  global current_task, sensor_results
71
  try:
 
77
  sensor_results = payload
78
  mqtt_client.publish(
79
  OT2_COMMAND_TOPIC,
80
+ json.dumps(
81
+ {
82
+ "command": {"sensor_status": "read"},
83
+ "experiment_id": payload["experiment_id"],
84
+ "session_id": payload["session_id"],
85
+ }
86
+ ),
87
  )
88
  except Exception as e:
89
  print(f"Error processing MQTT message: {e}")
90
 
91
+
92
  mqtt_client.on_connect = on_connect
93
  mqtt_client.on_message = on_message
94
  mqtt_client.connect(MQTT_BROKER, MQTT_PORT)
95
  mqtt_client.loop_start()
96
 
97
+
98
  def handle_sensor_status(payload):
99
  global current_task, sensor_results
100
  if "in_place" in json.dumps(payload):
101
  mqtt_client.publish(
102
  SENSOR_COMMAND_TOPIC,
103
+ json.dumps(
104
+ {
105
+ "command": {
106
+ "R": current_task["R"],
107
+ "Y": current_task["Y"],
108
+ "B": current_task["B"],
109
+ "well": current_task["well"],
110
+ },
111
+ "experiment_id": current_task["experiment_id"],
112
+ "session_id": current_task["session_id"],
113
+ }
114
+ ),
115
  )
116
  elif payload["status"]["sensor_status"] == "charging":
117
+
118
  experiment_result = {
119
+ "Status": "Complete",
120
+ "Message": "Experiment completed successfully!",
121
+ "Student ID": current_task["session_id"],
122
+ "Command": {
123
+ "R": current_task["R"],
124
+ "Y": current_task["Y"],
125
+ "B": current_task["B"],
126
+ "well": current_task["well"],
127
+ },
128
+ "Sensor Data": sensor_results["sensor_data"],
129
+ "Experiment ID": current_task["experiment_id"],
130
+ }
131
+ # Store full result in result queue
132
  result_queue.put(experiment_result)
133
 
134
+ # Create a version of experiment_result without "Status" and "Message" for database storage
135
+ db_data = {
136
+ key: experiment_result[key]
137
+ for key in experiment_result
138
+ if key not in ["Status", "Message"]
139
+ }
140
 
141
  save_result(db_data)
142
 
 
150
  global current_task, queue_counter
151
  task_start_time = None
152
  TIMEOUT_SECONDS = 165 # 2min45s
153
+
154
  while True:
155
  if current_task:
156
  # Check for timeout
 
158
  print("sending timeout message to OT-2")
159
  mqtt_client.publish(
160
  OT2_COMMAND_TOPIC,
161
+ json.dumps(
162
+ {
163
+ "command": {"sensor_status": "sensor_timeout"},
164
+ "experiment_id": current_task["experiment_id"],
165
+ "session_id": current_task["session_id"],
166
+ }
167
+ ),
168
+ )
169
+ result_queue.put(
170
+ {
171
+ "Status": "Error",
172
+ "Message": "Experiment timed out",
173
+ "Student ID": current_task["session_id"],
174
+ "Command": {
175
+ "R": current_task["R"],
176
+ "Y": current_task["Y"],
177
+ "B": current_task["B"],
178
+ "well": current_task["well"],
179
+ },
180
+ "Experiment ID": current_task["experiment_id"],
181
+ }
182
+ )
183
  current_task = None
184
  task_start_time = None
185
  continue
186
 
187
  if not current_task and not task_queue.empty():
188
  # Fetch a new task from the queue
189
+ student_id, experiment_id = (
190
+ task_queue.get()
191
+ ) # NOTE: We'll store (student_id, experiment_id) instead of task
192
  queue_counter -= 1
193
  task_start_time = time.time()
194
 
195
  # NOTE: We retrieve the actual task from tasks_dict
196
  current_task = tasks_dict[(student_id, experiment_id)]
197
+
198
  # Mark status as "processing"
199
  current_task["status"] = "processing"
200
+
201
  mqtt_client.publish(
202
  OT2_COMMAND_TOPIC,
203
+ json.dumps(
204
+ {
205
+ "command": {
206
+ "R": current_task["R"],
207
+ "Y": current_task["Y"],
208
+ "B": current_task["B"],
209
+ "well": current_task["well"],
210
+ },
211
+ "experiment_id": current_task["experiment_id"],
212
+ "session_id": current_task["session_id"],
213
+ }
214
+ ),
215
  )
216
+
217
  time.sleep(1)
218
 
219
 
 
230
  gr.update(interactive=False, value=0),
231
  gr.update(interactive=False, value=0),
232
  "Please enter a Student ID",
233
+ gr.update(interactive=False),
234
  ]
235
+
236
  quota_remaining = check_student_quota(student_id)
237
+
 
238
  if quota_remaining <= 0:
239
  return [
240
  gr.update(interactive=False, value=0),
241
  gr.update(interactive=False, value=0),
242
  gr.update(interactive=False, value=0),
243
  "No experiments remaining. Please contact administrator.",
244
+ gr.update(interactive=False),
245
  ]
246
+
247
  return [
248
  gr.update(interactive=True, value=0),
249
  gr.update(interactive=True, value=0),
250
  gr.update(interactive=True, value=0),
251
  f"Student ID verified. Available experiments: {quota_remaining}\nCurrent queue length: {queue_counter} experiment(s)",
252
+ gr.update(interactive=True),
253
  ]
254
 
255
+
256
  def update_status_with_queue(R, Y, B):
257
  """Check if RYB inputs are valid and return updated queue info"""
258
  global queue_counter
 
260
  total = R + Y + B
261
  return [
262
  f"{validation_result['message']}\nCurrent queue length: {queue_counter} experiment(s)",
263
+ gr.update(interactive=(total <= 300)),
264
  ]
265
 
266
+
267
  def update_queue_display():
268
  """Refresh queue info for the UI"""
269
  global current_task, queue_counter
270
  num_available_wells = len(find_unused_wells())
271
  try:
272
+ print(f"[DEBUG] Updating queue display - Counter: {queue_counter}")
273
  if current_task:
274
  status = f"""### Current Queue Status
275
  - Active experiment: Yes
 
289
 
290
  def add_to_queue(student_id, R, Y, B):
291
  global queue_counter
292
+
293
  if student_id == "debug":
294
  yield {
295
+ "Status": "Error",
296
+ "Message": "Debug ID cannot submit to real experiment queue. Please use your student id to submit experiment.",
297
  }
298
  return
299
 
 
300
  # Validate RYB inputs
301
  validation_result = validate_ryb_input(R, Y, B)
302
  if not validation_result["is_valid"]:
303
+ yield {"Status": "Error", "Message": validation_result["message"]}
 
 
 
304
  return
305
+
306
  # Check quota
307
  quota_remaining = check_student_quota(student_id)
308
  if quota_remaining <= 0:
309
+ yield {"Status": "Error", "Message": "No experiments remaining"}
 
 
 
310
  return
311
+
312
  # Select well
313
  experiment_id = secrets.token_hex(4)
314
  try:
 
316
  if not empty_wells:
317
  raise ValueError("No available wells")
318
  selected_well = empty_wells[0]
 
319
 
320
  except Exception as e:
321
+ yield {"Status": "Error", "Message": str(e)}
 
 
 
322
  return
323
+
324
  # NOTE: Create the task and store it in tasks_dict
325
  task = {
326
  "R": R,
 
332
  "status": "queued",
333
  }
334
  tasks_dict[(student_id, experiment_id)] = task # Keep track globally
335
+
336
  # Put only (student_id, experiment_id) in the Queue
337
  task_queue.put((student_id, experiment_id))
338
  queue_counter += 1
339
  update_used_wells([selected_well])
340
  decrement_student_quota(student_id)
341
 
 
342
  print(f"Task added: {task}")
343
+
344
  # First yield: "Queued"
345
  yield {
346
  "Status": "Queued",
 
348
  "Student ID": student_id,
349
  "Experiment ID": experiment_id,
350
  "Well": selected_well,
351
+ "Volumes": {"R": R, "Y": Y, "B": B},
352
  }
353
+
354
  # NOTE: Wait until the task's status becomes 'processing'
355
  # This ensures we only yield "Running" when the backend actually starts the job.
356
  while tasks_dict[(student_id, experiment_id)]["status"] == "queued":
357
  time.sleep(20)
358
+
359
  # Second yield: "Running" (happens only after status is 'processing')
360
  yield {
361
  "Status": "Running",
362
  "Student ID": student_id,
363
  "Experiment ID": experiment_id,
364
  "Well": selected_well,
365
+ "Volumes": {"R": R, "Y": Y, "B": B},
366
  }
367
 
368
  # Finally, wait for the result
369
  result = result_queue.get()
370
  yield result
371
 
372
+
373
  def debug_experiment(student_id, R, Y, B):
374
  if student_id != "debug":
375
  return {"Status": "Error", "Message": "Invalid debug request"}
376
+
377
  experiment_id = "debug-" + secrets.token_hex(4)
378
 
379
  yield {
380
+ "Status": "Queued",
381
+ "Position": "debug",
382
+ "Student ID": student_id,
383
+ "Experiment ID": experiment_id,
384
+ "Well": "DEBUG-A1",
385
+ "Volumes": {"R": R, "Y": Y, "B": B},
386
+ }
387
+
388
  time.sleep(1)
389
 
390
  yield {
391
+ "Status": "Running",
392
+ "Student ID": student_id,
393
+ "Experiment ID": experiment_id,
394
+ "Well": "DEBUG-A1",
395
+ "Volumes": {"R": R, "Y": Y, "B": B},
396
+ }
397
+
398
  time.sleep(1)
399
  result_debug = {
400
+ "Status": "Complete",
401
+ "Message": "Debug mode - simulated result (no actual experiment performed)",
402
+ "Student ID": student_id,
403
+ "Command": {"R": R, "Y": Y, "B": B, "well": "DEBUG-A1"},
404
+ "Sensor Data": {
405
+ "ch583": 2800,
406
+ "ch670": 3000,
407
+ "ch510": 1700,
408
+ "ch410": 240,
409
+ "ch620": 3900,
410
+ "ch470": 1000,
411
+ "ch550": 2400,
412
+ "ch440": 900,
413
+ },
414
+ "Experiment ID": experiment_id,
415
+ }
 
 
 
 
 
416
 
417
  yield result_debug
418
+
419
 
420
  with gr.Blocks(title="OT-2 Liquid Color Matching Experiment Queue") as demo:
421
  gr.Markdown("## OT-2 Liquid Color Matching Experiment Queue")
422
+ gr.Markdown(
423
+ "Enter R, Y, and B volumes (in µL). Total volume must not exceed 300 µL.(a volume of exactly 300 µL is recommended)"
424
+ )
425
+
426
  with gr.Row():
427
  with gr.Column(scale=2):
428
  with gr.Row():
429
  student_id_input = gr.Textbox(
430
+ label="Student ID", placeholder="Enter your unique ID"
 
431
  )
432
  verify_id_btn = gr.Button("Verify ID")
433
+
434
+ r_slider = gr.Slider(
435
+ 0, 300, step=1, label="Red (R) Volume (µL)", interactive=False
436
+ )
437
+ y_slider = gr.Slider(
438
+ 0, 300, step=1, label="Yellow (Y) Volume (µL)", interactive=False
439
+ )
440
+ b_slider = gr.Slider(
441
+ 0, 300, step=1, label="Blue (B) Volume (µL)", interactive=False
442
+ )
443
  status_output = gr.Textbox(label="Status")
444
  submit_btn = gr.Button("Submit Experiment", interactive=False)
445
  result_output = gr.JSON(label="Experiment Status")
446
+
447
  with gr.Column(scale=1):
448
  gr.Markdown("### Queue Status")
449
  queue_status = gr.Markdown("Loading queue status...")
450
  update_status_btn = gr.Button("Refresh Queue Status")
451
  gr.Markdown("### YouTube Livestream")
452
+ # src="https://www.youtube.com/embed/live_stream?channel=UCHBzCfYpGwoqygH9YNh9A6g"
453
+ iframe_html = """
454
  <div style="position: relative; width: 100%; padding-top: 56.25%;">
455
  <iframe
456
  style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
 
462
  allowfullscreen>
463
  </iframe>
464
  </div>
465
+ """
466
  gr.HTML(iframe_html)
467
 
468
  verify_id_btn.click(
469
  verify_student_id,
470
  inputs=[student_id_input],
471
  outputs=[r_slider, y_slider, b_slider, status_output, submit_btn],
472
+ api_name="verify_student_id",
473
  )
474
 
475
  r_slider.change(
476
+ update_status_with_queue,
477
+ inputs=[r_slider, y_slider, b_slider],
478
+ outputs=[status_output, submit_btn],
479
  )
480
  y_slider.change(
481
+ update_status_with_queue,
482
+ inputs=[r_slider, y_slider, b_slider],
483
+ outputs=[status_output, submit_btn],
484
  )
485
  b_slider.change(
486
+ update_status_with_queue,
487
+ inputs=[r_slider, y_slider, b_slider],
488
+ outputs=[status_output, submit_btn],
489
  )
490
+
491
  # NOTE: concurrency_limit=3 is preserved; no changes here
492
  submit_btn.click(
493
  add_to_queue,
494
  inputs=[student_id_input, r_slider, y_slider, b_slider],
495
  outputs=result_output,
496
  api_name="submit",
497
+ concurrency_limit=8,
498
+ ).then(update_queue_display, None, queue_status)
 
 
 
 
499
 
500
  update_status_btn.click(
501
+ update_queue_display, None, queue_status, api_name="update_queue_display"
 
 
 
502
  )
503
 
504
+ demo.load(update_queue_display, None, queue_status)
 
 
 
 
505
 
506
+ debug_btn = gr.Button("Debug Submit", visible=False)
507
  debug_btn.click(
508
+ debug_experiment,
509
+ inputs=[student_id_input, r_slider, y_slider, b_slider],
510
+ outputs=result_output,
511
+ api_name="debug",
512
  )
513
 
514
 
515
+ demo.queue
516
 
517
  if __name__ == "__main__":
518
+ demo.launch()