puda-drivers 0.0.9__tar.gz → 0.0.10__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.9 → puda_drivers-0.0.10}/PKG-INFO +56 -118
  2. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/README.md +55 -117
  3. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/pyproject.toml +1 -1
  4. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/machines/first.py +51 -5
  5. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/uv.lock +1 -1
  6. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/.gitignore +0 -0
  7. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/LICENSE +0 -0
  8. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/__init__.py +0 -0
  9. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/core/__init__.py +0 -0
  10. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/core/logging.py +0 -0
  11. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/core/position.py +0 -0
  12. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/core/serialcontroller.py +0 -0
  13. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/cv/__init__.py +0 -0
  14. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/cv/camera.py +0 -0
  15. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/labware/__init__.py +0 -0
  16. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/labware/labware.py +0 -0
  17. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/labware/opentrons_96_tiprack_300ul.json +0 -0
  18. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/labware/polyelectric_8_wellplate_30000ul.json +0 -0
  19. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/labware/trash_bin.json +0 -0
  20. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/machines/__init__.py +0 -0
  21. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/move/__init__.py +0 -0
  22. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/move/deck.py +0 -0
  23. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/move/gcode.py +0 -0
  24. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/move/grbl/__init__.py +0 -0
  25. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/move/grbl/api.py +0 -0
  26. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/move/grbl/constants.py +0 -0
  27. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/py.typed +0 -0
  28. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/transfer/liquid/sartorius/__init__.py +0 -0
  29. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/transfer/liquid/sartorius/api.py +0 -0
  30. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/transfer/liquid/sartorius/constants.py +0 -0
  31. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/transfer/liquid/sartorius/sartorius.py +0 -0
  32. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/tests/example.py +0 -0
  33. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/tests/first.py +0 -0
  34. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/tests/pipette.py +0 -0
  35. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/tests/qubot.py +0 -0
  36. {puda_drivers-0.0.9 → puda_drivers-0.0.10}/tests/webcam.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: puda-drivers
3
- Version: 0.0.9
3
+ Version: 0.0.10
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
@@ -80,160 +80,96 @@ setup_logging(
80
80
 
81
81
  When file logging is enabled, logs are saved to timestamped files (unless a custom name is provided) in the `logs/` folder. The logs folder is created automatically if it doesn't exist.
82
82
 
83
- ### Gantry Control (GCode)
83
+ ### First Machine Example
84
84
 
85
- ```python
86
- from puda_drivers.move import GCodeController
87
-
88
- # Initialize and connect to a G-code device
89
- gantry = GCodeController(port_name="/dev/ttyACM0", feed=3000)
90
- gantry.connect()
91
-
92
- # Configure axis limits for safety (recommended)
93
- gantry.set_axis_limits("X", 0, 200)
94
- gantry.set_axis_limits("Y", -200, 0)
95
- gantry.set_axis_limits("Z", -100, 0)
96
- gantry.set_axis_limits("A", -180, 180)
97
-
98
- # Home the gantry
99
- gantry.home()
85
+ The `First` machine integrates motion control, deck management, liquid handling, and camera capabilities:
100
86
 
101
- # Move to absolute position (validated against limits)
102
- gantry.move_absolute(x=50.0, y=-100.0, z=-10.0)
103
-
104
- # Move relative to current position (validated after conversion to absolute)
105
- gantry.move_relative(x=20.0, y=-10.0)
106
-
107
- # Query current position
108
- position = gantry.query_position()
109
- print(f"Current position: {position}")
87
+ ```python
88
+ import logging
89
+ from puda_drivers.machines import First
90
+ from puda_drivers.core.logging import setup_logging
110
91
 
111
- # Disconnect when done
112
- gantry.disconnect()
113
- ```
92
+ # Configure logging
93
+ setup_logging(
94
+ enable_file_logging=False,
95
+ log_level=logging.DEBUG,
96
+ )
114
97
 
115
- **Axis Limits and Validation**: The `move_absolute()` and `move_relative()` methods automatically validate that target positions are within configured axis limits. If a position is outside the limits, a `ValueError` is raised before any movement is executed. Use `set_axis_limits()` to configure limits for each axis.
98
+ # Initialize the First machine
99
+ machine = First(
100
+ qubot_port="/dev/ttyACM0",
101
+ sartorius_port="/dev/ttyUSB0",
102
+ camera_index=0,
103
+ )
116
104
 
117
- ### Liquid Handling (Sartorius)
105
+ # Connect all devices
106
+ machine.connect()
118
107
 
119
- ```python
120
- from puda_drivers.transfer.liquid.sartorius import SartoriusController
108
+ # Home the gantry
109
+ machine.qubot.home()
121
110
 
122
- # Initialize and connect to pipette
123
- pipette = SartoriusController(port_name="/dev/ttyUSB0")
124
- pipette.connect()
125
- pipette.initialize()
111
+ # Initialize the pipette
112
+ machine.pipette.initialize()
126
113
 
127
- # Attach tip
128
- pipette.attach_tip()
114
+ # Load labware onto the deck
115
+ machine.load_deck({
116
+ "C1": "trash_bin",
117
+ "C2": "polyelectric_8_wellplate_30000ul",
118
+ "A3": "opentrons_96_tiprack_300ul",
119
+ })
129
120
 
130
- # Aspirate liquid
131
- pipette.aspirate(amount=50.0) # 50 µL
121
+ # Start video recording
122
+ machine.camera.start_video_recording()
132
123
 
133
- # Dispense liquid
134
- pipette.dispense(amount=50.0)
124
+ # Perform liquid handling operations
125
+ 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")
135
129
 
136
- # Eject tip
137
- pipette.eject_tip()
130
+ # Stop video recording
131
+ machine.camera.stop_video_recording()
138
132
 
139
- # Disconnect when done
140
- pipette.disconnect()
133
+ # Disconnect all devices
134
+ machine.disconnect()
141
135
  ```
142
136
 
143
- ### Combined Workflow
137
+ **Discovering Available Methods**: To explore what methods are available on any class instance, you can use Python's built-in `help()` function:
144
138
 
145
139
  ```python
146
- from puda_drivers.move import GCodeController
147
- from puda_drivers.transfer.liquid.sartorius import SartoriusController
148
-
149
- # Initialize both devices
150
- gantry = GCodeController(port_name="/dev/ttyACM0")
151
- pipette = SartoriusController(port_name="/dev/ttyUSB0")
152
-
153
- gantry.connect()
154
- pipette.connect()
155
-
156
- # Move to source well
157
- gantry.move_absolute(x=50.0, y=-50.0, z=-20.0)
158
- pipette.aspirate(amount=50.0)
159
-
160
- # Move to destination well
161
- gantry.move_absolute(x=150.0, y=-150.0, z=-20.0)
162
- pipette.dispense(amount=50.0)
163
-
164
- # Cleanup
165
- pipette.eject_tip()
166
- gantry.disconnect()
167
- pipette.disconnect()
140
+ machine = First()
141
+ help(machine) # See methods for the First machine
142
+ help(machine.qubot) # See GCodeController methods
143
+ help(machine.pipette) # See SartoriusController methods
144
+ help(machine.camera) # See CameraController methods
168
145
  ```
169
146
 
170
- ## Device Support
171
-
172
- ### Motion Systems
147
+ Alternatively, you can read the source code directly in the `src/puda_drivers/` directory.
173
148
 
174
- - **QuBot** (GCode) - Multi-axis gantry systems compatible with G-code commands
175
- - Supports X, Y, Z, and A axes
176
- - Configurable feed rates
177
- - Position synchronization and homing
178
- - Automatic axis limit validation for safe operation
149
+ ## Device Support
179
150
 
180
- ### Liquid Handling
151
+ The following device types are supported:
181
152
 
153
+ - **GCode** - G-code compatible motion systems (e.g., QuBot)
182
154
  - **Sartorius rLINE®** - Electronic pipettes and robotic dispensers
183
- - Aspirate and dispense operations
184
- - Tip attachment and ejection
185
- - Configurable speeds and volumes
155
+ - **Camera** - Webcams and USB cameras for image and video capture
186
156
 
187
- ## Error Handling
188
-
189
- ### Axis Limit Validation
190
-
191
- Both `move_absolute()` and `move_relative()` validate positions against configured axis limits before executing any movement. If a position is outside the limits, a `ValueError` is raised:
192
-
193
- ```python
194
- from puda_drivers.move import GCodeController
195
-
196
- gantry = GCodeController(port_name="/dev/ttyACM0")
197
- gantry.connect()
198
-
199
- # Set axis limits
200
- gantry.set_axis_limits("X", 0, 200)
201
- gantry.set_axis_limits("Y", -200, 0)
202
-
203
- try:
204
- # This will raise ValueError: Value 250 outside axis limits [0, 200]
205
- gantry.move_absolute(x=250.0, y=-50.0)
206
- except ValueError as e:
207
- print(f"Move rejected: {e}")
208
-
209
- # Relative moves are also validated after conversion to absolute positions
210
- try:
211
- # If current X is 150, moving 100 more would exceed the limit
212
- gantry.move_relative(x=100.0)
213
- except ValueError as e:
214
- print(f"Move rejected: {e}")
215
- ```
216
-
217
- Validation errors are automatically logged at the ERROR level before the exception is raised.
218
-
219
- ### Logging Best Practices
157
+ ## Logging Best Practices
220
158
 
221
159
  For production applications, configure logging at the start of your script:
222
160
 
223
161
  ```python
224
162
  import logging
225
163
  from puda_drivers.core.logging import setup_logging
226
- from puda_drivers.move import GCodeController
227
164
 
228
165
  # Configure logging first, before initializing devices
229
166
  setup_logging(
230
167
  enable_file_logging=True,
231
168
  log_level=logging.INFO,
232
- log_file_name="gantry_operation"
169
+ log_file_name="experiment"
233
170
  )
234
171
 
235
172
  # Now all device operations will be logged
236
- gantry = GCodeController(port_name="/dev/ttyACM0")
237
173
  # ... rest of your code
238
174
  ```
239
175
 
@@ -265,6 +201,8 @@ sartorius_ports = list_serial_ports(filter_desc="Sartorius")
265
201
 
266
202
  ### Setup Development Environment
267
203
 
204
+ First, install `uv` if you haven't already. See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/) for platform-specific instructions.
205
+
268
206
  ```bash
269
207
  # Create virtual environment
270
208
  uv venv
@@ -59,160 +59,96 @@ setup_logging(
59
59
 
60
60
  When file logging is enabled, logs are saved to timestamped files (unless a custom name is provided) in the `logs/` folder. The logs folder is created automatically if it doesn't exist.
61
61
 
62
- ### Gantry Control (GCode)
62
+ ### First Machine Example
63
63
 
64
- ```python
65
- from puda_drivers.move import GCodeController
66
-
67
- # Initialize and connect to a G-code device
68
- gantry = GCodeController(port_name="/dev/ttyACM0", feed=3000)
69
- gantry.connect()
70
-
71
- # Configure axis limits for safety (recommended)
72
- gantry.set_axis_limits("X", 0, 200)
73
- gantry.set_axis_limits("Y", -200, 0)
74
- gantry.set_axis_limits("Z", -100, 0)
75
- gantry.set_axis_limits("A", -180, 180)
76
-
77
- # Home the gantry
78
- gantry.home()
64
+ The `First` machine integrates motion control, deck management, liquid handling, and camera capabilities:
79
65
 
80
- # Move to absolute position (validated against limits)
81
- gantry.move_absolute(x=50.0, y=-100.0, z=-10.0)
82
-
83
- # Move relative to current position (validated after conversion to absolute)
84
- gantry.move_relative(x=20.0, y=-10.0)
85
-
86
- # Query current position
87
- position = gantry.query_position()
88
- print(f"Current position: {position}")
66
+ ```python
67
+ import logging
68
+ from puda_drivers.machines import First
69
+ from puda_drivers.core.logging import setup_logging
89
70
 
90
- # Disconnect when done
91
- gantry.disconnect()
92
- ```
71
+ # Configure logging
72
+ setup_logging(
73
+ enable_file_logging=False,
74
+ log_level=logging.DEBUG,
75
+ )
93
76
 
94
- **Axis Limits and Validation**: The `move_absolute()` and `move_relative()` methods automatically validate that target positions are within configured axis limits. If a position is outside the limits, a `ValueError` is raised before any movement is executed. Use `set_axis_limits()` to configure limits for each axis.
77
+ # Initialize the First machine
78
+ machine = First(
79
+ qubot_port="/dev/ttyACM0",
80
+ sartorius_port="/dev/ttyUSB0",
81
+ camera_index=0,
82
+ )
95
83
 
96
- ### Liquid Handling (Sartorius)
84
+ # Connect all devices
85
+ machine.connect()
97
86
 
98
- ```python
99
- from puda_drivers.transfer.liquid.sartorius import SartoriusController
87
+ # Home the gantry
88
+ machine.qubot.home()
100
89
 
101
- # Initialize and connect to pipette
102
- pipette = SartoriusController(port_name="/dev/ttyUSB0")
103
- pipette.connect()
104
- pipette.initialize()
90
+ # Initialize the pipette
91
+ machine.pipette.initialize()
105
92
 
106
- # Attach tip
107
- pipette.attach_tip()
93
+ # Load labware onto the deck
94
+ machine.load_deck({
95
+ "C1": "trash_bin",
96
+ "C2": "polyelectric_8_wellplate_30000ul",
97
+ "A3": "opentrons_96_tiprack_300ul",
98
+ })
108
99
 
109
- # Aspirate liquid
110
- pipette.aspirate(amount=50.0) # 50 µL
100
+ # Start video recording
101
+ machine.camera.start_video_recording()
111
102
 
112
- # Dispense liquid
113
- pipette.dispense(amount=50.0)
103
+ # Perform liquid handling operations
104
+ 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")
114
108
 
115
- # Eject tip
116
- pipette.eject_tip()
109
+ # Stop video recording
110
+ machine.camera.stop_video_recording()
117
111
 
118
- # Disconnect when done
119
- pipette.disconnect()
112
+ # Disconnect all devices
113
+ machine.disconnect()
120
114
  ```
121
115
 
122
- ### Combined Workflow
116
+ **Discovering Available Methods**: To explore what methods are available on any class instance, you can use Python's built-in `help()` function:
123
117
 
124
118
  ```python
125
- from puda_drivers.move import GCodeController
126
- from puda_drivers.transfer.liquid.sartorius import SartoriusController
127
-
128
- # Initialize both devices
129
- gantry = GCodeController(port_name="/dev/ttyACM0")
130
- pipette = SartoriusController(port_name="/dev/ttyUSB0")
131
-
132
- gantry.connect()
133
- pipette.connect()
134
-
135
- # Move to source well
136
- gantry.move_absolute(x=50.0, y=-50.0, z=-20.0)
137
- pipette.aspirate(amount=50.0)
138
-
139
- # Move to destination well
140
- gantry.move_absolute(x=150.0, y=-150.0, z=-20.0)
141
- pipette.dispense(amount=50.0)
142
-
143
- # Cleanup
144
- pipette.eject_tip()
145
- gantry.disconnect()
146
- pipette.disconnect()
119
+ machine = First()
120
+ help(machine) # See methods for the First machine
121
+ help(machine.qubot) # See GCodeController methods
122
+ help(machine.pipette) # See SartoriusController methods
123
+ help(machine.camera) # See CameraController methods
147
124
  ```
148
125
 
149
- ## Device Support
150
-
151
- ### Motion Systems
126
+ Alternatively, you can read the source code directly in the `src/puda_drivers/` directory.
152
127
 
153
- - **QuBot** (GCode) - Multi-axis gantry systems compatible with G-code commands
154
- - Supports X, Y, Z, and A axes
155
- - Configurable feed rates
156
- - Position synchronization and homing
157
- - Automatic axis limit validation for safe operation
128
+ ## Device Support
158
129
 
159
- ### Liquid Handling
130
+ The following device types are supported:
160
131
 
132
+ - **GCode** - G-code compatible motion systems (e.g., QuBot)
161
133
  - **Sartorius rLINE®** - Electronic pipettes and robotic dispensers
162
- - Aspirate and dispense operations
163
- - Tip attachment and ejection
164
- - Configurable speeds and volumes
134
+ - **Camera** - Webcams and USB cameras for image and video capture
165
135
 
166
- ## Error Handling
167
-
168
- ### Axis Limit Validation
169
-
170
- Both `move_absolute()` and `move_relative()` validate positions against configured axis limits before executing any movement. If a position is outside the limits, a `ValueError` is raised:
171
-
172
- ```python
173
- from puda_drivers.move import GCodeController
174
-
175
- gantry = GCodeController(port_name="/dev/ttyACM0")
176
- gantry.connect()
177
-
178
- # Set axis limits
179
- gantry.set_axis_limits("X", 0, 200)
180
- gantry.set_axis_limits("Y", -200, 0)
181
-
182
- try:
183
- # This will raise ValueError: Value 250 outside axis limits [0, 200]
184
- gantry.move_absolute(x=250.0, y=-50.0)
185
- except ValueError as e:
186
- print(f"Move rejected: {e}")
187
-
188
- # Relative moves are also validated after conversion to absolute positions
189
- try:
190
- # If current X is 150, moving 100 more would exceed the limit
191
- gantry.move_relative(x=100.0)
192
- except ValueError as e:
193
- print(f"Move rejected: {e}")
194
- ```
195
-
196
- Validation errors are automatically logged at the ERROR level before the exception is raised.
197
-
198
- ### Logging Best Practices
136
+ ## Logging Best Practices
199
137
 
200
138
  For production applications, configure logging at the start of your script:
201
139
 
202
140
  ```python
203
141
  import logging
204
142
  from puda_drivers.core.logging import setup_logging
205
- from puda_drivers.move import GCodeController
206
143
 
207
144
  # Configure logging first, before initializing devices
208
145
  setup_logging(
209
146
  enable_file_logging=True,
210
147
  log_level=logging.INFO,
211
- log_file_name="gantry_operation"
148
+ log_file_name="experiment"
212
149
  )
213
150
 
214
151
  # Now all device operations will be logged
215
- gantry = GCodeController(port_name="/dev/ttyACM0")
216
152
  # ... rest of your code
217
153
  ```
218
154
 
@@ -244,6 +180,8 @@ sartorius_ports = list_serial_ports(filter_desc="Sartorius")
244
180
 
245
181
  ### Setup Development Environment
246
182
 
183
+ First, install `uv` if you haven't already. See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/) for platform-specific instructions.
184
+
247
185
  ```bash
248
186
  # Create virtual environment
249
187
  uv venv
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "puda-drivers"
3
- version = "0.0.9"
3
+ version = "0.0.10"
4
4
  description = "Hardware drivers for the PUDA platform."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -7,6 +7,7 @@ This class demonstrates the integration of:
7
7
  - SartoriusController: Handles liquid handling operations
8
8
  """
9
9
 
10
+ import logging
10
11
  import time
11
12
  from typing import Optional, Dict, Tuple, Type, Union
12
13
  from puda_drivers.move import GCodeController, Deck
@@ -108,21 +109,36 @@ class First:
108
109
  camera_index=camera_index if camera_index is not None else self.DEFAULT_CAMERA_INDEX,
109
110
  )
110
111
 
112
+ # Initialize logger
113
+ self._logger = logging.getLogger(__name__)
114
+ self._logger.info(
115
+ "First machine initialized with qubot_port='%s', sartorius_port='%s', camera_index=%s",
116
+ qubot_port or self.DEFAULT_QUBOT_PORT,
117
+ sartorius_port or self.DEFAULT_SARTORIUS_PORT,
118
+ camera_index if camera_index is not None else self.DEFAULT_CAMERA_INDEX,
119
+ )
120
+
111
121
  def connect(self):
112
122
  """Connect all controllers."""
123
+ self._logger.info("Connecting all controllers")
113
124
  self.qubot.connect()
114
125
  self.pipette.connect()
115
126
  self.camera.connect()
127
+ self._logger.info("All controllers connected successfully")
116
128
 
117
129
  def disconnect(self):
118
130
  """Disconnect all controllers."""
131
+ self._logger.info("Disconnecting all controllers")
119
132
  self.qubot.disconnect()
120
133
  self.pipette.disconnect()
121
134
  self.camera.disconnect()
135
+ self._logger.info("All controllers disconnected successfully")
122
136
 
123
137
  def load_labware(self, slot: str, labware_name: str):
124
138
  """Load a labware object into a slot."""
139
+ self._logger.info("Loading labware '%s' into slot '%s'", labware_name, slot)
125
140
  self.deck.load_labware(slot=slot, labware_name=labware_name)
141
+ self._logger.debug("Labware '%s' loaded into slot '%s'", labware_name, slot)
126
142
 
127
143
  def load_deck(self, deck_layout: Dict[str, Type[StandardLabware]]):
128
144
  """
@@ -139,63 +155,84 @@ class First:
139
155
  "C1": Rubbish,
140
156
  })
141
157
  """
158
+ self._logger.info("Loading deck layout with %d labware items", len(deck_layout))
142
159
  for slot, labware_name in deck_layout.items():
143
160
  self.load_labware(slot=slot, labware_name=labware_name)
161
+ self._logger.info("Deck layout loaded successfully")
144
162
 
145
163
  def attach_tip(self, slot: str, well: Optional[str] = None):
146
164
  """Attach a tip from a slot."""
147
165
  if self.pipette.is_tip_attached():
166
+ self._logger.error("Cannot attach tip: tip already attached")
148
167
  raise ValueError("Tip already attached")
149
168
 
169
+ self._logger.info("Attaching tip from slot '%s'%s", slot, f", well '{well}'" if well else "")
150
170
  pos = self.get_absolute_z_position(slot, well)
171
+ self._logger.debug("Moving to position %s for tip attachment", pos)
151
172
  # return the offset from the origin
152
173
  self.qubot.move_absolute(position=pos)
153
174
 
154
175
  # attach tip (move slowly down)
176
+ insert_depth = self.deck[slot].get_insert_depth()
177
+ self._logger.debug("Moving down by %s mm to insert tip", insert_depth)
155
178
  self.qubot.move_relative(
156
- position=Position(z=-self.deck[slot].get_insert_depth()),
179
+ position=Position(z=-insert_depth),
157
180
  feed=500
158
181
  )
159
182
  self.pipette.set_tip_attached(attached=True)
183
+ self._logger.info("Tip attached successfully, homing Z axis")
160
184
  # must home Z axis after, as pressing in tip might cause it to lose steps
161
185
  self.qubot.home(axis="Z")
186
+ self._logger.debug("Z axis homed after tip attachment")
162
187
 
163
188
  def drop_tip(self, slot: str, well: str):
164
189
  """Drop a tip into a slot."""
165
190
  if not self.pipette.is_tip_attached():
191
+ self._logger.error("Cannot drop tip: no tip attached")
166
192
  raise ValueError("Tip not attached")
167
193
 
194
+ self._logger.info("Dropping tip into slot '%s', well '%s'", slot, well)
168
195
  pos = self.get_absolute_z_position(slot, well)
169
196
  # move up by the tip length
170
197
  pos += Position(z=self.TIP_LENGTH)
171
- print("moving Z to", pos)
198
+ self._logger.debug("Moving to position %s (adjusted for tip length) for tip drop", pos)
172
199
  self.qubot.move_absolute(position=pos)
173
200
 
201
+ self._logger.debug("Ejecting tip")
174
202
  self.pipette.eject_tip()
175
203
  time.sleep(5)
176
204
  self.pipette.set_tip_attached(attached=False)
205
+ self._logger.info("Tip dropped successfully")
177
206
 
178
207
  def aspirate_from(self, slot:str, well:str, amount:int):
179
208
  """Aspirate a volume of liquid from a slot."""
180
209
  if not self.pipette.is_tip_attached():
210
+ self._logger.error("Cannot aspirate: no tip attached")
181
211
  raise ValueError("Tip not attached")
182
212
 
213
+ self._logger.info("Aspirating %d µL from slot '%s', well '%s'", amount, slot, well)
183
214
  pos = self.get_absolute_z_position(slot, well)
184
- print("moving Z to", pos)
215
+ self._logger.debug("Moving Z axis to position %s", pos)
185
216
  self.qubot.move_absolute(position=pos)
217
+ self._logger.debug("Aspirating %d µL", amount)
186
218
  self.pipette.aspirate(amount=amount)
187
219
  time.sleep(5)
220
+ self._logger.info("Aspiration completed: %d µL from slot '%s', well '%s'", amount, slot, well)
188
221
 
189
222
  def dispense_to(self, slot:str, well:str, amount:int):
190
223
  """Dispense a volume of liquid to a slot."""
191
224
  if not self.pipette.is_tip_attached():
225
+ self._logger.error("Cannot dispense: no tip attached")
192
226
  raise ValueError("Tip not attached")
193
227
 
228
+ self._logger.info("Dispensing %d µL to slot '%s', well '%s'", amount, slot, well)
194
229
  pos = self.get_absolute_z_position(slot, well)
195
- print("moving Z to", pos)
230
+ self._logger.debug("Moving Z axis to position %s", pos)
196
231
  self.qubot.move_absolute(position=pos)
232
+ self._logger.debug("Dispensing %d µL", amount)
197
233
  self.pipette.dispense(amount=amount)
198
234
  time.sleep(5)
235
+ self._logger.info("Dispense completed: %d µL to slot '%s', well '%s'", amount, slot, well)
199
236
 
200
237
  def get_slot_origin(self, slot: str) -> Position:
201
238
  """
@@ -212,8 +249,11 @@ class First:
212
249
  """
213
250
  slot = slot.upper()
214
251
  if slot not in self.SLOT_ORIGINS:
252
+ self._logger.error("Invalid slot name: '%s'. Must be one of %s", slot, list(self.SLOT_ORIGINS.keys()))
215
253
  raise KeyError(f"Invalid slot name: {slot}. Must be one of {list(self.SLOT_ORIGINS.keys())}")
216
- return self.SLOT_ORIGINS[slot]
254
+ pos = self.SLOT_ORIGINS[slot]
255
+ self._logger.debug("Slot origin for '%s': %s", slot, pos)
256
+ return pos
217
257
 
218
258
  def get_absolute_z_position(self, slot: str, well: Optional[str] = None) -> Position:
219
259
  """
@@ -237,6 +277,9 @@ class First:
237
277
  # get z
238
278
  z = Position(z=self.deck[slot].get_height() - self.CEILING_HEIGHT)
239
279
  pos += z
280
+ self._logger.debug("Absolute Z position for slot '%s', well '%s': %s", slot, well, pos)
281
+ else:
282
+ self._logger.debug("Absolute Z position for slot '%s': %s", slot, pos)
240
283
  return pos
241
284
 
242
285
  def get_absolute_a_position(self, slot: str, well: Optional[str] = None) -> Position:
@@ -252,4 +295,7 @@ class First:
252
295
  # get a
253
296
  a = Position(a=self.deck[slot].get_height() - self.CEILING_HEIGHT)
254
297
  pos += a
298
+ self._logger.debug("Absolute A position for slot '%s', well '%s': %s", slot, well, pos)
299
+ else:
300
+ self._logger.debug("Absolute A position for slot '%s': %s", slot, pos)
255
301
  return pos
@@ -186,7 +186,7 @@ wheels = [
186
186
 
187
187
  [[package]]
188
188
  name = "puda-drivers"
189
- version = "0.0.9"
189
+ version = "0.0.10"
190
190
  source = { editable = "." }
191
191
  dependencies = [
192
192
  { name = "opencv-python" },
File without changes
File without changes