puda-drivers 0.0.18__py3-none-any.whl → 0.0.19__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- puda_drivers/labware/MEA_cell_MTP.json +56 -0
- puda_drivers/labware/labware.py +44 -18
- puda_drivers/labware/polyelectric_8_wellplate_30000ul.json +2 -1
- puda_drivers/labware/trash_bin.json +2 -1
- puda_drivers/machines/first.py +230 -165
- puda_drivers/measure/electrode.py +0 -0
- puda_drivers/move/gcode.py +14 -23
- {puda_drivers-0.0.18.dist-info → puda_drivers-0.0.19.dist-info}/METADATA +1 -1
- {puda_drivers-0.0.18.dist-info → puda_drivers-0.0.19.dist-info}/RECORD +11 -9
- {puda_drivers-0.0.18.dist-info → puda_drivers-0.0.19.dist-info}/WHEEL +0 -0
- {puda_drivers-0.0.18.dist-info → puda_drivers-0.0.19.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
|
|
3
|
+
"ordering": [
|
|
4
|
+
[
|
|
5
|
+
"A1"
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
],
|
|
9
|
+
"brand": {
|
|
10
|
+
"brand": "Custom",
|
|
11
|
+
"brandId": []
|
|
12
|
+
},
|
|
13
|
+
"metadata": {
|
|
14
|
+
"displayName": "MEA cell MTP",
|
|
15
|
+
"displayCategory": "functionalLabware",
|
|
16
|
+
"tags": []
|
|
17
|
+
},
|
|
18
|
+
"dimensions": {
|
|
19
|
+
"xDimension": 127,
|
|
20
|
+
"yDimension": 85,
|
|
21
|
+
"zDimension": 28
|
|
22
|
+
},
|
|
23
|
+
"wells": {
|
|
24
|
+
"A1": {
|
|
25
|
+
"shape": "square",
|
|
26
|
+
"x": 63.5,
|
|
27
|
+
"y": 42.5,
|
|
28
|
+
"z": 25
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"groups": [
|
|
32
|
+
{
|
|
33
|
+
"metadata": {
|
|
34
|
+
"wellBottomShape": "flat"
|
|
35
|
+
},
|
|
36
|
+
"wells": [
|
|
37
|
+
"A1"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"parameters": {
|
|
42
|
+
"format": "irregular",
|
|
43
|
+
"quirks": [],
|
|
44
|
+
"isTiprack": false,
|
|
45
|
+
"isMagneticModuleCompatible": false,
|
|
46
|
+
"loadName": "MEA_cell_MTP"
|
|
47
|
+
},
|
|
48
|
+
"namespace": "custom_beta",
|
|
49
|
+
"version": 1,
|
|
50
|
+
"schemaVersion": 2,
|
|
51
|
+
"cornerOffsetFromSlot": {
|
|
52
|
+
"x": 0,
|
|
53
|
+
"y": 0,
|
|
54
|
+
"z": 0
|
|
55
|
+
}
|
|
56
|
+
}
|
puda_drivers/labware/labware.py
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import inspect
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Dict, Any, List
|
|
6
|
+
from typing import Dict, Any, List, Optional
|
|
7
7
|
from abc import ABC
|
|
8
8
|
from puda_drivers.core import Position
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class StandardLabware(ABC):
|
|
12
12
|
"""
|
|
13
|
-
Generic Parent Class for all Labware on a microplate
|
|
13
|
+
Generic Parent Class for all Labware on a microplate
|
|
14
14
|
"""
|
|
15
15
|
def __init__(self, labware_name: str):
|
|
16
16
|
"""
|
|
@@ -63,7 +63,7 @@ class StandardLabware(ABC):
|
|
|
63
63
|
|
|
64
64
|
with open(definition_path, "r", encoding="utf-8") as f:
|
|
65
65
|
return json.load(f)
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
def __str__(self):
|
|
68
68
|
"""
|
|
69
69
|
Return a string representation of the labware.
|
|
@@ -92,7 +92,7 @@ class StandardLabware(ABC):
|
|
|
92
92
|
List of well identifiers (e.g., ["A1", "A2", "B1", ...])
|
|
93
93
|
"""
|
|
94
94
|
return list(self._wells.keys())
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
def get_well_position(self, well_id: str) -> Position:
|
|
97
97
|
"""
|
|
98
98
|
Get the position of a well from definition.json.
|
|
@@ -110,39 +110,65 @@ class StandardLabware(ABC):
|
|
|
110
110
|
well_id_upper = well_id.upper()
|
|
111
111
|
if well_id_upper not in self._wells:
|
|
112
112
|
raise KeyError(f"Well '{well_id}' not found in tip rack definition")
|
|
113
|
-
|
|
113
|
+
|
|
114
114
|
# Get the well data from the definition
|
|
115
115
|
well_data = self._wells.get(well_id_upper, {})
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
# Return position of the well (x, y are already center coordinates)
|
|
118
118
|
return Position(
|
|
119
119
|
x=well_data.get("x", 0.0),
|
|
120
120
|
y=well_data.get("y", 0.0),
|
|
121
|
-
z=well_data.get("z", 0.0),
|
|
121
|
+
z=well_data.get("z", 0.0),
|
|
122
122
|
)
|
|
123
|
-
|
|
124
|
-
def get_height(self) -> float:
|
|
123
|
+
|
|
124
|
+
def get_height(self, well_name: Optional[str] = None) -> float:
|
|
125
125
|
"""
|
|
126
126
|
Get the height of the labware.
|
|
127
|
-
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
well_name: Optional well name within the labware (e.g., "A1" for a well in a tiprack)
|
|
130
|
+
If not provided, the height of the labware is returned (zDimension).
|
|
128
131
|
Returns:
|
|
129
|
-
Height of the labware (zDimension)
|
|
132
|
+
Height of the labware (zDimension) or the height of the well (z)
|
|
130
133
|
|
|
131
134
|
Raises:
|
|
132
|
-
KeyError: If dimensions or zDimension is not found in the definition
|
|
135
|
+
KeyError: If dimensions or zDimension is not found in the definition, or if well_name is not found in the labware
|
|
133
136
|
"""
|
|
134
137
|
dimensions = self._definition.get("dimensions")
|
|
135
138
|
if dimensions is None:
|
|
136
139
|
raise KeyError("'dimensions' not found in labware definition")
|
|
137
140
|
|
|
138
|
-
if
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
if well_name:
|
|
142
|
+
well_data = self._wells.get(well_name.upper(), {})
|
|
143
|
+
if well_data is None:
|
|
144
|
+
raise KeyError(f"Well '{well_name}' not found in labware definition")
|
|
145
|
+
return well_data.get("z")
|
|
146
|
+
else:
|
|
147
|
+
if "zDimension" not in dimensions:
|
|
148
|
+
raise KeyError("'zDimension' not found in labware dimensions")
|
|
149
|
+
return dimensions["zDimension"]
|
|
150
|
+
|
|
143
151
|
def get_insert_depth(self) -> float:
|
|
144
152
|
"""
|
|
145
|
-
Get the insert depth of the labware.
|
|
153
|
+
Get the insert depth of the labware. This should be added to all labware definitions.
|
|
154
|
+
|
|
155
|
+
insert_depth represents the maximum internal Z-axis clearance of the
|
|
156
|
+
labware. It is calculated as the absolute difference between the Top
|
|
157
|
+
Plane (the highest point of the well opening) and the Internal Floor
|
|
158
|
+
(the physical bottom of the well).
|
|
159
|
+
|
|
160
|
+
Top of Labware (Z = 0 reference)
|
|
161
|
+
│ │
|
|
162
|
+
┌─────┴──────┴─────┐ <─── Opening
|
|
163
|
+
│ │
|
|
164
|
+
│ SPACE │ }
|
|
165
|
+
│ AVAILABLE │ }
|
|
166
|
+
│ FOR │ } insert_depth
|
|
167
|
+
│ INSERTION │ } (e.g., 40mm)
|
|
168
|
+
│ │ }
|
|
169
|
+
└──────┬───────────┘
|
|
170
|
+
│
|
|
171
|
+
Bottom of Well (Limit)
|
|
146
172
|
|
|
147
173
|
Returns:
|
|
148
174
|
Insert depth of the labware
|
puda_drivers/machines/first.py
CHANGED
|
@@ -29,12 +29,7 @@ class First:
|
|
|
29
29
|
|
|
30
30
|
# Default configuration values
|
|
31
31
|
DEFAULT_QUBOT_PORT = "/dev/ttyACM0"
|
|
32
|
-
DEFAULT_QUBOT_BAUDRATE = 9600
|
|
33
|
-
DEFAULT_QUBOT_FEEDRATE = 3000
|
|
34
|
-
|
|
35
32
|
DEFAULT_SARTORIUS_PORT = "/dev/ttyUSB0"
|
|
36
|
-
DEFAULT_SARTORIUS_BAUDRATE = 9600
|
|
37
|
-
|
|
38
33
|
DEFAULT_CAMERA_INDEX = 0
|
|
39
34
|
|
|
40
35
|
# origin position of Z and A axes
|
|
@@ -52,8 +47,11 @@ class First:
|
|
|
52
47
|
# Height from z and a origin to the deck
|
|
53
48
|
CEILING_HEIGHT = 192.2
|
|
54
49
|
|
|
55
|
-
# Tip length
|
|
56
|
-
TIP_LENGTH = 59
|
|
50
|
+
# Pipette Tip length
|
|
51
|
+
TIP_LENGTH = 59 # mm
|
|
52
|
+
|
|
53
|
+
# Electrode length
|
|
54
|
+
ELECTRODE_LENGTH = 2 # mm
|
|
57
55
|
|
|
58
56
|
# Slot origins (the bottom left corner of the slot relative to the deck origin)
|
|
59
57
|
SLOT_ORIGINS = {
|
|
@@ -146,6 +144,18 @@ class First:
|
|
|
146
144
|
self.pipette.initialize()
|
|
147
145
|
time.sleep(3) # need to wait for the pipette to initialize
|
|
148
146
|
self._logger.info("Machine startup complete - ready for operations")
|
|
147
|
+
|
|
148
|
+
def home(self):
|
|
149
|
+
"""
|
|
150
|
+
Home the qubot gantry to establish a known position.
|
|
151
|
+
|
|
152
|
+
This method homes the gantry to its reference position, which is useful
|
|
153
|
+
for establishing a known starting point before operations or after
|
|
154
|
+
potential position drift.
|
|
155
|
+
"""
|
|
156
|
+
self._logger.info("Homing qubot gantry...")
|
|
157
|
+
self.qubot.home()
|
|
158
|
+
self._logger.info("Qubot gantry homing complete")
|
|
149
159
|
|
|
150
160
|
def shutdown(self):
|
|
151
161
|
"""
|
|
@@ -158,9 +168,19 @@ class First:
|
|
|
158
168
|
self.pipette.disconnect()
|
|
159
169
|
self.camera.disconnect()
|
|
160
170
|
self._logger.info("Machine shutdown complete")
|
|
171
|
+
|
|
172
|
+
def wait(self, seconds: float):
|
|
173
|
+
"""
|
|
174
|
+
Wait for a specified number of seconds.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
seconds: Number of seconds to wait (can be a float for fractional seconds)
|
|
178
|
+
"""
|
|
179
|
+
self._logger.debug("Waiting for %.2f seconds", seconds)
|
|
180
|
+
time.sleep(seconds)
|
|
181
|
+
self._logger.debug("Waited for %.2f seconds", seconds)
|
|
161
182
|
|
|
162
183
|
### Queue (public commands) ###
|
|
163
|
-
|
|
164
184
|
async def get_position(self) -> Dict[str, Union[Dict[str, float], int]]:
|
|
165
185
|
"""
|
|
166
186
|
Get the current position of the machine. Both QuBot and Sartorius are queried.
|
|
@@ -183,47 +203,47 @@ class First:
|
|
|
183
203
|
Get the current deck layout.
|
|
184
204
|
|
|
185
205
|
Returns:
|
|
186
|
-
Dictionary mapping slot names (e.g., "A1") to labware classes.
|
|
206
|
+
Dictionary mapping deck slot names (e.g., "A1") to labware classes.
|
|
187
207
|
|
|
188
208
|
Raises:
|
|
189
209
|
None
|
|
190
210
|
"""
|
|
191
211
|
return self.deck.to_dict()
|
|
192
212
|
|
|
193
|
-
def load_labware(self,
|
|
213
|
+
def load_labware(self, deck_slot: str, labware_name: str):
|
|
194
214
|
"""
|
|
195
|
-
Load a labware object into a slot.
|
|
215
|
+
Load a labware object into a deck slot.
|
|
196
216
|
|
|
197
217
|
Args:
|
|
198
|
-
|
|
218
|
+
deck_slot: Deck slot name (e.g., 'A1', 'B2')
|
|
199
219
|
labware_name: Name of the labware class to load
|
|
200
220
|
|
|
201
221
|
Raises:
|
|
202
|
-
KeyError: If
|
|
222
|
+
KeyError: If deck_slot is not found in deck
|
|
203
223
|
"""
|
|
204
|
-
self._logger.info("Loading labware '%s' into slot '%s'", labware_name,
|
|
205
|
-
self.deck.load_labware(slot=
|
|
206
|
-
self._logger.debug("Labware '%s' loaded into slot '%s'", labware_name,
|
|
224
|
+
self._logger.info("Loading labware '%s' into deck slot '%s'", labware_name, deck_slot)
|
|
225
|
+
self.deck.load_labware(slot=deck_slot, labware_name=labware_name)
|
|
226
|
+
self._logger.debug("Labware '%s' loaded into deck slot '%s'", labware_name, deck_slot)
|
|
207
227
|
|
|
208
|
-
def remove_labware(self,
|
|
228
|
+
def remove_labware(self, deck_slot: str):
|
|
209
229
|
"""
|
|
210
|
-
Remove labware from a slot.
|
|
230
|
+
Remove labware from a deck slot.
|
|
211
231
|
|
|
212
232
|
Args:
|
|
213
|
-
|
|
233
|
+
deck_slot: Deck slot name (e.g., 'A1', 'B2')
|
|
214
234
|
|
|
215
235
|
Raises:
|
|
216
|
-
KeyError: If
|
|
236
|
+
KeyError: If deck_slot is not found in deck
|
|
217
237
|
"""
|
|
218
|
-
self.deck.empty_slot(slot=
|
|
219
|
-
self._logger.debug("
|
|
238
|
+
self.deck.empty_slot(slot=deck_slot)
|
|
239
|
+
self._logger.debug("Deck slot '%s' emptied", deck_slot)
|
|
220
240
|
|
|
221
241
|
def load_deck(self, deck_layout: Dict[str, Type[StandardLabware]]):
|
|
222
242
|
"""
|
|
223
243
|
Load multiple labware into the deck at once.
|
|
224
244
|
|
|
225
245
|
Args:
|
|
226
|
-
deck_layout: Dictionary mapping slot names (e.g., "A1") to labware classes.
|
|
246
|
+
deck_layout: Dictionary mapping deck slot names (e.g., "A1") to labware classes.
|
|
227
247
|
Each class will be instantiated automatically.
|
|
228
248
|
|
|
229
249
|
Example:
|
|
@@ -234,17 +254,18 @@ class First:
|
|
|
234
254
|
})
|
|
235
255
|
"""
|
|
236
256
|
self._logger.info("Loading deck layout with %d labware items", len(deck_layout))
|
|
237
|
-
for
|
|
238
|
-
self.load_labware(
|
|
257
|
+
for deck_slot, labware_name in deck_layout.items():
|
|
258
|
+
self.load_labware(deck_slot=deck_slot, labware_name=labware_name)
|
|
239
259
|
self._logger.info("Deck layout loaded successfully")
|
|
240
260
|
|
|
241
|
-
|
|
261
|
+
### Pipette operations ###
|
|
262
|
+
def attach_tip(self, deck_slot: str, well_name: Optional[str] = None):
|
|
242
263
|
"""
|
|
243
|
-
Attach a tip from a slot.
|
|
264
|
+
Attach a tip from a deck slot.
|
|
244
265
|
|
|
245
266
|
Args:
|
|
246
|
-
|
|
247
|
-
|
|
267
|
+
deck_slot: Deck slot name (e.g., 'A1', 'B2')
|
|
268
|
+
well_name: Optional well name within the deck slot (e.g., 'A1' for a well in a tiprack)
|
|
248
269
|
|
|
249
270
|
Note:
|
|
250
271
|
This method is idempotent - if a tip is already attached, it will
|
|
@@ -254,21 +275,20 @@ class First:
|
|
|
254
275
|
self._logger.warning("Tip already attached - skipping attachment (idempotent operation)")
|
|
255
276
|
return
|
|
256
277
|
|
|
257
|
-
self._logger.info("Attaching tip from slot '%s'%s",
|
|
258
|
-
pos = self._get_absolute_z_position(
|
|
278
|
+
self._logger.info("Attaching tip from deck slot '%s'%s", deck_slot, f", well '{well_name}'" if well_name else "")
|
|
279
|
+
pos = self._get_absolute_z_position(deck_slot, well_name)
|
|
259
280
|
self._logger.debug("Moving to position %s for tip attachment", pos)
|
|
260
281
|
# return the offset from the origin
|
|
261
282
|
self.qubot.move_absolute(position=pos)
|
|
262
283
|
|
|
263
284
|
# attach tip (move slowly down)
|
|
264
|
-
labware = self.deck[
|
|
285
|
+
labware = self.deck[deck_slot]
|
|
265
286
|
if labware is None:
|
|
266
|
-
self._logger.error("Cannot attach tip: no labware loaded in slot '%s'",
|
|
267
|
-
raise ValueError(f"No labware loaded in slot '{
|
|
268
|
-
|
|
269
|
-
self._logger.debug("Moving down by %s mm to insert tip", insert_depth)
|
|
287
|
+
self._logger.error("Cannot attach tip: no labware loaded in deck slot '%s'", deck_slot)
|
|
288
|
+
raise ValueError(f"No labware loaded in deck slot '{deck_slot}'. Load labware before attaching tips.")
|
|
289
|
+
self._logger.debug("Moving down by %s mm to insert tip", labware.get_insert_depth())
|
|
270
290
|
self.qubot.move_relative(
|
|
271
|
-
position=Position(z=-
|
|
291
|
+
position=Position(z=-labware.get_insert_depth()),
|
|
272
292
|
feed=500
|
|
273
293
|
)
|
|
274
294
|
self.pipette.set_tip_attached(attached=True)
|
|
@@ -277,33 +297,33 @@ class First:
|
|
|
277
297
|
self.qubot.home(axis="Z")
|
|
278
298
|
self._logger.debug("Z axis homed after tip attachment")
|
|
279
299
|
|
|
280
|
-
def drop_tip(self, *,
|
|
300
|
+
def drop_tip(self, *, deck_slot: str, well_name: str, height_from_bottom: float = 0.0):
|
|
281
301
|
"""
|
|
282
|
-
Drop a tip into a slot.
|
|
302
|
+
Drop a tip into a deck slot.
|
|
283
303
|
|
|
284
304
|
Args:
|
|
285
|
-
|
|
286
|
-
|
|
305
|
+
deck_slot: Deck slot name (e.g., 'A1', 'B2')
|
|
306
|
+
well_name: Well name within the deck slot (e.g., 'A1' for a well in a tiprack)
|
|
287
307
|
height_from_bottom: Height from the bottom of the well in mm. Defaults to 0.0.
|
|
288
|
-
Positive values move up from the bottom.
|
|
289
|
-
may cause a ValueError if the resulting position is outside
|
|
290
|
-
the Z axis limits.
|
|
308
|
+
Must be non-negative. Positive values move up from the bottom.
|
|
291
309
|
|
|
292
310
|
Raises:
|
|
293
|
-
ValueError: If no tip is attached,
|
|
294
|
-
the Z axis limits.
|
|
311
|
+
ValueError: If no tip is attached, if height_from_bottom is negative, or if
|
|
312
|
+
the resulting position is outside the Z axis limits.
|
|
295
313
|
"""
|
|
314
|
+
if height_from_bottom < 0:
|
|
315
|
+
self._logger.error("height_from_bottom must be non-negative, got %f", height_from_bottom)
|
|
316
|
+
raise ValueError(f"height_from_bottom must be non-negative, got {height_from_bottom}")
|
|
317
|
+
|
|
296
318
|
if not self.pipette.is_tip_attached():
|
|
297
319
|
self._logger.error("Cannot drop tip: no tip attached")
|
|
298
320
|
raise ValueError("Tip not attached")
|
|
299
321
|
|
|
300
|
-
self._logger.info("Dropping tip into slot '%s', well '%s'",
|
|
301
|
-
pos = self._get_absolute_z_position(
|
|
322
|
+
self._logger.info("Dropping tip into deck slot '%s', well '%s'", deck_slot, well_name)
|
|
323
|
+
pos = self._get_absolute_z_position(deck_slot, well_name)
|
|
302
324
|
# add height from bottom
|
|
303
325
|
pos += Position(z=height_from_bottom)
|
|
304
|
-
|
|
305
|
-
pos += Position(z=self.TIP_LENGTH)
|
|
306
|
-
self._logger.debug("Moving to position %s (adjusted for tip length) for tip drop", pos)
|
|
326
|
+
self._logger.debug("Moving to position %s for tip drop", pos)
|
|
307
327
|
self.qubot.move_absolute(position=pos)
|
|
308
328
|
|
|
309
329
|
self._logger.debug("Ejecting tip")
|
|
@@ -312,71 +332,204 @@ class First:
|
|
|
312
332
|
self.pipette.set_tip_attached(attached=False)
|
|
313
333
|
self._logger.info("Tip dropped successfully")
|
|
314
334
|
|
|
315
|
-
def aspirate_from(self, *,
|
|
335
|
+
def aspirate_from(self, *, deck_slot: str, well_name: str, amount: int, height_from_bottom: float = 0.0):
|
|
316
336
|
"""
|
|
317
|
-
Aspirate a volume of liquid from a slot.
|
|
337
|
+
Aspirate a volume of liquid from a deck slot.
|
|
318
338
|
|
|
319
339
|
Args:
|
|
320
|
-
|
|
321
|
-
|
|
340
|
+
deck_slot: Deck slot name (e.g., 'A1', 'B2')
|
|
341
|
+
well_name: Well name within the deck slot (e.g., 'A1')
|
|
322
342
|
amount: Volume to aspirate in µL
|
|
323
343
|
height_from_bottom: Height from the bottom of the well in mm. Defaults to 0.0.
|
|
324
|
-
Positive values move up from the bottom.
|
|
325
|
-
may cause a ValueError if the resulting position is outside
|
|
326
|
-
the Z axis limits.
|
|
344
|
+
Must be non-negative. Positive values move up from the bottom.
|
|
327
345
|
|
|
328
346
|
Raises:
|
|
329
|
-
ValueError: If no tip is attached,
|
|
330
|
-
the Z axis limits.
|
|
347
|
+
ValueError: If no tip is attached, if height_from_bottom is negative, or if
|
|
348
|
+
the resulting position is outside the Z axis limits.
|
|
331
349
|
"""
|
|
350
|
+
if height_from_bottom < 0:
|
|
351
|
+
self._logger.error("height_from_bottom must be non-negative, got %f", height_from_bottom)
|
|
352
|
+
raise ValueError(f"height_from_bottom must be non-negative, got {height_from_bottom}")
|
|
353
|
+
|
|
332
354
|
if not self.pipette.is_tip_attached():
|
|
333
355
|
self._logger.error("Cannot aspirate: no tip attached")
|
|
334
356
|
raise ValueError("Tip not attached")
|
|
335
357
|
|
|
336
|
-
self._logger.info("Aspirating %d µL from slot '%s', well '%s'", amount,
|
|
337
|
-
|
|
358
|
+
self._logger.info("Aspirating %d µL from deck slot '%s', well '%s'", amount, deck_slot, well_name)
|
|
359
|
+
|
|
360
|
+
pos = self._get_absolute_z_position(deck_slot, well_name)
|
|
338
361
|
# add height from bottom
|
|
339
362
|
pos += Position(z=height_from_bottom)
|
|
363
|
+
# subtract insert depth to get the bottom of the well
|
|
364
|
+
pos -= Position(z=self.deck[deck_slot].get_insert_depth())
|
|
365
|
+
|
|
340
366
|
self._logger.debug("Moving Z axis to position %s", pos)
|
|
341
367
|
self.qubot.move_absolute(position=pos)
|
|
342
|
-
|
|
343
368
|
self._logger.debug("Aspirating %d µL", amount)
|
|
344
369
|
self.pipette.aspirate(amount=amount)
|
|
345
370
|
time.sleep(5)
|
|
346
|
-
self._logger.info("Aspiration completed: %d µL from slot '%s', well '%s'", amount,
|
|
371
|
+
self._logger.info("Aspiration completed: %d µL from deck slot '%s', well '%s'", amount, deck_slot, well_name)
|
|
347
372
|
|
|
348
|
-
def dispense_to(self, *,
|
|
373
|
+
def dispense_to(self, *, deck_slot: str, well_name: str, amount: int, height_from_bottom: float = 0.0):
|
|
349
374
|
"""
|
|
350
|
-
Dispense a volume of liquid to a slot.
|
|
375
|
+
Dispense a volume of liquid to a deck slot.
|
|
351
376
|
|
|
352
377
|
Args:
|
|
353
|
-
|
|
354
|
-
|
|
378
|
+
deck_slot: Deck slot name (e.g., 'A1', 'B2')
|
|
379
|
+
well_name: Well name within the deck slot (e.g., 'A1')
|
|
355
380
|
amount: Volume to dispense in µL
|
|
356
381
|
height_from_bottom: Height from the bottom of the well in mm. Defaults to 0.0.
|
|
357
|
-
Positive values move up from the bottom.
|
|
358
|
-
may cause a ValueError if the resulting position is outside
|
|
359
|
-
the Z axis limits.
|
|
382
|
+
Must be non-negative. Positive values move up from the bottom.
|
|
360
383
|
|
|
361
384
|
Raises:
|
|
362
|
-
ValueError: If no tip is attached,
|
|
363
|
-
the Z axis limits.
|
|
385
|
+
ValueError: If no tip is attached, if height_from_bottom is negative, or if
|
|
386
|
+
the resulting position is outside the Z axis limits.
|
|
364
387
|
"""
|
|
388
|
+
if height_from_bottom < 0:
|
|
389
|
+
self._logger.error("height_from_bottom must be non-negative, got %f", height_from_bottom)
|
|
390
|
+
raise ValueError(f"height_from_bottom must be non-negative, got {height_from_bottom}")
|
|
391
|
+
|
|
365
392
|
if not self.pipette.is_tip_attached():
|
|
366
393
|
self._logger.error("Cannot dispense: no tip attached")
|
|
367
394
|
raise ValueError("Tip not attached")
|
|
368
395
|
|
|
369
|
-
self._logger.info("Dispensing %d µL to slot '%s', well '%s'", amount,
|
|
370
|
-
|
|
396
|
+
self._logger.info("Dispensing %d µL to deck slot '%s', well '%s'", amount, deck_slot, well_name)
|
|
397
|
+
|
|
398
|
+
pos = self._get_absolute_z_position(deck_slot, well_name)
|
|
371
399
|
# add height from bottom
|
|
372
400
|
pos += Position(z=height_from_bottom)
|
|
401
|
+
# subtract insert depth to get the bottom of the well
|
|
402
|
+
pos -= Position(z=self.deck[deck_slot].get_insert_depth())
|
|
403
|
+
|
|
373
404
|
self._logger.debug("Moving Z axis to position %s", pos)
|
|
374
405
|
self.qubot.move_absolute(position=pos)
|
|
375
|
-
|
|
376
406
|
self._logger.debug("Dispensing %d µL", amount)
|
|
377
407
|
self.pipette.dispense(amount=amount)
|
|
378
408
|
time.sleep(5)
|
|
379
|
-
self._logger.info("Dispense completed: %d µL to slot '%s', well '%s'", amount,
|
|
409
|
+
self._logger.info("Dispense completed: %d µL to deck slot '%s', well '%s'", amount, deck_slot, well_name)
|
|
410
|
+
|
|
411
|
+
# Electrode operations
|
|
412
|
+
def move_electrode(self, deck_slot: str, well_name: str, height_from_bottom: float = 0.0):
|
|
413
|
+
"""
|
|
414
|
+
Move the electrode to a deck slot.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
deck_slot: Deck slot name (e.g., 'A1', 'B2')
|
|
418
|
+
well_name: Well name within the deck slot (e.g., 'A1')
|
|
419
|
+
height_from_bottom: Height from the bottom of the well in mm. Defaults to 0.0.
|
|
420
|
+
Must be non-negative. Positive values move up from the bottom.
|
|
421
|
+
|
|
422
|
+
Raises:
|
|
423
|
+
ValueError: If height_from_bottom is negative.
|
|
424
|
+
"""
|
|
425
|
+
if height_from_bottom < 0:
|
|
426
|
+
self._logger.error("height_from_bottom must be non-negative, got %f", height_from_bottom)
|
|
427
|
+
raise ValueError(f"height_from_bottom must be non-negative, got {height_from_bottom}")
|
|
428
|
+
|
|
429
|
+
pos = self._get_absolute_a_position(deck_slot, well_name)
|
|
430
|
+
pos += Position(a=height_from_bottom)
|
|
431
|
+
self.qubot.move_absolute(position=pos)
|
|
432
|
+
self._logger.info("Electrode moved to deck slot '%s', well '%s' at height %s mm from bottom", deck_slot, well_name, height_from_bottom)
|
|
433
|
+
|
|
434
|
+
# Helper methods
|
|
435
|
+
def _get_slot_origin(self, deck_slot: str) -> Position:
|
|
436
|
+
"""
|
|
437
|
+
Get the origin coordinates of a deck slot.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
deck_slot: Deck slot name (e.g., 'A1', 'B2')
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Position for the deck slot origin
|
|
444
|
+
|
|
445
|
+
Raises:
|
|
446
|
+
KeyError: If deck_slot name is invalid
|
|
447
|
+
"""
|
|
448
|
+
deck_slot = deck_slot.upper()
|
|
449
|
+
if deck_slot not in self.SLOT_ORIGINS:
|
|
450
|
+
self._logger.error("Invalid deck slot name: '%s'. Must be one of %s", deck_slot, list(self.SLOT_ORIGINS.keys()))
|
|
451
|
+
raise KeyError(f"Invalid deck slot name: {deck_slot}. Must be one of {list(self.SLOT_ORIGINS.keys())}")
|
|
452
|
+
pos = self.SLOT_ORIGINS[deck_slot]
|
|
453
|
+
self._logger.debug("Deck slot origin for '%s': %s", deck_slot, pos)
|
|
454
|
+
return pos
|
|
455
|
+
|
|
456
|
+
def _get_absolute_z_position(self, deck_slot: str, well_name: Optional[str] = None) -> Position:
|
|
457
|
+
"""
|
|
458
|
+
Get the absolute position for a deck slot (and optionally a well within that deck slot) based on the origin
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
deck_slot: Deck slot name (e.g., 'A1', 'B2')
|
|
462
|
+
well_name: Optional well name within the deck slot (e.g., 'A1' for a well in a tiprack)
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Position with absolute coordinates
|
|
466
|
+
|
|
467
|
+
Raises:
|
|
468
|
+
ValueError: If well_name is specified but no labware is loaded in the deck slot
|
|
469
|
+
"""
|
|
470
|
+
# Get deck slot origin
|
|
471
|
+
pos = self._get_slot_origin(deck_slot)
|
|
472
|
+
|
|
473
|
+
# relative well position from deck slot origin
|
|
474
|
+
if well_name:
|
|
475
|
+
labware = self.deck[deck_slot]
|
|
476
|
+
if labware is None:
|
|
477
|
+
self._logger.error("Cannot get well position: no labware loaded in deck slot '%s'", deck_slot)
|
|
478
|
+
raise ValueError(f"No labware loaded in deck slot '{deck_slot}'. Load labware before accessing wells.")
|
|
479
|
+
well_pos = labware.get_well_position(well_name).get_xy()
|
|
480
|
+
# the deck is rotated 90 degrees clockwise for this machine
|
|
481
|
+
pos += well_pos.swap_xy()
|
|
482
|
+
# get z
|
|
483
|
+
pos += Position(z=labware.get_height() - self.CEILING_HEIGHT)
|
|
484
|
+
# if tip attached, add tip length
|
|
485
|
+
if self.pipette.is_tip_attached():
|
|
486
|
+
pos += Position(z=self.TIP_LENGTH)
|
|
487
|
+
self._logger.debug("Absolute Z position for deck slot '%s', well '%s': %s", deck_slot, well_name, pos)
|
|
488
|
+
else:
|
|
489
|
+
self._logger.debug("Absolute Z position for deck slot '%s': %s", deck_slot, pos)
|
|
490
|
+
return pos
|
|
491
|
+
|
|
492
|
+
def _get_absolute_a_position(self, deck_slot: str, well_name: Optional[str] = None) -> Position:
|
|
493
|
+
"""
|
|
494
|
+
Get the absolute position for a deck slot (and optionally a well within that deck slot) based on the origin
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
deck_slot: Deck slot name (e.g., 'A1', 'B2')
|
|
498
|
+
well_name: Optional well name within the deck slot (e.g., 'A1' for a well in a tiprack)
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Position with absolute coordinates
|
|
502
|
+
|
|
503
|
+
Raises:
|
|
504
|
+
ValueError: If well_name is specified but no labware is loaded in the deck slot
|
|
505
|
+
"""
|
|
506
|
+
# get x and y
|
|
507
|
+
pos = self._get_slot_origin(deck_slot)
|
|
508
|
+
pos -= self.A_ORIGIN # subtract the origin to get the absolute position
|
|
509
|
+
|
|
510
|
+
# Get labware for a-axis positioning
|
|
511
|
+
labware = self.deck[deck_slot]
|
|
512
|
+
if labware is None:
|
|
513
|
+
self._logger.error("Cannot get electrode position: no labware loaded in deck slot '%s'", deck_slot)
|
|
514
|
+
raise ValueError(f"No labware loaded in deck slot '{deck_slot}'. Load labware before moving electrode.")
|
|
515
|
+
|
|
516
|
+
# get x and y for well if specified
|
|
517
|
+
if well_name:
|
|
518
|
+
well_pos = labware.get_well_position(well_name).get_xy()
|
|
519
|
+
pos += well_pos.swap_xy()
|
|
520
|
+
|
|
521
|
+
# get a (applies to both with and without well_name)
|
|
522
|
+
pos += Position(a=labware.get_height() - self.CEILING_HEIGHT)
|
|
523
|
+
pos += Position(a=self.ELECTRODE_LENGTH)
|
|
524
|
+
|
|
525
|
+
if well_name:
|
|
526
|
+
self._logger.debug("Absolute A position for deck slot '%s', well '%s': %s", deck_slot, well_name, pos)
|
|
527
|
+
else:
|
|
528
|
+
self._logger.debug("Absolute A position for deck slot '%s': %s", deck_slot, pos)
|
|
529
|
+
|
|
530
|
+
return pos
|
|
531
|
+
|
|
532
|
+
### Camera operations ###
|
|
380
533
|
|
|
381
534
|
def start_video_recording(
|
|
382
535
|
self,
|
|
@@ -461,94 +614,6 @@ class First:
|
|
|
461
614
|
"""
|
|
462
615
|
return self.camera.capture_image(save=save, filename=filename)
|
|
463
616
|
|
|
464
|
-
### Private methods ###
|
|
465
|
-
|
|
466
|
-
def _get_slot_origin(self, slot: str) -> Position:
|
|
467
|
-
"""
|
|
468
|
-
Get the origin coordinates of a slot.
|
|
469
|
-
|
|
470
|
-
Args:
|
|
471
|
-
slot: Slot name (e.g., 'A1', 'B2')
|
|
472
|
-
|
|
473
|
-
Returns:
|
|
474
|
-
Position for the slot origin
|
|
475
|
-
|
|
476
|
-
Raises:
|
|
477
|
-
KeyError: If slot name is invalid
|
|
478
|
-
"""
|
|
479
|
-
slot = slot.upper()
|
|
480
|
-
if slot not in self.SLOT_ORIGINS:
|
|
481
|
-
self._logger.error("Invalid slot name: '%s'. Must be one of %s", slot, list(self.SLOT_ORIGINS.keys()))
|
|
482
|
-
raise KeyError(f"Invalid slot name: {slot}. Must be one of {list(self.SLOT_ORIGINS.keys())}")
|
|
483
|
-
pos = self.SLOT_ORIGINS[slot]
|
|
484
|
-
self._logger.debug("Slot origin for '%s': %s", slot, pos)
|
|
485
|
-
return pos
|
|
486
|
-
|
|
487
|
-
def _get_absolute_z_position(self, slot: str, well: Optional[str] = None) -> Position:
|
|
488
|
-
"""
|
|
489
|
-
Get the absolute position for a slot (and optionally a well within that slot) based on the origin
|
|
490
|
-
|
|
491
|
-
Args:
|
|
492
|
-
slot: Slot name (e.g., 'A1', 'B2')
|
|
493
|
-
well: Optional well name within the slot (e.g., 'A1' for a well in a tiprack)
|
|
494
|
-
|
|
495
|
-
Returns:
|
|
496
|
-
Position with absolute coordinates
|
|
497
|
-
|
|
498
|
-
Raises:
|
|
499
|
-
ValueError: If well is specified but no labware is loaded in the slot
|
|
500
|
-
"""
|
|
501
|
-
# Get slot origin
|
|
502
|
-
pos = self._get_slot_origin(slot)
|
|
503
|
-
|
|
504
|
-
# relative well position from slot origin
|
|
505
|
-
if well:
|
|
506
|
-
labware = self.deck[slot]
|
|
507
|
-
if labware is None:
|
|
508
|
-
self._logger.error("Cannot get well position: no labware loaded in slot '%s'", slot)
|
|
509
|
-
raise ValueError(f"No labware loaded in slot '{slot}'. Load labware before accessing wells.")
|
|
510
|
-
well_pos = labware.get_well_position(well).get_xy()
|
|
511
|
-
# the deck is rotated 90 degrees clockwise for this machine
|
|
512
|
-
pos += well_pos.swap_xy()
|
|
513
|
-
# get z
|
|
514
|
-
pos += Position(z=labware.get_height() - self.CEILING_HEIGHT)
|
|
515
|
-
self._logger.debug("Absolute Z position for slot '%s', well '%s': %s", slot, well, pos)
|
|
516
|
-
else:
|
|
517
|
-
self._logger.debug("Absolute Z position for slot '%s': %s", slot, pos)
|
|
518
|
-
return pos
|
|
519
|
-
|
|
520
|
-
def _get_absolute_a_position(self, slot: str, well: Optional[str] = None) -> Position:
|
|
521
|
-
"""
|
|
522
|
-
Get the absolute position for a slot (and optionally a well within that slot) based on the origin
|
|
523
|
-
|
|
524
|
-
Args:
|
|
525
|
-
slot: Slot name (e.g., 'A1', 'B2')
|
|
526
|
-
well: Optional well name within the slot (e.g., 'A1' for a well in a tiprack)
|
|
527
|
-
|
|
528
|
-
Returns:
|
|
529
|
-
Position with absolute coordinates
|
|
530
|
-
|
|
531
|
-
Raises:
|
|
532
|
-
ValueError: If well is specified but no labware is loaded in the slot
|
|
533
|
-
"""
|
|
534
|
-
pos = self._get_slot_origin(slot)
|
|
535
|
-
|
|
536
|
-
if well:
|
|
537
|
-
labware = self.deck[slot]
|
|
538
|
-
if labware is None:
|
|
539
|
-
self._logger.error("Cannot get well position: no labware loaded in slot '%s'", slot)
|
|
540
|
-
raise ValueError(f"No labware loaded in slot '{slot}'. Load labware before accessing wells.")
|
|
541
|
-
well_pos = labware.get_well_position(well).get_xy()
|
|
542
|
-
pos += well_pos.swap_xy()
|
|
543
|
-
|
|
544
|
-
# get a
|
|
545
|
-
a = Position(a=labware.get_height() - self.CEILING_HEIGHT)
|
|
546
|
-
pos += a
|
|
547
|
-
self._logger.debug("Absolute A position for slot '%s', well '%s': %s", slot, well, pos)
|
|
548
|
-
else:
|
|
549
|
-
self._logger.debug("Absolute A position for slot '%s': %s", slot, pos)
|
|
550
|
-
return pos
|
|
551
|
-
|
|
552
617
|
### Control (immediate commands) ###
|
|
553
618
|
|
|
554
619
|
def pause(self):
|
|
File without changes
|
puda_drivers/move/gcode.py
CHANGED
|
@@ -332,30 +332,21 @@ class GCodeController(SerialController):
|
|
|
332
332
|
>>> pos = Position(x=10, y=20)
|
|
333
333
|
>>> controller.move_absolute(position=pos) # z and a use current values
|
|
334
334
|
"""
|
|
335
|
-
# Fill in missing axes with current positions
|
|
336
|
-
final_x = position.x if position.has_axis("x") else self._current_position.x
|
|
337
|
-
final_y = position.y if position.has_axis("y") else self._current_position.y
|
|
338
|
-
final_z = position.z if position.has_axis("z") else self._current_position.z
|
|
339
|
-
final_a = position.a if position.has_axis("a") else self._current_position.a
|
|
340
|
-
|
|
341
|
-
# Create final position for validation and execution
|
|
342
|
-
final_position = Position(x=final_x, y=final_y, z=final_z, a=final_a)
|
|
343
|
-
|
|
344
335
|
# Validate positions before executing move
|
|
345
|
-
self._validate_move_positions(position=
|
|
336
|
+
self._validate_move_positions(position=position)
|
|
346
337
|
|
|
347
338
|
feed_rate = feed if feed is not None else self._feed
|
|
348
339
|
self._logger.info(
|
|
349
340
|
"Preparing absolute move to X:%s, Y:%s, Z:%s, A:%s at F:%s",
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
341
|
+
position.x,
|
|
342
|
+
position.y,
|
|
343
|
+
position.z,
|
|
344
|
+
position.a,
|
|
354
345
|
feed_rate,
|
|
355
346
|
)
|
|
356
347
|
|
|
357
348
|
return self._execute_move(
|
|
358
|
-
position=
|
|
349
|
+
position=position,
|
|
359
350
|
feed=feed_rate
|
|
360
351
|
)
|
|
361
352
|
|
|
@@ -429,9 +420,9 @@ class GCodeController(SerialController):
|
|
|
429
420
|
All coordinates are treated as absolute positions.
|
|
430
421
|
|
|
431
422
|
Safe move pattern:
|
|
432
|
-
1. If X or Y movement is needed, first move Z to
|
|
423
|
+
1. If X or Y movement is needed, first move Z and A to safe height
|
|
433
424
|
2. Then move X, Y to target
|
|
434
|
-
3. Finally move Z
|
|
425
|
+
3. Finally move Z or A to target (only the one that needs to move, the other stays where it is)
|
|
435
426
|
|
|
436
427
|
Args:
|
|
437
428
|
position: Position with absolute positions for X, Y, Z, A axes
|
|
@@ -440,8 +431,8 @@ class GCodeController(SerialController):
|
|
|
440
431
|
# Check if any movement is needed
|
|
441
432
|
needs_x_move = abs(position.x - self._current_position.x) > self.TOLERANCE
|
|
442
433
|
needs_y_move = abs(position.y - self._current_position.y) > self.TOLERANCE
|
|
443
|
-
needs_z_move = abs(position.z -
|
|
444
|
-
needs_a_move = abs(position.a -
|
|
434
|
+
needs_z_move = abs(position.z - 0) > self.TOLERANCE
|
|
435
|
+
needs_a_move = abs(position.a - 0) > self.TOLERANCE
|
|
445
436
|
|
|
446
437
|
if not (needs_x_move or needs_y_move or needs_z_move or needs_a_move):
|
|
447
438
|
self._logger.warning(
|
|
@@ -450,7 +441,7 @@ class GCodeController(SerialController):
|
|
|
450
441
|
return
|
|
451
442
|
|
|
452
443
|
if needs_z_move and needs_a_move:
|
|
453
|
-
self._logger.
|
|
444
|
+
self._logger.error(
|
|
454
445
|
"Move command issued with both Z and A movement. This is not supported. Skipping transmission."
|
|
455
446
|
)
|
|
456
447
|
raise ValueError("Move command issued with both Z and A movement. This is not supported.")
|
|
@@ -463,7 +454,7 @@ class GCodeController(SerialController):
|
|
|
463
454
|
self._logger.debug(
|
|
464
455
|
"Safe move: Raising Z and A to safe height (%s) before XY movement", self.SAFE_MOVE_HEIGHT
|
|
465
456
|
)
|
|
466
|
-
move_cmd = f"G1 Z
|
|
457
|
+
move_cmd = f"G1 Z{self.SAFE_MOVE_HEIGHT} A{self.SAFE_MOVE_HEIGHT} F{self._z_feed}"
|
|
467
458
|
self.execute(move_cmd)
|
|
468
459
|
self._wait_for_move()
|
|
469
460
|
self._current_position.z = self.SAFE_MOVE_HEIGHT
|
|
@@ -493,13 +484,13 @@ class GCodeController(SerialController):
|
|
|
493
484
|
if needs_y_move:
|
|
494
485
|
self._current_position.y = position.y
|
|
495
486
|
|
|
496
|
-
# Step 3: Move Z
|
|
487
|
+
# Step 3: Move Z or A to target (only the one that needs to move)
|
|
497
488
|
if needs_z_move:
|
|
498
489
|
move_cmd = f"G1 Z{position.z} F{self._z_feed}"
|
|
499
490
|
self.execute(move_cmd)
|
|
500
491
|
self._wait_for_move()
|
|
501
492
|
self._current_position.z = position.z
|
|
502
|
-
|
|
493
|
+
if needs_a_move:
|
|
503
494
|
move_cmd = f"G1 A{position.a} F{self._z_feed}"
|
|
504
495
|
self.execute(move_cmd)
|
|
505
496
|
self._wait_for_move()
|
|
@@ -6,16 +6,18 @@ puda_drivers/core/position.py,sha256=f4efmDSrKKCtqrR-GUJxVitPG20MiuGSDOWt-9TVISk
|
|
|
6
6
|
puda_drivers/core/serialcontroller.py,sha256=8Yf9DYIhtQTIVGB1Pm6COa6qgJcM0ZxdGwOGW8Om9ss,8654
|
|
7
7
|
puda_drivers/cv/__init__.py,sha256=DYiPwYOLSUsZ9ivWiHoMGGD3MZ3Ygu9qLYja_OiUodU,100
|
|
8
8
|
puda_drivers/cv/camera.py,sha256=Tzxt1h3W5Z8XFanud_z-PhhJ2UYsC4OEhcak9TVu8jM,16490
|
|
9
|
+
puda_drivers/labware/MEA_cell_MTP.json,sha256=y3wYHp4BO4Uokg6makMq37hj5vt_WbG8de9zpDL7r0k,1052
|
|
9
10
|
puda_drivers/labware/__init__.py,sha256=RlRxrJn2zyzyxv4c1KGt8Gmxv2cRO8V4zZVnnyL-I00,288
|
|
10
|
-
puda_drivers/labware/labware.py,sha256=
|
|
11
|
+
puda_drivers/labware/labware.py,sha256=YjMTCilvjhXZqx9qqcPgRRO68FjAChavV9OAGMXbtZ8,6678
|
|
11
12
|
puda_drivers/labware/opentrons_96_tiprack_300ul.json,sha256=jmNaworu688GEgFdxMxNRSaEp4ZOg9IumFk8bVHSzYY,19796
|
|
12
|
-
puda_drivers/labware/polyelectric_8_wellplate_30000ul.json,sha256=
|
|
13
|
-
puda_drivers/labware/trash_bin.json,sha256=
|
|
13
|
+
puda_drivers/labware/polyelectric_8_wellplate_30000ul.json,sha256=bqmUEqF5JJHZGWwUbemUuJyNL2jJKR5lhiKdUn4MVgI,3128
|
|
14
|
+
puda_drivers/labware/trash_bin.json,sha256=idVyjKtNdqKRT6UrYK20j867EDeV5CD5jRqgZ9Qy74M,618
|
|
14
15
|
puda_drivers/machines/__init__.py,sha256=zmIk_r2T8nbPA68h3Cko8N6oL7ncoBpmvhNcAqzHmc4,45
|
|
15
|
-
puda_drivers/machines/first.py,sha256=
|
|
16
|
+
puda_drivers/machines/first.py,sha256=B0SZTeaE3Gi2OaC2UB6Oez3HQqK2ZgegiKjn3T22njE,25584
|
|
17
|
+
puda_drivers/measure/electrode.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
18
|
puda_drivers/move/__init__.py,sha256=NKIKckcqgyviPM0EGFcmIoaqkJM4qekR4babfdddRzM,96
|
|
17
19
|
puda_drivers/move/deck.py,sha256=CprlB8i9jvNImrc5IpHUZSSvvFe8uei76fNQ4iN6n34,2394
|
|
18
|
-
puda_drivers/move/gcode.py,sha256=
|
|
20
|
+
puda_drivers/move/gcode.py,sha256=nbPCo5SGB2yzDMR3VdbTJDcFyTkImBAJSlUTjC42eZI,23248
|
|
19
21
|
puda_drivers/move/grbl/__init__.py,sha256=vBeeti8DVN2dACi1rLmHN_UGIOdo0s-HZX6mIepLV5I,98
|
|
20
22
|
puda_drivers/move/grbl/api.py,sha256=loj8_Vap7S9qaD0ReHhgxr9Vkl6Wp7DGzyLkZyZ6v_k,16995
|
|
21
23
|
puda_drivers/move/grbl/constants.py,sha256=4736CRDzLGWVqGscLajMlrIQMyubsHfthXi4RF1CHNg,9585
|
|
@@ -23,7 +25,7 @@ puda_drivers/transfer/liquid/sartorius/__init__.py,sha256=7fljIbu0KkumsI3NI3O64d
|
|
|
23
25
|
puda_drivers/transfer/liquid/sartorius/api.py,sha256=jxwIJmY2k1K2ts6NC2ZgFTe4MOiH8TGnJeqYOqNa3rE,28250
|
|
24
26
|
puda_drivers/transfer/liquid/sartorius/constants.py,sha256=mcsjLrVBH-RSodH-pszstwcEL9wwbV0vOgHbGNxZz9w,2770
|
|
25
27
|
puda_drivers/transfer/liquid/sartorius/rLine.py,sha256=FWEFO9tZN3cbneiozXRs77O-47Jg_8vYzWJvUrWpYxA,14531
|
|
26
|
-
puda_drivers-0.0.
|
|
27
|
-
puda_drivers-0.0.
|
|
28
|
-
puda_drivers-0.0.
|
|
29
|
-
puda_drivers-0.0.
|
|
28
|
+
puda_drivers-0.0.19.dist-info/METADATA,sha256=Hu24oaNZDP82BLnWmmjHA42VoKBrjLAX3ibcH0LDcjM,8409
|
|
29
|
+
puda_drivers-0.0.19.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
30
|
+
puda_drivers-0.0.19.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
|
|
31
|
+
puda_drivers-0.0.19.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|