puda-drivers 0.0.10__tar.gz → 0.0.12__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.
Files changed (36) hide show
  1. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/PKG-INFO +11 -16
  2. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/README.md +10 -15
  3. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/pyproject.toml +1 -1
  4. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/labware/polyelectric_8_wellplate_30000ul.json +1 -1
  5. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/labware/trash_bin.json +1 -1
  6. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/machines/first.py +174 -15
  7. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/tests/first.py +12 -11
  8. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/uv.lock +1 -1
  9. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/.gitignore +0 -0
  10. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/LICENSE +0 -0
  11. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/__init__.py +0 -0
  12. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/core/__init__.py +0 -0
  13. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/core/logging.py +0 -0
  14. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/core/position.py +0 -0
  15. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/core/serialcontroller.py +0 -0
  16. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/cv/__init__.py +0 -0
  17. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/cv/camera.py +0 -0
  18. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/labware/__init__.py +0 -0
  19. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/labware/labware.py +0 -0
  20. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/labware/opentrons_96_tiprack_300ul.json +0 -0
  21. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/machines/__init__.py +0 -0
  22. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/move/__init__.py +0 -0
  23. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/move/deck.py +0 -0
  24. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/move/gcode.py +0 -0
  25. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/move/grbl/__init__.py +0 -0
  26. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/move/grbl/api.py +0 -0
  27. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/move/grbl/constants.py +0 -0
  28. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/py.typed +0 -0
  29. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/transfer/liquid/sartorius/__init__.py +0 -0
  30. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/transfer/liquid/sartorius/api.py +0 -0
  31. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/transfer/liquid/sartorius/constants.py +0 -0
  32. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/src/puda_drivers/transfer/liquid/sartorius/sartorius.py +0 -0
  33. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/tests/example.py +0 -0
  34. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/tests/pipette.py +0 -0
  35. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/tests/qubot.py +0 -0
  36. {puda_drivers-0.0.10 → puda_drivers-0.0.12}/tests/webcam.py +0 -0
@@ -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:
@@ -64,6 +64,7 @@ When file logging is enabled, logs are saved to timestamped files (unless a cust
64
64
  The `First` machine integrates motion control, deck management, liquid handling, and camera capabilities:
65
65
 
66
66
  ```python
67
+ import time
67
68
  import logging
68
69
  from puda_drivers.machines import First
69
70
  from puda_drivers.core.logging import setup_logging
@@ -81,14 +82,8 @@ machine = First(
81
82
  camera_index=0,
82
83
  )
83
84
 
84
- # Connect all devices
85
- machine.connect()
86
-
87
- # Home the gantry
88
- machine.qubot.home()
89
-
90
- # Initialize the pipette
91
- machine.pipette.initialize()
85
+ # Start up the machine (connects all controllers, homes gantry, and initializes pipette)
86
+ machine.startup()
92
87
 
93
88
  # Load labware onto the deck
94
89
  machine.load_deck({
@@ -98,19 +93,19 @@ machine.load_deck({
98
93
  })
99
94
 
100
95
  # Start video recording
101
- machine.camera.start_video_recording()
96
+ machine.start_video_recording()
102
97
 
103
98
  # Perform liquid handling operations
104
99
  machine.attach_tip(slot="A3", well="G8")
105
- machine.aspirate_from(slot="C2", well="A1", amount=100)
106
- machine.dispense_to(slot="C2", well="B4", amount=100)
107
- machine.drop_tip(slot="C1", well="A1")
100
+ machine.aspirate_from(slot="C2", well="A1", amount=100, height_from_bottom=10.0)
101
+ machine.dispense_to(slot="C2", well="B4", amount=100, height_from_bottom=30.0)
102
+ machine.drop_tip(slot="C1", well="A1", height_from_bottom=10)
108
103
 
109
104
  # Stop video recording
110
- machine.camera.stop_video_recording()
105
+ machine.stop_video_recording()
111
106
 
112
- # Disconnect all devices
113
- machine.disconnect()
107
+ # Shutdown the machine (gracefully disconnects all controllers)
108
+ machine.shutdown()
114
109
  ```
115
110
 
116
111
  **Discovering Available Methods**: To explore what methods are available on any class instance, you can use Python's built-in `help()` function:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "puda-drivers"
3
- version = "0.0.10"
3
+ version = "0.0.12"
4
4
  description = "Hardware drivers for the PUDA platform."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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,7 @@
1
1
  """Test script for the First machine driver."""
2
2
 
3
3
  import random
4
+ import time
4
5
  import logging
5
6
  from puda_drivers.machines import First
6
7
  from puda_drivers.labware import get_available_labware
@@ -8,7 +9,7 @@ from puda_drivers.core import setup_logging
8
9
 
9
10
  setup_logging(
10
11
  enable_file_logging=False,
11
- log_level=logging.DEBUG, # Use logging.DEBUG to see all (DEBUG, INFO, WARNING, ERROR, CRITICAL) logs
12
+ log_level=logging.INFO, # Use logging.DEBUG to see all (DEBUG, INFO, WARNING, ERROR, CRITICAL) logs
12
13
  )
13
14
 
14
15
  if __name__ == "__main__":
@@ -34,15 +35,15 @@ if __name__ == "__main__":
34
35
  print(machine.deck)
35
36
  print(machine.deck["C2"])
36
37
 
37
- machine.connect()
38
- machine.qubot.home()
39
- machine.pipette.initialize()
38
+ machine.startup() # Connects all controllers, homes gantry, and initializes pipette
40
39
 
41
- machine.camera.start_video_recording()
40
+ # machine.record_video(duration_seconds=10, filename="test.mp4")
41
+ machine.start_video_recording()
42
42
  machine.attach_tip(slot="A3", well="G8")
43
- machine.aspirate_from(slot="C2", well="A1", amount=100)
44
- machine.dispense_to(slot="C2", well="B4", amount=100)
45
- machine.drop_tip(slot="C1", well="A1")
43
+ machine.aspirate_from(slot="C2", well="A1", amount=100, height_from_bottom=10)
44
+ # machine.capture_image()
45
+ machine.dispense_to(slot="C2", well="B4", amount=100, height_from_bottom=50)
46
+ machine.drop_tip(slot="C1", well="A1", height_from_bottom=10)
46
47
 
47
48
  # tiprack_wells = machine.deck["A3"].wells
48
49
  # # get pick up pipette one by one and drop it in the trash bin
@@ -58,7 +59,7 @@ if __name__ == "__main__":
58
59
  # machine.drop_tip("C1", "A1")
59
60
 
60
61
 
61
- machine.camera.stop_video_recording()
62
+ machine.stop_video_recording()
62
63
 
63
- # Disconnect machine
64
- machine.disconnect()
64
+ # Shutdown machine
65
+ machine.shutdown()
@@ -186,7 +186,7 @@ wheels = [
186
186
 
187
187
  [[package]]
188
188
  name = "puda-drivers"
189
- version = "0.0.10"
189
+ version = "0.0.12"
190
190
  source = { editable = "." }
191
191
  dependencies = [
192
192
  { name = "opencv-python" },
File without changes
File without changes