Playing with the $100K Robots for Biology Automation

Posted by – June 26, 2009

The Tecan Genesis Workstation 200: It’s an industrial benchtop robot for liquid handling with multiple arms for tray handling and pipetting.

The robot’s operations are complex, so an integrated development environment is used to program it (though biologists wouldn’t call it an integrated development environment; maybe they’d call it a scripting application?), with custom graphical scripting language (GUI-based) and script verification/compilation. Luckily though, the application allows third party software access and has the ability to control the robotics hardware using a minimal command set. So what to do? Hack it, of course; in this case, with Perl. This is only a headache due to Microsoft Windows incompatibilities & limitations — rarely is anything on Windows as straightforward as Unix — so as usual with Microsoft Windows software, it took about three times longer than normal to figure out Microsoft’s quirks. Give me OS/X (a real Unix) any day. Now, on to the source code!

Proceeding step by step: first, write a small script to access the robot locally. Secondly, write a small script which runs the robot in a generic set of commands. Then, write a small script to access the robot via a network socket using TELNET, and finally, write a password-protected client-server which allows a remote network program anywhere on the network to control the robotic hardware. (Or: finally allow access to the robot from the entire internet through a simple web interface?)

Accessing the robot using a local named pipe interface (named pipes, by the way, are subtly broken on Windows, because Microsoft doesn’t support POSIX / Unix standards properly):

#!/usr/bin/perl
# vim:set nocompatible expandtab tabstop=4 shiftwidth=4 ai:
#
# jcline@ieee.org 2009-06-26
#
#
# Access Tecan Gemini using named pipe & control

&Main1;
exit;

sub Main1 {

    # Notes on gemini named pipe:
    #   - must run gemini application first
    #   - login is not required(?)
    #   - must terminate commands with \0   (undocumented)
    #   - input terminated by \0
    #   - Big problem: can not use F_NOBLOCK on windows with activeperl so
    #   must read char at a time and check for \0 on input
    #   - must use binmode() for pipe
    #   - if command sent is not terminated by \0, then tecan s/w will
    #    send the same buffer as before, i.e. "GET_STATUSpreviousstuff"
    #   - commands are case-insensitive
    #   - Use the gemini app "Gemini Log" window to view cmds/answers
    #   - The variables (like Set_RomaNo) use 0-based index (0,1,..) whereas
    #       the gemini GUI uses 1-based index (1,2,..)
    $pipename="\\\\.\\pipe\\gemini";
    use Fcntl;
    $| = 1;
    sysopen(CMD, $pipename, O_RDWR) || die "cant open $pipename";
    binmode(CMD);
    print "\n\n- version:\n";
    print CMD "GET_VERSION\0";
    do { read(CMD, $_, 1); print STDERR $_; } while ($_ ne "\0");
    print "\n- status:\n";
    print CMD "GET_STATUS\0";
    do { read(CMD, $_, 1); print STDERR $_; } while ($_ ne "\0");

    # Provide a console for the user to enter commands executed immediately
    print "\n\n";
    while (!$stop) {
        print STDERR "\nEnter tecan cmd: ";
        $cmd = ;
        chomp($cmd);
        next if !$cmd;
        print STDERR "Command> $cmd\n";
        print CMD "$cmd\0";
        print "Reply: ";
        do { read(CMD, $_, 1); print STDERR $_; } while ($_ ne "\0");
        print "\n";
    }

    close(CMD);
}

sub Main2 {
    # The following is straight from perl doc Win32::Pipe
    # This does not work
    use Win32::Pipe;
    # JC: Pipe name begins with \\ to assure local server context
    $pipename='\\\\.pipe\gemini';
    $Pipe = new Win32::Pipe($pipename) || warn;
    $Result = $Pipe->Connect() || warn;
    $Result = $Pipe->Write("GET_STATUS") || warn;
    $Data = $Pipe->Read() || die;
    print $Data;
    $Pipe->Disconnect() || die;
    $Pipe->Close();

}

sub Main3 {
    # This does not work

    use Fcntl;
    $pipename="\\\\.\\pipe\\gemini";
    $OUTPUT_AUTOFLUSH =1;
    $INPUT_RECORD_SEPARATOR = "\0";

    sysopen(CMD, $pipename, O_RDWR) || die "can't open $pipename";

    $cmd="GET_VERSION\0"; syswrite(CMD, $cmd, length($cmd));
    $reply = "";
    do { $l = sysread(CMD, $_, 1); if ($l) { $reply .= $_; } print $l; } while ($l);
    print $reply;
    $cmd="GET_STATUS\0"; syswrite(CMD, $cmd, length($cmd));
    $reply = "";
    do { $l = sysread(CMD, $_, 1); $reply .= $_ if $l; } while ($l);
    print $reply;
    close(CMD);
}

__DATA__
get_version
get_status
SET_ROMANO;0
ROMA_RELATIVE;100;0;0

__END__

Now after verifying the above, let’s make the robot arms do a little dance, and clean up the software.

#!/usr/bin/perl
# vim:set nocompatible expandtab tabstop=4 shiftwidth=4 ai:
#
# jcline@ieee.org 2009-06-26
#
#
# Access Tecan Gemini using named pipe & control
# Send the DATA section to the robot after verifying it is IDLE

&Main;
exit;

sub Main {

    $version = Init();
    ig (!($version =~ /0;Version 4.1.0.0/)) {
        Shutdown();
        die "Robot version mismatch: Exit.\n";
    }

    while ($_ = <DATA>) {
        last if /^__END__/;
        push(@cmds, $_);
    }

    for (1..10) {
        foreach $cmd (@cmds) {
            Write($cmd);
            $reply = Read();
            print STDERR "$cmd   $reply\n";
        }
    }
    print "Done\n";

    # Console access to robot
    print "\n\n";
    while (!$stop) {
        print STDERR "\nEnter tecan cmd: ";
        $cmd = <STDIN>;
        chomp($cmd);
        next if !$cmd;
        print STDERR "Command> $cmd\n";
        Write($cmd);
        $reply = Read();
        print "Reply: $reply\n";
    }

    close(CMD);
}

sub Init {
    # Notes on gemini named pipe:
    #   - must run gemini application first
    #   - login is not required(?)
    #   - must terminate commands with \0   (undocumented)
    #   - input terminated by \0
    #   - Big problem: can not use F_NOBLOCK on windows with activeperl so
    #   must read char at a time and check for \0 on input
    #   - must use binmode() for pipe
    #   - if command sent is not terminated by \0, then tecan s/w will
    #    send the same buffer as before, i.e. "GET_STATUSpreviousstuff"
    #   - commands are case-insensitive
    #   - Use the gemini app "Gemini Log" window to view cmds/answers
    #   - The variables (like Set_RomaNo) use 0-based index (0,1,..) whereas
    #       the gemini GUI uses 1-based index (1,2,..)
    my $pipename="\\\\.\\pipe\\gemini";
    my $version;
    my $status;
    use Fcntl;
    $| = 1;
    sysopen(CMD, $pipename, O_RDWR) || die "cant open $pipename";
    binmode(CMD);
    Write("GET_VERSION");
    $version = Read();
    print STDERR "\nVersion: $version\n";
    Write("GET_STATUS");
    $status = Read();
    print STDERR "\nStatus: $status\n";
    if (!($status =~ /IDLE/)) {
        Shutdown();
        die "Robot not idle: Exit\n";
    }
    return $version;

}
sub Read {
    my $data;
    my $_;
    do { read(CMD, $_, 1); $data .= $_; } while ($_ ne "\0");
    return $data;
}
sub Write {
    my $data = @_[0];
    $data =~ s/\r\n\t//go;
    print CMD $data . "\0";
}
sub Shutdown {
    close(CMD);
}

__DATA__
SET_ROMANO;0
ROMA_RELATIVE;100;0;0
roma_relative;0;50;0
roma_relative;0;-50;0
ROMA_RELATIVE;-100;0;0
__END__

Now to make it socket-based so either the same computer or any other computer on the network can access it after providing a simple password. This will allow for arbitrary programs (written in C, or Java, or Perl, or Python, or ….) to send commands to the robotics hardware. The network code is basically a bunch of copy-paste from the Perl manpages for IO:Socket.

The server:

#!/usr/bin/perl
# vim:set nocompatible expandtab tabstop=4 shiftwidth=4 ai:
#
# jcline@ieee.org 2009-06-26
#
#
# Access Tecan Gemini using named pipe & control
# Open socket to receive network commands, send the commands to robot

&Main;
exit;

sub Main {

    $version = Init();
    if (!($version =~ /0;Version 4.1.0.0/)) {
        Shutdown();
        die "Robot version mismatch: Exit.\n";
    }

    use IO::Socket;
    use Net::hostent;              # for OO version of gethostbyaddr

    $PORT = 8080;                  # pick something not in use

    $server = IO::Socket::INET->new( Proto     => 'tcp',
                                  LocalPort => $PORT,
                                  Listen    => SOMAXCONN,
                                  Reuse     => 1);

    die "can't setup server" unless $server;
    system("ipconfig");
    print "[Server $0 accepting clients on port $PORT]\n";

    while ($client = $server->accept()) {
        $client->autoflush(1);
        print $client "Welcome to $0\n";
        $hostinfo = gethostbyaddr($client->peeraddr);
        printf "[Connect from %s]\n",
            $hostinfo ? $hostinfo->name : $client->peerhost;

        # Cheap authentication
        while (<$client>) {
            last if (/^secretpassword/);
            print $client "\n";
        }

        # Run commands
        while (<$client>) {
            next unless /\S/;       # blank line
            last if /end/oi;
            s/[\r\n\t\0]//g;
            print STDERR "$_\n";
            Write($_);
            $result = Read();
            print STDERR $result . "\n";
            print $client "\n$_\n>$result" . "\n";
        }
        close $client;
    }

    close(CMD);
}

sub Init {
    # Notes on gemini named pipe:
    #   - must run gemini application first
    #   - login is not required(?)
    #   - must terminate commands with \0   (undocumented)
    #   - input terminated by \0
    #   - Big problem: can not use F_NOBLOCK on windows with activeperl so
    #   must read char at a time and check for \0 on input
    #   - must use binmode() for pipe
    #   - if command sent is not terminated by \0, then tecan s/w will
    #    send the same buffer as before, i.e. "GET_STATUSpreviousstuff"
    #   - commands are case-insensitive
    #   - Use the gemini app "Gemini Log" window to view cmds/answers
    #   - The variables (like Set_RomaNo) use 0-based index (0,1,..) whereas
    #       the gemini GUI uses 1-based index (1,2,..)
    my $pipename="\\\\.\\pipe\\gemini";
    my $version;
    my $status;
    use Fcntl;
    $| = 1;
    sysopen(CMD, $pipename, O_RDWR) || die "cant open $pipename";
    binmode(CMD);
    Write("GET_VERSION");
    $version = Read();
    print STDERR "\nVersion: $version\n";
    Write("GET_STATUS");
    $status = Read();
    print STDERR "\nStatus: $status\n";
    if (!($status =~ /IDLE/)) {
        Shutdown();
        die "Robot not idle: Exit\n";
    }
    return $version;

}
sub Read {
    my $data;
    my $_;
    do { read(CMD, $_, 1); $data .= $_; } while ($_ ne "\0");
    return $data;
}
sub Write {
    my $data = @_[0];
    $data =~ s/\r\n\t//go;
    print CMD $data . "\0";
}
sub Shutdown {
    close(CMD);
}

__END__

The client:

— which acts like a keyboard-joystick to move the robot around, using the keys A,S,D,W and Q,E, plus Z to quit, similar to any good keyboard video game :

#!/usr/bin/perl -w
# vim:set nocompatible expandtab tabstop=4 shiftwidth=4 ai:
#
# jcline@ieee.org 2009-06-26
#
#
# Access Tecan Gemini using named pipe & control
# Open socket to receive network commands, send the commands to robot

use IO::Socket;
unless (@ARGV > 1) { die "usage: $0 host port\n" }
$host = shift(@ARGV);
$port = shift(@ARGV);

$remote = IO::Socket::INET->new( Proto     => "tcp",
				 PeerAddr  => $host,
				 PeerPort  => $port);
unless ($remote) { die "cannot connect to $host:$port" }
$remote->autoflush(1);
print $remote "secretpassword\n";
print $remote "SET_ROMANO;0\n";
binmode(STDIN);
while (1) {
    $_ = <$remote>;
    print "$_\n";
    sysread(STDIN, $_, 1);
    if (/d/i) { print $remote "ROMA_RELATIVE;1;0;0\n"; next; }
    if (/a/i) { print $remote "ROMA_RELATIVE;-1;0;0\n"; next; }
    if (/w/i) { print $remote "ROMA_RELATIVE;0;-1;0\n"; next; }
    if (/s/i) { print $remote "ROMA_RELATIVE;0;1;0\n"; next; }
    if (/q/i) { print $remote "ROMA_RELATIVE;0;0;-1\n"; next; }
    if (/e/i) { print $remote "ROMA_RELATIVE;0;0;1\n"; next; }
    if (/z/i) { last; }
}

close $remote;
__END__

Done!

Now that’s a fun Friday afternoon project.

2 Comments on Playing with the $100K Robots for Biology Automation

  1. Eric Berquist says:

    So, I’ve got a Tecan as well (a Freedom EVO 150), and as you know, the GUI leaves much to be desired, and like you, I want to script it. Unlike you, I don’t know Perl, but I’m willing to learn whatever I might need to.

    My question is, where do I start? I can’t find anything in my manuals about starting off with VBScript, nevermind using another language.

    It’s nice to see someone who has merged a love of computers/electronics and science…I can’t seem to find much on it.

  2. Eric –

    Any language can work through the named pipe. The Gem_Pipe.pdf documentation that I have is electronic only (which leaves a lot to be desired) and describes all the commands, which are text strings. So the best language to use is one which can handle strings easily. I wouldn’t touch VBScript with a 10 mile pole, since it’s a Microsoft product and limited to running on only their buggy machines. If it’s the only language you know, I suggest learning python which is basic-like. I would bet even Matlab might work to talk to Tecan, since Matlab can handle strings too, though it would be a bit awkward. I wouldn’t want to use C either, because handling the strings wouldn’t be as easy.

    In summary I would check out python. There are a few python-for-biologists books (bioinformatics using python) out there.

    Do this; google for: tecanprogramming site:groups.yahoo.com. There is a yahoo group which has VBScript source code examples, which I found after writing my post.

    I was initially very skeptical of the GUI performance, but I’ve changed my mind a bit. The GUI makes some very intelligent decisions which make life easier, in most cases, so it is still completely usable on it’s own. Things like: changing the number of pipettes which graphically selects wells, or automatically incrementing pipette wells (row or column) within loops, etc. Believe me, I have used supposedly “GUI scripting” equipment with far worse GUI interfaces; in comparison Tecan is doing well with theirs.

    Of course it can’t offer the power of, say, running arbitrary Java programs and sending commands over the network… or, running a server which analyzes protocols from protocol-online.org to translate it automatically into Tecan-instructions.. Big things like that need external applications.