Integrating liquid handling chemistry

Lego robotics and programming in Python.

Group Size 3-4

Grade Level 9-11

Time required 4-5 hrs

Aim of the lesson

In this lesson the students are going to build and program a liquid handling robot from a single Lego Mindstorms kit EV3 core set and some easily accessible additional parts. This robot can pipette liquids and address 20 cuvettes placed on a ruler. The three motors and the color sensor included in the robot set will be controlled over the EV3 Python programming language.


Worksheet 1 – Building + Manual control of robot

Fig. 1

Robot overview

1. Build pipette robot using CAD file

There are five structural modules (1_Pipette, 2_Back, 3_Front, 4_Top, 5_Trolley), the control brick module (6_Brick), and two optional modules (7_Sensor, 8_Gears) that make up the whole robot. You’ll be provided with the following CAD files to construct these parts separately.

CAD files:

  • 0_all_in_one.lxf

  • 1_Pipette.lxf

  • 3_Front.lxf

  • 4_Top.lxf

  • 5_Trolley.lxf

  • 6_Brick.lxf

  • 7_Sensor.lxf

  • 8_Gears_(with_2_Back).lxf

Open each CAD file with the Lego Digital Designer (LDD) software and switch to building mode (F7) to build robot modules by following the step-by-step instructions. The CAD file 0_all_in_one.lxf contains the whole robot. This file gives an overview and shows how the different pieces come together.

P.S. There is also an html version of the building guide, if you haven’t installed the LDD software.

Add the syringe to the pipette head and the cuvettes to the ruler.

To combine the syringe with the Lego part, a red Lego peg included in the kit can be glued to a syringe plunger. Cuvettes can be mounted on the robot via double-side tape on the ruler. Steps to be followed are shown in the following two figures.


Fig 2. To attach the syringe’s plunger to the robot, cut off the top of the plunger, insert it into the red Lego piece and apply some instant glue. Cut away some of the plastic holding piece of the syringe’s tube in order to fit it into the robot. Cuvettes can be easily placed onto a ruler using double-sided tape.


Fig 3

  1. Prepared syringe with super glued red Lego piece and cut holder.

B-C) Insert syringe while the green piece is temporarily removed. Then reinstall the green piece to lock the syringe in place.

  1. Double-sided carpet tape on a plastic ruler allows securely placement of up to 20 cuvettes. Cuvettes can be replaced many times and even small liquid spills do not affect the tape’s performance critically.

  2. A color sensor mounted behind the cuvettes allows for reading concentration and colors. F) Gears allow us to manually move the trolley over a crank.

3. Boot the Lego smart brick with (ev3dev) operating system.

Insert into the EV3 brick the readymade EV3Dev SD card, which your teacher will give to you.

4. Set the communication between the Lego brick and the computer

Set up the network connection between the PC and the brick, as described here. You may also set up a graphical SSH connection between the EV3 and the PC, as described here.

5. Develop a Python program to control the robot manually.

You have to develop a Python program that allows controlling all three motors by the buttons on the brick and with two touch sensors. In particular, two buttons will move the piston/syringe (1 button fast, 1 button slow), left/right buttons on the brick will move the trolley, and up/down buttons will move the pipette head.

6. Check if your robot is working properly

  • Can you move the trolley left/right? ☐YES ☐NO

  • Can you lift/lower the pipette head? ☐YES ☐NO

  • Can you fill/empty the syringe? ☐YES ☐NO

Worksheet 2 – Experiments

1. Experiment A: Manual operation – color mixing

  • Place 3 cuvettes on the trolley. Make sure they are aligned with the pipette tip: They should be just below the pipette tip.

  • Pick two colors and fill them into two cuvettes. About 80% full.


  • What colors did you pick?

Answer: ____________________________________

  • Use your program for manually mixing the two liquids in the third cuvette.

  • What color resulted after you mixed them together?

Answer: ____________________________________

2. Experiment B: Manual operation – Dilution series

  • Fill one cuvette with colored water 75% full.

  • Fill tap water into 6 more cuvettes 75% full.

  • Dip pipette tip into the colored water.

  • Fill the syringe by pressing the touch button.

  • Move tip to second cuvette and empty syringe by pressing the button.

  • Fill/empty twice to mix the solution.

  • Repeat in series to obtain something similar to the picture below.

Dilution series result


Now program the robot with Python code to do all the dilution series work completely automated with a preprogrammed sequence.

Speed, direction and degrees turned have to be adjusted.

  • What is the dilution factor from one cuvette to the next cuvette?

Answer: ____________________________________

  • What is the dilution factor from the first to the last cuvette?

Answer: ____________________________________

  • Write the color intensity of the top right cuvette.

Answer: ____________________________________

Color and concentration readout are a bit tricky. The color sensor must be placed as close as possible in front of a full cuvette. For best readouts, a white paper can be placed directly on the other side of the cuvette. The values (reflected light) can be readout in the software.

3. Experiment C: Density layers – Manual control

The teacher will give you 4 colored solutions with different salt content.

  • Which of the solutions will be the densest and therefore sink to the bottom?

  • High salt content, ☐ medium salt content, or ☐ low/no salt

  • Use your first program to manually transfer two solutions into a third well.

  • Do a 1st test by manually trying to layer blue and green.

  • Try to put in blue first. Then put green under it. Does it work? ☐Yes ☐No

  • Try to put in blue first and then put green on top. Does it work? ☐Yes ☐No

  • Try to put in green first. Then put blue under it. Does it work? ☐Yes ☐No

  • Try to put in green first and then put blue on top. Does it work? ☐Yes ☐No

  • 1: blue vs. green

Top: ____________________________________

Bottom: ____________________________________

  • 2: blue vs. red

Top: ____________________________________

Bottom: ____________________________________

  • 3: green vs. red

Top: ____________________________________

Bottom: ____________________________________

  • 4: green vs. yellow

Top: ____________________________________

Bottom: ____________________________________

  • After tests 1-4: can you tell in what order the colors form stable layers if you add all four into one cuvette?

Top: ____________________________________

Second: ____________________________________

Third: ____________________________________

Bottom: ____________________________________

  • Try to layer the 4 solutions so they stay separated.

  • Tip: Try a different order of pipetting.

  • Tip: Try different speeds of ejection.

  • Tip: Try bottom-up vs. top-down.

Density layer results


  • Call the teacher to check if your layers look good.

  • What was your strategy so the liquids mixed the least?

Answer: ____________________________________

Final questions

  • Why do the fluids mix sometimes and sometimes not?

Answer: ____________________________________

  • What floats best in water:

☐ A gold bar, ☐ block of wood, or ☐ a human swimmer?

  • What floats worst?

  • Why?

Answer: ____________________________________

  • Where is it easier to float: in a lake, the pacific, or the Dead Sea?

☐Lake ☐Pacific ☐Dead Sea

Explain your choice.

Answer: ____________________________________

Useful tips



Before you start with any liquids inside the syringe and cuvettes, you should make sure that your robots operate correctly in “dry-mode”.

Filling the syringe

To fill the syringe completely, you can always use a 180° turn from the empty position.

Release liquid

To release liquid, you can turn any degree from 1°-180° to release the desired amount. Please note that not every degree results in the same amount released! This is due to the theoretical issue that the motor rotates at a constant speed that is translated to a non-linear speed of the plunger (sinusoidal behavior) as well as practical issues such as trapped air inside the syringe which leads to a delayed release due to the air’s compressibility. Also, different syringes and syringe tips have an influence on the amount of liquid released per degree turned.

Therefore it is recommended to measure and calibrate the syringe code to your needs. In general it is safer to release liquids slower than taking them up.

Move the trolley

To find the right distance that the trolley has to move from one to the next cuvette, you can align the pipette tip over one cuvette and manually move to the next cuvette. The integrated sensor shows you how far you have travelled. This is the amount you can use to automatically travel from one cuvette to the next.

Color sensor

To use the color sensor to read out concentrations as shown below, you must place the sensor in the right place in the back of the cuvette. It must be aligned perfectly to make sure that only one cuvette is read. Also make sure to only -



Fig 4.

Color sensor placement behind a cuvette

Short help on programming

Commands/functions needed for the lesson

React to button presses and releases In this script’s loop, the highlighted command btn.process() checks for any change in the state of the buttons. If it detects a change then it triggers the corresponding ‘events’. For example, if it detects that the left button has just been pressed then it triggers a ‘left button state change’ event and a ‘button change’ event. It also assigns a value of True to the parameter state if the button is pressed and a value of False if the button is released. Event handlers respond to the events. For example, if the left button is pressed then the ‘left button state change’ event will trigger the highlighted on_left event handler which will call the function ‘left’ (the function does not have to have this name).

#!/usr/bin/env python3

from ev3dev.ev3 import \*

from time import sleep

btn = Button()

# Do something when state of any button changes:

def left(state):

  if state:

    print('Left button pressed')


    print('Left button released')

def right(state):  # neater use of 'if' follows:

  print('Right button pressed' if state else 'Right button released')

def up(state):

  print('Up button pressed' if state else 'Up button released')

def down(state):

  print('Down button pressed' if state else 'Down button released')

def enter(state):

  print('Enter button pressed' if state else 'Enter button released')

def backspace(state):

  print('Backspace button pressed' if state else 'Backspace button released')

  btn.on_left = left

  btn.on_right = right

  btn.on_up = up

  btn.on_down = down

  btn.on_enter = enter

  btn.on_backspace = backspace

  while True:  # This loop checks buttons state continuously,

    # calls appropriate event handlers

  **btn.process()** # Check for currently pressed buttons.

  # If the new state differs from the old state,

  # call the appropriate button event handlers.

    sleep(0.01)  # buttons state will be checked every 0.01 second

  # If running this script via SSH, press Ctrl+C to quit

  # if running this script from Brickman, long-press backspace button to quit

Using Motors

In EV3 Python you set the target speed by setting a value for speed_sp or ‘speed setpoint’. For the EV3 motors, speed_sp is in degrees per second so if you set speed_sp to 360 the motor will try to rotate 360° per second or one rotation per second. For the standard EV3 large motors, a speed_sp value of 1000 (degrees per second) is roughly equivalent to a power value of 100 in the standard Lego EV3 software (EV3-G). Therefore with the large motors you should always use speed_sp values in the range -1000 to +1000. In fact I recommend that you use values in the range -900 to +900 with the large motors because they may not be capable of accurately achieving speeds higher than that.

With the standard EV3 medium **motor it should be safe to use values of speed_sp up to**1400 **but **it is very convenient to assume that the maximum advisable value is 1000so that the medium motor can be considered to behave just like the large motor.

Note that speed_sp represents the TARGET speed in degrees per second but motors are of course subject to the laws of physics so the real speed may sometimes not correspond to the requested speed. For example, if you run this code mB.run_timed(time_sp=600, speed_sp=600) then you might expect the motor to turn 0.6s*600°/s=360°=1 rotation but in reality it will turn significantly less (maybe 15% less) because the inertia of the motor stops the motor from reaching its target speed instantly and therefore for a short period at the beginning of the motion the motor turns less fast than requested.

When *low *values of speed_sp are used the movements can again differ from what was requested but in this case the motor tends to move *faster *than requested, for reasons that are not yet well understood. For example, when using speed_sp=100 the motor may turn about 23% faster than requested.

For the official motor documentation click HERE.

You may want to make one or more motors turn at a given speed

  • through a given angle or number of rotations

  • for a given time

  • ‘forever’ (until the motor is stopped by a stop() command later during program execution)

Turn motor through a given angle Use run_to_rel_pos(position_sp=<angle in degrees>, speed_sp=<value>). If you want the motor to run backwards then use a negative value for position_sp rather than for speed_sp. Using a negative value for speed_sp will not work because the sign of speed_sp is ignored by this command (but not by others). Use speed_sp values between 0 and 1000 and try to avoid using values above 900 since your motor may not be able to deliver the requested speed above 900.


To make a large motor on port B turn through 360° at speed 900 and optionally apply a ‘hold’ (like a strong brake - see later):

#!/usr/bin/env python3

# so that script can be run from Brickman

from ev3dev.ev3 import *

from time import sleep

m = LargeMotor('outB')

m.run_to_rel_pos(position_sp=360, speed_sp=900, stop_action="hold")

sleep(5)   # Give the motor time to move

Run motor for a given time

Use  run_timed(time_sp=<time in milliseconds>, speed_sp=<value>)

Note that any negative sign for time_sp is ignored.


This example runs a large motor attached to port B backwards for 3 seconds with a ‘speed setpoint’ set to -750 (equivalent to a power setting of -75 in the standard EV3 software). It will work even if a second large motor is also plugged in to another motor port, since the port letter is specified. Without the last line, the program would end as soon as the motor starts to turn, so you probably wouldn’t see it move at all. Waiting for 5 seconds (1+4) ensures that the motor can turn for 3 seconds before the program ends.

#!/usr/bin/env python3

# So program can be run from Brickman

from ev3dev.ev3 import *

from time import sleep

m = LargeMotor('outB')

m.run_timed(time_sp=3000, speed_sp=-750)

print("set speed (speed_sp) = " + str(m.speed_sp))

sleep(1)  # it takes a moment for the motor to start moving

print("actual speed = " + str(m.speed))


he above example prints the value of speed_sp to the terminal window, or console. I’ve used a ‘+’ operator to concatenate (join) the text string on the left with the speed_sp value on the right, but the speed_sp value is an integer so I had to convert it to a text string with the str()function before the concatenation could take place. I think it is reasonable for newbies to use the ‘+’ operator in this way, but there is another method which is often preferred by more advanced Python programmers - see this discussion and this page. Note as you run the above program that the print statement is run as soon as the motor STARTS to move - the program does not wait until the motor has finished moving before running the next command.

The example also gives a value (m.speed) for the actual speed of the motor, one second after the motor was told to start turning. The one second delay is needed because it takes a moment for the motor to get going - without the delay the actual speed would probably be given as zero because the motor would not yet have begun turning.

Interestingly, the above program also works if the third line is changed to

m = Motor(‘outB’)

but does not work (with a large motor still attached to port B) if this line is used:

m = MediumMotor(‘outB’)

Plug a touch sensor into any sensor port

ts = TouchSensor()

Here ts is the name we choose to use for the sensor. You can use any name you wish. This command will search the ports for a Touch Sensor and save its location if it finds one. You will get an error if it doesn’t find one. If you have more than one and want to specify: ts = TouchSensor(‘in1’)

Reading the Touch Sensor


This function will return a numeric value. Since this is a touch sensor, there are only two possible values: 0 tells you the button is not currently pushed, 1 tells you the button is pushed.

Plug an color sensor into any sensor port cl = ColorSensor()

Here cl is the name we choose to use for the sensor.

Set the color sensor in reflect mode.

# Sensor operation mode - reflected light

cl.mode = ‘COL-REFLECT’

In this mode the color sensor emits light.

Reading the Color Sensor


This function will return a numeric value between 0 and 100 which represents the reflected light intensity. EV3 color sensor in COL-COLOR mode When the EV3 color sensor is in COL-COLOR mode it tries to recognize the color of standard Lego bricks placed about 5-6mm in front of the sensor (the distance is critical) and returns a corresponding integer value between 0 (unknown) and 7 (brown). The program below reads the integer once per second, converts it into the corresponding text string using a tuple and displays the string in the console. Press the touch sensor button for at least a second to stop the program. To make the program speak the colors as well as displaying their text strings, uncomment the line highlighted in blue. This will slightly increase the time between measurements as it includes a wait() function to ensure that the speech was not interrupted.

#!/usr/bin/env python3

# so that script can be run from Brickman

from ev3dev.ev3 import *

from time import sleep

# Connect EV3 color and touch sensors to any sensor ports

cl = ColorSensor()

ts = TouchSensor()

# Put the color sensor into COL-COLOR mode.



while not ts.value():    # Stop program by pressing touch sensor button






#!/usr/bin/env python3


if <condition1>:


elif <condition2>:





We can put as many elif as we want.

elif and else parts are optional

Loop (while-statement)

#!/usr/bin/env pseudocode

while <condition1>:


The loop ends when the condition becomes false. If it is false from the beginning there is no execution of the <commands>

Loop (for-statement)

#!/usr/bin/env python3

for x in range(0, 3):

  print x

This loop prints numbers from 0 to 2. User-Defined Procedures A Function is a series of Python statements begins by a def, followed by the function name and enclosed in parenthesis. A Function may or may not return a value. A Function procedure can take arguments (constants, variables, or expressions that are passed by a calling procedure). If a Function procedure has no arguments, its def statement should include an empty set of parentheses (). Parameters can also be defined within the parenthesis. The parenthesis are followed by a colon (:) to end the first line.

The end of the function is marked by the loss of whitespace in the next line of the code (ending the code block). It is common practice to use a return statement followed by the argument to return a value. You may also finish a function with a return statement and a simple colon (;).

In the following example, the Celsius def calculates degrees Celsius from degrees Fahrenheit. When the def is called from the ConvertTemp def procedure, a variable containing the argument value is passed to the def. The result of the calculation is returned to the calling procedure and displayed in a message box.

#!/usr/bin/env python3

def Celsius(fDegrees):

  _Celsius = (fDegrees - 32) * 5 / 9

  return _Celsius;

  # Use this code to call the Celsius function

  temp = raw_input("Please enter the temperature in degrees F.", 1)

  MsgBox "The temperature is " & Celsius(temp) & " degrees C."
Next Section - Pythagorean theorem