puda-drivers 0.0.10__py3-none-any.whl → 0.0.12__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.
@@ -30,7 +30,7 @@
30
30
  "dimensions": {
31
31
  "xDimension": 128,
32
32
  "yDimension": 85.5,
33
- "zDimension": 74
33
+ "zDimension": 54
34
34
  },
35
35
  "wells": {
36
36
  "A1": {
@@ -11,7 +11,7 @@
11
11
  "dimensions": {
12
12
  "xDimension": 127.76,
13
13
  "yDimension": 85.48,
14
- "zDimension": 20
14
+ "zDimension": 0
15
15
  },
16
16
  "wells": {
17
17
  "A1": {
@@ -9,7 +9,9 @@ This class demonstrates the integration of:
9
9
 
10
10
  import logging
11
11
  import time
12
+ from pathlib import Path
12
13
  from typing import Optional, Dict, Tuple, Type, Union
14
+ import numpy as np
13
15
  from puda_drivers.move import GCodeController, Deck
14
16
  from puda_drivers.core import Position
15
17
  from puda_drivers.transfer.liquid.sartorius import SartoriusController
@@ -118,21 +120,44 @@ class First:
118
120
  camera_index if camera_index is not None else self.DEFAULT_CAMERA_INDEX,
119
121
  )
120
122
 
121
- def connect(self):
122
- """Connect all controllers."""
123
- self._logger.info("Connecting all controllers")
123
+ def startup(self):
124
+ """
125
+ Start up the machine by connecting all controllers and initializing subsystems.
126
+
127
+ This method:
128
+ - Connects to all controllers (gantry, pipette, camera)
129
+ - Homes the gantry to establish a known position
130
+ - Initializes the pipette to reset it to a known state
131
+
132
+ The machine is ready for operations after this method completes.
133
+ """
134
+ self._logger.info("Starting up machine and connecting all controllers")
124
135
  self.qubot.connect()
125
136
  self.pipette.connect()
126
137
  self.camera.connect()
127
138
  self._logger.info("All controllers connected successfully")
128
139
 
129
- def disconnect(self):
130
- """Disconnect all controllers."""
131
- self._logger.info("Disconnecting all controllers")
140
+ # Home the gantry to establish known position
141
+ self._logger.info("Homing gantry...")
142
+ self.qubot.home()
143
+
144
+ # Initialize the pipette (all pipette operations need to wait 5 seconds for completion)
145
+ self._logger.info("Initializing pipette...")
146
+ self.pipette.initialize()
147
+ time.sleep(5)
148
+ self._logger.info("Machine startup complete - ready for operations")
149
+
150
+ def shutdown(self):
151
+ """
152
+ Gracefully shut down the machine by disconnecting all controllers.
153
+
154
+ This method ensures all connections are properly closed and resources are released.
155
+ """
156
+ self._logger.info("Shutting down machine and disconnecting all controllers")
132
157
  self.qubot.disconnect()
133
158
  self.pipette.disconnect()
134
159
  self.camera.disconnect()
135
- self._logger.info("All controllers disconnected successfully")
160
+ self._logger.info("Machine shutdown complete")
136
161
 
137
162
  def load_labware(self, slot: str, labware_name: str):
138
163
  """Load a labware object into a slot."""
@@ -185,14 +210,30 @@ class First:
185
210
  self.qubot.home(axis="Z")
186
211
  self._logger.debug("Z axis homed after tip attachment")
187
212
 
188
- def drop_tip(self, slot: str, well: str):
189
- """Drop a tip into a slot."""
213
+ def drop_tip(self, slot: str, well: str, height_from_bottom: float = 0.0):
214
+ """
215
+ Drop a tip into a slot.
216
+
217
+ Args:
218
+ slot: Slot name (e.g., 'A1', 'B2')
219
+ well: Well name within the slot (e.g., 'A1' for a well in a tiprack)
220
+ height_from_bottom: Height from the bottom of the well in mm. Defaults to 0.0.
221
+ Positive values move up from the bottom. Negative values
222
+ may cause a ValueError if the resulting position is outside
223
+ the Z axis limits.
224
+
225
+ Raises:
226
+ ValueError: If no tip is attached, or if the resulting position is outside
227
+ the Z axis limits.
228
+ """
190
229
  if not self.pipette.is_tip_attached():
191
230
  self._logger.error("Cannot drop tip: no tip attached")
192
231
  raise ValueError("Tip not attached")
193
232
 
194
233
  self._logger.info("Dropping tip into slot '%s', well '%s'", slot, well)
195
234
  pos = self.get_absolute_z_position(slot, well)
235
+ # add height from bottom
236
+ pos += Position(z=height_from_bottom)
196
237
  # move up by the tip length
197
238
  pos += Position(z=self.TIP_LENGTH)
198
239
  self._logger.debug("Moving to position %s (adjusted for tip length) for tip drop", pos)
@@ -204,31 +245,67 @@ class First:
204
245
  self.pipette.set_tip_attached(attached=False)
205
246
  self._logger.info("Tip dropped successfully")
206
247
 
207
- def aspirate_from(self, slot:str, well:str, amount:int):
208
- """Aspirate a volume of liquid from a slot."""
248
+ def aspirate_from(self, slot:str, well:str, amount:int, height_from_bottom: float = 0.0):
249
+ """
250
+ Aspirate a volume of liquid from a slot.
251
+
252
+ Args:
253
+ slot: Slot name (e.g., 'A1', 'B2')
254
+ well: Well name within the slot (e.g., 'A1')
255
+ amount: Volume to aspirate in µL
256
+ height_from_bottom: Height from the bottom of the well in mm. Defaults to 0.0.
257
+ Positive values move up from the bottom. Negative values
258
+ may cause a ValueError if the resulting position is outside
259
+ the Z axis limits.
260
+
261
+ Raises:
262
+ ValueError: If no tip is attached, or if the resulting position is outside
263
+ the Z axis limits.
264
+ """
209
265
  if not self.pipette.is_tip_attached():
210
266
  self._logger.error("Cannot aspirate: no tip attached")
211
267
  raise ValueError("Tip not attached")
212
268
 
213
269
  self._logger.info("Aspirating %d µL from slot '%s', well '%s'", amount, slot, well)
214
270
  pos = self.get_absolute_z_position(slot, well)
271
+ # add height from bottom
272
+ pos += Position(z=height_from_bottom)
215
273
  self._logger.debug("Moving Z axis to position %s", pos)
216
274
  self.qubot.move_absolute(position=pos)
275
+
217
276
  self._logger.debug("Aspirating %d µL", amount)
218
277
  self.pipette.aspirate(amount=amount)
219
278
  time.sleep(5)
220
279
  self._logger.info("Aspiration completed: %d µL from slot '%s', well '%s'", amount, slot, well)
221
280
 
222
- def dispense_to(self, slot:str, well:str, amount:int):
223
- """Dispense a volume of liquid to a slot."""
281
+ def dispense_to(self, slot:str, well:str, amount:int, height_from_bottom: float = 0.0):
282
+ """
283
+ Dispense a volume of liquid to a slot.
284
+
285
+ Args:
286
+ slot: Slot name (e.g., 'A1', 'B2')
287
+ well: Well name within the slot (e.g., 'A1')
288
+ amount: Volume to dispense in µL
289
+ height_from_bottom: Height from the bottom of the well in mm. Defaults to 0.0.
290
+ Positive values move up from the bottom. Negative values
291
+ may cause a ValueError if the resulting position is outside
292
+ the Z axis limits.
293
+
294
+ Raises:
295
+ ValueError: If no tip is attached, or if the resulting position is outside
296
+ the Z axis limits.
297
+ """
224
298
  if not self.pipette.is_tip_attached():
225
299
  self._logger.error("Cannot dispense: no tip attached")
226
300
  raise ValueError("Tip not attached")
227
301
 
228
302
  self._logger.info("Dispensing %d µL to slot '%s', well '%s'", amount, slot, well)
229
303
  pos = self.get_absolute_z_position(slot, well)
304
+ # add height from bottom
305
+ pos += Position(z=height_from_bottom)
230
306
  self._logger.debug("Moving Z axis to position %s", pos)
231
307
  self.qubot.move_absolute(position=pos)
308
+
232
309
  self._logger.debug("Dispensing %d µL", amount)
233
310
  self.pipette.dispense(amount=amount)
234
311
  time.sleep(5)
@@ -275,8 +352,7 @@ class First:
275
352
  # the deck is rotated 90 degrees clockwise for this machine
276
353
  pos += well_pos.swap_xy()
277
354
  # get z
278
- z = Position(z=self.deck[slot].get_height() - self.CEILING_HEIGHT)
279
- pos += z
355
+ pos += Position(z=self.deck[slot].get_height() - self.CEILING_HEIGHT)
280
356
  self._logger.debug("Absolute Z position for slot '%s', well '%s': %s", slot, well, pos)
281
357
  else:
282
358
  self._logger.debug("Absolute Z position for slot '%s': %s", slot, pos)
@@ -299,3 +375,86 @@ class First:
299
375
  else:
300
376
  self._logger.debug("Absolute A position for slot '%s': %s", slot, pos)
301
377
  return pos
378
+
379
+ def start_video_recording(
380
+ self,
381
+ filename: Optional[Union[str, Path]] = None,
382
+ fps: Optional[float] = None
383
+ ) -> Path:
384
+ """
385
+ Start recording a video.
386
+
387
+ Args:
388
+ filename: Optional filename for the video. If not provided, a timestamped
389
+ filename will be generated. If provided without extension, .mp4 will be added.
390
+ fps: Optional frames per second for the video. Defaults to 30.0 if not specified.
391
+
392
+ Returns:
393
+ Path to the video file where recording is being saved
394
+
395
+ Raises:
396
+ IOError: If camera is not connected or recording fails to start
397
+ ValueError: If already recording
398
+ """
399
+ return self.camera.start_video_recording(filename=filename, fps=fps)
400
+
401
+ def stop_video_recording(self) -> Optional[Path]:
402
+ """
403
+ Stop recording a video.
404
+
405
+ Returns:
406
+ Path to the saved video file, or None if no recording was in progress
407
+
408
+ Raises:
409
+ IOError: If video writer fails to release
410
+ """
411
+ return self.camera.stop_video_recording()
412
+
413
+ def record_video(
414
+ self,
415
+ duration_seconds: float,
416
+ filename: Optional[Union[str, Path]] = None,
417
+ fps: Optional[float] = None
418
+ ) -> Path:
419
+ """
420
+ Record a video for a specified duration.
421
+
422
+ Args:
423
+ duration_seconds: Duration of the video in seconds
424
+ filename: Optional filename for the video. If not provided, a timestamped
425
+ filename will be generated. If provided without extension, .mp4 will be added.
426
+ fps: Optional frames per second for the video. Defaults to 30.0 if not specified.
427
+
428
+ Returns:
429
+ Path to the saved video file
430
+
431
+ Raises:
432
+ IOError: If camera is not connected or recording fails
433
+ """
434
+ return self.camera.record_video(
435
+ duration_seconds=duration_seconds,
436
+ filename=filename,
437
+ fps=fps
438
+ )
439
+
440
+ def capture_image(
441
+ self,
442
+ save: bool = False,
443
+ filename: Optional[Union[str, Path]] = None
444
+ ) -> np.ndarray:
445
+ """
446
+ Capture a single image from the camera.
447
+
448
+ Args:
449
+ save: If True, save the image to the captures folder
450
+ filename: Optional filename for the saved image. If not provided and save=True,
451
+ a timestamped filename will be generated. If provided without extension,
452
+ .jpg will be added.
453
+
454
+ Returns:
455
+ Captured image as a numpy array (BGR format)
456
+
457
+ Raises:
458
+ IOError: If camera is not connected or capture fails
459
+ """
460
+ return self.camera.capture_image(save=save, filename=filename)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: puda-drivers
3
- Version: 0.0.10
3
+ Version: 0.0.12
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
@@ -85,6 +85,7 @@ When file logging is enabled, logs are saved to timestamped files (unless a cust
85
85
  The `First` machine integrates motion control, deck management, liquid handling, and camera capabilities:
86
86
 
87
87
  ```python
88
+ import time
88
89
  import logging
89
90
  from puda_drivers.machines import First
90
91
  from puda_drivers.core.logging import setup_logging
@@ -102,14 +103,8 @@ machine = First(
102
103
  camera_index=0,
103
104
  )
104
105
 
105
- # Connect all devices
106
- machine.connect()
107
-
108
- # Home the gantry
109
- machine.qubot.home()
110
-
111
- # Initialize the pipette
112
- machine.pipette.initialize()
106
+ # Start up the machine (connects all controllers, homes gantry, and initializes pipette)
107
+ machine.startup()
113
108
 
114
109
  # Load labware onto the deck
115
110
  machine.load_deck({
@@ -119,19 +114,19 @@ machine.load_deck({
119
114
  })
120
115
 
121
116
  # Start video recording
122
- machine.camera.start_video_recording()
117
+ machine.start_video_recording()
123
118
 
124
119
  # Perform liquid handling operations
125
120
  machine.attach_tip(slot="A3", well="G8")
126
- machine.aspirate_from(slot="C2", well="A1", amount=100)
127
- machine.dispense_to(slot="C2", well="B4", amount=100)
128
- machine.drop_tip(slot="C1", well="A1")
121
+ machine.aspirate_from(slot="C2", well="A1", amount=100, height_from_bottom=10.0)
122
+ machine.dispense_to(slot="C2", well="B4", amount=100, height_from_bottom=30.0)
123
+ machine.drop_tip(slot="C1", well="A1", height_from_bottom=10)
129
124
 
130
125
  # Stop video recording
131
- machine.camera.stop_video_recording()
126
+ machine.stop_video_recording()
132
127
 
133
- # Disconnect all devices
134
- machine.disconnect()
128
+ # Shutdown the machine (gracefully disconnects all controllers)
129
+ machine.shutdown()
135
130
  ```
136
131
 
137
132
  **Discovering Available Methods**: To explore what methods are available on any class instance, you can use Python's built-in `help()` function:
@@ -9,10 +9,10 @@ puda_drivers/cv/camera.py,sha256=Tzxt1h3W5Z8XFanud_z-PhhJ2UYsC4OEhcak9TVu8jM,164
9
9
  puda_drivers/labware/__init__.py,sha256=RlRxrJn2zyzyxv4c1KGt8Gmxv2cRO8V4zZVnnyL-I00,288
10
10
  puda_drivers/labware/labware.py,sha256=hZhOzSyb1GP_bm1LvQsREx4YSqLCWBTKNWDgDqfFavI,5317
11
11
  puda_drivers/labware/opentrons_96_tiprack_300ul.json,sha256=jmNaworu688GEgFdxMxNRSaEp4ZOg9IumFk8bVHSzYY,19796
12
- puda_drivers/labware/polyelectric_8_wellplate_30000ul.json,sha256=NwXMgHBYnIIFasmrthTDTTV7n1M5DYQBDWh-KYsq1gI,3104
13
- puda_drivers/labware/trash_bin.json,sha256=X4TDNDzGbCtJSWlYgGYHUzdnVKj62SggGDNf7z5S0OE,581
12
+ puda_drivers/labware/polyelectric_8_wellplate_30000ul.json,sha256=esu2tej0ORs7Pfd4HwoQVUpU5mPvp2AYzE3zsCC2FDk,3104
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=9Im7jiw2duOBbBQEOxVkqFApPDRlGfI87OoyJ-M4SJE,12122
15
+ puda_drivers/machines/first.py,sha256=yk_Kh9f_yjSlRHjYEOzvrMxADBvlTH0AIV1VLvm2sp8,18339
16
16
  puda_drivers/move/__init__.py,sha256=NKIKckcqgyviPM0EGFcmIoaqkJM4qekR4babfdddRzM,96
17
17
  puda_drivers/move/deck.py,sha256=yq2B4WMqj0hQvHt8HoJskP10u1DUyKwUnjP2c9gJ174,1397
18
18
  puda_drivers/move/gcode.py,sha256=Aw7la4RkPw737hW5sKl6WiPCmmTnsjLvG4mOb-RwVSc,22592
@@ -23,7 +23,7 @@ puda_drivers/transfer/liquid/sartorius/__init__.py,sha256=QGpKz5YUwa8xCdSMXeZ0iR
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/sartorius.py,sha256=bW838jYOAfLlbUqtsKRipZ-RLjrNTcZ7riYV6I4w8G8,13728
26
- puda_drivers-0.0.10.dist-info/METADATA,sha256=oEbn6NvV9bXfJZusYV49ri1V1aPFR3n2qjKTp3vJ5R0,6991
27
- puda_drivers-0.0.10.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
28
- puda_drivers-0.0.10.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
29
- puda_drivers-0.0.10.dist-info/RECORD,,
26
+ puda_drivers-0.0.12.dist-info/METADATA,sha256=saXg221vJnpqzC4t6Cn7Kcc1ZX0WPBLQkclr_vFaRgQ,7071
27
+ puda_drivers-0.0.12.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
28
+ puda_drivers-0.0.12.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
29
+ puda_drivers-0.0.12.dist-info/RECORD,,