puda-drivers 0.0.14__tar.gz → 0.0.16__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/PKG-INFO +1 -1
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/pyproject.toml +1 -1
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/core/serialcontroller.py +32 -23
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/machines/first.py +26 -8
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/move/gcode.py +44 -25
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/transfer/liquid/sartorius/rLine.py +8 -5
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/tests/pipette.py +2 -1
- puda_drivers-0.0.16/tests/test_position_polling.py +115 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/uv.lock +1 -1
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/.gitignore +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/LICENSE +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/README.md +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/__init__.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/core/__init__.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/core/logging.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/core/position.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/cv/__init__.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/cv/camera.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/labware/__init__.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/labware/labware.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/labware/opentrons_96_tiprack_300ul.json +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/labware/polyelectric_8_wellplate_30000ul.json +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/labware/trash_bin.json +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/machines/__init__.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/move/__init__.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/move/deck.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/move/grbl/__init__.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/move/grbl/api.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/move/grbl/constants.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/py.typed +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/transfer/liquid/sartorius/__init__.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/transfer/liquid/sartorius/api.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/transfer/liquid/sartorius/constants.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/tests/example.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/tests/first.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/tests/qubot.py +0 -0
- {puda_drivers-0.0.14 → puda_drivers-0.0.16}/tests/webcam.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: puda-drivers
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.16
|
|
4
4
|
Summary: Hardware drivers for the PUDA platform.
|
|
5
5
|
Project-URL: Homepage, https://github.com/zhao-bears/puda-drivers
|
|
6
6
|
Project-URL: Issues, https://github.com/zhao-bears/puda-drivers/issues
|
|
@@ -4,6 +4,7 @@ Generic Serial Controller for communicating with devices over serial ports.
|
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
6
|
import logging
|
|
7
|
+
import threading
|
|
7
8
|
from typing import Optional, List, Tuple
|
|
8
9
|
from abc import ABC, abstractmethod
|
|
9
10
|
import serial
|
|
@@ -49,6 +50,9 @@ class SerialController(ABC):
|
|
|
49
50
|
self.baudrate = baudrate
|
|
50
51
|
self.timeout = timeout
|
|
51
52
|
self._logger = logger
|
|
53
|
+
|
|
54
|
+
# lock to prevent concurrent access to the serial port
|
|
55
|
+
self._lock = threading.Lock()
|
|
52
56
|
|
|
53
57
|
def connect(self) -> None:
|
|
54
58
|
"""
|
|
@@ -103,8 +107,9 @@ class SerialController(ABC):
|
|
|
103
107
|
|
|
104
108
|
def _send_command(self, command: str) -> None:
|
|
105
109
|
"""
|
|
106
|
-
Sends a custom protocol command
|
|
107
|
-
|
|
110
|
+
Sends a custom protocol command.
|
|
111
|
+
Note: This method should be called while holding self._lock to ensure
|
|
112
|
+
atomic command/response pairing.
|
|
108
113
|
"""
|
|
109
114
|
if not self.is_connected or not self._serial:
|
|
110
115
|
self._logger.error(
|
|
@@ -116,7 +121,6 @@ class SerialController(ABC):
|
|
|
116
121
|
|
|
117
122
|
self._logger.info("-> Sending: %r", command)
|
|
118
123
|
|
|
119
|
-
# Send the command
|
|
120
124
|
try:
|
|
121
125
|
self._serial.reset_input_buffer() # clear input buffer
|
|
122
126
|
self._serial.reset_output_buffer() # clear output buffer
|
|
@@ -130,7 +134,7 @@ class SerialController(ABC):
|
|
|
130
134
|
|
|
131
135
|
except serial.SerialException as e:
|
|
132
136
|
self._logger.error(
|
|
133
|
-
"Serial error writing
|
|
137
|
+
"Serial error writing command '%s'. Error: %s",
|
|
134
138
|
command,
|
|
135
139
|
e,
|
|
136
140
|
)
|
|
@@ -198,11 +202,14 @@ class SerialController(ABC):
|
|
|
198
202
|
|
|
199
203
|
This method combines sending a command and reading its response into a
|
|
200
204
|
single atomic operation, ensuring the response corresponds to the command
|
|
201
|
-
that was just sent.
|
|
202
|
-
|
|
205
|
+
that was just sent. The entire operation is protected by a lock to prevent
|
|
206
|
+
concurrent commands from interfering with each other.
|
|
207
|
+
|
|
208
|
+
This is the preferred method for commands that require a response.
|
|
203
209
|
|
|
204
210
|
Args:
|
|
205
211
|
command: Command string to send (should include protocol terminator if needed)
|
|
212
|
+
value: Optional value parameter for the command
|
|
206
213
|
|
|
207
214
|
Returns:
|
|
208
215
|
Response string from the device
|
|
@@ -211,20 +218,22 @@ class SerialController(ABC):
|
|
|
211
218
|
serial.SerialException: If device is not connected or communication fails
|
|
212
219
|
serial.SerialTimeoutException: If no response is received within timeout
|
|
213
220
|
"""
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
self.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
self.
|
|
221
|
+
# Hold the lock for the entire send+read operation to ensure atomicity
|
|
222
|
+
# This prevents concurrent commands from mixing up responses
|
|
223
|
+
with self._lock:
|
|
224
|
+
# Increase timeout by 60 seconds for G28 (homing) command
|
|
225
|
+
original_timeout = self.timeout
|
|
226
|
+
if "G28" in command.upper():
|
|
227
|
+
self.timeout = original_timeout + 60
|
|
228
|
+
# Also update the serial connection's timeout if connected
|
|
229
|
+
if self.is_connected and self._serial:
|
|
230
|
+
self._serial.timeout = self.timeout
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
self._send_command(self._build_command(command, value))
|
|
234
|
+
return self._read_response()
|
|
235
|
+
finally:
|
|
236
|
+
# Restore original timeout
|
|
237
|
+
self.timeout = original_timeout
|
|
238
|
+
if self.is_connected and self._serial:
|
|
239
|
+
self._serial.timeout = original_timeout
|
|
@@ -159,7 +159,7 @@ class First:
|
|
|
159
159
|
self.camera.disconnect()
|
|
160
160
|
self._logger.info("Machine shutdown complete")
|
|
161
161
|
|
|
162
|
-
def get_position(self) -> Dict[str, float]:
|
|
162
|
+
async def get_position(self) -> Dict[str, Union[Dict[str, float], int]]:
|
|
163
163
|
"""
|
|
164
164
|
Get the current position of the machine. Both QuBot and Sartorius are queried.
|
|
165
165
|
|
|
@@ -168,8 +168,8 @@ class First:
|
|
|
168
168
|
Returns:
|
|
169
169
|
Dictionary containing the current position of the machine and it's components.
|
|
170
170
|
"""
|
|
171
|
-
qubot_position = self.qubot.get_position()
|
|
172
|
-
sartorius_position = self.pipette.get_position()
|
|
171
|
+
qubot_position = await self.qubot.get_position()
|
|
172
|
+
sartorius_position = await self.pipette.get_position()
|
|
173
173
|
|
|
174
174
|
return {
|
|
175
175
|
"qubot": qubot_position.to_dict(),
|
|
@@ -234,7 +234,11 @@ class First:
|
|
|
234
234
|
self.qubot.move_absolute(position=pos)
|
|
235
235
|
|
|
236
236
|
# attach tip (move slowly down)
|
|
237
|
-
|
|
237
|
+
labware = self.deck[slot]
|
|
238
|
+
if labware is None:
|
|
239
|
+
self._logger.error("Cannot attach tip: no labware loaded in slot '%s'", slot)
|
|
240
|
+
raise ValueError(f"No labware loaded in slot '{slot}'. Load labware before attaching tips.")
|
|
241
|
+
insert_depth = labware.get_insert_depth()
|
|
238
242
|
self._logger.debug("Moving down by %s mm to insert tip", insert_depth)
|
|
239
243
|
self.qubot.move_relative(
|
|
240
244
|
position=Position(z=-insert_depth),
|
|
@@ -378,17 +382,24 @@ class First:
|
|
|
378
382
|
|
|
379
383
|
Returns:
|
|
380
384
|
Position with absolute coordinates
|
|
385
|
+
|
|
386
|
+
Raises:
|
|
387
|
+
ValueError: If well is specified but no labware is loaded in the slot
|
|
381
388
|
"""
|
|
382
389
|
# Get slot origin
|
|
383
390
|
pos = self.get_slot_origin(slot)
|
|
384
391
|
|
|
385
392
|
# relative well position from slot origin
|
|
386
393
|
if well:
|
|
387
|
-
|
|
394
|
+
labware = self.deck[slot]
|
|
395
|
+
if labware is None:
|
|
396
|
+
self._logger.error("Cannot get well position: no labware loaded in slot '%s'", slot)
|
|
397
|
+
raise ValueError(f"No labware loaded in slot '{slot}'. Load labware before accessing wells.")
|
|
398
|
+
well_pos = labware.get_well_position(well).get_xy()
|
|
388
399
|
# the deck is rotated 90 degrees clockwise for this machine
|
|
389
400
|
pos += well_pos.swap_xy()
|
|
390
401
|
# get z
|
|
391
|
-
pos += Position(z=
|
|
402
|
+
pos += Position(z=labware.get_height() - self.CEILING_HEIGHT)
|
|
392
403
|
self._logger.debug("Absolute Z position for slot '%s', well '%s': %s", slot, well, pos)
|
|
393
404
|
else:
|
|
394
405
|
self._logger.debug("Absolute Z position for slot '%s': %s", slot, pos)
|
|
@@ -404,15 +415,22 @@ class First:
|
|
|
404
415
|
|
|
405
416
|
Returns:
|
|
406
417
|
Position with absolute coordinates
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
ValueError: If well is specified but no labware is loaded in the slot
|
|
407
421
|
"""
|
|
408
422
|
pos = self.get_slot_origin(slot)
|
|
409
423
|
|
|
410
424
|
if well:
|
|
411
|
-
|
|
425
|
+
labware = self.deck[slot]
|
|
426
|
+
if labware is None:
|
|
427
|
+
self._logger.error("Cannot get well position: no labware loaded in slot '%s'", slot)
|
|
428
|
+
raise ValueError(f"No labware loaded in slot '{slot}'. Load labware before accessing wells.")
|
|
429
|
+
well_pos = labware.get_well_position(well).get_xy()
|
|
412
430
|
pos += well_pos.swap_xy()
|
|
413
431
|
|
|
414
432
|
# get a
|
|
415
|
-
a = Position(a=
|
|
433
|
+
a = Position(a=labware.get_height() - self.CEILING_HEIGHT)
|
|
416
434
|
pos += a
|
|
417
435
|
self._logger.debug("Absolute A position for slot '%s', well '%s': %s", slot, well, pos)
|
|
418
436
|
else:
|
|
@@ -7,9 +7,9 @@ absolute coordinates, with relative moves converted to absolute internally.
|
|
|
7
7
|
Supports homing and position synchronization.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
import time
|
|
11
10
|
import re
|
|
12
11
|
import logging
|
|
12
|
+
import asyncio
|
|
13
13
|
from dataclasses import dataclass
|
|
14
14
|
from typing import Optional, Dict, Tuple, Union
|
|
15
15
|
|
|
@@ -508,9 +508,15 @@ class GCodeController(SerialController):
|
|
|
508
508
|
|
|
509
509
|
return self._current_position
|
|
510
510
|
|
|
511
|
-
def get_position(self) -> Position:
|
|
511
|
+
async def get_position(self) -> Position:
|
|
512
512
|
"""
|
|
513
|
-
Get the current machine position (M114 command).
|
|
513
|
+
Get the current machine position (M114 command) asynchronously.
|
|
514
|
+
|
|
515
|
+
This method can be called even when the machine is moving from other commands,
|
|
516
|
+
as it runs the blocking serial communication in a separate thread.
|
|
517
|
+
|
|
518
|
+
If the M114 command takes more than 1 second to respond, falls back to
|
|
519
|
+
returning the internally tracked position instead.
|
|
514
520
|
|
|
515
521
|
Returns:
|
|
516
522
|
Position containing X, Y, Z, and A positions
|
|
@@ -519,28 +525,39 @@ class GCodeController(SerialController):
|
|
|
519
525
|
Returns an empty Position if the query fails or no positions are found.
|
|
520
526
|
"""
|
|
521
527
|
self._logger.info("Querying current machine position (M114).")
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
528
|
+
# Run the blocking execute call in a thread pool to allow concurrent operations
|
|
529
|
+
# with a 1 second timeout - if it takes longer, fall back to internal position
|
|
530
|
+
try:
|
|
531
|
+
res: str = await asyncio.wait_for(
|
|
532
|
+
asyncio.to_thread(self.execute, "M114"),
|
|
533
|
+
timeout=1.0
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Extract position values using regex
|
|
537
|
+
pattern = re.compile(r"([XYZA]):(-?\d+\.\d+)")
|
|
538
|
+
matches = pattern.findall(res)
|
|
529
539
|
|
|
530
|
-
|
|
531
|
-
try:
|
|
532
|
-
position_data[axis.lower()] = float(value_str)
|
|
533
|
-
except ValueError:
|
|
534
|
-
self._logger.error(
|
|
535
|
-
"Failed to convert position value '%s' for axis %s to float.",
|
|
536
|
-
value_str,
|
|
537
|
-
axis,
|
|
538
|
-
)
|
|
539
|
-
continue
|
|
540
|
+
position_data: Dict[str, float] = {}
|
|
540
541
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
542
|
+
for axis, value_str in matches:
|
|
543
|
+
try:
|
|
544
|
+
position_data[axis.lower()] = float(value_str)
|
|
545
|
+
except ValueError:
|
|
546
|
+
self._logger.error(
|
|
547
|
+
"Failed to convert position value '%s' for axis %s to float.",
|
|
548
|
+
value_str,
|
|
549
|
+
axis,
|
|
550
|
+
)
|
|
551
|
+
continue
|
|
552
|
+
|
|
553
|
+
position = Position.from_dict(position_data)
|
|
554
|
+
self._logger.info("Query position complete. Retrieved positions: %s", position)
|
|
555
|
+
return position
|
|
556
|
+
except asyncio.TimeoutError:
|
|
557
|
+
self._logger.warning(
|
|
558
|
+
"M114 command timed out after 1 second. Falling back to internal position."
|
|
559
|
+
)
|
|
560
|
+
return self.get_internal_position()
|
|
544
561
|
|
|
545
562
|
def _sync_position(self) -> Tuple[bool, Position]:
|
|
546
563
|
"""
|
|
@@ -556,11 +573,13 @@ class GCodeController(SerialController):
|
|
|
556
573
|
|
|
557
574
|
Note:
|
|
558
575
|
This method may recursively call itself if a correction move is made.
|
|
576
|
+
Since this method calls move_absolute (which is blocking), it remains
|
|
577
|
+
synchronous. The async get_position() is called using asyncio.run().
|
|
559
578
|
"""
|
|
560
579
|
self._logger.info("Starting position synchronization check (M114).")
|
|
561
580
|
|
|
562
|
-
# Query the actual machine position
|
|
563
|
-
queried_position = self.get_position()
|
|
581
|
+
# Query the actual machine position (async method called from sync context)
|
|
582
|
+
queried_position = asyncio.run(self.get_position())
|
|
564
583
|
|
|
565
584
|
if not queried_position.get_axes():
|
|
566
585
|
self._logger.error("Query position failed. Cannot synchronize.")
|
{puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/transfer/liquid/sartorius/rLine.py
RENAMED
|
@@ -9,14 +9,13 @@ Reference: https://api.sartorius.com/document-hub/dam/download/34901/Sartorius-r
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
|
+
import asyncio
|
|
12
13
|
from typing import Optional
|
|
13
14
|
from puda_drivers.core.serialcontroller import SerialController
|
|
14
15
|
from .constants import STATUS_CODES
|
|
15
16
|
|
|
16
17
|
class SartoriusDeviceError(Exception):
|
|
17
18
|
"""Custom exception raised when the Sartorius device reports an error."""
|
|
18
|
-
|
|
19
|
-
pass
|
|
20
19
|
|
|
21
20
|
class SartoriusController(SerialController):
|
|
22
21
|
"""
|
|
@@ -372,15 +371,19 @@ class SartoriusController(SerialController):
|
|
|
372
371
|
|
|
373
372
|
return json.dumps(status_data)
|
|
374
373
|
|
|
375
|
-
def get_position(self) -> int:
|
|
374
|
+
async def get_position(self) -> int:
|
|
376
375
|
"""
|
|
377
|
-
Query the current position of the pipette (DP command).
|
|
376
|
+
Query the current position of the pipette (DP command) asynchronously.
|
|
377
|
+
|
|
378
|
+
This method can be called even when the pipette is performing other operations,
|
|
379
|
+
as it runs the blocking serial communication in a separate thread.
|
|
378
380
|
|
|
379
381
|
Returns:
|
|
380
382
|
Current position in steps
|
|
381
383
|
"""
|
|
382
384
|
self._logger.info("** Querying Position (DP) **")
|
|
383
|
-
|
|
385
|
+
# Run the blocking execute call in a thread pool to allow concurrent operations
|
|
386
|
+
response = await asyncio.to_thread(self.execute, command="DP")
|
|
384
387
|
self._logger.info("** Position: %s steps **\n", response)
|
|
385
388
|
return response
|
|
386
389
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import asyncio
|
|
2
3
|
from puda_drivers.transfer.liquid.sartorius import SartoriusController
|
|
3
4
|
from puda_drivers.core.logging import setup_logging
|
|
4
5
|
|
|
@@ -52,7 +53,7 @@ def test_pipette_operations():
|
|
|
52
53
|
pipette.set_outward_speed(3)
|
|
53
54
|
pipette.get_outward_speed()
|
|
54
55
|
pipette.run_to_position(100)
|
|
55
|
-
pipette.get_position()
|
|
56
|
+
asyncio.run(pipette.get_position())
|
|
56
57
|
|
|
57
58
|
# 3. Eject Tip (if any)
|
|
58
59
|
print("[STEP 3] Ejecting Tip (if any)...")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Test script to poll get_position every second during machine operations."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from puda_drivers.machines import First
|
|
8
|
+
from puda_drivers.labware import get_available_labware
|
|
9
|
+
from puda_drivers.core import setup_logging
|
|
10
|
+
|
|
11
|
+
setup_logging(
|
|
12
|
+
enable_file_logging=False,
|
|
13
|
+
log_level=logging.INFO,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def poll_position(machine: First, stop_event: asyncio.Event):
|
|
18
|
+
"""
|
|
19
|
+
Poll get_position every second and print the results.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
machine: First machine instance
|
|
23
|
+
stop_event: Event to signal when to stop polling
|
|
24
|
+
"""
|
|
25
|
+
while not stop_event.is_set():
|
|
26
|
+
try:
|
|
27
|
+
position = await machine.get_position()
|
|
28
|
+
timestamp = datetime.utcnow().isoformat() + "Z"
|
|
29
|
+
result = {
|
|
30
|
+
"timestamp": timestamp,
|
|
31
|
+
"qubot": position.get("qubot", {}),
|
|
32
|
+
"pipette": position.get("pipette", ""),
|
|
33
|
+
}
|
|
34
|
+
print(json.dumps(result))
|
|
35
|
+
except (ValueError, IOError, RuntimeError) as e:
|
|
36
|
+
timestamp = datetime.utcnow().isoformat() + "Z"
|
|
37
|
+
result = {
|
|
38
|
+
"timestamp": timestamp,
|
|
39
|
+
"qubot": {},
|
|
40
|
+
"pipette": "",
|
|
41
|
+
"error": str(e),
|
|
42
|
+
}
|
|
43
|
+
print(json.dumps(result))
|
|
44
|
+
|
|
45
|
+
# Wait 1 second, but check stop_event periodically
|
|
46
|
+
try:
|
|
47
|
+
await asyncio.wait_for(stop_event.wait(), timeout=1.0)
|
|
48
|
+
break
|
|
49
|
+
except asyncio.TimeoutError:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_operations(machine: First):
|
|
54
|
+
"""
|
|
55
|
+
Run the machine operations synchronously.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
machine: First machine instance
|
|
59
|
+
"""
|
|
60
|
+
machine.start_video_recording()
|
|
61
|
+
machine.attach_tip(slot="A3", well="G8")
|
|
62
|
+
machine.aspirate_from(slot="C2", well="A1", amount=100, height_from_bottom=10)
|
|
63
|
+
machine.dispense_to(slot="C2", well="B4", amount=100, height_from_bottom=50)
|
|
64
|
+
machine.drop_tip(slot="C1", well="A1", height_from_bottom=10)
|
|
65
|
+
machine.stop_video_recording()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def main():
|
|
69
|
+
"""Main async function to run operations and position polling concurrently."""
|
|
70
|
+
# Connect machine
|
|
71
|
+
machine = First(
|
|
72
|
+
qubot_port="/dev/ttyACM0",
|
|
73
|
+
sartorius_port="/dev/ttyUSB0",
|
|
74
|
+
camera_index=0,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# View available labware
|
|
78
|
+
print(get_available_labware())
|
|
79
|
+
|
|
80
|
+
# Define deck layout declaratively and load all at once
|
|
81
|
+
machine.load_deck(
|
|
82
|
+
{
|
|
83
|
+
"C1": "trash_bin",
|
|
84
|
+
"C2": "polyelectric_8_wellplate_30000ul",
|
|
85
|
+
"A3": "opentrons_96_tiprack_300ul",
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
print(machine.deck)
|
|
90
|
+
print(machine.deck["C2"])
|
|
91
|
+
|
|
92
|
+
machine.startup() # Connects all controllers, homes gantry, and initializes pipette
|
|
93
|
+
|
|
94
|
+
# Create event to signal when operations are done
|
|
95
|
+
stop_event = asyncio.Event()
|
|
96
|
+
|
|
97
|
+
# Start position polling task
|
|
98
|
+
polling_task = asyncio.create_task(poll_position(machine, stop_event))
|
|
99
|
+
|
|
100
|
+
# Run operations in a thread pool (since they're blocking)
|
|
101
|
+
try:
|
|
102
|
+
await asyncio.to_thread(run_operations, machine)
|
|
103
|
+
finally:
|
|
104
|
+
# Signal polling to stop
|
|
105
|
+
stop_event.set()
|
|
106
|
+
# Wait for polling task to finish
|
|
107
|
+
await polling_task
|
|
108
|
+
|
|
109
|
+
# Shutdown machine
|
|
110
|
+
machine.shutdown()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
asyncio.run(main())
|
|
115
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/labware/opentrons_96_tiprack_300ul.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/transfer/liquid/sartorius/__init__.py
RENAMED
|
File without changes
|
{puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/transfer/liquid/sartorius/api.py
RENAMED
|
File without changes
|
{puda_drivers-0.0.14 → puda_drivers-0.0.16}/src/puda_drivers/transfer/liquid/sartorius/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|