OT-2 Hardware Control API¶
The Hardware Control API is a Python interface to the Opentrons hardware. It is intended for internal testing and is not version controlled. Please restrict use of this interface to what is absolutely necessary, and use only methods and attributes whose names do not begin with an underscore.
How To Get A Hardware Controller¶
import asyncio
from typing import cast
from opentrons.hardware_control import ThreadManager, API
from opentrons.types import Point, Mount
from opentrons.hardware_control.types import Axis
hardware: API = cast(API, ThreadManager(API.build_hardware_controller))
# You can use the hardware object to call async methods. The cast is to
# make VS Code or mypy understand those calls.
asyncio.run(hardware.cache_instruments()) # search for instruments
synch_hardware = hardware.sync # type: ignore
# You can use the synch_hardware object to call methods normally. Unfortunately
# no tooling will understand this
synch_hardware.cache_instruments() # search for instruments
There’s a lot here, and the following sections go into it.
Input Values, Typing, and Structure¶
The hardware control API is primarily intended for use by other parts of the Opentrons package and uses concepts like
enums that are used elsewhere in the package and therefore imported
from other parts of the package. This is all in aid of having more structure, and having the way you use the
hardware control API be easy to verify and check. It’s heavily recommended to use a development environment that
can infer requirements from the structure of the Opentrons package, like VSCode. In
addition, using tools like mypy (installable with pip install mypy
) on the
scripts you’re developing can really help make sure those scripts work correctly the first time (though there are
some challenges to correct use of both, stemming from weird organization of the hardware control API).
If you import opentrons.types
and opentrons.hardware_control.types
you should have all of the definitions
necessary to interact with the hardware control API. For instance, opentrons.types
provides Mount
and Point
;
opentrons.hardware_control.types
provides CriticalPoint
, and Axis
.
You also need to import opentrons.hardware_control
to get the API
object itself (which has classmethods to
build it like build_hardware_controller
) and the ThreadManager
class (which lets you run the hardware controller
in a different thread, and optionally access it using synchronous patterns).
Asyncio and Async/Await¶
All of the hardware controller methods that actually cause motion are defined with async def
instead of just
def
. This means that they are coroutines
designed to work with Python’s asyncio structure.
Coroutines execute piece-by-piece instead of all at once, by doing chunks of work and then letting the system decide what to do next. They are used when you want to do several tasks at once, and each task isn’t that computationally hard but does involve waiting for input from other systems - for instance, the normal example is a webserver that has to serve many page requests but doesn’t have to do much to actually build the page. In our case, this API is async because it’s used by a server that is async, and because talking to something over a serial connection is another great example of when to use coroutines like this (the general term is IO-concurrency).
- There are two key things to know about coroutines:
to execute the code in the coroutine, it’s not enough to just call it; you have to
await
ityou can only
await
things when running in an async context, meaning inside a call toasyncio.run
import asyncio
async def say_hello():
print("hello")
hello() # doesn't print hello
async def run_async():
hello() # doesn't print hello
await hello() # prints hello
asyncio.run(await hello())
This looks painful, because it is. It’s a result of the API being written for use in other parts of the Opentrons package.
It’s also the best way to interact with the hardware control API. There is a way to interact with it where you
don’t have to do this (see below) but using that SynchronousAdapter
prevents tooling like mypy
or the
built in analysis in Visual Studio Code from properly understanding how the hardware controller works. We, sadly,
recommend that test scripts use this, by
Making the main function they run async
calling that function with asyncio.run instead of calling it directly
Synchronous Adapters? Thread Managers? What?¶
There are two other ways to interact with the hardware controller, which both grow out of it being async and also
it not doing a good job of being async. Even though it’s async, calls to the smoothie still take a long time (for
now). That means that even though everything is async def
, it can still block your code. This is the reason
behind the ThreadManager
. Creating a ThreadManager
instead of an API
object will run the API in an
async.run
call inside another thread. You can then treat the ThreadManager
exactly like an API
- call
all the same methods, access all the same attributes - and they will get moved between threads and work. The
downside is that no tooling understands this. You can get around this by annotating it as
in the example, and that’s what the hardware: API = cast(API, ThreadManager(API.build_hardware_controller))
call does. The part after the :
tells tooling what it should treat the hardware
object as, and the cast
call (which does nothing at runtime) tells the tooling
“OK, ignore that this is actually a ThreadManager
”.
If you want to ignore the async
and await
stuff, once you have a ThreadManager
you can access the sync
attribute. This gives you an instance of a class called
SynchronousAdapter
, which wraps every call and access to the hardware control API and
makes them work like normal python method calls and attribute accesses. The downside is
that, for real this time with no workarounds, no tooling can handle this and you’re kind
of on your own.