puda-drivers 0.0.15__py3-none-any.whl → 0.0.16__py3-none-any.whl
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/core/serialcontroller.py +32 -23
- puda_drivers/machines/first.py +24 -6
- puda_drivers/move/gcode.py +33 -20
- {puda_drivers-0.0.15.dist-info → puda_drivers-0.0.16.dist-info}/METADATA +1 -1
- {puda_drivers-0.0.15.dist-info → puda_drivers-0.0.16.dist-info}/RECORD +7 -7
- {puda_drivers-0.0.15.dist-info → puda_drivers-0.0.16.dist-info}/WHEEL +0 -0
- {puda_drivers-0.0.15.dist-info → puda_drivers-0.0.16.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
puda_drivers/machines/first.py
CHANGED
|
@@ -159,7 +159,7 @@ class First:
|
|
|
159
159
|
self.camera.disconnect()
|
|
160
160
|
self._logger.info("Machine shutdown complete")
|
|
161
161
|
|
|
162
|
-
async 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
|
|
|
@@ -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:
|
puda_drivers/move/gcode.py
CHANGED
|
@@ -514,6 +514,9 @@ class GCodeController(SerialController):
|
|
|
514
514
|
|
|
515
515
|
This method can be called even when the machine is moving from other commands,
|
|
516
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.
|
|
517
520
|
|
|
518
521
|
Returns:
|
|
519
522
|
Position containing X, Y, Z, and A positions
|
|
@@ -523,28 +526,38 @@ class GCodeController(SerialController):
|
|
|
523
526
|
"""
|
|
524
527
|
self._logger.info("Querying current machine position (M114).")
|
|
525
528
|
# Run the blocking execute call in a thread pool to allow concurrent operations
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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)
|
|
531
539
|
|
|
532
|
-
|
|
540
|
+
position_data: Dict[str, float] = {}
|
|
533
541
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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()
|
|
548
561
|
|
|
549
562
|
def _sync_position(self) -> Tuple[bool, Position]:
|
|
550
563
|
"""
|
|
@@ -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
|
|
@@ -3,7 +3,7 @@ puda_drivers/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
3
3
|
puda_drivers/core/__init__.py,sha256=XbCdXsU6NMDsmEAtavAGiSZZPla5d7zc2L7Qx9qKHdY,214
|
|
4
4
|
puda_drivers/core/logging.py,sha256=prOeJ3CGEbm37TtMRyAOTQQiMU5_ImZTRXmcUJxkenc,2892
|
|
5
5
|
puda_drivers/core/position.py,sha256=f4efmDSrKKCtqrR-GUJxVitPG20MiuGSDOWt-9TVISk,12628
|
|
6
|
-
puda_drivers/core/serialcontroller.py,sha256=
|
|
6
|
+
puda_drivers/core/serialcontroller.py,sha256=aIyEpA8J4R7yBfIq0roMqbD-nxEwuwaB8c_7zaOXg_A,9100
|
|
7
7
|
puda_drivers/cv/__init__.py,sha256=DYiPwYOLSUsZ9ivWiHoMGGD3MZ3Ygu9qLYja_OiUodU,100
|
|
8
8
|
puda_drivers/cv/camera.py,sha256=Tzxt1h3W5Z8XFanud_z-PhhJ2UYsC4OEhcak9TVu8jM,16490
|
|
9
9
|
puda_drivers/labware/__init__.py,sha256=RlRxrJn2zyzyxv4c1KGt8Gmxv2cRO8V4zZVnnyL-I00,288
|
|
@@ -12,10 +12,10 @@ puda_drivers/labware/opentrons_96_tiprack_300ul.json,sha256=jmNaworu688GEgFdxMxN
|
|
|
12
12
|
puda_drivers/labware/polyelectric_8_wellplate_30000ul.json,sha256=esu2tej0ORs7Pfd4HwoQVUpU5mPvp2AYzE3zsCC2FDk,3104
|
|
13
13
|
puda_drivers/labware/trash_bin.json,sha256=Hk4MXO48P28jG7F87DUd9Ja4c_P7kAy3karPQ965i9Y,580
|
|
14
14
|
puda_drivers/machines/__init__.py,sha256=zmIk_r2T8nbPA68h3Cko8N6oL7ncoBpmvhNcAqzHmc4,45
|
|
15
|
-
puda_drivers/machines/first.py,sha256=
|
|
15
|
+
puda_drivers/machines/first.py,sha256=5xFjMwCHM4wZz2_VjEMXPMyXndtXjcnt1WL_GWSSRbs,20725
|
|
16
16
|
puda_drivers/move/__init__.py,sha256=NKIKckcqgyviPM0EGFcmIoaqkJM4qekR4babfdddRzM,96
|
|
17
17
|
puda_drivers/move/deck.py,sha256=yq2B4WMqj0hQvHt8HoJskP10u1DUyKwUnjP2c9gJ174,1397
|
|
18
|
-
puda_drivers/move/gcode.py,sha256=
|
|
18
|
+
puda_drivers/move/gcode.py,sha256=nS1dmt90_89BUjlHh0VNqGLeRhyIdJ4-8Ps_7YlW-B0,23735
|
|
19
19
|
puda_drivers/move/grbl/__init__.py,sha256=vBeeti8DVN2dACi1rLmHN_UGIOdo0s-HZX6mIepLV5I,98
|
|
20
20
|
puda_drivers/move/grbl/api.py,sha256=loj8_Vap7S9qaD0ReHhgxr9Vkl6Wp7DGzyLkZyZ6v_k,16995
|
|
21
21
|
puda_drivers/move/grbl/constants.py,sha256=4736CRDzLGWVqGscLajMlrIQMyubsHfthXi4RF1CHNg,9585
|
|
@@ -23,7 +23,7 @@ puda_drivers/transfer/liquid/sartorius/__init__.py,sha256=7fljIbu0KkumsI3NI3O64d
|
|
|
23
23
|
puda_drivers/transfer/liquid/sartorius/api.py,sha256=jxwIJmY2k1K2ts6NC2ZgFTe4MOiH8TGnJeqYOqNa3rE,28250
|
|
24
24
|
puda_drivers/transfer/liquid/sartorius/constants.py,sha256=mcsjLrVBH-RSodH-pszstwcEL9wwbV0vOgHbGNxZz9w,2770
|
|
25
25
|
puda_drivers/transfer/liquid/sartorius/rLine.py,sha256=FWEFO9tZN3cbneiozXRs77O-47Jg_8vYzWJvUrWpYxA,14531
|
|
26
|
-
puda_drivers-0.0.
|
|
27
|
-
puda_drivers-0.0.
|
|
28
|
-
puda_drivers-0.0.
|
|
29
|
-
puda_drivers-0.0.
|
|
26
|
+
puda_drivers-0.0.16.dist-info/METADATA,sha256=kD7z3BQmYQAihFsf7T_f_iL3ttiLyY4nDegdEfmuElk,7102
|
|
27
|
+
puda_drivers-0.0.16.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
28
|
+
puda_drivers-0.0.16.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
|
|
29
|
+
puda_drivers-0.0.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|