OT-2 HTTP Driver End-to-End Example#

This tutorial walks through a complete OT-2 workflow using the AFL-automation Opentrons driver stack. It is based on the repository notebook for preparing samples with OT2Prepare and demonstrates how to set up the robot, define stocks and targets, and execute a preparation protocol that includes both shaking and temperature control.

By the end, you will have connected to a robot, loaded pipettes and labware, prepared a sample from stock solutions, mixed it on a heater-shaker, moved it to a temperature module, and shut the system down cleanly.

This page is meant to be read as a full worked example rather than a task-specific reference.

What You Will Do#

In this tutorial, you will:

  • connect to an OT-2 over its HTTP interface

  • reset the driver state before starting a run

  • load pipettes, tip racks, custom labware, and modules

  • define stock solutions and a target sample

  • prepare the sample automatically

  • shake the sample and move it to a temperature-controlled location

  • deactivate modules and reset the robot at the end

Connection and Robot Requirements#

This tutorial requires a direct Ethernet connection between the OT-2 and the control computer running the notebook or script. That connection may be provided through USB-B Ethernet or through a LAN connection, but the control computer must be able to reach the OT-2 over the network.

Before you begin, confirm that your robot meets at least these requirements:

  • firmware version v1.1.0-25e5cea or newer

  • supported Protocol API versions from v2.0 through v2.28

You will also need the OT-2 IP address. You can find it in the Opentrons app under the network settings for the robot. Update the robot_ip field in the driver initialization below with that address. The port should remain 31950 unless you have changed it on your system.

Prerequisites#

Before you begin, make sure you have:

  • installed AFL-automation with Opentrons support

  • an Ethernet connection between the OT-2 and the control computer running this tutorial

  • the OT-2 IP address from the Opentrons app network settings

  • local copies of any custom labware JSON files used in the workflow

  • a physical deck layout that matches the slots and modules used below

Install the Opentrons extra if needed:

pip install AFL-automation[opentrons]

Step 1: Create the Driver#

Start by creating an OT2Prepare instance and pointing it at the robot. This tutorial uses the preparation-oriented wrapper because it combines deck control with stock and sample preparation logic.

Replace the example robot_ip value below with the IP address of your own OT-2.

from AFL.automation.prepare.OT2Prepare import OT2Prepare
import json
import time

driver = OT2Prepare(
    overrides={
        "robot_ip": "169.254.59.185",
        "robot_port": "31950",
    }
)

If you are repeating the tutorial, clear any previous state before continuing.

driver.reset_stocks()
driver.reset_deck()
driver.reset()

Step 2: Load Tip Racks and Pipettes#

Next, load the tip racks and attach the pipettes that will be used for transfers. The left mount carries a p20_single_gen2 and the right mount carries a p300_single_gen2.

driver.load_labware(name="opentrons_96_tiprack_20ul", slot="10")
driver.load_instrument(
    name="p20_single_gen2",
    mount="left",
    tip_rack_slots=["10"],
)

driver.load_labware(name="opentrons_96_tiprack_300ul", slot="11")
driver.load_instrument(
    name="p300_single_gen2",
    mount="right",
    tip_rack_slots=["11"],
)

At this point the driver can choose between the loaded pipettes when it plans transfers.

Step 3: Load Custom Source Labware#

The stock solutions in this example live in a custom vial holder. Load the labware definition from JSON and send it to the robot as part of the labware load call.

with open("./labware/ice_slurry_holder_20ml_3x2.json", "r") as f:
    custom_labware_def = json.load(f)

driver.load_labware(
    name="ice_slurry_holder",
    slot="2",
    labware_json=custom_labware_def,
)

When labware_json is provided, the driver uploads the definition and can reuse it in later runs.

Step 4: Load the Heater-Shaker Assembly#

Now load a heater-shaker module in slot 4 and place the destination plate on top of it. In this example, the adapter and plate are loaded as separate steps.

heater_shaker_id = driver.load_module("heaterShakerModuleV1", slot="4")

driver.unlatch_shaker(module_id=heater_shaker_id)
driver.load_labware(
    name="opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep",
    slot="4",
    module=heater_shaker_id,
)
driver.load_labware(
    name="nest_96_wellplate_2ml_deep",
    slot="4",
    module=heater_shaker_id,
)
driver.latch_shaker(module_id=heater_shaker_id)

This is the destination where the prepared sample will initially be mixed.

Step 5: Load the Temperature Module#

The final sample is transferred to a vial holder mounted on a temperature module.

temp_module_id = driver.load_module("temperatureModuleV1", slot="3")

with open("./labware/5ml_vial_holder_1x1_hightemp.json", "r") as f:
    vial_holder_def = json.load(f)

driver.load_labware(
    name="vial_holder",
    slot="3",
    labware_json=vial_holder_def,
    module=temp_module_id,
)

Notice that module-backed labware is associated with the module identifier rather than treated as a plain deck slot load.

Step 6: Define Components and Stock Solutions#

With the deck configured, define the components and stock solutions that the preparation layer will use to plan the sample.

driver.reset_stocks()
driver.add_component(name="H2O", formula="H2O", density="1.0 g/ml")
driver.add_component(name="YCl3", formula="YCl3")
driver.add_component(name="BSA")

driver.add_stock({
    "name": "stock_BSA",
    "location": "2A1",
    "concentrations": {"BSA": "200 mg/ml"},
    "volumes": {"H2O": "20 ml"},
    "total_volume": "20 ml",
    "solutes": ["BSA"],
})

driver.add_stock({
    "name": "stock_YCl3",
    "location": "2A2",
    "molarities": {"YCl3": "1 mol/L"},
    "volumes": {"H2O": "10 ml"},
    "total_volume": "10 ml",
    "solutes": ["YCl3"],
})

driver.add_stock({
    "name": "stock_H2O",
    "location": "2A3",
    "volumes": {"H2O": "20 ml"},
    "total_volume": "20 ml",
})

The stock metadata is what allows the driver to compute a feasible preparation plan.

Step 7: Define and Prepare the Target Sample#

Now describe the sample you want to make and ask the driver whether it can be prepared from the stocks currently on deck.

target = {
    "name": "bsa_ycl3_sample",
    "concentrations": {"BSA": "175 mg/ml"},
    "molarities": {"YCl3": "43 mmol/L"},
    "volumes": {"H2O": "1 ml"},
    "total_volume": "1 ml",
    "solutes": ["BSA", "YCl3"],
    "location": "4A1",
}

feasible = driver.is_feasible(target)[0]
print("Feasible solution:", feasible)

If the target is feasible, execute the preparation into the destination well.

result, dest = driver.prepare(target, dest=target["location"])

During this step, the driver selects an appropriate loaded pipette and breaks transfers into smaller operations when necessary.

Step 8: Mix the Sample and Move It#

After the sample is prepared, briefly mix it on the heater-shaker and then transfer it to the vial holder on the temperature module.

driver.set_shake(400, module_id=heater_shaker_id)
time.sleep(5)
driver.stop_shake(module_id=heater_shaker_id)

driver.transfer(
    source=result["destination"],
    dest="3A1",
    volume=float(result["total_volume"].replace("ul", "").strip()),
    source_z_offset=1.0,
)

The source_z_offset helps keep the tip slightly above the bottom of the well during aspiration.

Step 9: Control the Temperature Module#

With the sample in its final location, step through a few temperatures and inspect the module status after each change.

for temp_c in [10.0, 30.0, 50.0]:
    print(f"Setting sample temperature to {temp_c} C")
    driver.set_tempmodule_temperature(temp_module_id, temp_c)
    time.sleep(10)
    driver.get_tempmodule_status()

driver.deactivate_tempmodule(temp_module_id)

This demonstrates both active temperature control and clean module shutdown.

Step 10: Finish the Run#

When the example is complete, home the robot and reset the driver state.

driver.home()
driver.reset()

What This Tutorial Demonstrated#

You have now walked through a full OT-2 HTTP driver example that combines:

  • deck setup

  • custom labware loading

  • module control

  • stock-aware sample preparation

  • direct liquid transfer

  • temperature control

  • end-of-run cleanup

From here, you can adapt the same pattern to your own deck layouts, stock definitions, and sample recipes.