puda-drivers 0.0.17__py3-none-any.whl → 0.0.18__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.
@@ -3,9 +3,8 @@
3
3
  import json
4
4
  import inspect
5
5
  from pathlib import Path
6
- from typing import Dict, Any
6
+ from typing import Dict, Any, List
7
7
  from abc import ABC
8
- from typing import List
9
8
  from puda_drivers.core import Position
10
9
 
11
10
 
@@ -141,10 +141,10 @@ class First:
141
141
  self._logger.info("Homing gantry...")
142
142
  self.qubot.home()
143
143
 
144
- # Initialize the pipette (all pipette operations need to wait 5 seconds for completion)
144
+ # Initialize the pipette
145
145
  self._logger.info("Initializing pipette...")
146
146
  self.pipette.initialize()
147
- time.sleep(5)
147
+ time.sleep(3) # need to wait for the pipette to initialize
148
148
  self._logger.info("Machine startup complete - ready for operations")
149
149
 
150
150
  def shutdown(self):
@@ -159,6 +159,8 @@ class First:
159
159
  self.camera.disconnect()
160
160
  self._logger.info("Machine shutdown complete")
161
161
 
162
+ ### Queue (public commands) ###
163
+
162
164
  async def get_position(self) -> Dict[str, Union[Dict[str, float], int]]:
163
165
  """
164
166
  Get the current position of the machine. Both QuBot and Sartorius are queried.
@@ -175,8 +177,18 @@ class First:
175
177
  "qubot": qubot_position.to_dict(),
176
178
  "pipette": sartorius_position,
177
179
  }
180
+
181
+ def get_deck(self):
182
+ """
183
+ Get the current deck layout.
178
184
 
179
- ### Labware management ###
185
+ Returns:
186
+ Dictionary mapping slot names (e.g., "A1") to labware classes.
187
+
188
+ Raises:
189
+ None
190
+ """
191
+ return self.deck.to_dict()
180
192
 
181
193
  def load_labware(self, slot: str, labware_name: str):
182
194
  """
@@ -205,19 +217,6 @@ class First:
205
217
  """
206
218
  self.deck.empty_slot(slot=slot)
207
219
  self._logger.debug("Slot '%s' emptied", slot)
208
-
209
- def get_deck(self):
210
- """
211
- Get the current deck layout.
212
-
213
- Returns:
214
- Dictionary mapping slot names (e.g., "A1") to labware classes.
215
-
216
- Raises:
217
- None
218
- """
219
- return self.deck.to_dict()
220
-
221
220
 
222
221
  def load_deck(self, deck_layout: Dict[str, Type[StandardLabware]]):
223
222
  """
@@ -239,8 +238,6 @@ class First:
239
238
  self.load_labware(slot=slot, labware_name=labware_name)
240
239
  self._logger.info("Deck layout loaded successfully")
241
240
 
242
- ### Liquid handling ###
243
-
244
241
  def attach_tip(self, slot: str, well: Optional[str] = None):
245
242
  """
246
243
  Attach a tip from a slot.
@@ -258,7 +255,7 @@ class First:
258
255
  return
259
256
 
260
257
  self._logger.info("Attaching tip from slot '%s'%s", slot, f", well '{well}'" if well else "")
261
- pos = self.get_absolute_z_position(slot, well)
258
+ pos = self._get_absolute_z_position(slot, well)
262
259
  self._logger.debug("Moving to position %s for tip attachment", pos)
263
260
  # return the offset from the origin
264
261
  self.qubot.move_absolute(position=pos)
@@ -280,7 +277,7 @@ class First:
280
277
  self.qubot.home(axis="Z")
281
278
  self._logger.debug("Z axis homed after tip attachment")
282
279
 
283
- def drop_tip(self, slot: str, well: str, height_from_bottom: float = 0.0):
280
+ def drop_tip(self, *, slot: str, well: str, height_from_bottom: float = 0.0):
284
281
  """
285
282
  Drop a tip into a slot.
286
283
 
@@ -301,7 +298,7 @@ class First:
301
298
  raise ValueError("Tip not attached")
302
299
 
303
300
  self._logger.info("Dropping tip into slot '%s', well '%s'", slot, well)
304
- pos = self.get_absolute_z_position(slot, well)
301
+ pos = self._get_absolute_z_position(slot, well)
305
302
  # add height from bottom
306
303
  pos += Position(z=height_from_bottom)
307
304
  # move up by the tip length
@@ -315,7 +312,7 @@ class First:
315
312
  self.pipette.set_tip_attached(attached=False)
316
313
  self._logger.info("Tip dropped successfully")
317
314
 
318
- def aspirate_from(self, slot:str, well:str, amount:int, height_from_bottom: float = 0.0):
315
+ def aspirate_from(self, *, slot: str, well: str, amount: int, height_from_bottom: float = 0.0):
319
316
  """
320
317
  Aspirate a volume of liquid from a slot.
321
318
 
@@ -337,7 +334,7 @@ class First:
337
334
  raise ValueError("Tip not attached")
338
335
 
339
336
  self._logger.info("Aspirating %d µL from slot '%s', well '%s'", amount, slot, well)
340
- pos = self.get_absolute_z_position(slot, well)
337
+ pos = self._get_absolute_z_position(slot, well)
341
338
  # add height from bottom
342
339
  pos += Position(z=height_from_bottom)
343
340
  self._logger.debug("Moving Z axis to position %s", pos)
@@ -348,7 +345,7 @@ class First:
348
345
  time.sleep(5)
349
346
  self._logger.info("Aspiration completed: %d µL from slot '%s', well '%s'", amount, slot, well)
350
347
 
351
- def dispense_to(self, slot:str, well:str, amount:int, height_from_bottom: float = 0.0):
348
+ def dispense_to(self, *, slot: str, well: str, amount: int, height_from_bottom: float = 0.0):
352
349
  """
353
350
  Dispense a volume of liquid to a slot.
354
351
 
@@ -370,7 +367,7 @@ class First:
370
367
  raise ValueError("Tip not attached")
371
368
 
372
369
  self._logger.info("Dispensing %d µL to slot '%s', well '%s'", amount, slot, well)
373
- pos = self.get_absolute_z_position(slot, well)
370
+ pos = self._get_absolute_z_position(slot, well)
374
371
  # add height from bottom
375
372
  pos += Position(z=height_from_bottom)
376
373
  self._logger.debug("Moving Z axis to position %s", pos)
@@ -380,8 +377,93 @@ class First:
380
377
  self.pipette.dispense(amount=amount)
381
378
  time.sleep(5)
382
379
  self._logger.info("Dispense completed: %d µL to slot '%s', well '%s'", amount, slot, well)
380
+
381
+ def start_video_recording(
382
+ self,
383
+ filename: Optional[Union[str, Path]] = None,
384
+ fps: Optional[float] = None
385
+ ) -> Path:
386
+ """
387
+ Start recording a video.
383
388
 
384
- def get_slot_origin(self, slot: str) -> Position:
389
+ Args:
390
+ filename: Optional filename for the video. If not provided, a timestamped
391
+ filename will be generated. If provided without extension, .mp4 will be added.
392
+ fps: Optional frames per second for the video. Defaults to 30.0 if not specified.
393
+
394
+ Returns:
395
+ Path to the video file where recording is being saved
396
+
397
+ Raises:
398
+ IOError: If camera is not connected or recording fails to start
399
+ ValueError: If already recording
400
+ """
401
+ return self.camera.start_video_recording(filename=filename, fps=fps)
402
+
403
+ def stop_video_recording(self) -> Optional[Path]:
404
+ """
405
+ Stop recording a video.
406
+
407
+ Returns:
408
+ Path to the saved video file, or None if no recording was in progress
409
+
410
+ Raises:
411
+ IOError: If video writer fails to release
412
+ """
413
+ return self.camera.stop_video_recording()
414
+
415
+ def record_video(
416
+ self,
417
+ duration_seconds: float,
418
+ filename: Optional[Union[str, Path]] = None,
419
+ fps: Optional[float] = None
420
+ ) -> Path:
421
+ """
422
+ Record a video for a specified duration.
423
+
424
+ Args:
425
+ duration_seconds: Duration of the video in seconds
426
+ filename: Optional filename for the video. If not provided, a timestamped
427
+ filename will be generated. If provided without extension, .mp4 will be added.
428
+ fps: Optional frames per second for the video. Defaults to 30.0 if not specified.
429
+
430
+ Returns:
431
+ Path to the saved video file
432
+
433
+ Raises:
434
+ IOError: If camera is not connected or recording fails
435
+ """
436
+ return self.camera.record_video(
437
+ duration_seconds=duration_seconds,
438
+ filename=filename,
439
+ fps=fps
440
+ )
441
+
442
+ def capture_image(
443
+ self,
444
+ save: bool = False,
445
+ filename: Optional[Union[str, Path]] = None
446
+ ) -> np.ndarray:
447
+ """
448
+ Capture a single image from the camera.
449
+
450
+ Args:
451
+ save: If True, save the image to the captures folder
452
+ filename: Optional filename for the saved image. If not provided and save=True,
453
+ a timestamped filename will be generated. If provided without extension,
454
+ .jpg will be added.
455
+
456
+ Returns:
457
+ Captured image as a numpy array (BGR format)
458
+
459
+ Raises:
460
+ IOError: If camera is not connected or capture fails
461
+ """
462
+ return self.camera.capture_image(save=save, filename=filename)
463
+
464
+ ### Private methods ###
465
+
466
+ def _get_slot_origin(self, slot: str) -> Position:
385
467
  """
386
468
  Get the origin coordinates of a slot.
387
469
 
@@ -402,7 +484,7 @@ class First:
402
484
  self._logger.debug("Slot origin for '%s': %s", slot, pos)
403
485
  return pos
404
486
 
405
- def get_absolute_z_position(self, slot: str, well: Optional[str] = None) -> Position:
487
+ def _get_absolute_z_position(self, slot: str, well: Optional[str] = None) -> Position:
406
488
  """
407
489
  Get the absolute position for a slot (and optionally a well within that slot) based on the origin
408
490
 
@@ -417,7 +499,7 @@ class First:
417
499
  ValueError: If well is specified but no labware is loaded in the slot
418
500
  """
419
501
  # Get slot origin
420
- pos = self.get_slot_origin(slot)
502
+ pos = self._get_slot_origin(slot)
421
503
 
422
504
  # relative well position from slot origin
423
505
  if well:
@@ -435,7 +517,7 @@ class First:
435
517
  self._logger.debug("Absolute Z position for slot '%s': %s", slot, pos)
436
518
  return pos
437
519
 
438
- def get_absolute_a_position(self, slot: str, well: Optional[str] = None) -> Position:
520
+ def _get_absolute_a_position(self, slot: str, well: Optional[str] = None) -> Position:
439
521
  """
440
522
  Get the absolute position for a slot (and optionally a well within that slot) based on the origin
441
523
 
@@ -449,7 +531,7 @@ class First:
449
531
  Raises:
450
532
  ValueError: If well is specified but no labware is loaded in the slot
451
533
  """
452
- pos = self.get_slot_origin(slot)
534
+ pos = self._get_slot_origin(slot)
453
535
 
454
536
  if well:
455
537
  labware = self.deck[slot]
@@ -467,85 +549,22 @@ class First:
467
549
  self._logger.debug("Absolute A position for slot '%s': %s", slot, pos)
468
550
  return pos
469
551
 
470
- def start_video_recording(
471
- self,
472
- filename: Optional[Union[str, Path]] = None,
473
- fps: Optional[float] = None
474
- ) -> Path:
475
- """
476
- Start recording a video.
477
-
478
- Args:
479
- filename: Optional filename for the video. If not provided, a timestamped
480
- filename will be generated. If provided without extension, .mp4 will be added.
481
- fps: Optional frames per second for the video. Defaults to 30.0 if not specified.
482
-
483
- Returns:
484
- Path to the video file where recording is being saved
485
-
486
- Raises:
487
- IOError: If camera is not connected or recording fails to start
488
- ValueError: If already recording
489
- """
490
- return self.camera.start_video_recording(filename=filename, fps=fps)
552
+ ### Control (immediate commands) ###
491
553
 
492
- def stop_video_recording(self) -> Optional[Path]:
554
+ def pause(self):
493
555
  """
494
- Stop recording a video.
495
-
496
- Returns:
497
- Path to the saved video file, or None if no recording was in progress
498
-
499
- Raises:
500
- IOError: If video writer fails to release
556
+ Pause the execution of queued commands.
501
557
  """
502
- return self.camera.stop_video_recording()
558
+ print("Pausing machine")
503
559
 
504
- def record_video(
505
- self,
506
- duration_seconds: float,
507
- filename: Optional[Union[str, Path]] = None,
508
- fps: Optional[float] = None
509
- ) -> Path:
560
+ def resume(self):
510
561
  """
511
- Record a video for a specified duration.
512
-
513
- Args:
514
- duration_seconds: Duration of the video in seconds
515
- filename: Optional filename for the video. If not provided, a timestamped
516
- filename will be generated. If provided without extension, .mp4 will be added.
517
- fps: Optional frames per second for the video. Defaults to 30.0 if not specified.
518
-
519
- Returns:
520
- Path to the saved video file
521
-
522
- Raises:
523
- IOError: If camera is not connected or recording fails
562
+ Resume the execution of queued commands.
524
563
  """
525
- return self.camera.record_video(
526
- duration_seconds=duration_seconds,
527
- filename=filename,
528
- fps=fps
529
- )
530
-
531
- def capture_image(
532
- self,
533
- save: bool = False,
534
- filename: Optional[Union[str, Path]] = None
535
- ) -> np.ndarray:
564
+ print("Resuming machine")
565
+
566
+ def cancel(self):
536
567
  """
537
- Capture a single image from the camera.
538
-
539
- Args:
540
- save: If True, save the image to the captures folder
541
- filename: Optional filename for the saved image. If not provided and save=True,
542
- a timestamped filename will be generated. If provided without extension,
543
- .jpg will be added.
544
-
545
- Returns:
546
- Captured image as a numpy array (BGR format)
547
-
548
- Raises:
549
- IOError: If camera is not connected or capture fails
568
+ Cancel the execution of queued commands.
550
569
  """
551
- return self.camera.capture_image(save=save, filename=filename)
570
+ print("Cancelling machine")
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: puda-drivers
3
- Version: 0.0.17
3
+ Version: 0.0.18
4
4
  Summary: Hardware drivers for the PUDA platform.
5
- Project-URL: Homepage, https://github.com/PUDAP/puda-drivers
6
- Project-URL: Issues, https://github.com/PUDAP/puda-drivers/issues
5
+ Project-URL: Homepage, https://github.com/PUDAP/puda
6
+ Project-URL: Issues, https://github.com/PUDAP/puda/issues
7
7
  Author-email: zhao <20024592+agentzhao@users.noreply.github.com>
8
8
  License-Expression: MIT
9
9
  License-File: LICENSE
@@ -18,7 +18,6 @@ Requires-Python: >=3.10
18
18
  Requires-Dist: nats-py>=2.12.0
19
19
  Requires-Dist: opencv-python>=4.12.0.88
20
20
  Requires-Dist: pyserial~=3.5
21
- Requires-Dist: pytest>=9.0.2
22
21
  Description-Content-Type: text/markdown
23
22
 
24
23
  # puda-drivers
@@ -260,6 +259,9 @@ uv run pytest tests/ --cov=puda_drivers --cov-report=html
260
259
  # Build distribution packages
261
260
  uv build
262
261
 
262
+ # cd to puda project root
263
+ cd ...
264
+
263
265
  # Publish to PyPI
264
266
  uv publish
265
267
  # Username: __token__
@@ -7,12 +7,12 @@ puda_drivers/core/serialcontroller.py,sha256=8Yf9DYIhtQTIVGB1Pm6COa6qgJcM0ZxdGwO
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
10
- puda_drivers/labware/labware.py,sha256=hZhOzSyb1GP_bm1LvQsREx4YSqLCWBTKNWDgDqfFavI,5317
10
+ puda_drivers/labware/labware.py,sha256=zugMMMLWAOINj8K5czc62AvmvlFbb0vkQDAWw_cBMyE,5299
11
11
  puda_drivers/labware/opentrons_96_tiprack_300ul.json,sha256=jmNaworu688GEgFdxMxNRSaEp4ZOg9IumFk8bVHSzYY,19796
12
12
  puda_drivers/labware/polyelectric_8_wellplate_30000ul.json,sha256=esu2tej0ORs7Pfd4HwoQVUpU5mPvp2AYzE3zsCC2FDk,3104
13
13
  puda_drivers/labware/trash_bin.json,sha256=6dH0prXKJVXMRm096rPSiJgMr0nPRtHUKo7Z6xIxdnA,596
14
14
  puda_drivers/machines/__init__.py,sha256=zmIk_r2T8nbPA68h3Cko8N6oL7ncoBpmvhNcAqzHmc4,45
15
- puda_drivers/machines/first.py,sha256=GYYEUGi6xx6ev-ByX-_y9_GnMaBdhPCeiu-bCnKDZw0,21417
15
+ puda_drivers/machines/first.py,sha256=JFah4Y4X55nigwgkEcebOvPdWHNXudfkQpVyHzp5xSQ,21856
16
16
  puda_drivers/move/__init__.py,sha256=NKIKckcqgyviPM0EGFcmIoaqkJM4qekR4babfdddRzM,96
17
17
  puda_drivers/move/deck.py,sha256=CprlB8i9jvNImrc5IpHUZSSvvFe8uei76fNQ4iN6n34,2394
18
18
  puda_drivers/move/gcode.py,sha256=ioqRS-Kom5S7Pyp9-wg4MRXGIbIDEz7SsihBBOM2m30,23784
@@ -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.17.dist-info/METADATA,sha256=YFBVlnKBI0al6Z6F9w0urGImcN133jmzsJVTcojNfEc,8420
27
- puda_drivers-0.0.17.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
28
- puda_drivers-0.0.17.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
29
- puda_drivers-0.0.17.dist-info/RECORD,,
26
+ puda_drivers-0.0.18.dist-info/METADATA,sha256=JQCr3vVSNyE7DLRxJT7_w3X4v9d_U-JpCnrQwLtJrgw,8409
27
+ puda_drivers-0.0.18.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
28
+ puda_drivers-0.0.18.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
29
+ puda_drivers-0.0.18.dist-info/RECORD,,