2
2
3
3
from __future__ import annotations
4
4
from typing import TYPE_CHECKING , Optional , Type , Any , List
5
+ from typing_extensions import TypedDict # note: need this instead of typing for py<3.12
5
6
6
7
from pydantic .json_schema import SkipJsonSchema
7
8
from pydantic import BaseModel , Field
26
27
LabwareMovementStrategy ,
27
28
LabwareOffsetVector ,
28
29
LabwareMovementOffsetData ,
30
+ LabwareLocationSequence ,
31
+ NotOnDeckLocationSequenceComponent ,
32
+ OFF_DECK_LOCATION ,
29
33
)
30
34
from ..errors import (
31
35
LabwareMovementNotAllowedError ,
@@ -100,6 +104,31 @@ class MoveLabwareResult(BaseModel):
100
104
" so the default of (0, 0, 0) will be used."
101
105
),
102
106
)
107
+ eventualDestinationLocationSequence : LabwareLocationSequence | None = Field (
108
+ None ,
109
+ description = (
110
+ "The full location in which this labware will eventually reside. This will typically be the same as its "
111
+ "immediate destination, but if this labware is going to the trash then this field will be off deck."
112
+ ),
113
+ )
114
+ immediateDestinationLocationSequence : LabwareLocationSequence | None = Field (
115
+ None ,
116
+ description = (
117
+ "The full location to which this labware is being moved, right now."
118
+ ),
119
+ )
120
+ originLocationSequence : LabwareLocationSequence | None = Field (
121
+ None ,
122
+ description = "The full location down to the deck of the labware before this command." ,
123
+ )
124
+
125
+
126
+ class ErrorDetails (TypedDict ):
127
+ """Location details for a failed gripper move."""
128
+
129
+ originLocationSequence : LabwareLocationSequence
130
+ immediateDestinationLocationSequence : LabwareLocationSequence
131
+ eventualDestinationLocationSequence : LabwareLocationSequence
103
132
104
133
105
134
class GripperMovementError (ErrorOccurrence ):
@@ -112,6 +141,8 @@ class GripperMovementError(ErrorOccurrence):
112
141
113
142
errorType : Literal ["gripperMovement" ] = "gripperMovement"
114
143
144
+ errorInfo : ErrorDetails
145
+
115
146
116
147
_ExecuteReturn = SuccessData [MoveLabwareResult ] | DefinedErrorData [GripperMovementError ]
117
148
@@ -152,6 +183,11 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C
152
183
f"Cannot move fixed trash labware '{ current_labware_definition .parameters .loadName } '."
153
184
)
154
185
186
+ origin_location_sequence = self ._state_view .geometry .get_location_sequence (
187
+ params .labwareId
188
+ )
189
+ eventual_destination_location_sequence : LabwareLocationSequence | None = None
190
+
155
191
if isinstance (params .newLocation , AddressableAreaLocation ):
156
192
area_name = params .newLocation .addressableAreaName
157
193
if (
@@ -181,9 +217,19 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C
181
217
y = 0 ,
182
218
z = 0 ,
183
219
)
220
+ eventual_destination_location_sequence = [
221
+ NotOnDeckLocationSequenceComponent (
222
+ logicalLocationName = OFF_DECK_LOCATION
223
+ )
224
+ ]
184
225
elif fixture_validation .is_trash (area_name ):
185
226
# When dropping labware in the trash bins we want to ensure they are lids
186
227
# and enforce a y-axis drop offset to ensure they fall within the trash bin
228
+ eventual_destination_location_sequence = [
229
+ NotOnDeckLocationSequenceComponent (
230
+ logicalLocationName = OFF_DECK_LOCATION
231
+ )
232
+ ]
187
233
if labware_validation .validate_definition_is_lid (
188
234
self ._state_view .labware .get_definition (params .labwareId )
189
235
):
@@ -298,6 +344,16 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C
298
344
if trash_lid_drop_offset :
299
345
user_offset_data .dropOffset += trash_lid_drop_offset
300
346
347
+ immediate_destination_location_sequence = (
348
+ self ._state_view .geometry .get_predicted_location_sequence (
349
+ validated_new_loc
350
+ )
351
+ )
352
+ if eventual_destination_location_sequence is None :
353
+ eventual_destination_location_sequence = (
354
+ immediate_destination_location_sequence
355
+ )
356
+
301
357
try :
302
358
# Skips gripper moves when using virtual gripper
303
359
await self ._labware_movement .move_labware_with_gripper (
@@ -314,20 +370,23 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C
314
370
# todo(mm, 2024-09-26): Catch LabwareNotPickedUpError when that exists and
315
371
# move_labware_with_gripper() raises it.
316
372
) as exception :
317
- gripper_movement_error : GripperMovementError | None = (
318
- GripperMovementError (
319
- id = self ._model_utils .generate_id (),
320
- createdAt = self ._model_utils .get_timestamp (),
321
- errorCode = exception .code .value .code ,
322
- detail = exception .code .value .detail ,
323
- wrappedErrors = [
324
- ErrorOccurrence .from_failed (
325
- id = self ._model_utils .generate_id (),
326
- createdAt = self ._model_utils .get_timestamp (),
327
- error = exception ,
328
- )
329
- ],
330
- )
373
+ gripper_movement_error : GripperMovementError | None = GripperMovementError (
374
+ id = self ._model_utils .generate_id (),
375
+ createdAt = self ._model_utils .get_timestamp (),
376
+ errorCode = exception .code .value .code ,
377
+ detail = exception .code .value .detail ,
378
+ errorInfo = {
379
+ "originLocationSequence" : origin_location_sequence ,
380
+ "immediateDestinationLocationSequence" : immediate_destination_location_sequence ,
381
+ "eventualDestinationLocationSequence" : eventual_destination_location_sequence ,
382
+ },
383
+ wrappedErrors = [
384
+ ErrorOccurrence .from_failed (
385
+ id = self ._model_utils .generate_id (),
386
+ createdAt = self ._model_utils .get_timestamp (),
387
+ error = exception ,
388
+ )
389
+ ],
331
390
)
332
391
else :
333
392
gripper_movement_error = None
@@ -343,7 +402,27 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C
343
402
344
403
elif params .strategy == LabwareMovementStrategy .MANUAL_MOVE_WITH_PAUSE :
345
404
# Pause to allow for manual labware movement
405
+ immediate_destination_location_sequence = (
406
+ self ._state_view .geometry .get_predicted_location_sequence (
407
+ params .newLocation
408
+ )
409
+ )
410
+ if eventual_destination_location_sequence is None :
411
+ eventual_destination_location_sequence = (
412
+ immediate_destination_location_sequence
413
+ )
414
+
346
415
await self ._run_control .wait_for_resume ()
416
+ else :
417
+ immediate_destination_location_sequence = (
418
+ self ._state_view .geometry .get_predicted_location_sequence (
419
+ params .newLocation
420
+ )
421
+ )
422
+ if eventual_destination_location_sequence is None :
423
+ eventual_destination_location_sequence = (
424
+ immediate_destination_location_sequence
425
+ )
347
426
348
427
# We may have just moved the labware that contains the current well out from
349
428
# under the pipette. Clear the current location to reflect the fact that the
@@ -398,7 +477,12 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C
398
477
)
399
478
400
479
return SuccessData (
401
- public = MoveLabwareResult (offsetId = new_offset_id ),
480
+ public = MoveLabwareResult (
481
+ offsetId = new_offset_id ,
482
+ originLocationSequence = origin_location_sequence ,
483
+ immediateDestinationLocationSequence = immediate_destination_location_sequence ,
484
+ eventualDestinationLocationSequence = eventual_destination_location_sequence ,
485
+ ),
402
486
state_update = state_update ,
403
487
)
404
488
0 commit comments