puda-drivers 0.0.9__tar.gz → 0.0.11__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.
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/PKG-INFO +56 -118
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/README.md +55 -117
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/pyproject.toml +1 -1
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/labware/polyelectric_8_wellplate_30000ul.json +1 -1
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/labware/trash_bin.json +1 -1
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/machines/first.py +110 -13
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/tests/first.py +2 -2
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/uv.lock +1 -1
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/.gitignore +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/LICENSE +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/core/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/core/logging.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/core/position.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/core/serialcontroller.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/cv/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/cv/camera.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/labware/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/labware/labware.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/labware/opentrons_96_tiprack_300ul.json +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/machines/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/move/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/move/deck.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/move/gcode.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/move/grbl/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/move/grbl/api.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/move/grbl/constants.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/py.typed +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/transfer/liquid/sartorius/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/transfer/liquid/sartorius/api.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/transfer/liquid/sartorius/constants.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/transfer/liquid/sartorius/sartorius.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/tests/example.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/tests/pipette.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/tests/qubot.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.11}/tests/webcam.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: puda-drivers
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.11
|
|
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
|
-
###
|
|
83
|
+
### First Machine Example
|
|
84
84
|
|
|
85
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
92
|
+
# Configure logging
|
|
93
|
+
setup_logging(
|
|
94
|
+
enable_file_logging=False,
|
|
95
|
+
log_level=logging.DEBUG,
|
|
96
|
+
)
|
|
114
97
|
|
|
115
|
-
|
|
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
|
-
|
|
105
|
+
# Connect all devices
|
|
106
|
+
machine.connect()
|
|
118
107
|
|
|
119
|
-
|
|
120
|
-
|
|
108
|
+
# Home the gantry
|
|
109
|
+
machine.qubot.home()
|
|
121
110
|
|
|
122
|
-
# Initialize
|
|
123
|
-
pipette
|
|
124
|
-
pipette.connect()
|
|
125
|
-
pipette.initialize()
|
|
111
|
+
# Initialize the pipette
|
|
112
|
+
machine.pipette.initialize()
|
|
126
113
|
|
|
127
|
-
#
|
|
128
|
-
|
|
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
|
-
#
|
|
131
|
-
|
|
121
|
+
# Start video recording
|
|
122
|
+
machine.camera.start_video_recording()
|
|
132
123
|
|
|
133
|
-
#
|
|
134
|
-
|
|
124
|
+
# Perform liquid handling operations
|
|
125
|
+
machine.attach_tip(slot="A3", well="G8")
|
|
126
|
+
machine.aspirate_from(slot="C2", well="A1", amount=100, height_from_bottom=10.0)
|
|
127
|
+
machine.dispense_to(slot="C2", well="B4", amount=100, height_from_bottom=30.0)
|
|
128
|
+
machine.drop_tip(slot="C1", well="A1")
|
|
135
129
|
|
|
136
|
-
#
|
|
137
|
-
|
|
130
|
+
# Stop video recording
|
|
131
|
+
machine.camera.stop_video_recording()
|
|
138
132
|
|
|
139
|
-
# Disconnect
|
|
140
|
-
|
|
133
|
+
# Disconnect all devices
|
|
134
|
+
machine.disconnect()
|
|
141
135
|
```
|
|
142
136
|
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
#
|
|
150
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
### Motion Systems
|
|
147
|
+
Alternatively, you can read the source code directly in the `src/puda_drivers/` directory.
|
|
173
148
|
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
- Tip attachment and ejection
|
|
185
|
-
- Configurable speeds and volumes
|
|
155
|
+
- **Camera** - Webcams and USB cameras for image and video capture
|
|
186
156
|
|
|
187
|
-
##
|
|
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="
|
|
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
|
-
###
|
|
62
|
+
### First Machine Example
|
|
63
63
|
|
|
64
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
71
|
+
# Configure logging
|
|
72
|
+
setup_logging(
|
|
73
|
+
enable_file_logging=False,
|
|
74
|
+
log_level=logging.DEBUG,
|
|
75
|
+
)
|
|
93
76
|
|
|
94
|
-
|
|
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
|
-
|
|
84
|
+
# Connect all devices
|
|
85
|
+
machine.connect()
|
|
97
86
|
|
|
98
|
-
|
|
99
|
-
|
|
87
|
+
# Home the gantry
|
|
88
|
+
machine.qubot.home()
|
|
100
89
|
|
|
101
|
-
# Initialize
|
|
102
|
-
pipette
|
|
103
|
-
pipette.connect()
|
|
104
|
-
pipette.initialize()
|
|
90
|
+
# Initialize the pipette
|
|
91
|
+
machine.pipette.initialize()
|
|
105
92
|
|
|
106
|
-
#
|
|
107
|
-
|
|
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
|
-
#
|
|
110
|
-
|
|
100
|
+
# Start video recording
|
|
101
|
+
machine.camera.start_video_recording()
|
|
111
102
|
|
|
112
|
-
#
|
|
113
|
-
|
|
103
|
+
# Perform liquid handling operations
|
|
104
|
+
machine.attach_tip(slot="A3", well="G8")
|
|
105
|
+
machine.aspirate_from(slot="C2", well="A1", amount=100, height_from_bottom=10.0)
|
|
106
|
+
machine.dispense_to(slot="C2", well="B4", amount=100, height_from_bottom=30.0)
|
|
107
|
+
machine.drop_tip(slot="C1", well="A1")
|
|
114
108
|
|
|
115
|
-
#
|
|
116
|
-
|
|
109
|
+
# Stop video recording
|
|
110
|
+
machine.camera.stop_video_recording()
|
|
117
111
|
|
|
118
|
-
# Disconnect
|
|
119
|
-
|
|
112
|
+
# Disconnect all devices
|
|
113
|
+
machine.disconnect()
|
|
120
114
|
```
|
|
121
115
|
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
#
|
|
129
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
### Motion Systems
|
|
126
|
+
Alternatively, you can read the source code directly in the `src/puda_drivers/` directory.
|
|
152
127
|
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
- Tip attachment and ejection
|
|
164
|
-
- Configurable speeds and volumes
|
|
134
|
+
- **Camera** - Webcams and USB cameras for image and video capture
|
|
165
135
|
|
|
166
|
-
##
|
|
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="
|
|
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
|
|
@@ -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,136 @@ 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=-
|
|
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")
|
|
187
|
+
|
|
188
|
+
def drop_tip(self, slot: str, well: str, height_from_bottom: float = 0.0):
|
|
189
|
+
"""
|
|
190
|
+
Drop a tip into a slot.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
slot: Slot name (e.g., 'A1', 'B2')
|
|
194
|
+
well: Well name within the slot (e.g., 'A1' for a well in a tiprack)
|
|
195
|
+
height_from_bottom: Height from the bottom of the well in mm. Defaults to 0.0.
|
|
196
|
+
Positive values move up from the bottom. Negative values
|
|
197
|
+
may cause a ValueError if the resulting position is outside
|
|
198
|
+
the Z axis limits.
|
|
162
199
|
|
|
163
|
-
|
|
164
|
-
|
|
200
|
+
Raises:
|
|
201
|
+
ValueError: If no tip is attached, or if the resulting position is outside
|
|
202
|
+
the Z axis limits.
|
|
203
|
+
"""
|
|
165
204
|
if not self.pipette.is_tip_attached():
|
|
205
|
+
self._logger.error("Cannot drop tip: no tip attached")
|
|
166
206
|
raise ValueError("Tip not attached")
|
|
167
207
|
|
|
208
|
+
self._logger.info("Dropping tip into slot '%s', well '%s'", slot, well)
|
|
168
209
|
pos = self.get_absolute_z_position(slot, well)
|
|
210
|
+
# add height from bottom
|
|
211
|
+
pos += Position(z=height_from_bottom)
|
|
169
212
|
# move up by the tip length
|
|
170
213
|
pos += Position(z=self.TIP_LENGTH)
|
|
171
|
-
|
|
214
|
+
self._logger.debug("Moving to position %s (adjusted for tip length) for tip drop", pos)
|
|
172
215
|
self.qubot.move_absolute(position=pos)
|
|
173
216
|
|
|
217
|
+
self._logger.debug("Ejecting tip")
|
|
174
218
|
self.pipette.eject_tip()
|
|
175
219
|
time.sleep(5)
|
|
176
220
|
self.pipette.set_tip_attached(attached=False)
|
|
221
|
+
self._logger.info("Tip dropped successfully")
|
|
222
|
+
|
|
223
|
+
def aspirate_from(self, slot:str, well:str, amount:int, height_from_bottom: float = 0.0):
|
|
224
|
+
"""
|
|
225
|
+
Aspirate a volume of liquid from a slot.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
slot: Slot name (e.g., 'A1', 'B2')
|
|
229
|
+
well: Well name within the slot (e.g., 'A1')
|
|
230
|
+
amount: Volume to aspirate in µL
|
|
231
|
+
height_from_bottom: Height from the bottom of the well in mm. Defaults to 0.0.
|
|
232
|
+
Positive values move up from the bottom. Negative values
|
|
233
|
+
may cause a ValueError if the resulting position is outside
|
|
234
|
+
the Z axis limits.
|
|
177
235
|
|
|
178
|
-
|
|
179
|
-
|
|
236
|
+
Raises:
|
|
237
|
+
ValueError: If no tip is attached, or if the resulting position is outside
|
|
238
|
+
the Z axis limits.
|
|
239
|
+
"""
|
|
180
240
|
if not self.pipette.is_tip_attached():
|
|
241
|
+
self._logger.error("Cannot aspirate: no tip attached")
|
|
181
242
|
raise ValueError("Tip not attached")
|
|
182
243
|
|
|
244
|
+
self._logger.info("Aspirating %d µL from slot '%s', well '%s'", amount, slot, well)
|
|
183
245
|
pos = self.get_absolute_z_position(slot, well)
|
|
184
|
-
|
|
246
|
+
# add height from bottom
|
|
247
|
+
pos += Position(z=height_from_bottom)
|
|
248
|
+
self._logger.debug("Moving Z axis to position %s", pos)
|
|
185
249
|
self.qubot.move_absolute(position=pos)
|
|
250
|
+
|
|
251
|
+
self._logger.debug("Aspirating %d µL", amount)
|
|
186
252
|
self.pipette.aspirate(amount=amount)
|
|
187
253
|
time.sleep(5)
|
|
254
|
+
self._logger.info("Aspiration completed: %d µL from slot '%s', well '%s'", amount, slot, well)
|
|
188
255
|
|
|
189
|
-
def dispense_to(self, slot:str, well:str, amount:int):
|
|
190
|
-
"""
|
|
256
|
+
def dispense_to(self, slot:str, well:str, amount:int, height_from_bottom: float = 0.0):
|
|
257
|
+
"""
|
|
258
|
+
Dispense a volume of liquid to a slot.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
slot: Slot name (e.g., 'A1', 'B2')
|
|
262
|
+
well: Well name within the slot (e.g., 'A1')
|
|
263
|
+
amount: Volume to dispense in µL
|
|
264
|
+
height_from_bottom: Height from the bottom of the well in mm. Defaults to 0.0.
|
|
265
|
+
Positive values move up from the bottom. Negative values
|
|
266
|
+
may cause a ValueError if the resulting position is outside
|
|
267
|
+
the Z axis limits.
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
ValueError: If no tip is attached, or if the resulting position is outside
|
|
271
|
+
the Z axis limits.
|
|
272
|
+
"""
|
|
191
273
|
if not self.pipette.is_tip_attached():
|
|
274
|
+
self._logger.error("Cannot dispense: no tip attached")
|
|
192
275
|
raise ValueError("Tip not attached")
|
|
193
276
|
|
|
277
|
+
self._logger.info("Dispensing %d µL to slot '%s', well '%s'", amount, slot, well)
|
|
194
278
|
pos = self.get_absolute_z_position(slot, well)
|
|
195
|
-
|
|
279
|
+
# add height from bottom
|
|
280
|
+
pos += Position(z=height_from_bottom)
|
|
281
|
+
self._logger.debug("Moving Z axis to position %s", pos)
|
|
196
282
|
self.qubot.move_absolute(position=pos)
|
|
283
|
+
|
|
284
|
+
self._logger.debug("Dispensing %d µL", amount)
|
|
197
285
|
self.pipette.dispense(amount=amount)
|
|
198
286
|
time.sleep(5)
|
|
287
|
+
self._logger.info("Dispense completed: %d µL to slot '%s', well '%s'", amount, slot, well)
|
|
199
288
|
|
|
200
289
|
def get_slot_origin(self, slot: str) -> Position:
|
|
201
290
|
"""
|
|
@@ -212,8 +301,11 @@ class First:
|
|
|
212
301
|
"""
|
|
213
302
|
slot = slot.upper()
|
|
214
303
|
if slot not in self.SLOT_ORIGINS:
|
|
304
|
+
self._logger.error("Invalid slot name: '%s'. Must be one of %s", slot, list(self.SLOT_ORIGINS.keys()))
|
|
215
305
|
raise KeyError(f"Invalid slot name: {slot}. Must be one of {list(self.SLOT_ORIGINS.keys())}")
|
|
216
|
-
|
|
306
|
+
pos = self.SLOT_ORIGINS[slot]
|
|
307
|
+
self._logger.debug("Slot origin for '%s': %s", slot, pos)
|
|
308
|
+
return pos
|
|
217
309
|
|
|
218
310
|
def get_absolute_z_position(self, slot: str, well: Optional[str] = None) -> Position:
|
|
219
311
|
"""
|
|
@@ -235,8 +327,10 @@ class First:
|
|
|
235
327
|
# the deck is rotated 90 degrees clockwise for this machine
|
|
236
328
|
pos += well_pos.swap_xy()
|
|
237
329
|
# get z
|
|
238
|
-
|
|
239
|
-
|
|
330
|
+
pos += Position(z=self.deck[slot].get_height() - self.CEILING_HEIGHT)
|
|
331
|
+
self._logger.debug("Absolute Z position for slot '%s', well '%s': %s", slot, well, pos)
|
|
332
|
+
else:
|
|
333
|
+
self._logger.debug("Absolute Z position for slot '%s': %s", slot, pos)
|
|
240
334
|
return pos
|
|
241
335
|
|
|
242
336
|
def get_absolute_a_position(self, slot: str, well: Optional[str] = None) -> Position:
|
|
@@ -252,4 +346,7 @@ class First:
|
|
|
252
346
|
# get a
|
|
253
347
|
a = Position(a=self.deck[slot].get_height() - self.CEILING_HEIGHT)
|
|
254
348
|
pos += a
|
|
349
|
+
self._logger.debug("Absolute A position for slot '%s', well '%s': %s", slot, well, pos)
|
|
350
|
+
else:
|
|
351
|
+
self._logger.debug("Absolute A position for slot '%s': %s", slot, pos)
|
|
255
352
|
return pos
|
|
@@ -40,8 +40,8 @@ if __name__ == "__main__":
|
|
|
40
40
|
|
|
41
41
|
machine.camera.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)
|
|
43
|
+
machine.aspirate_from(slot="C2", well="A1", amount=100, height_from_bottom=10)
|
|
44
|
+
machine.dispense_to(slot="C2", well="B4", amount=100, height_from_bottom=50)
|
|
45
45
|
machine.drop_tip(slot="C1", well="A1")
|
|
46
46
|
|
|
47
47
|
# tiprack_wells = machine.deck["A3"].wells
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/labware/opentrons_96_tiprack_300ul.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/transfer/liquid/sartorius/__init__.py
RENAMED
|
File without changes
|
{puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/transfer/liquid/sartorius/api.py
RENAMED
|
File without changes
|
{puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/transfer/liquid/sartorius/constants.py
RENAMED
|
File without changes
|
{puda_drivers-0.0.9 → puda_drivers-0.0.11}/src/puda_drivers/transfer/liquid/sartorius/sartorius.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|