{ "cells": [ { "cell_type": "markdown", "id": "mantis-00", "metadata": {}, "source": [ "# Formulatrix Mantis\n", "\n", "The Mantis is a liquid dispenser from Formulatrix that dispenses through exchangeable chips. PyLabRobot connects to it, identifies it, reads its firmware versions, name, motors and pressure sensors, brings the arm and Z up and parks them at stop (v4), locks and unlocks the plate clamp, reads which chip sits in each input slot, and shows colours on a v4's LED bar. Nothing else in this driver moves the device, switches the pump on or opens a valve." ] }, { "cell_type": "markdown", "id": "mantis-01", "metadata": {}, "source": [ "```{device-card} formulatrix-mantis\n", "```" ] }, { "cell_type": "markdown", "id": "mantis-02", "metadata": {}, "source": [ "```{warning}\n", "This driver has run on one Mantis v4, on macOS and on Windows; no v3 has run it yet. The arm's bring-up and the stop that parks it have not yet run on a device through this driver; run them with the head clear and someone at the power switch. `setup()` logs a warning on a firmware set not seen on a Mantis yet, which includes every v3. If you run this guide on a Mantis, please [report the result](../../needs-testing.md).\n", "```" ] }, { "cell_type": "markdown", "id": "mantis-03", "metadata": {}, "source": [ "## Properties\n", "\n", "| | |\n", "|---|---|\n", "| Generations | v3 and v4, told apart at setup |\n", "| Reads | firmware versions, device name, motor limits and state, the arm's absolute encoders (v4), line pressures and the pressure board's state |\n", "| Interface | USB, an FTDI bridge (VID 0x0403 / PID 0x6010): interface B for commands, interface A for the RFID reader |\n", "| Sequencer | queues PPI writes and moves of the three motors, and waits until each is played; internal, for the features that act |\n", "| Arm (v4) | brought up at setup from where its absolute encoders read; Z homed; parked at idle at stop |\n", "| Plate clamp | lock at one of the unit's levels, waiting for clamping done; unlock |\n", "| RFID | the chip in each input slot: serial, chip type, date of manufacture |\n", "| LED bar (v4) | one colour, one per LED, or an animation; shows setup, with the arm's bring-up; off at stop |\n", "| Unit configuration | the clamp's lock levels, each slot's RFID antenna and the arm's calibration, from the unit's configuration folder or a saved file; the first two else from a recording of its generation |\n", "| Not supported yet | moving the arm to a position, switching the pump or a pressure controller, chips, dispensing |" ] }, { "cell_type": "markdown", "id": "mantis-04", "metadata": {}, "source": [ "## How it talks\n", "\n", "The Mantis connects over USB through a two-channel FTDI bridge. Interface B carries binary frames to the device's boards: 0 for motion, 10 for pressure, 11 for the absolute position sensors (v4) and 12 for the LC3 board. The driver sends one command at a time and logs every event the device sends while connected, by what it carries. Interface A carries the RFID reader's commands.\n", "\n", "At setup the driver reads each board's firmware version. A device whose board 11 answers is a v4; one whose board 11 does not is taken to be a v3." ] }, { "cell_type": "markdown", "id": "mantis-05", "metadata": {}, "source": [ "## Physical setup\n", "\n", "1. Connect the Mantis to the computer with its USB cable and switch it on.\n", "2. Install the FTDI dependencies: `pip install \"pylabrobot[ftdi]\"` (see [Installation](../../getting-started/installation.md)).\n", "3. Close any other software connected to the Mantis." ] }, { "cell_type": "markdown", "id": "unit-configuration", "metadata": {}, "source": [ "## Unit configuration\n", "\n", "Some of a Mantis's values are set per unit and cannot be read off the device; they sit in the unit's `.config` files, in `Data/Device/Configs`. Give setup that folder once, then save the configuration: the saved file carries the unit's values from then on, and the folder is no longer needed. So far these are the plate clamp's lock levels, each input slot's RFID antenna and, on a v4, the arm's calibration: each arm motor's offset, absolute encoder reference and encoder correction.\n", "\n", "Without the clamp's levels or the antennas, a recording of a unit of the same generation supplies them, with a warning: the Mantis runs, less accurately. The arm's calibration never comes from a recording: without it the arm is not brought up and setup refuses. Where the folder and a saved file both hold a value, they must agree, or setup refuses. A file saved by an earlier version still loads: a value it lacks comes from the folder, else the recording, with one warning naming it." ] }, { "cell_type": "code", "execution_count": null, "id": "unit-configuration-save", "metadata": {}, "outputs": [], "source": [ "from pylabrobot.formulatrix import Mantis\n", "\n", "mantis = Mantis(name=\"mantis\", oem_config_directory=\"path/to/Data/Device/Configs\")" ] }, { "cell_type": "markdown", "id": "mantis-08", "metadata": {}, "source": [ "## Setup\n", "\n", "`setup()` opens both links, reads the firmware versions, the motor limits and the pressure sensors' calibration, takes the unit's values and sets up the RFID reader. On a v4 it then brings the arm and Z up, as in the recorded v4 startup: it energises the arm motors, declares them where their absolute encoders read, moves them to idle, homes Z, and moves to idle again. It does so only if the head stands near idle, in front of or behind it; from anywhere else it refuses, the arm unpowered: park the arm at idle first. `setup(skip_arm=True)` reads only and moves nothing. On a v4 the LED bar shows the setup, the arm's bring-up with it, green for a second at its end, then the idle animation." ] }, { "cell_type": "code", "id": "mantis-09", "metadata": {}, "source": [ "await mantis.setup()\n", "mantis.driver.configuration" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "mantis-10", "metadata": {}, "source": [ "### Saved configuration\n", "\n", "Save what the device reported and the unit's values. Build the device with that file from then on, `Mantis(name=\"mantis\", declared_configuration_json=\"my_mantis.json\")`, with no folder: setup refuses a device that disagrees with it on the generation or the motor limits." ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-11", "metadata": {}, "outputs": [], "source": [ "mantis.driver.save_configuration(path=\"my_mantis.json\")" ] }, { "cell_type": "markdown", "id": "mantis-simulation-text", "metadata": {}, "source": [ "### Simulation\n", "\n", "`Mantis(simulation=True)` builds a Mantis with nothing behind it; nothing reaches a wire. It takes its configuration, lock levels and RFID antennas from a Mantis v4's recording (`RECORDING_MANTIS_V4`, or a file given as `declared_configuration_json`), its reads answer from it, its plate clamp's state changes at once, every declared RFID slot reads empty, and its LED bar takes every command. Its arm stands at idle, with no offsets unless the file declares the arm's calibration, and setup brings it up at once; its pump reads off, every line at 0, with the recorded sensor calibration. Its sequencer, once started, plays each queued item at once. The device name is not simulated." ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-simulation-code", "metadata": {}, "outputs": [], "source": [ "simulated = Mantis(name=\"simulated_mantis\", simulation=True)\n", "await simulated.setup()\n", "simulated.driver.configuration" ] }, { "cell_type": "markdown", "id": "mantis-simulation-stop-text", "metadata": {}, "source": [ "Stop the simulated Mantis when done with it." ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-simulation-stop-code", "metadata": {}, "outputs": [], "source": [ "await simulated.stop()" ] }, { "cell_type": "markdown", "id": "mantis-viewer-1", "metadata": {}, "source": [ "## Visualizer\n", "\n", "Show the Mantis in the 3D visualizer. `start()` serves it and opens it in a browser." ] }, { "cell_type": "code", "id": "mantis-viewer-2", "metadata": {}, "source": [ "from pylabrobot.visualizer3D import Viewer3D\n", "\n", "viewer = Viewer3D(mantis, name=\"mantis_hello_world\")\n", "await viewer.start()" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "mantis-12", "metadata": {}, "source": [ "## Firmware versions\n", "\n", "Each board's firmware version, by board, as setup read them; None for a board that did not answer." ] }, { "cell_type": "code", "id": "mantis-13", "metadata": {}, "source": [ "mantis.driver.configuration.firmware_versions" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "mantis-14", "metadata": {}, "source": [ "## Name\n", "\n", "The name the motion firmware reports, `LiquidHandler` on the v4 we have run. Whether it differs between units is not known; it does not identify this Mantis." ] }, { "cell_type": "code", "id": "mantis-15", "metadata": {}, "source": [ "await mantis.driver.request_name()" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "mantis-16", "metadata": {}, "source": [ "## Motors\n", "\n", "Motors 0 and 1 drive the arm, motor 2 drives Z. Values are in the device's units. Each motor's limits, as setup read them:" ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-17", "metadata": {}, "outputs": [], "source": [ "mantis.driver.configuration.motor_limits" ] }, { "cell_type": "markdown", "id": "motor-state", "metadata": {}, "source": [ "### Motor state\n", "\n", "A motor's status word; `MOTOR_STATUS_ERROR_BITS` names the bits that report a fault:" ] }, { "cell_type": "code", "execution_count": null, "id": "motor-status", "metadata": {}, "outputs": [], "source": [ "await mantis.driver.request_motor_status(motor=0)" ] }, { "cell_type": "markdown", "id": "motor-positions", "metadata": {}, "source": [ "Its demand position, where the controller drives it. It is not measured: it reads back the last position declared to the motor until the motor moves. Its servo encoder position counts only while the device is powered, from about 0 at power-on wherever the motor sits." ] }, { "cell_type": "code", "execution_count": null, "id": "motor-position", "metadata": {}, "outputs": [], "source": [ "await mantis.driver.request_motor_position(motor=0)" ] }, { "cell_type": "code", "execution_count": null, "id": "motor-encoder-position", "metadata": {}, "outputs": [], "source": [ "await mantis.driver.request_encoder_position(motor=0)" ] }, { "cell_type": "markdown", "id": "motor-settings", "metadata": {}, "source": [ "Its following-error check (enabled, the largest following error), its three currents and its configuration, as the device sends them:" ] }, { "cell_type": "code", "execution_count": null, "id": "motor-following-error", "metadata": {}, "outputs": [], "source": [ "await mantis.driver.request_following_error_configuration(motor=0)" ] }, { "cell_type": "code", "execution_count": null, "id": "motor-currents", "metadata": {}, "outputs": [], "source": [ "await mantis.driver.request_motor_currents(motor=0)" ] }, { "cell_type": "code", "execution_count": null, "id": "motor-configuration", "metadata": {}, "outputs": [], "source": [ "await mantis.driver.request_motor_configuration(motor=0)" ] }, { "cell_type": "markdown", "id": "absolute-encoders", "metadata": {}, "source": [ "### Absolute encoders (v4)\n", "\n", "Board 11 reads the absolute encoder of each arm motor, 0 and 1, kept across power-off. The reading is raw, not yet an angle. Each motor's first read after connecting is not valid; the driver reads again until one is." ] }, { "cell_type": "code", "execution_count": null, "id": "absolute-encoder-position", "metadata": {}, "outputs": [], "source": [ "await mantis.driver.request_absolute_encoder_position(motor=0)" ] }, { "cell_type": "markdown", "id": "mantis-arm-1", "metadata": {}, "source": [ "## Arm (v4)\n", "\n", "Motors 0 and 1 each turn a pair of links; the two long links meet at the head. The arm frame has its origin where motor 0 turns its links, in mm; at idle the head stands at about (15.0, 31.2). Each motor's position, as the controller holds it: the arm motors in degrees, Z in mm down from its home flag." ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-arm-2", "metadata": {}, "outputs": [], "source": [ "await mantis.arm.request_joint_position()" ] }, { "cell_type": "markdown", "id": "mantis-arm-3", "metadata": {}, "source": [ "Each arm motor's angle from its absolute encoder, corrected with the unit's calibration, and where the head is:" ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-arm-4", "metadata": {}, "outputs": [], "source": [ "await mantis.arm.request_absolute_joint_position()" ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-arm-5", "metadata": {}, "outputs": [], "source": [ "await mantis.arm.request_head_position()" ] }, { "cell_type": "markdown", "id": "mantis-arm-6", "metadata": {}, "source": [ "Whether the head can reach a point, in the arm frame; nothing is clamped. The bring-up and the stop move at the arm's defaults, in degrees/s and mm/s:" ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-arm-7", "metadata": {}, "outputs": [], "source": [ "mantis.arm.is_xy_reachable(x=15.0, y=60.0)" ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-arm-8", "metadata": {}, "outputs": [], "source": [ "mantis.arm.default_arm_speed, mantis.arm.default_z_speed" ] }, { "cell_type": "markdown", "id": "pressure", "metadata": {}, "source": [ "## Pressure\n", "\n", "The pump exists only when the pressure board (board 10) answered at setup. Its three pressure controllers, `\"bottle\"`, `\"chip\"` and `\"vacuum\"`, each regulate one line from a feedback sensor. Nothing here switches the pump or a controller on.\n", "\n", "Each sensor's calibration, (scale, offset), as setup read it; it is never written nor saved. The offsets are re-zeroed on the device between sessions, so setup reads them every time:" ] }, { "cell_type": "code", "execution_count": null, "id": "pressure-calibration", "metadata": {}, "outputs": [], "source": [ "mantis.driver.configuration.feedback_sensor_parameters" ] }, { "cell_type": "markdown", "id": "pressure-measure", "metadata": {}, "source": [ "A line's pressure, in psi; inHg on the vacuum:" ] }, { "cell_type": "code", "execution_count": null, "id": "pressure-measure-chip", "metadata": {}, "outputs": [], "source": [ "await mantis.pump.measure_pressure(controller=\"chip\")" ] }, { "cell_type": "markdown", "id": "pressure-state", "metadata": {}, "source": [ "Whether the pump runs, whether a controller regulates, and its status: 0 off, 1 settled, 2 unsettled." ] }, { "cell_type": "code", "execution_count": null, "id": "pump-on", "metadata": {}, "outputs": [], "source": [ "await mantis.pump.request_pump_on()" ] }, { "cell_type": "code", "execution_count": null, "id": "controller-enabled", "metadata": {}, "outputs": [], "source": [ "await mantis.pump.request_controller_enabled(controller=\"chip\")" ] }, { "cell_type": "code", "execution_count": null, "id": "controller-status", "metadata": {}, "outputs": [], "source": [ "await mantis.pump.request_controller_status(controller=\"chip\")" ] }, { "cell_type": "markdown", "id": "mantis-clamp-text", "metadata": {}, "source": [ "## Plate clamp\n", "\n", "The clamp holds the plate on the deck. `lock` releases it, locks it at one of the unit's levels, 1 the weakest, and returns once the device reports clamping done; if it does not, the clamp is released again and an error is raised. `stop()` releases a clamp that does not read unclamping done.\n", "\n", "It exists only when the LC3 board (board 12) answered at setup. Its levels are the declared ones:" ] }, { "cell_type": "code", "execution_count": null, "id": "plate-clamp-levels", "metadata": {}, "outputs": [], "source": [ "mantis.plate_clamp.levels" ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-clamp-lock-code", "metadata": {}, "outputs": [], "source": [ "await mantis.plate_clamp.lock(level=2)" ] }, { "cell_type": "markdown", "id": "mantis-clamp-state-text", "metadata": {}, "source": [ "The clamp's state: 0 idle, 1 stopped, 2 clamping, 3 clamping done, 4 clamping error timeout, 5 unclamping, 6 unclamping done, 7 unclamping error timeout, 8 motor fault. A lock that reads 4 or 8, or an unlock that reads 7 or 8, raises `PlateClampError` at once." ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-clamp-state-code", "metadata": {}, "outputs": [], "source": [ "await mantis.plate_clamp.request_state()" ] }, { "cell_type": "markdown", "id": "mantis-clamp-unlock-text", "metadata": {}, "source": [ "Release the clamp:" ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-clamp-unlock-code", "metadata": {}, "outputs": [], "source": [ "await mantis.plate_clamp.unlock()" ] }, { "cell_type": "markdown", "id": "rfid", "metadata": {}, "source": [ "## RFID\n", "\n", "The RFID reader reads the tag of the chip in each input slot, through that slot's antenna. A slot with no chip reads None. It exists only when the reader answered on interface A at setup; a reader that does not answer leaves setup working, without it. Each slot's antenna, as declared:" ] }, { "cell_type": "code", "execution_count": null, "id": "rfid-antennas", "metadata": {}, "outputs": [], "source": [ "mantis.rfid.slot_to_antenna" ] }, { "cell_type": "markdown", "id": "rfid-read", "metadata": {}, "source": [ "Read one slot, or every slot in turn:" ] }, { "cell_type": "code", "execution_count": null, "id": "rfid-read-slot", "metadata": {}, "outputs": [], "source": [ "await mantis.rfid.request_tag(slot=1)" ] }, { "cell_type": "code", "execution_count": null, "id": "rfid-read-all", "metadata": {}, "outputs": [], "source": [ "await mantis.rfid.request_tags()" ] }, { "cell_type": "markdown", "id": "led-bar", "metadata": {}, "source": [ "## LED bar\n", "\n", "The v4's LED bar shows what the device does: blue while setup runs, green for a second when it is done, then the idle animation; off at stop. An action that fails leaves it red. It exists only on a v4 whose LC3 board (board 12) answered at setup; a v3 has none. Its LED count is the unit's (`NumberOfLed` in `Device.config`), as declared:" ] }, { "cell_type": "code", "execution_count": null, "id": "led-bar-count", "metadata": {}, "outputs": [], "source": [ "mantis.led_bar.number_of_leds" ] }, { "cell_type": "markdown", "id": "led-bar-color-text", "metadata": {}, "source": [ "Show one colour on every LED: a name (`\"blue\"`, `\"green\"`, ...), a web hex or an RGB565 value. Any animation stops first:" ] }, { "cell_type": "code", "execution_count": null, "id": "led-bar-color", "metadata": {}, "outputs": [], "source": [ "await mantis.led_bar.set_color(color=\"#FF0080\")" ] }, { "cell_type": "markdown", "id": "led-bar-per-led-text", "metadata": {}, "source": [ "Or one colour per LED, as many as it has:" ] }, { "cell_type": "code", "execution_count": null, "id": "led-bar-per-led", "metadata": {}, "outputs": [], "source": [ "await mantis.led_bar.set_color(color=[\"green\"] * 10 + [\"off\"] * 10)" ] }, { "cell_type": "markdown", "id": "led-bar-action-color-text", "metadata": {}, "source": [ "Each action's colour is an attribute of the bar (`setup_color`, `setup_done_color`, `error_color`, `dispense_progress_color`, ...), checked when it is set; each bar has its own:" ] }, { "cell_type": "code", "execution_count": null, "id": "led-bar-action-color", "metadata": {}, "outputs": [], "source": [ "mantis.led_bar.setup_color = \"#FF0080\"" ] }, { "cell_type": "markdown", "id": "led-bar-progress-text", "metadata": {}, "source": [ "Show progress: a share of the LEDs from the left in `dispense_progress_color`, the rest off. Each frame holds the link about 62 ms, so show at most one per LED in an action:" ] }, { "cell_type": "code", "execution_count": null, "id": "led-bar-progress", "metadata": {}, "outputs": [], "source": [ "await mantis.led_bar.show_progress(done=3, total=8)" ] }, { "cell_type": "markdown", "id": "led-bar-start-animation-text", "metadata": {}, "source": [ "Play the idle animation, `\"wave\"`, repeating:" ] }, { "cell_type": "code", "execution_count": null, "id": "led-bar-start-animation", "metadata": {}, "outputs": [], "source": [ "await mantis.led_bar.start_animation(effect=\"wave\")" ] }, { "cell_type": "markdown", "id": "led-bar-stop-animation-text", "metadata": {}, "source": [ "Stop it; the bar holds what it shows:" ] }, { "cell_type": "code", "execution_count": null, "id": "led-bar-stop-animation", "metadata": {}, "outputs": [], "source": [ "await mantis.led_bar.stop_animation()" ] }, { "cell_type": "markdown", "id": "led-bar-off-text", "metadata": {}, "source": [ "Turn every LED off. `Mantis(show_actions=False)` shows no action's colour, only the idle animation from setup on." ] }, { "cell_type": "code", "execution_count": null, "id": "led-bar-off", "metadata": {}, "outputs": [], "source": [ "await mantis.led_bar.turn_off()" ] }, { "cell_type": "markdown", "id": "mantis-sequencer-00", "metadata": {}, "source": [ "## Events and the sequencer\n", "\n", "The device reports events unasked: a motor starting or ending a move, a motor error, the sequencer playing a queued item, an input changing. The driver logs each by what it carries. A motor error fails at once every wait for a motion event, with `MantisMotionError`.\n", "\n", "The sequencer on board 0 plays queued items in order, each held for its duration in the sequencer's time steps, 1 ms on the v4. It is internal: the features that act queue through it. Here it holds the valve bank (PPI address 40) at 31, every valve closed, for about half a second: clear and start the sequencer, queue the write, then wait until the device reports it played. A write the device answers as not queued, its queue full, is sent again under a new item for at most `default_queue_full_timeout` seconds; no write is resent otherwise." ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-sequencer-01", "metadata": {}, "outputs": [], "source": [ "await mantis.driver._unchecked_fw_clear_sequencer()\n", "await mantis.driver._unchecked_fw_start_sequencer()\n", "item = await mantis.driver._queue_ppi_write(duration=500, start_address=40, values=[31])\n", "await mantis.driver._wait_for_queue_item(item=item, timeout=10)" ] }, { "cell_type": "code", "execution_count": null, "id": "mantis-sequencer-02", "metadata": {}, "outputs": [], "source": [ "mantis.driver.default_queue_full_timeout" ] }, { "cell_type": "markdown", "id": "mantis-20", "metadata": {}, "source": [ "## Stop\n", "\n", "Stop the visualizer and close the link. On a v4, `stop()` first aborts the sequencer and stops every motor; an arm brought up is then parked at idle and Z lowered to 20 mm, as in the recorded close, unless the head is away from idle. Then the plate clamp is released." ] }, { "cell_type": "code", "id": "mantis-21", "metadata": {}, "source": [ "await viewer.stop()\n", "await mantis.stop()" ], "execution_count": null, "outputs": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }