Connect PyLabRobot to OT2

I am trying to connect an OT2 with PLR but i cannot connect.
I am running the code from a windows 10 computer.
I have done no prior installation on the OT or any Zadig settup. I don’t know if this is needed.
I have pip installed opentrons library.
I attempted to run both on web and USB IP.

I am running the following code:
from pylabrobot.liquid_handling import LiquidHandler
from pylabrobot.liquid_handling.backends import OpentronsBackend
from pylabrobot.resources.opentrons.deck import OTDeck
lh = LiquidHandler(backend=OpentronsBackend(host=“10.199.253.164”), deck=OTDeck())

await lh.setup()

The code fails on the last line, and gives the following error message:

Exception in thread Thread-8 (callback):
Traceback (most recent call last):
File “C:\Users\vikmol\AppData\Local\Programs\Python\Python310\Lib\threading.py”, line 1016, in _bootstrap_inner
self.run()
File “C:\Users\vikmol\AppData\Local\Programs\Python\Python310\Lib\threading.py”, line 953, in run
self._target(*self._args, **self._kwargs)
File “c:\users\vikmol\onedrive\dokumenter\github\pylabrobot_dalsa\pylabrobot\liquid_handling\liquid_handler.py”, line 163, in callback
loop.run_until_complete(func(*args, **kwargs))
File “C:\Users\vikmol\AppData\Local\Programs\Python\Python310\Lib\asyncio\base_events.py”, line 649, in run_until_complete
return future.result()
File “c:\users\vikmol\onedrive\dokumenter\github\pylabrobot_dalsa\pylabrobot\liquid_handling\backends\opentrons_backend.py”, line 219, in assigned_resource_callback
ot_api.labware.add(
File “c:\Users\vikmol\OneDrive\Dokumenter\GitHub\pylabrobot_DALSA\venv\lib\site-packages\ot_api\decorators.py”, line 23, in wrapper
return f(*args, **kwargs)
File “c:\Users\vikmol\OneDrive\Dokumenter\GitHub\pylabrobot_DALSA\venv\lib\site-packages\ot_api\decorators.py”, line 48, in wrapper
error_class = get_ot_error(error_data[“errorType”])
File “c:\Users\vikmol\OneDrive\Dokumenter\GitHub\pylabrobot_DALSA\venv\lib\site-packages\ot_api\decorators.py”, line 36, in get_ot_error
return getattr(ot_errors, name)
AttributeError: module ‘opentrons.protocol_engine.errors’ has no attribute ‘AreaNotInDeckConfigurationError’

There are no obvious errors in your code. You don’t need Zadig in this case. The connection to the machine is established correctly.

Could you initialize the OTDeck with OTDeck(no_trash=True) and see if that works? Discarding tips will not work in this case, but it is helpful for debugging.

This works, but i cannot see or use the trash bin. How do i add the trash?
I have tried:

tiprack = lh.get_resource(“tip_rack1”)
await lh.pick_up_tips(tiprack[“A1”])

from pylabrobot.resources.trash import Trash
lh.deck._assign_trash
trash = lh.deck.get_trash_area
print(lh.deck.summary())

Deck: 624.3mm x 565.2mm

±----------------±----------------±----------------+
| | | |
| 10: plate_02 | 11: plate_01 | 12: Empty |
| | | |
±----------------±----------------±----------------+
| | | |
| 7: Empty | 8: Empty | 9: Empty |
| | | |
±----------------±----------------±----------------+
| | | |
| 4: Empty | 5: tip_rack2 | 6: Empty |
| | | |
±----------------±----------------±----------------+
| | | |
| 1: Empty | 2: Empty | 3: tip_rack1 |
| | | |
±----------------±----------------±----------------+

await lh.drop_tips(tip_spots=[trash]*1)

--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[17], line 1 ----> 1 await lh.drop_tips(trash) File [c:\users\vikmol\onedrive\dokumenter\github\pylabrobot_dalsa\pylabrobot\machine.py:20](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/machine.py:20), in need_setup_finished..wrapper(self, *args, **kwargs) [18](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/machine.py:18) if not self.setup_finished: [19](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/machine.py:19) raise RuntimeError(“The setup has not finished. See setup.”) —> [20](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/machine.py:20) return await func(self, *args, **kwargs) File [c:\users\vikmol\onedrive\dokumenter\github\pylabrobot_dalsa\pylabrobot\liquid_handling\liquid_handler.py:456](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:456), in LiquidHandler.drop_tips(self, tip_spots, use_channels, offsets, allow_nonzero_volume, **backend_kwargs) [405](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:405) @need_setup_finished [406](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:406) async def drop_tips( [407](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:407) self, (…) [412](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:412) **backend_kwargs [413](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:413) ): [414](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:414) “”" Drop tips to a resource. [415](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:415) [416](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:416) Examples: (…) [453](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:453) HasTipError: If a spot already has a tip. [454](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:454) “”" → [456](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:456) self._assert_resources_exist(tip_spots) [458](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:458) offsets = expand(offsets, len(tip_spots))

[232](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:232) # see of top parent of resource is the deck (i.e. resource is assigned to deck) [233](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:233) while resource.parent is not None: [234](file:///C:/users/vikmol/onedrive/dokumenter/github/pylabrobot_dalsa/pylabrobot/liquid_handling/liquid_handler.py:234) resource = resource.parent TypeError: ‘method’ object is not iterable

Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings

This appears to be an error on the Opentrons: after the labware is defined (opentrons_backend.py:211), you get an error saying the defined resource does not in fact exist (AreaNotInDeckConfigurationError) when you try to add a resource to deck using that definition.

It’s curious that the error class is not found in your installation of the Opentrons package.

Could you report the output of

python -c "import opentrons.protocol_engine.errors as ot_errors; print(getattr(ot_errors, 'AreaNotInDeckConfigurationError'))"

run in the terminal?

What’s the output of

pip freeze | grep opentrons

?

(venv) C:\Users\vikmol\OneDrive\Dokumenter\GitHub\pylabrobot_DALSA>python -c “import opentrons.protocol_engine.errors as ot_errors; print(getattr(ot_errors, ‘AreaNotInDeckConfigurationError’))”
Traceback (most recent call last):
File “”, line 1, in
AttributeError: module ‘opentrons.protocol_engine.errors’ has no attribute ‘AreaNotInDeckConfigurationError’

i cannot get grep to work. I looked through the pip freeze output and grabbed the opentrons outputs:
opentrons==7.0.2
opentrons-http-api-client==0.1.2
opentrons-shared-data==7.0.2

Ah, could you update opentrons? pip install --upgrade opentrons. (tested successfully with 7.1.0; got the same error with 7.0.2)

I have updated the opentrons package.
Unfortunately the problem persists.
I can see the trash_container in the summary now. but i cannot run the command await lh.drop_tips(tip_spots=[trash]*1) without getting an error.

Here is my code:
from pylabrobot.liquid_handling import LiquidHandler

from pylabrobot.liquid_handling.backends import OpentronsBackend

from pylabrobot.resources.opentrons.deck import OTDeck

lh = LiquidHandler(backend=OpentronsBackend(host=“10.199.253.164”), deck=OTDeck(no_trash=True)) # This seems to work

await lh.setup()

from pylabrobot.resources.opentrons import (
opentrons_96_filtertiprack_20ul
)
from pylabrobot.resources.corning_costar import (
Cos_96_EZWash,
Cos_96_DW_1mL
)

tips1 = opentrons_96_filtertiprack_20ul(name=“tip_rack1”)
tips2 = opentrons_96_filtertiprack_20ul(name=“tip_rack2”)
plate1 = Cos_96_EZWash(name=“plate_01”)
plate2 = Cos_96_DW_1mL(name=“plate_02”)

lh.deck.assign_child_at_slot(tips1, slot=3)
lh.deck.assign_child_at_slot(tips2, slot=5)
lh.deck.assign_child_at_slot(plate1, slot=11)
lh.deck.assign_child_at_slot(plate2, slot=10)

print(lh.deck.summary())

Deck: 624.3mm x 565.2mm

±----------------±----------------±----------------+
| | | |
| 10: plate_02 | 11: plate_01 | 12: Empty |
| | | |
±----------------±----------------±----------------+
| | | |
| 7: Empty | 8: Empty | 9: Empty |
| | | |
±----------------±----------------±----------------+
| | | |
| 4: Empty | 5: tip_rack2 | 6: Empty |
| | | |
±----------------±----------------±----------------+
| | | |
| 1: Empty | 2: Empty | 3: tip_rack1 |
| | | |
±----------------±----------------±----------------+

tiprack = lh.get_resource(“tip_rack1”)
await lh.pick_up_tips(tiprack[“A1”])
from pylabrobot.resources.trash import Trash
lh.deck._assign_trash
trash = lh.deck.get_trash_area()
print(lh.deck.summary())

Deck: 624.3mm x 565.2mm

±----------------±----------------±----------------+
| | | |
| 10: plate_02 | 11: plate_01 | 12: trash_co… |
| | | |
±----------------±----------------±----------------+
| | | |
| 7: Empty | 8: Empty | 9: Empty |
| | | |
±----------------±----------------±----------------+
| | | |
| 4: Empty | 5: tip_rack2 | 6: Empty |
| | | |
±----------------±----------------±----------------+
| | | |
| 1: Empty | 2: Empty | 3: tip_rack1 |
| | | |
±----------------±----------------±----------------+

await lh.drop_tips(tip_spots=[trash]*1)


KeyError Traceback (most recent call last)
Cell In[22], line 1
----> 1 await lh.drop_tips(tip_spots=[trash]*1)
3 # await lh.drop_tips(

File c:\users\vikmol\onedrive\dokumenter\github\pylabrobot_dalsa\pylabrobot\machine.py:20, in need_setup_finished..wrapper(self, *args, **kwargs)
18 if not self.setup_finished:
19 raise RuntimeError(“The setup has not finished. See setup.”)
—> 20 return await func(self, *args, **kwargs)

File c:\users\vikmol\onedrive\dokumenter\github\pylabrobot_dalsa\pylabrobot\liquid_handling\liquid_handler.py:497, in LiquidHandler.drop_tips(self, tip_spots, use_channels, offsets, allow_nonzero_volume, **backend_kwargs)
495 op.resource.tracker.rollback()
496 self.head[channel].rollback()
→ 497 self._trigger_callback(
498 “drop_tips”,
499 liquid_handler=self,
500 operations=drops,
501 use_channels=use_channels,
502 error=error,
503 **backend_kwargs,
504 )
505 else:
506 for channel, op in zip(use_channels, drops):

File c:\users\vikmol\onedrive\dokumenter\github\pylabrobot_dalsa\pylabrobot\liquid_handling\liquid_handler.py:1523, in LiquidHandler._trigger_callback(self, method_name, error, *args, **kwargs)
1521 callback(self, *args, error=error, **kwargs)
1522 elif error is not None:
→ 1523 raise error

File c:\users\vikmol\onedrive\dokumenter\github\pylabrobot_dalsa\pylabrobot\liquid_handling\liquid_handler.py:490, in LiquidHandler.drop_tips(self, tip_spots, use_channels, offsets, allow_nonzero_volume, **backend_kwargs)
487 del backend_kwargs[extra]
489 try:
→ 490 await self.backend.drop_tips(ops=drops, use_channels=use_channels, **backend_kwargs)
491 except Exception as error: # pylint: disable=broad-except
492 for channel, op in zip(use_channels, drops):

File c:\users\vikmol\onedrive\dokumenter\github\pylabrobot_dalsa\pylabrobot\liquid_handling\backends\opentrons_backend.py:306, in OpentronsBackend.drop_tips(self, ops, use_channels)
303 # this feels wrong, why should backends check?
304 assert op.resource.parent is not None, “must not be a floating resource”
→ 306 labware_id = self.defined_labware[op.resource.parent.name] # get name of tip rack
307 tip_max_volume = 20 # op.resource.maximal_volume
308 pipette_id = self.select_tip_pipette(tip_max_volume, with_tip=True)

KeyError: ‘trash_container’
await lh.drop_tips(tip_spots=[trash]*1)

This is actually a different error, that ultimately originates in OpentronsBackend.assigned_resource_callback (like the error above), but this time ot_api.labware.define and ot_api.labware.add actually succeeded. Nothing here touches on the opentrons library that you updated.

I’d be curious to know what the contents of defined_labware are right before you discard/drop the tip. In the error, it appears that the container was not part of that. However, when I simulate your scenario where the deck is initialized without a trash area, but it is added later, I still see that all callback are called correctly.

Could you insert print(lh.backend.defined_labware) right before you call lh.drop_tips?

It looks like you forgot the () in the code pasted here, but judging by the output you did have them in the code that was executed.

Actually i wrote it without (), because if i write:
lh.deck._assign_trash()
Then i get this error:

Exception in thread Thread-10 (callback):
Traceback (most recent call last):
File “C:\Users\vikmol\AppData\Local\Programs\Python\Python310\Lib\threading.py”, line 1016, in _bootstrap_inner
self.run()
File “C:\Users\vikmol\AppData\Local\Programs\Python\Python310\Lib\threading.py”, line 953, in run
self._target(*self._args, **self._kwargs)
File “c:\users\vikmol\onedrive\dokumenter\github\pylabrobot_dalsa\pylabrobot\liquid_handling\liquid_handler.py”, line 163, in callback
loop.run_until_complete(func(*args, **kwargs))
File “C:\Users\vikmol\AppData\Local\Programs\Python\Python310\Lib\asyncio\base_events.py”, line 649, in run_until_complete
return future.result()
File “c:\users\vikmol\onedrive\dokumenter\github\pylabrobot_dalsa\pylabrobot\liquid_handling\backends\opentrons_backend.py”, line 219, in assigned_resource_callback
ot_api.labware.add(
File “c:\Users\vikmol\OneDrive\Dokumenter\GitHub\pylabrobot_DALSA\venv\lib\site-packages\ot_api\decorators.py”, line 23, in wrapper
return f(*args, **kwargs)
File “c:\Users\vikmol\OneDrive\Dokumenter\GitHub\pylabrobot_DALSA\venv\lib\site-packages\ot_api\decorators.py”, line 49, in wrapper
raise error_class(error_data[“detail”])
opentrons.protocol_engine.errors.exceptions.AreaNotInDeckConfigurationError: Error 4000 GENERAL_ERROR (AreaNotInDeckConfigurationError): 12 not provided by deck configuration.

print(lh.backend.defined_labware)
Yields the result:

{‘tip_rack1’: ‘tip_rack1’, ‘tip_rack2’: ‘tip_rack2’, ‘plate_01’: ‘plate_01’, ‘plate_02’: ‘plate_02’}

It also works fine when i use the simulation. In simulation i can also leave out no_trash=True in:
lh = LiquidHandler(backend=OpentronsBackend(host=“10.199.253.164”), deck=OTDeck(no_trash=True))

i am using python version 3.10.7
Running the robot over the web IP.

What’s the version of the OT server? Can you install an update?

I ran into the same error (12 not provided by deck configuration). It seemed to have been caused by robot server version 7.1.1. Downgrading to 7.0.2 fixed the issue.

1 Like

Great idea Lukas!

I will try that.
It is a good short term solution for me but it will cause some problems down the line as my colleges still use the OT2 through the OT2 app, so they will be prompted to update the robot software. Will see if there is a way to get around this.

On page 38 of the OT2 user manual pdf:

Disable Opentrons App Update Notifications
The user can turn off Opentrons App update notification pop-ups. The controls to do so are in the modal itself as well as a toggle in App Settings > General > Software Update Alerts. This setting only disables the pop-up. It does not stop the app from continuing to (gently) notify the user that an app update is available.

An old-school post-it note on the pc telling people to not update the software may also be needed :upside_down_face:

1 Like

Preferably we make PLR compatible with both versions. I hope to work on that some time this week.

3 Likes

Hi, I’ve been having the same problems, and have tried the same solution.
The good news is that downgrading the robot’s server to 7.0.2 allows to create a new OTDeck with trash slot, the problem is that when the system executes

trash = liquidHandler.deck.get_trash_area()
  await liquidHandler.drop_tips(tip_spots=[trash])

the tips are being dropped in slot #9, instead of the trash

Hey Lukas
How do i downgrade to version 7.0.2?

I have been able to find the github version:

But i do not know how to download it. I have searched online but cannot find any old version of the installation in the opentrons-app installation site:

Hi Vilhelm,

I followed the instructions on this page: https://support.opentrons.com/s/article/Downgrade-to-an-older-software-version

1 Like

Here is the bug report that points on how to fix it in pylabrobot: bug: Impossible to use 'dropTip' command with fixedTrash on OT-2 and robot server version 7.1.0 · Issue #14590 · Opentrons/opentrons · GitHub

Basically we would have to add two new commands to ot_api’s lh.py:

@command
def move_to_addressable_area_for_drop_tip(
  pipette_id: str,
  offset_x: float = 0,
  offset_y: float = 0,
  offset_z: float = 0,
  run_id: Optional[str]=None
):
  params = {
    "pipetteId": pipette_id,
    "addressableAreaName": "fixedTrash",
    "wellName": "A1",
    "wellLocation": {
      "origin": "default",
      "offset": {
        "x": offset_x,
        "y": offset_y,
        "z": offset_z
      }
    },
    "alternateDropLocation": False
  }

  return ot_api.runs.enqueue_command("moveToAddressableAreaForDropTip", params, intent="setup", run_id=run_id)

@command
def drop_tip_in_place(
  pipette_id: str,
  run_id: Optional[str]=None
):
  params = {
    "pipetteId": pipette_id
  }

  return ot_api.runs.enqueue_command("dropTipInPlace", params, intent="setup", run_id=run_id)

and here is the rough diff of the changes I made to pylabrobot itself:

diff --git a/pylabrobot/liquid_handling/backends/opentrons_backend.py b/pylabrobot/liquid_handling/backends/opentrons_backend.py
index a811a6d..33a4ea9 100644
--- a/pylabrobot/liquid_handling/backends/opentrons_backend.py
+++ b/pylabrobot/liquid_handling/backends/opentrons_backend.py
@@ -332,9 +332,12 @@ class OpentronsBackend(LiquidHandlerBackend):
     # this feels wrong, why should backends check?
     assert op.resource.parent is not None, "must not be a floating resource"
 
-    labware_id = self.defined_labware[op.resource.parent.name] # get name of tip rack
+    if op.resource.name == 'trash':
+      labware_id = 'fixedTrash'
+    else:
+      labware_id = self.defined_labware[op.resource.parent.name] # get name of tip rack
     tip_max_volume = op.tip.maximal_volume
-    pipette_id = self.select_tip_pipette(tip_max_volume, with_tip=True)
+    pipette_id = self.select_tip_pipette(tip_max_volume, with_tip=True, use_pipette_id=use_pipette_id)
     if not pipette_id:
       raise NoChannelError("No pipette channel of right type with tip available.")
 
@@ -346,8 +349,13 @@ class OpentronsBackend(LiquidHandlerBackend):
     # ad-hoc offset adjustment that makes it smoother.
     offset_z += 10
 
-    ot_api.lh.drop_tip(labware_id, well_name=op.resource.name, pipette_id=pipette_id,
-      offset_x=offset_x, offset_y=offset_y, offset_z=offset_z)
+    if labware_id == 'fixedTrash':
+      ot_api.lh.move_to_addressable_area_for_drop_tip(pipette_id=pipette_id,
+        offset_x=offset_x, offset_y=offset_y, offset_z=offset_z)
+      ot_api.lh.drop_tip_in_place(pipette_id=pipette_id)
+    else:
+      ot_api.lh.drop_tip(labware_id, well_name=op.resource.name, pipette_id=pipette_id,
+        offset_x=offset_x, offset_y=offset_y, offset_z=offset_z)
 
     if pipette_id == self.left_pipette["pipetteId"]:
       self.left_pipette_has_tip = False
diff --git a/pylabrobot/resources/opentrons/deck.py b/pylabrobot/resources/opentrons/deck.py
index 7971ccf..baead32 100644
--- a/pylabrobot/resources/opentrons/deck.py
+++ b/pylabrobot/resources/opentrons/deck.py
@@ -64,7 +64,7 @@ class OTDeck(Deck):
     # so this location is no longer needed and we just use Coordinate.zero().
     # The actual location of the trash is determined by the slot number (12).
     trash_container.assign_child_resource(actual_trash, location=Coordinate.zero())
-    self.assign_child_at_slot(trash_container, 12)
+    self.resources['trash'] = actual_trash
 
   def assign_child_resource(
     self,
2 Likes

Awesome!!

Do you want to create a PR?

I’ll do some more tests, just to be sure it’s working as expected, with no surprises, and then I’ll do it :slight_smile:

2 Likes