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.
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/PKG-INFO +56 -118
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/README.md +55 -117
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/pyproject.toml +1 -1
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/machines/first.py +51 -5
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/uv.lock +1 -1
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/.gitignore +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/LICENSE +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/core/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/core/logging.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/core/position.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/core/serialcontroller.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/cv/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/cv/camera.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/labware/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/labware/labware.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/labware/opentrons_96_tiprack_300ul.json +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/labware/polyelectric_8_wellplate_30000ul.json +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/labware/trash_bin.json +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/machines/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/move/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/move/deck.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/move/gcode.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/move/grbl/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/move/grbl/api.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/move/grbl/constants.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/py.typed +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/transfer/liquid/sartorius/__init__.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/transfer/liquid/sartorius/api.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/transfer/liquid/sartorius/constants.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/transfer/liquid/sartorius/sartorius.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/tests/example.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/tests/first.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/tests/pipette.py +0 -0
- {puda_drivers-0.0.9 → puda_drivers-0.0.10}/tests/qubot.py +0 -0
- {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.
|
|
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
|
-
###
|
|
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)
|
|
127
|
+
machine.dispense_to(slot="C2", well="B4", amount=100)
|
|
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)
|
|
106
|
+
machine.dispense_to(slot="C2", well="B4", amount=100)
|
|
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,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=-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.10}/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
|
|
File without changes
|
|
File without changes
|
{puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/transfer/liquid/sartorius/__init__.py
RENAMED
|
File without changes
|
{puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/transfer/liquid/sartorius/api.py
RENAMED
|
File without changes
|
{puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/transfer/liquid/sartorius/constants.py
RENAMED
|
File without changes
|
{puda_drivers-0.0.9 → puda_drivers-0.0.10}/src/puda_drivers/transfer/liquid/sartorius/sartorius.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|