{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Validating against log file example\n",
    "\n",
    "All communication between PLR and the outside world (the hardware) happens through the io layer. This is a layer below backends, and is responsible for sending and receiving messages to and from the hardware. Schematically,\n",
    "\n",
    "```\n",
    "Frontends <-> backends <-> io <-> hardware\n",
    "```\n",
    "\n",
    "PLR supports capturing all communication in the io layer, both write and read commands. This can later be played back to validate that a protocol has not changed. The key here is that if we send the same commands to the hardware, the hardware will do the same thing. \"Reading\" data (from the capture file) is useful because some protocols are dynamic and depend on responses from the hardware.\n",
    "\n",
    "In this notebook, we will run a simple protocol on a robot while capturing all data passing through the io layer. We will then replay the capture file while executing the protocol again to demonstrate how validation works. Finally, we slightly modify the protocol and show that the validation fails."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Defining a simple protocol"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pylabrobot\n",
    "from pylabrobot.liquid_handling import LiquidHandler\n",
    "from pylabrobot.liquid_handling.backends import STAR\n",
    "from pylabrobot.resources.hamilton import STARDeck\n",
    "\n",
    "backend = STAR()\n",
    "lh = LiquidHandler(backend=backend, deck=STARDeck())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pylabrobot.resources import TIP_CAR_480_A00, HT\n",
    "tip_car = TIP_CAR_480_A00(name=\"tip_car\")\n",
    "tip_car[0] = tr = HT(name=\"ht\")\n",
    "lh.deck.assign_child_resource(tip_car, rails=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "async def simple_protocol(tips):\n",
    "  await lh.setup()\n",
    "  await lh.pick_up_tips(tips)\n",
    "  await lh.return_tips()\n",
    "  await lh.stop()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Capturing data during protocol execution"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Do a real run first, without validating against a capture file. This will generate the capture file you can later compare against.\n",
    "\n",
    "While it might seem cumbersome, this actually ensures you have a real working protocol before doing validation. The benefit of using capture files is whenever you change your protocol and have seen it run, you can just grab the capture file and use it for validation. No need to manually write tests."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Validation file written to validation.json\n"
     ]
    }
   ],
   "source": [
    "validation_file = \"./validation.json\"\n",
    "pylabrobot.start_capture(validation_file)\n",
    "await simple_protocol(tr[\"A1:H1\"])\n",
    "pylabrobot.stop_capture()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The validation file is just json:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{\n",
      "  \"version\": \"0.1.6\",\n",
      "  \"commands\": [\n",
      "    {\n",
      "      \"module\": \"usb\",\n",
      "      \"device_id\": \"[0x8af:0x8000][][]\",\n",
      "      \"action\": \"write\",\n",
      "      \"data\": \"C0RTid0001\"\n",
      "    },\n",
      "    {\n",
      "      \"module\": \"usb\",\n",
      "      \"device_id\": \"[0x8af:0x8000][][]\",\n",
      "      \"action\": \"read\",\n",
      "      \"data\": \"C0RTid0001er00/00rt0 0 0 0 0 0 0 0\"\n",
      "    },\n"
     ]
    }
   ],
   "source": [
    "!head -n15 validation.json"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Replaying the capture file for validation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "On validation, before calling setup, run `pylabrobot.validate` to enable the validation. Pass a capture file that contains the commands we should check against.\n",
    "\n",
    "Call `pylabrobot.end_validation` at the end to make sure there are no remaining commands in the capture file. This marks the end of the validation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Validation successful!\n"
     ]
    }
   ],
   "source": [
    "pylabrobot.validate(validation_file)\n",
    "await simple_protocol(tr[\"A1:H1\"])\n",
    "pylabrobot.end_validation()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Failing validation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "When validation is not successful, we use the Needleman-Wunsch algorithm to find the difference between the expected and the actual output."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "expected: C0TPid0009xp01179 01179 01179 01179 01179 01179 01179 01179yp1458 1368 1278 1188 1098 1008 0918 0828tm1 1 1 1 1 1 1 1tt01tp2266tz2166th2450td0\n",
      "actual:   C0TPid0009xp01269 01269 01269 01269 01269 01269 01269 01269yp1458 1368 1278 1188 1098 1008 0918 0828tm1 1 1 1 1 1 1 1tt01tp2266tz2166th2450td0\n",
      "                        ^^    ^^    ^^    ^^    ^^    ^^    ^^    ^^                                                                                    \n"
     ]
    },
    {
     "ename": "ValidationError",
     "evalue": "Data mismatch: difference was written to stdout.",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mValidationError\u001b[0m                           Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[7], line 2\u001b[0m\n\u001b[1;32m      1\u001b[0m pylabrobot\u001b[38;5;241m.\u001b[39mvalidate(validation_file)\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01mawait\u001b[39;00m simple_protocol(tr[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mA2:H2\u001b[39m\u001b[38;5;124m\"\u001b[39m])\n\u001b[1;32m      3\u001b[0m pylabrobot\u001b[38;5;241m.\u001b[39mend_validation()\n",
      "Cell \u001b[0;32mIn[3], line 3\u001b[0m, in \u001b[0;36msimple_protocol\u001b[0;34m(tips)\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[38;5;28;01masync\u001b[39;00m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21msimple_protocol\u001b[39m(tips):\n\u001b[1;32m      2\u001b[0m   \u001b[38;5;28;01mawait\u001b[39;00m lh\u001b[38;5;241m.\u001b[39msetup()\n\u001b[0;32m----> 3\u001b[0m   \u001b[38;5;28;01mawait\u001b[39;00m lh\u001b[38;5;241m.\u001b[39mpick_up_tips(tips)\n\u001b[1;32m      4\u001b[0m   \u001b[38;5;28;01mawait\u001b[39;00m lh\u001b[38;5;241m.\u001b[39mreturn_tips()\n\u001b[1;32m      5\u001b[0m   \u001b[38;5;28;01mawait\u001b[39;00m lh\u001b[38;5;241m.\u001b[39mstop()\n",
      "File \u001b[0;32m~/retro/pylabrobot/pylabrobot/machines/machine.py:35\u001b[0m, in \u001b[0;36mneed_setup_finished.<locals>.wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m     33\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msetup_finished:\n\u001b[1;32m     34\u001b[0m   \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mThe setup has not finished. See `setup`.\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m---> 35\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mawait\u001b[39;00m func(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n",
      "File \u001b[0;32m~/retro/pylabrobot/pylabrobot/liquid_handling/liquid_handler.py:467\u001b[0m, in \u001b[0;36mLiquidHandler.pick_up_tips\u001b[0;34m(self, tip_spots, use_channels, offsets, **backend_kwargs)\u001b[0m\n\u001b[1;32m    464\u001b[0m   (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhead[channel]\u001b[38;5;241m.\u001b[39mcommit \u001b[38;5;28;01mif\u001b[39;00m success \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhead[channel]\u001b[38;5;241m.\u001b[39mrollback)()\n\u001b[1;32m    466\u001b[0m \u001b[38;5;66;03m# trigger callback\u001b[39;00m\n\u001b[0;32m--> 467\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_trigger_callback\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m    468\u001b[0m \u001b[43m  \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mpick_up_tips\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m    469\u001b[0m \u001b[43m  \u001b[49m\u001b[43mliquid_handler\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m    470\u001b[0m \u001b[43m  \u001b[49m\u001b[43moperations\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpickups\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m    471\u001b[0m \u001b[43m  \u001b[49m\u001b[43muse_channels\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43muse_channels\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m    472\u001b[0m \u001b[43m  \u001b[49m\u001b[43merror\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43merror\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m    473\u001b[0m \u001b[43m  \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mbackend_kwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m    474\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n",
      "File \u001b[0;32m~/retro/pylabrobot/pylabrobot/liquid_handling/liquid_handler.py:2204\u001b[0m, in \u001b[0;36mLiquidHandler._trigger_callback\u001b[0;34m(self, method_name, error, *args, **kwargs)\u001b[0m\n\u001b[1;32m   2202\u001b[0m   callback(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39margs, error\u001b[38;5;241m=\u001b[39merror, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[1;32m   2203\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m error \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m-> 2204\u001b[0m   \u001b[38;5;28;01mraise\u001b[39;00m error\n",
      "File \u001b[0;32m~/retro/pylabrobot/pylabrobot/liquid_handling/liquid_handler.py:451\u001b[0m, in \u001b[0;36mLiquidHandler.pick_up_tips\u001b[0;34m(self, tip_spots, use_channels, offsets, **backend_kwargs)\u001b[0m\n\u001b[1;32m    449\u001b[0m error: Optional[\u001b[38;5;167;01mException\u001b[39;00m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m    450\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 451\u001b[0m   \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mbackend\u001b[38;5;241m.\u001b[39mpick_up_tips(ops\u001b[38;5;241m=\u001b[39mpickups, use_channels\u001b[38;5;241m=\u001b[39muse_channels, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mbackend_kwargs)\n\u001b[1;32m    452\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[1;32m    453\u001b[0m   error \u001b[38;5;241m=\u001b[39m e\n",
      "File \u001b[0;32m~/retro/pylabrobot/pylabrobot/liquid_handling/backends/hamilton/STAR.py:1484\u001b[0m, in \u001b[0;36mSTAR.pick_up_tips\u001b[0;34m(self, ops, use_channels, begin_tip_pick_up_process, end_tip_pick_up_process, minimum_traverse_height_at_beginning_of_a_command, pickup_method)\u001b[0m\n\u001b[1;32m   1481\u001b[0m pickup_method \u001b[38;5;241m=\u001b[39m pickup_method \u001b[38;5;129;01mor\u001b[39;00m tip\u001b[38;5;241m.\u001b[39mpickup_method\n\u001b[1;32m   1483\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m-> 1484\u001b[0m   \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpick_up_tip(\n\u001b[1;32m   1485\u001b[0m     x_positions\u001b[38;5;241m=\u001b[39mx_positions,\n\u001b[1;32m   1486\u001b[0m     y_positions\u001b[38;5;241m=\u001b[39my_positions,\n\u001b[1;32m   1487\u001b[0m     tip_pattern\u001b[38;5;241m=\u001b[39mchannels_involved,\n\u001b[1;32m   1488\u001b[0m     tip_type_idx\u001b[38;5;241m=\u001b[39mttti,\n\u001b[1;32m   1489\u001b[0m     begin_tip_pick_up_process\u001b[38;5;241m=\u001b[39mbegin_tip_pick_up_process,\n\u001b[1;32m   1490\u001b[0m     end_tip_pick_up_process\u001b[38;5;241m=\u001b[39mend_tip_pick_up_process,\n\u001b[1;32m   1491\u001b[0m     minimum_traverse_height_at_beginning_of_a_command\u001b[38;5;241m=\u001b[39mminimum_traverse_height_at_beginning_of_a_command,\n\u001b[1;32m   1492\u001b[0m     pickup_method\u001b[38;5;241m=\u001b[39mpickup_method,\n\u001b[1;32m   1493\u001b[0m   )\n\u001b[1;32m   1494\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m STARFirmwareError \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[1;32m   1495\u001b[0m   \u001b[38;5;28;01mif\u001b[39;00m plr_e \u001b[38;5;241m:=\u001b[39m convert_star_firmware_error_to_plr_error(e):\n",
      "File \u001b[0;32m~/retro/pylabrobot/pylabrobot/liquid_handling/backends/hamilton/STAR.py:98\u001b[0m, in \u001b[0;36mneed_iswap_parked.<locals>.wrapper\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m     93\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39miswap_installed \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39miswap_parked:\n\u001b[1;32m     94\u001b[0m   \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpark_iswap(\n\u001b[1;32m     95\u001b[0m     minimum_traverse_height_at_beginning_of_a_command\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mint\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_iswap_traversal_height \u001b[38;5;241m*\u001b[39m \u001b[38;5;241m10\u001b[39m)\n\u001b[1;32m     96\u001b[0m   )\n\u001b[0;32m---> 98\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m method(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[1;32m    100\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m result\n",
      "File \u001b[0;32m~/retro/pylabrobot/pylabrobot/liquid_handling/backends/hamilton/STAR.py:4062\u001b[0m, in \u001b[0;36mSTAR.pick_up_tip\u001b[0;34m(self, x_positions, y_positions, tip_pattern, tip_type_idx, begin_tip_pick_up_process, end_tip_pick_up_process, minimum_traverse_height_at_beginning_of_a_command, pickup_method)\u001b[0m\n\u001b[1;32m   4055\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m (\n\u001b[1;32m   4056\u001b[0m   \u001b[38;5;241m0\u001b[39m \u001b[38;5;241m<\u001b[39m\u001b[38;5;241m=\u001b[39m end_tip_pick_up_process \u001b[38;5;241m<\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;241m3600\u001b[39m\n\u001b[1;32m   4057\u001b[0m ), \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mend_tip_pick_up_process must be between 0 and 3600\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m   4058\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m (\n\u001b[1;32m   4059\u001b[0m   \u001b[38;5;241m0\u001b[39m \u001b[38;5;241m<\u001b[39m\u001b[38;5;241m=\u001b[39m minimum_traverse_height_at_beginning_of_a_command \u001b[38;5;241m<\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;241m3600\u001b[39m\n\u001b[1;32m   4060\u001b[0m ), \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mminimum_traverse_height_at_beginning_of_a_command must be between 0 and 3600\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m-> 4062\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msend_command(\n\u001b[1;32m   4063\u001b[0m   module\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mC0\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m   4064\u001b[0m   command\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mTP\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m   4065\u001b[0m   tip_pattern\u001b[38;5;241m=\u001b[39mtip_pattern,\n\u001b[1;32m   4066\u001b[0m   read_timeout\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m60\u001b[39m,\n\u001b[1;32m   4067\u001b[0m   xp\u001b[38;5;241m=\u001b[39m[\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mx\u001b[38;5;132;01m:\u001b[39;00m\u001b[38;5;124m05\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m x \u001b[38;5;129;01min\u001b[39;00m x_positions],\n\u001b[1;32m   4068\u001b[0m   yp\u001b[38;5;241m=\u001b[39m[\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00my\u001b[38;5;132;01m:\u001b[39;00m\u001b[38;5;124m04\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m y \u001b[38;5;129;01min\u001b[39;00m y_positions],\n\u001b[1;32m   4069\u001b[0m   tm\u001b[38;5;241m=\u001b[39mtip_pattern,\n\u001b[1;32m   4070\u001b[0m   tt\u001b[38;5;241m=\u001b[39m\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtip_type_idx\u001b[38;5;132;01m:\u001b[39;00m\u001b[38;5;124m02\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m   4071\u001b[0m   tp\u001b[38;5;241m=\u001b[39m\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mbegin_tip_pick_up_process\u001b[38;5;132;01m:\u001b[39;00m\u001b[38;5;124m04\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m   4072\u001b[0m   tz\u001b[38;5;241m=\u001b[39m\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mend_tip_pick_up_process\u001b[38;5;132;01m:\u001b[39;00m\u001b[38;5;124m04\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m   4073\u001b[0m   th\u001b[38;5;241m=\u001b[39m\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mminimum_traverse_height_at_beginning_of_a_command\u001b[38;5;132;01m:\u001b[39;00m\u001b[38;5;124m04\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m   4074\u001b[0m   td\u001b[38;5;241m=\u001b[39mpickup_method\u001b[38;5;241m.\u001b[39mvalue,\n\u001b[1;32m   4075\u001b[0m )\n",
      "File \u001b[0;32m~/retro/pylabrobot/pylabrobot/liquid_handling/backends/hamilton/base.py:247\u001b[0m, in \u001b[0;36mHamiltonLiquidHandler.send_command\u001b[0;34m(self, module, command, auto_id, tip_pattern, write_timeout, read_timeout, wait, fmt, **kwargs)\u001b[0m\n\u001b[1;32m    222\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Send a firmware command to the Hamilton machine.\u001b[39;00m\n\u001b[1;32m    223\u001b[0m \n\u001b[1;32m    224\u001b[0m \u001b[38;5;124;03mArgs:\u001b[39;00m\n\u001b[0;32m   (...)\u001b[0m\n\u001b[1;32m    237\u001b[0m \u001b[38;5;124;03m  A dictionary containing the parsed response, or None if no response was read within `timeout`.\u001b[39;00m\n\u001b[1;32m    238\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m    240\u001b[0m cmd, id_ \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_assemble_command(\n\u001b[1;32m    241\u001b[0m   module\u001b[38;5;241m=\u001b[39mmodule,\n\u001b[1;32m    242\u001b[0m   command\u001b[38;5;241m=\u001b[39mcommand,\n\u001b[0;32m   (...)\u001b[0m\n\u001b[1;32m    245\u001b[0m   \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[1;32m    246\u001b[0m )\n\u001b[0;32m--> 247\u001b[0m resp \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_write_and_read_command(\n\u001b[1;32m    248\u001b[0m   id_\u001b[38;5;241m=\u001b[39mid_,\n\u001b[1;32m    249\u001b[0m   cmd\u001b[38;5;241m=\u001b[39mcmd,\n\u001b[1;32m    250\u001b[0m   write_timeout\u001b[38;5;241m=\u001b[39mwrite_timeout,\n\u001b[1;32m    251\u001b[0m   read_timeout\u001b[38;5;241m=\u001b[39mread_timeout,\n\u001b[1;32m    252\u001b[0m   wait\u001b[38;5;241m=\u001b[39mwait,\n\u001b[1;32m    253\u001b[0m )\n\u001b[1;32m    254\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m resp \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m fmt \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m    255\u001b[0m   \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_parse_response(resp, fmt)\n",
      "File \u001b[0;32m~/retro/pylabrobot/pylabrobot/liquid_handling/backends/hamilton/base.py:267\u001b[0m, in \u001b[0;36mHamiltonLiquidHandler._write_and_read_command\u001b[0;34m(self, id_, cmd, write_timeout, read_timeout, wait)\u001b[0m\n\u001b[1;32m    258\u001b[0m \u001b[38;5;28;01masync\u001b[39;00m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_write_and_read_command\u001b[39m(\n\u001b[1;32m    259\u001b[0m   \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m    260\u001b[0m   id_: Optional[\u001b[38;5;28mint\u001b[39m],\n\u001b[0;32m   (...)\u001b[0m\n\u001b[1;32m    264\u001b[0m   wait: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m,\n\u001b[1;32m    265\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Optional[\u001b[38;5;28mstr\u001b[39m]:\n\u001b[1;32m    266\u001b[0m \u001b[38;5;250m  \u001b[39m\u001b[38;5;124;03m\"\"\"Write a command to the Hamilton machine and read the response.\"\"\"\u001b[39;00m\n\u001b[0;32m--> 267\u001b[0m   \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mio\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mwrite\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcmd\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mencode\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mwrite_timeout\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m    269\u001b[0m   \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m wait:\n\u001b[1;32m    270\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n",
      "File \u001b[0;32m~/retro/pylabrobot/pylabrobot/io/usb.py:325\u001b[0m, in \u001b[0;36mUSBValidator.write\u001b[0;34m(self, data, timeout)\u001b[0m\n\u001b[1;32m    323\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m next_command\u001b[38;5;241m.\u001b[39mdata \u001b[38;5;241m==\u001b[39m data\u001b[38;5;241m.\u001b[39mdecode(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124municode_escape\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[1;32m    324\u001b[0m   align_sequences(expected\u001b[38;5;241m=\u001b[39mnext_command\u001b[38;5;241m.\u001b[39mdata, actual\u001b[38;5;241m=\u001b[39mdata\u001b[38;5;241m.\u001b[39mdecode(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124municode_escape\u001b[39m\u001b[38;5;124m\"\u001b[39m))\n\u001b[0;32m--> 325\u001b[0m   \u001b[38;5;28;01mraise\u001b[39;00m ValidationError(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mData mismatch: difference was written to stdout.\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
      "\u001b[0;31mValidationError\u001b[0m: Data mismatch: difference was written to stdout."
     ]
    }
   ],
   "source": [
    "pylabrobot.validate(validation_file)\n",
    "await simple_protocol(tr[\"A2:H2\"])\n",
    "pylabrobot.end_validation()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "env",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.19"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}