Monday, 11 January 2016

Lesson 1 - The art of doing nothing

EV3 Direct commands - Lesson 01

LEGO's EV3 is a fascinating game tool. Its standard way to program is LEGO's graphical programming tool. You can write programs, transfer them to your EV3 brick and start them. But there is another way to interact with your EV3. Look at it as a server and send commands to it, which will be answered with data and/or actions. In this case, the machine, your program runs on, is the client. This opens fascinating new perspectives. If the program runs on your smartphone, you win great chances of interactivity and handiness. If your client is a PC or Laptop, you win a comfortable keyboard and display. Another new option is the combination of multiple EV3s in one robot. One client communicates with multiple servers, which allows robots with unlimited numbers of motors and sensors. Or think of your EV3 as a machine, that produces data. The client can continually receive data from the EV3s sensors, which also opens new chances. If you want to enter this new world, you have to use EV3s direct commands, which needs some work of your mind. If you are ready to invest it, then read further on, if not, be happy with your EV3 as it is and wait until others present cool new applications.

Before we start with the real stuff, we cast a first glance to the communication protocols of the EV3 and to the character of LEGO Direct Commands. Your EV3 provides three types of communication, bluetooth, wifi and USB. Bluetooth and USB can be used without any additional equipment, the communication via wifi needs a dongle. All three allow you, to develop applications, that run on any computer and communicate with your EV3 brick. Maybe you know EV3s predecessor, the NXT and its communication protocol. It offered about 20 system commands, which resembled the call of functions. LEGO changed its philosophy, the EV3 offers a command syntax in machine code. On one side this means freedom to realize real algorithms but on the other side it became harder to get in.

The official documents, I found, are pure technical documentation. They lack in motivation and in educational aspects. I hope, this text will be a comfortable entrance to the inside of your EV3. If you are already a programmer or you try to get one, you will immerse into the world of bits and bytes. If not, it will become hard but the only alternative is, to keep your hands apart. Give it a try, beneath the effective communication with your LEGO EV3 you will learn a lot about machine code, the basics of all computers.

Documents to know

LEGO has published good and detailed documentation, which you can download from: http://www.lego.com/en-gb/mindstorms/downloads. For our purpose, you absolutely need to look at the documents, you find under the title Advanced Users - Developer Kits (PC/MAC). The document EV3 Firmware Developer Kit is the reference book of the LEGO EV3 direct commands. I hope you will intensively read it.

There exists a communication library in C#, that uses direct commands to communicate with a LEGO EV3. If you like to use software out of the box and if you like C#, this may be your choice: http://www.monobrick.dk.

My original idea was, not to publish any sources. Programming is more fun, when the functionality grows step by step. Bugs are part of the game, searching for bugs is hard but the daily life of any programming person. Short conclusion, the code grew to a volume and complexity, where the original idea became unrealistic. I have published the code on github, from where you can download: ev3-python3.

Lesson 1 - The art of doing nothing

You did it, you really want to step in, that's fine! This lesson is about the most basic communication. We will implement a first call and reply cycle. Via wifi, bluetooth or USB you will send information to your EV3 and you will get a well defined answer. Don't hold your breath, we will not start with an application, that fills the world with wonder. It's the opposite, it will do absolutely nothing. This sounds less than it is, if you manage to do that, lean back and feel happy, you are on the way.

Little excursion to the nomenclature of bits and bytes

Maybe you already know, how to write binary and hexadecimal numbers, the meaning of big and little endian and so on. If you really can write the value 156 as a 4-bytes integer in binary notation and little endian format, then you can skip this excursion. If not, you need to read it, because you really need to know that.

Let's start with the basics! Nearly all modern computers group 8 bits to one byte and address their memory per byte (your EV3 is a modern computer and does so). In the following we use the following notation for binary numbers: 0b 1001 1100

The leading 0b tells us, that a binary notation of a number will follow, the 8 digits per byte are grouped 4 by 4 as half bytes. It is the binary notation of the number 156, which you can read as:
156 = 1*128 + 0*64 + 0*32 + 1*16 + 1*8 + 1*4 + 0*2 + 0*1. Alternative interpretations of the same byte are possible. It can be red as a sequence of 8 flags or it can be the ascii code of the sign £. The interpretation depends on the context. In the moment we concentrate on numbers.

Binary notation is very lengthy, so it is a common habit to write the half bytes as hexadecimal numbers, where the letters A to F are the digits for the numbers 10 to 15. Hexadecimal notation is compact and its transformation from and to binary notation is easy. This is because one hexadecimal digit stands for one half byte. The notation for hexadecimal numbers (here the value 156) is: 0x 9C. You can read it as: 156 = 9*16 + 12*1. The leading 0x tells us, that hexadecimal notation will follow. Because of its compactness, we can write and read larger numbers. As a 4-byte integer, the value 156 is written as: 0x 00 00 00 9C

We will separate the bytes by colons ":" or vertical bars "|". We use vertical bars for the high order separation and colons for the low order. We will write the 2-byte integer of the value 156 as: 0x|00:9C| Now we can hold lists of values in a single row. The sequence of 255 (as an unsigned 1-byte integer), 156 (as 2-byte integer) and 65,536 (as 4-byte integer) is written as: 0x|FF|00:9C|00:01:00:00|

What about negative numbers? Most computer languages distinct between signed and unsigned integers. If integers are signed, their first bit becomes a flag for negative sign and the integer gets another range. A signed 1-byte integer has the range -128 to 127, a signed 2-byte integer the range -32,768 to 32,767 and so on. The value of negative numbers is calculated as the minimal value (-128, -32,768 etc.) plus the value of the rest. The lowest value of a signed 1-byte integer, -128 is written as: 0b 1000 0000 or 0x|80| and the value -1 as signed 2-byte integer (-32,768 + 32,767): 0b 1111 1111 1111 1111 or 0x|FF:FF|

And what is little endian? O.k. I will not keep back this secret. The little endian format inverses the positions of the bytes (what you are used to, is named big endian). The value 156 as 2-byte integer in little endian format is written as: 0x|9C:00|

Maybe this sounds like a bad joke, but I'm sorry, EV3 direct commands read and write all numbers in little endian format, which is not my fault. But I can give you some comfort. First, this lesson uses numbers sparely. Second, there exist good tools to manage little endian numbers. In python, you can use the struct module, in java, ByteBuffer may be the object of your choice.

The direct command for doing nothing

The first example presents the simplest of all possible direct commands. You will send a message to your EV3 and hopefully it will answer. Let's look at the message to send, it consists of the following 8 bytes:

------------------------- \ len \ cnt \ty\ hd \op\ ------------------------- 0x|06:00|2A:00|00|00:00|01| ------------------------- \ 6 \ 42 \Re\ 0,0 \N \ \ \ \ \ \o \ \ \ \ \ \p \ -------------------------

The message itself is the line, that starts wit 0x. On top of the message you see some annotations about the type of the parts of the message. The bottom shows annotations about their values. The 8 bytes of the message consist of the following parts:

  • Length of the message (bytes 0, 1): The first two byte are not part of the direct command itself. They are part of the communication protocols, which in case of the EV3 can be Wifi, Bluetooth or USB. The length is coded as a 2-byte unsigned integer in little endian format, 0x|06:00| therefore stands for the value 6.
  • Message counter (bytes 2, 3): The next two bytes are the footprint of this direct command. The message counter will be included in the answer and allows to match the direct command and its reply. This too is a 2-byte unsigned integer in little endian format. In our case we set the message counter to 0x|2A:00|, which is the value 42.
  • Message type (byte 4): It may have the following two values:
    • DIRECT_COMMAND_REPLY = 0x|00|
    • DIRECT_COMMAND_NO_REPLY = 0x|80|
    In our case we want the EV3 to reply the message.
  • Header (bytes 5, 6): The next two bytes, the last in front of the first operation are the header. It includes a combination of two numbers, which define the memory sizes of the direct command (yes, its plural, we have two memories, a local and a global one). We soon will come back to the details of this memory sizes. For the moment we are lucky, that our command does not need any memory, therefore we set the header to 0x|00:00|.
  • Operations (starting at byte 7): In our case one single byte, that stands for: opNOP = 0x|01|, do nothing, the idle operation of the EV3.

Sending Messages to the EV3

Our task is, to sent the above described message to our EV3. How to do it? You can choose between three communication protocols, Bluetooth, Wifi and USB and you can choose any programming language, that supports at least one of the communication protocols. In the following I present examples in python and in java. If you miss your favorite language, it would be great to translate the programs to your preferred computer language and send them to me. They will be published with credits at this place.

Bluetooth

You need access to a computer with Bluetooth enabled and you need to enable Bluetooth on your EV3. Next you have to couple the two devices. This can be initiated from the EV3 or from your computer. The process is described in the EV3's user guide. If you need help, you will find tutorials on the web, here a link to a LEGO page: http://www.lego.com/en-gb/mindstorms/support/. The coupling process will present you the mac-address of your EV3. You need to note it. As an alternative, you can read the mac-address from your EV3's display under Brick Info / ID.

python

You have to do the following steps:

  • Copy the code into a file with the name EV3_do_nothing_bluetooth.py.
  • Change the mac-address from 00:16:53:42:2B:99 to the value of your EV3.
  • Open a terminal and navigate to the directory of your program.
  • Run it by typing python3 EV3_do_nothing_bluetooth.py.


#!/usr/bin/env python3

import socket
import struct

class EV3():
    def __init__(self, host: str):
        self._socket = socket.socket(
            socket.AF_BLUETOOTH,
            socket.SOCK_STREAM,
            socket.BTPROTO_RFCOMM
        )
        self._socket.connect((host, 1))

    def __del__(self):
        if isinstance(self._socket, socket.socket):
            self._socket.close()

    def send_direct_cmd(self, ops: bytes, local_mem: int=0, global_mem: int=0) -> bytes:
        cmd = b''.join([
            struct.pack('<h', len(ops) + 5),
            struct.pack('<h', 42),
            DIRECT_COMMAND_REPLY,
            struct.pack('<h', local_mem*1024 + global_mem),
            ops
        ])
        self._socket.send(cmd)
        print_hex('Sent', cmd)
        reply = self._socket.recv(5 + global_mem)
        print_hex('Recv', reply)
        return reply

def print_hex(desc: str, data: bytes) -> None:
    print(desc + ' 0x|' + ':'.join('{:02X}'.format(byte) for byte in data) + '|')

DIRECT_COMMAND_REPLY = b'\x00'
opNop = b'\x01'
my_ev3 = EV3('00:16:53:42:2B:99')
ops_nothing = opNop
my_ev3.send_direct_cmd(ops_nothing)
    

java

My choice to communicate with bluetooth-devices is bluecove. After downloading the java-archive bluecove-2.1.0.jar (on Unix also bluecove-gpl-2.1.0.jar), you can add them to your classpath. On my Unix-machine, this is done by:
export CLASSPATH=$CLASSPATH:./bluecove-2.1.0.jar:./bluecove-gpl-2.1.0.jar
Then, you have to do the following steps:

  • Copy the code into a file with the name EV3_do_nothing_bluetooth.java.
  • Change the mac-address from 001653422B99 to the value of your EV3.
  • Open a terminal and navigate to the directory of your program.
  • Compile it by typing javac EV3_do_nothing_bluetooth.java.
  • Run it by typing java EV3_do_nothing_bluetooth


import java.io.*;
import javax.microedition.io.*;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import java.io.*;

public class EV3_do_nothing_bluetooth {
    static final String mac_addr = "001653422B99";

    static final byte  opNop                        = (byte)  0x01;
    static final byte  DIRECT_COMMAND_REPLY         = (byte)  0x00;

    static InputStream in;
    static OutputStream out;

    public static void connectBluetooth ()
 throws IOException {
     String s = "btspp://" + mac_addr + ":1";
     StreamConnection c = (StreamConnection) Connector.open(s);
     in = c.openInputStream();
     out = c.openOutputStream();
    }

    public static ByteBuffer sendDirectCmd (ByteBuffer operations,
         int local_mem, int global_mem)
 throws IOException {
 ByteBuffer buffer = ByteBuffer.allocateDirect(operations.position() + 7);
 buffer.order(ByteOrder.LITTLE_ENDIAN);
 buffer.putShort((short) (operations.position() + 5));   // length
 buffer.putShort((short) 42);                            // counter
 buffer.put(DIRECT_COMMAND_REPLY);                       // type
 buffer.putShort((short) (local_mem*1024 + global_mem)); // header
 for (int i=0; i < operations.position(); i++) {         // operations
     buffer.put(operations.get(i));
 }

 byte[] cmd = new byte [buffer.position()];
 for (int i=0; i<buffer.position(); i++) cmd[i] = buffer.get(i);
 out.write(cmd);
 printHex("Sent", buffer);

 byte[] reply = new byte[global_mem + 5];
 in.read(reply);
 buffer = ByteBuffer.wrap(reply);
 buffer.position(reply.length);
 printHex("Recv", buffer);

 return buffer;
    }

    public static void printHex(String desc, ByteBuffer buffer) {
 System.out.print(desc + " 0x|");
 for (int i= 0; i < buffer.position() - 1; i++) {
     System.out.printf("%02X:", buffer.get(i));
 }
 System.out.printf("%02X|", buffer.get(buffer.position() - 1));
 System.out.println();
    }

    public static void main (String args[] ) {
 try {
     connectBluetooth();

     ByteBuffer operations = ByteBuffer.allocateDirect(1);
     operations.put(opNop);

     ByteBuffer reply = sendDirectCmd(operations, 0, 0);
 }
 catch (Exception e) {
     e.printStackTrace(System.err);
 }
    }
}
 

Wifi

You need a wifi dongle to connect your EV3 to your local network. The first part of the following document describes this process: http://www.monobrick.dk/guides/how-to-establish-a-wifi-connection-with-the-ev3-brick/. Now your EV3 is part of the local network and has a network address. From all hosts of the network you can communicate with it. As described in the above mentioned document, it needs the following steps to establish a TCP/IP connection to the EV3:

  • Listen for a UDP broadcast from the EV3 on port 3015.
  • Send a UDP message back to the EV3 to make it accept a TCP/IP connection.
  • Establish a TCP/IP connection on port 5555.
  • Send an unlock message to the EV3 over TCP/IP.

python

You have to do the following steps:

  • Copy the code into a file with the name EV3_do_nothing_wifi.py.
  • Open a terminal and navigate to the directory of your program.
  • Run it by typing python3 EV3_do_nothing_wifi.py


#!/usr/bin/env python3

import socket
import struct
import re

class EV3():
    def __init__(self, host: str):
        # listen on port 3015 for a UDP broadcast from the EV3
        UDPSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        UDPSock.bind(('', 3015))
        data, addr = UDPSock.recvfrom(67)

        # pick serial number, port, name and protocol
        # from the broadcast message
        matcher = re.search('Serial-Number: (\w*)\s\n' +
                            'Port: (\d{4,4})\s\n' +
                            'Name: (\w+)\s\n' +
                            'Protocol: (\w+)\s\n',
                            data.decode('utf-8'))
        serial_number = matcher.group(1)
        port = matcher.group(2)
        name = matcher.group(3)
        protocol = matcher.group(4)
        if serial_number.upper() != host.replace(':', '').upper():
            self._socket = None
            raise ValueError('found ev3 but not ' + host)

        # Send an UDP message back to the EV3
        # to make it accept a TCP/IP connection
        UDPSock.sendto(' '.encode('utf-8'), (addr[0], int(port)))
        UDPSock.close()

        # Establish a TCP/IP connection with EV3s address and port
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._socket.connect((addr[0], int(port)))

        # Send an unlock message to the EV3 over TCP/IP
        msg = ''.join([
            'GET /target?sn=' + serial_number + 'VMTP1.0\n',
            'Protocol: ' + protocol
        ])
        self._socket.send(msg.encode('utf-8'))
        reply = self._socket.recv(16).decode('utf-8')
        if not reply.startswith('Accept:EV340'):
            raise IOError('No wifi connection to ' + name + ' established')

    def __del__(self):
        if isinstance(self._socket, socket.socket):
            self._socket.close()

    def send_direct_cmd(self, ops: bytes, local_mem: int=0, global_mem: int=0) -> bytes:
        cmd = b''.join([
            struct.pack('<h', len(ops) + 5),
            struct.pack('<h', 42),
            DIRECT_COMMAND_REPLY,
            struct.pack('<h', local_mem*1024 + global_mem),
            ops
        ])
        self._socket.send(cmd)
        print_hex('Sent', cmd)
        reply = self._socket.recv(5 + global_mem)
        print_hex('Recv', reply)
        return reply

def print_hex(desc: str, data: bytes) -> None:
    print(desc + ' 0x|' + ':'.join('{:02X}'.format(byte) for byte in data) + '|')

DIRECT_COMMAND_REPLY = b'\x00'
opNop = b'\x01'
my_ev3 = EV3('00:16:53:42:2B:99')
ops_nothing = opNop
my_ev3.send_direct_cmd(ops_nothing)
    

java

You have to do the following steps:

  • Copy the code into a file with the name EV3_do_nothing_wifi.java.
  • Open a terminal and navigate to the directory of your program.
  • Compile it by typing javac EV3_do_nothing_wifi.java.
  • Run it by typing java EV3_do_nothing_wifi.


import java.net.Socket;
import java.net.SocketException;
import java.net.ServerSocket;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.net.InetAddress;

import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.ByteOrder;

import java.io.*;
import java.util.regex.*;

public class EV3_do_nothing_wifi {
    static final byte  opNop                        = (byte)  0x01;
    static final byte  DIRECT_COMMAND_REPLY         = (byte)  0x00;

    static InputStream in;
    static OutputStream out;

    public static void connectWifi ()
 throws IOException, SocketException {
     // listen for a UDP broadcast from the EV3 on port 3015
     DatagramSocket listener = new DatagramSocket(3015);
     DatagramPacket packet_r = new DatagramPacket(new byte[67], 67);
     listener.receive(packet_r); // receive the broadcast message
     String broadcast_message = new String(packet_r.getData());
     /* pick serial number, port, name and protocol 
        from the broadcast message */
     Pattern broadcast_pattern = 
     Pattern.compile("Serial-Number: (\\w*)\\s\\n" +
                            "Port:\\s(\\d{4,4})\\s\\n" +
                            "Name:\\s(\\w+)\\s\\n" +
                            "Protocol:\\s(\\w+)\\s\\n");
     Matcher matcher = broadcast_pattern.matcher(broadcast_message);
     String serial_number, name, protocol;
     int port;
     if(matcher.matches()) {
  serial_number = matcher.group(1);
  port = Integer.valueOf(matcher.group(2));
  name = matcher.group(3);
  protocol = matcher.group(4);
     }
     else {
  throw new IOException("Unexpected Broadcast message: " + broadcast_message);
     }
     InetAddress adr = packet_r.getAddress();
     // connect the EV3 with its address and port
     listener.connect(adr, port);
     /* Send an UDP message back to the EV3 
        to make it accept a TCP/IP connection */
     listener.send(new DatagramPacket(new byte[1], 1));
     // close the UDP connection
     listener.close();

     // Establish a TCP/IP connection with EV3s address and port
     Socket socket = new Socket(adr, port);
     in     = socket.getInputStream();
     out    = socket.getOutputStream();

     // Send an unlock message to the EV3 over TCP/IP
     String unlock_message = "GET /target?sn=" + serial_number + 
                                    "VMTP1.0\n" + 
                                    "Protocol: " + protocol;
     out.write(unlock_message.getBytes());

     byte[] reply = new byte[16];           // read reply
     in.read(reply);
     if (! (new String(reply)).startsWith("Accept:EV340")) {
  throw new IOException("No wifi connection established " + name);
     } 
    }

    public static ByteBuffer sendDirectCmd (ByteBuffer operations,
         int local_mem, int global_mem)
 throws IOException {
 ByteBuffer buffer = ByteBuffer.allocateDirect(operations.position() + 7);
 buffer.order(ByteOrder.LITTLE_ENDIAN);
 buffer.putShort((short) (operations.position() + 5));   // length
 buffer.putShort((short) 42);                            // counter
 buffer.put(DIRECT_COMMAND_REPLY);                       // type
 buffer.putShort((short) (local_mem*1024 + global_mem)); // header
 for (int i=0; i<operations.position(); i++) {           // operations
     buffer.put(operations.get(i));
 }

 byte[] cmd = new byte [buffer.position()];
 for (int i=0; i<buffer.position(); i++) cmd[i] = buffer.get(i);
 out.write(cmd);
 printHex("Sent", buffer);

 byte[] reply = new byte[global_mem + 5];
 in.read(reply);
 buffer = ByteBuffer.wrap(reply);
 buffer.position(reply.length);
 printHex("Recv", buffer);

 return buffer;
    }

    public static void printHex(String desc, ByteBuffer buffer) {
 System.out.print(desc + " 0x|");
 for (int i= 0; i<buffer.position() - 1; i++) {
     System.out.printf("%02X:", buffer.get(i));
 }
 System.out.printf("%02X|", buffer.get(buffer.position() - 1));
 System.out.println();
    }

    public static void main (String args[] ) {
 try {
     connectWifi();

     ByteBuffer operations = ByteBuffer.allocateDirect(1);
     operations.put(opNop);

     ByteBuffer reply = sendDirectCmd(operations, 0, 0);
 }
 catch (Exception e) {
     e.printStackTrace(System.err);
 }
    }
}
    

USB

The Universal Serial Bus is an industrial standard to connect electronic devices. Your EV3 has a 2.0 Mini-B receptacle (titled PC). This is the comunication-protocol with the best performance, but it needs a wire. On your computer, where your program runs, you need the permission to communicate with the LEGO EV3. In my case, I had to add the following udev-rule (my operation system is Unix):
ATTRS{idVendor}=="0694",ATTRS{idProduct}=="0005",MODE="0666",GROUP=<group>

This identifies EV3-devices by their vendor-id 0x0694 and product-id 0x0005. The mode 0666 allows read- and write-access for all users, that belong to <group> (change <group> to a group, you are member of).

The EV3-USB-device descriptor shows one configuration with one interface and two endpoints, 0x01 for sending data to the EV3, 0x81 for receiving data from the EV3. The data are sent and received in packages of 1024 bytes.

python

Maybe, you need to install pyusb. For my system this was done with:
sudo pip3 install --pre pyusb If pyusb is installed, you have to do the following steps:

  • Copy the code into a file with the name EV3_do_nothing_usb.py.
  • Open a terminal and navigate to the directory of your program.
  • Run it by typing python3 EV3_do_nothing_usb.py


#!/usr/bin/env python3

import usb.core
import struct

class EV3():
    def __init__(self, host: str):
        self._device = usb.core.find(idVendor=ID_VENDOR_LEGO, idProduct=ID_PRODUCT_EV3)
        if self._device is None:
            raise RuntimeError("No Lego EV3 found")
        serial_number = usb.util.get_string(self._device, self._device.iSerialNumber)
        if serial_number.upper() != host.replace(':', '').upper():
            raise ValueError('found ev3 but not ' + host)
        if self._device.is_kernel_driver_active(0) is True:
            self._device.detach_kernel_driver(0)
        self._device.set_configuration()
        self._device.read(EP_IN, 1024, 100)

    def __del__(self): pass

    def send_direct_cmd(self, ops: bytes, local_mem: int=0, global_mem: int=0) -> bytes:
        cmd = b''.join([
            struct.pack('<h', len(ops) + 5),
            struct.pack('<h', 42),
            DIRECT_COMMAND_REPLY,
            struct.pack('<h', local_mem*1024 + global_mem),
            ops
        ])
        self._device.write(EP_OUT, cmd, 100)
        print_hex('Sent', cmd)
        reply = self._device.read(EP_IN, 1024, 100)[0:5+global_mem]
        print_hex('Recv', reply)
        return reply

def print_hex(desc: str, data: bytes) -> None:
    print(desc + ' 0x|' + ':'.join('{:02X}'.format(byte) for byte in data) + '|')

ID_VENDOR_LEGO = 0x0694
ID_PRODUCT_EV3 = 0x0005
EP_IN  = 0x81
EP_OUT = 0x01
DIRECT_COMMAND_REPLY = b'\x00'
opNop = b'\x01'
my_ev3 = EV3('00:16:53:42:2B:99')
ops_nothing = opNop
my_ev3.send_direct_cmd(ops_nothing)
    

java

My choice to communicate with usb-devices is usb4java. After the download of the java-archives, you can add them to your classpath. On my Unix-machine, this is done by:
export CLASSPATH=$CLASSPATH:./usb4java-1.2.0.jar:./libusb4java-1.2.0-linux-x86_64.jar Then, you have to do the following steps:

  • Copy the code into a file with the name EV3_do_nothing_usb.java.
  • Open a terminal and navigate to the directory of your program.
  • Compile it by typing javac EV3_do_nothing_usb.java.
  • Run it by typing java EV3_do_nothing_usb


import org.usb4java.Device;
import org.usb4java.DeviceDescriptor;
import org.usb4java.DeviceHandle;
import org.usb4java.DeviceList;
import org.usb4java.LibUsb;
import org.usb4java.LibUsbException;

import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.ByteOrder;

public class EV3_do_nothing_usb {
    static final short ID_VENDOR_LEGO = (short) 0x0694;
    static final short ID_PRODUCT_EV3 = (short) 0x0005;
    static final byte  EP_IN          = (byte)  0x81;
    static final byte  EP_OUT         = (byte)  0x01;

    static final byte  opNop                        = (byte)  0x01;
    static final byte  DIRECT_COMMAND_REPLY         = (byte)  0x00;

    static DeviceHandle handle;

    public static void connectUsb () {
 int result = LibUsb.init(null);
 Device device = null;
 DeviceList list = new DeviceList();
 result = LibUsb.getDeviceList(null, list);
 if (result < 0){
     throw new RuntimeException("Unable to get device list. Result=" + result);
 }
 boolean found = false;
 for (Device dev: list) {
     DeviceDescriptor descriptor = new DeviceDescriptor();
     result = LibUsb.getDeviceDescriptor(dev, descriptor);
     if (result != LibUsb.SUCCESS) {
  throw new LibUsbException("Unable to read device descriptor", result);
     }
     if (  descriptor.idVendor()  == ID_VENDOR_LEGO
    || descriptor.idProduct() == ID_PRODUCT_EV3) {
  device = dev;
  found = true;
  break;
     }
 }
 LibUsb.freeDeviceList(list, true);
 if (! found) throw new RuntimeException("Lego EV3 device not found.");

 handle = new DeviceHandle();
 result = LibUsb.open(device, handle);
 if (result != LibUsb.SUCCESS) {
     throw new LibUsbException("Unable to open USB device", result);
 }
 boolean detach = LibUsb.kernelDriverActive(handle, 0) != 0;

 if (detach) result = LibUsb.detachKernelDriver(handle, 0);
 if (result != LibUsb.SUCCESS) {
     throw new LibUsbException("Unable to detach kernel driver", result);
 }

 result = LibUsb.claimInterface(handle, 0);
 if (result != LibUsb.SUCCESS) {
     throw new LibUsbException("Unable to claim interface", result);
 }
    }

    public static ByteBuffer sendDirectCmd (ByteBuffer operations,
         int local_mem, int global_mem) {
 ByteBuffer buffer = ByteBuffer.allocateDirect(operations.position() + 7);
 buffer.order(ByteOrder.LITTLE_ENDIAN);
 buffer.putShort((short) (operations.position() + 5));   // length
 buffer.putShort((short) 42);                            // counter
 buffer.put(DIRECT_COMMAND_REPLY);                       // type
 buffer.putShort((short) (local_mem*1024 + global_mem)); // header
 for (int i=0; i < operations.position(); i++) {         // operations
     buffer.put(operations.get(i));
 }

 IntBuffer transferred = IntBuffer.allocate(1);
 int result = LibUsb.bulkTransfer(handle, EP_OUT, buffer, transferred, 100); 
 if (result != LibUsb.SUCCESS) {
     throw new LibUsbException("Unable to write data", transferred.get(0));
 }
 printHex("Sent", buffer);

 buffer = ByteBuffer.allocateDirect(1024);
 transferred = IntBuffer.allocate(1);
 result = LibUsb.bulkTransfer(handle, EP_IN, buffer, transferred, 100);
 if (result != LibUsb.SUCCESS) {
     throw new LibUsbException("Unable to read data", result);
 }
 buffer.position(global_mem + 5);
 printHex("Recv", buffer);

 return buffer;
    }

    public static void printHex(String desc, ByteBuffer buffer) {
 System.out.print(desc + " 0x|");
 for (int i= 0; i < buffer.position() - 1; i++) {
     System.out.printf("%02X:", buffer.get(i));
 }
 System.out.printf("%02X|", buffer.get(buffer.position() - 1));
 System.out.println();
    }

    public static void main (String args[] ) {
 try {
     connectUsb();

     ByteBuffer operations = ByteBuffer.allocateDirect(1);
     operations.put(opNop);

     ByteBuffer reply = sendDirectCmd(operations, 0, 0);

     LibUsb.releaseInterface(handle, 0);
     LibUsb.close(handle);
 }
 catch (Exception e) {
     e.printStackTrace(System.err);
 }
    }
}
    

Reply-Message

If you succeed with one of the alternatives, you get the following output, which is the reply message of your direct command:

---------------- \ len \ cnt \rs\ –--------------- 0x|03:00|2A:00|02| –--------------- \ 3 \ 42 \ok\ ----------------

The first two bytes are well known, it's the little endian message length of the reply. In our case, the reply message is 3 bytes long. The next two bytes are the message counter, well known too, the footprint of the message you sent, which was 42.

The last byte is the return status, with 2 possible values:

  • DIRECT_REPLY = 0x|02|: the direct command was successfully operated
  • DIRECT_REPLY_ERROR = 0x|04|: the direct command ended with an error.

If you really got this Reply-Message, you are inside. Congratulations!

Details of the Header

Above we skipped the description of the header details. It was mentioned, that the header holds two numbers, which define the sizes of memory.

The first number is the size of the local memory, which is the address space, where you can hold intermediate information. The second number describes the size of the global memory, which is the address space of the output. In case of DIRECT_COMMAND_REPLY, the global memory will be sent back as part of the reply.

The local memory has a maximum of 63 byte, the global memory a maximum of 1019 byte. That means, that the size of the local memory needs 6 bits, the size of the global memory needs 10 bits. All together it can be hold in two bytes, if one byte is shared. Exactly this is done. If you write the header bytes in opposite order, which is the familiar big endian and in binary notation with groups of half bytes, you get: 0b LLLL LLGG GGGG GGGG. The first 6 bits hold the size of the local memory with the range 0-63. The 10 bits at the end hold the size of the global memory with the range 0-1020. In little endian you get: 0b GGGG GGGG LLLL LLGG. If for instance your global memory has the size of 6 bytes and your local memory needs the size of 16 bytes, then your header is 0b 0000 0110 0100 0000 or in hex notation 0x 06 40.

This was the descriptive version, now a second approach in declarative manner. If local_mem is the size of the local memory and global_mem the size of the global memory, then calculate: header = local_mem * 1024 + global_mem Write header as a 2-byte-integer in little endian and you get the two header bytes. If you still have questions, wait for the following lessons, you will see lots of headers and learning from examples will hopefully answer your questions.

Variations of doing nothing

Before we leave our first example and close the first chapter, we will test two variants of the header. The first attempt is a direct command with 6 bytes global memory space:

------------------------- \ len \ cnt \ty\ hd \op\ ------------------------- 0x|06:00|2A:00|00|06:00|01| ------------------------- \ 6 \ 42 \Re\ 0,6 \N \ \ \ \ \ \o \ \ \ \ \ \p \ -------------------------

We expect to get a reply with 6 bytes of low value output. Therefore you have to increase the length of the answer from 5 to 11. If you do so, you will get:

---------------------------------- \ len \ cnt \rs\ Output \ –--------------------------------- 0x|09:00|2A:00|02|00:00:00:00:00:00| –--------------------------------- \ 9 \ 42 \ok\ \ ----------------------------------

We add 16 bytes of local memory space and change the direct command to the following:

------------------------- \ len \ cnt \ty\ hd \op\ ------------------------- 0x|06:00|2A:00|00|06:40|01| ------------------------- \ 6 \ 42 \Re\16,6 \N \ \ \ \ \ \o \ \ \ \ \ \p \ -------------------------

We expect the same reply as above and indeed:

---------------------------------- \ len \ cnt \rs\ Output \ –--------------------------------- 0x|09:00|2A:00|02|00:00:00:00:00:00| –--------------------------------- \ 9 \ 42 \ok\ \ ----------------------------------

Your Homework

Before continuing with lesson 2, you should do the following homework:

  • Translate one of the small programs into your preferred programming language and integrate it in the development environment, you like.
  • Prepare some tools because it will not be a pleasure to start from scratch over and over again. I think of the following design:
    • EV3 is a class.
    • BLUETOOTH, USB, WIFI, STD, ASYNC, SYNC and opNop are public constants.
    • Connecting the EV3 is part of the initialization of an EV3 object, that says, that the selection of the protocol is done by calling the constructor of an EV3 object with specific parameters. The EV3 object needs to remember its type of protocol. socket and device are private or protected variables of the EV3 object.
    • Sending data to the EV3 is done by EV3s method send_direct_cmd. You can take the functions of the examples as a blueprint, but inside you have to distinguish between the protocols.
    • For receiving data from the EV3, we use method wait_for_reply. You have to split the code from the functions send_direct_cmd into our two new methods send_direct_cmd and wait_for_reply.
    • Add a property verbosity, which regulates the printing of the sent direct commands and the received replies.
    • Add a property sync_mode, that regulates the communication behaviour with the following values:
      • SYNC: always use type DIRECT_COMMAND_REPLY and wait for replies.
      • ASYNC: never wait for replies, set DIRECT_COMMAND_NO_REPLY, when no global memory is used, else set DIRECT_COMMAND_REPLY.
      • STD: set DIRECT_COMMAND_NO_REPLY or DIRECT_COMMAND_REPLY like ASYNC, but in case of DIRECT_COMMAND_REPLY wait for replies.
    • msg_cnt is a private variable of the EV3 object, which increments per call of send_direct_cmd. Use it for setting the message counter.
    • As in the examples, message length, message counter, message type and header are automatically added inside send_direct_cmd. So the parameter ops of method send_direct_cmd really holds the operations and nothing else.
  • Make some performance tests and compare the three communication protocols (you will see, that USB is the fastest, Bluetooth the slowest and Wifi somewhere in between, but you may bet on the absolute values and the factors between the three protocols).
    • Repeat sending opNop as DIRECT_COMMAND_REPLY and calculate the average time for one send-and-receive-cycle.
    • Separate the time for connecting from the time for sending and receiving. You will connect once but it's the performance of the send-and-receive-cycle that may limit your application.

Conclusion

You started to write a class EV3, that communicates with your LEGO EV3 using direct commands. This class allows a free choice of the communication protocol and provides Bluetooth, Usb and Wifi. My language of choice is python3. I use pydoc3 to present the actual state of our project. I hope, you can easily translate it to the language, you prefer. For the moment, our class EV3 has the following API:


Help on module ev3:

NAME
    ev3 - LEGO EV3 direct commands

CLASSES
    builtins.object
        EV3
    
    class EV3(builtins.object)
     |  object to communicate with a LEGO EV3 using direct commands
     |  
     |  Methods defined here:
     |  
     |  __del__(self)
     |      closes the connection to the LEGO EV3
     |  
     |  __init__(self, protocol:str, host:str)
     |      Establish a connection to a LEGO EV3 device
     |      
     |      Arguments:
     |      protocol: 'Bluetooth', 'Usb' or 'Wifi'
     |      host: mac-address of the LEGO EV3 (f.i. '00:16:53:42:2B:99')
     |  
     |  send_direct_cmd(self, ops:bytes, local_mem:int=0, global_mem:int=0) -> bytes
     |      Send a direct command to the LEGO EV3
     |      
     |      Arguments:
     |      ops: holds netto data only (operations), the following fields are added:
     |        length: 2 bytes, little endian
     |        counter: 2 bytes, little endian
     |        type: 1 byte, DIRECT_COMMAND_REPLY or DIRECT_COMMAND_NO_REPLY
     |        header: 2 bytes, holds sizes of local and global memory
     |      
     |      Keyword Arguments:
     |      local_mem: size of the local memory
     |      global_mem: size of the global memory
     |      
     |      Returns: 
     |        sync_mode is STD: reply (if global_mem > 0) or message counter
     |        sync_mode is ASYNC: message counter
     |        sync_mode is SYNC: reply of the LEGO EV3
     |  
     |  wait_for_reply(self, counter:bytes) -> bytes
     |      Ask the LEGO EV3 for a reply and wait until it is received
     |      
     |      Arguments:
     |      counter: is the message counter of the corresponding send_direct_cmd
     |      
     |      Returns:
     |      reply to the direct command
     |  
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |  
     |  __dict__
     |      dictionary for instance variables (if defined)
     |  
     |  __weakref__
     |      list of weak references to the object (if defined)
     |  
     |  sync_mode
     |      sync mode (standard, asynchronous, synchronous)
     |      
     |      STD:   Use DIRECT_COMMAND_REPLY if global_mem > 0,
     |             wait for reply if there is one.
     |      ASYNC: Use DIRECT_COMMAND_REPLY if global_mem > 0,
     |             never wait for reply (it's the task of the calling program).
     |      SYNC:  Always use DIRECT_COMMAND_REPLY and wait for reply.
     |      
     |      The general idea is:
     |      ASYNC: Interruption or EV3 device queues direct commands,
     |             control directly comes back.
     |      SYNC:  EV3 device is blocked until direct command is finished,
     |             control comes back, when direct command is finished.               
     |      STD:   NO_REPLY like ASYNC with interruption or EV3 queuing,
     |             REPLY like SYNC, synchronicity of program and EV3 device.
     |  
     |  verbosity
     |      level of verbosity (prints on stdout).

DATA
    BLUETOOTH = 'Bluetooth'
    USB = 'Usb'
    WIFI = 'Wifi'
    STD = 'STD'
    ASYNC = 'ASYSNC'
    SYNC = 'SYNC'
    opNop = b'\x01'
      

My class EV3 is part of module ev3, the filename is ev3.py. I used the following program, to test my class EV3:


#!/usr/bin/env python3

import ev3

my_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
my_ev3.verbosity = 1

ops = ev3.opNop

print("*** SYNC ***")
my_ev3.sync_mode = ev3.SYNC
my_ev3.send_direct_cmd(ops)

print("*** ASYNC (no reply) ***")
my_ev3.sync_mode = ev3.ASYNC
my_ev3.send_direct_cmd(ops)

print("*** ASYNC (reply) ***")
counter_1 = my_ev3.send_direct_cmd(ops, global_mem=1)
counter_2 = my_ev3.send_direct_cmd(ops, global_mem=1)
my_ev3.wait_for_reply(counter_1)
my_ev3.wait_for_reply(counter_2)

print("*** STD (no reply) ***")
my_ev3.sync_mode = ev3.STD
my_ev3.send_direct_cmd(ops)

print("*** STD (reply) ***")
my_ev3.send_direct_cmd(ops, global_mem=5)
my_ev3.send_direct_cmd(ops, global_mem=5)
      
and got this output:

*** SYNC ***
15:15:05.084275 Sent 0x|06:00|2A:00|00|00:00|01|
15:15:05.168023 Recv 0x|03:00|2A:00|02|
*** ASYNC (no reply) ***
15:15:05.168548 Sent 0x|06:00|2B:00|80|00:00|01|
*** ASYNC (reply) ***
15:15:05.168976 Sent 0x|06:00|2C:00|00|01:00|01|
15:15:05.169315 Sent 0x|06:00|2D:00|00|01:00|01|
15:15:05.212077 Recv 0x|04:00|2C:00|02|00|
15:15:05.212708 Recv 0x|04:00|2D:00|02|00|
*** STD (no reply) ***
15:15:05.213034 Sent 0x|06:00|2E:00|80|00:00|01|
*** STD (reply) ***
15:15:05.213411 Sent 0x|06:00|2F:00|00|05:00|01|
15:15:05.254032 Recv 0x|08:00|2F:00|02|00:00:00:00:00|
15:15:05.254633 Sent 0x|06:00|30:00|00|05:00|01|
15:15:05.313027 Recv 0x|08:00|30:00|02|00:00:00:00:00|
      
A few remarks:
  • sync_mode = SYNC sets type = DIRECT_COMMAND_REPLY and automaticly waits for the reply, ok.
  • sync_mode = ASYNC sets type = DIRECT_COMMAND_NO_REPLY and does not wait, ok.
  • sync_mode = ASYNC with global memory sets type = DIRECT_COMMAND_REPLY and does not wait. The explicit call of method wait_for_reply gets the reply, ok.
  • Please consider this variant with special respect. We sent two direct commands, both were executed and the replies were hold by the EV3 device.
  • When we later asked for replies, we first read the reply of the first command. It seems as if the EV3 works as a FIFO (first in, first out).
  • But it may also execute parallel and repeat in the sequence of finishing the execution. We will come back to this point.
  • Mode ASYNC needs some discipline. If you forget to read a reply it will come, when you wait for a different thing. We use the message counter to uncover this situation!
  • Please be carefull with protocol USB! This may be too fast, if you send, as I did, the asynchronous commands directly one after the other.
  • sync_mode = STD without global memory sets type = DIRECT_COMMAND_NO_REPLY and doesn't wait for reply, ok.
  • sync_mode = STD with global memory sets type = DIRECT_COMMAND_REPLY and per direct command waits for reply, ok.
  • The message counter increments per direct command, ok.
  • The header correctly holds the size of the global memory, ok.

If you finished your homework, you are well prepared for new adventures. I would like to see you again in the next lesson.

8 comments:

  1. Hello,

    Thanks a lot for this detailed introduction ! It really helps getting started.

    I just wanted to share a troubleshooting tip for those who might run into the same problem I did.

    I kept receiving 0x|FF:00:00:00:00| instead of the desired 0x|03:00|2A:00|02| and finally found out that it was because the option "iPhone/iPad/iPod" was checked in the Bluetooth menu !

    Looking forward to the next lessons :)

    ReplyDelete
    Replies
    1. You are welcome! Thanks for your comment, I didn't know, that EV3's option changes the content of the messages.

      Delete
  2. Hello my name is ebrahim and tried to make the porcedimento via bluetooth using windows. I created the file using Notepad and saved as teste.py, open a command prompt, I went to the folder and typed teste.py and the window asking how desire to open the file to select the appropriate program! I'll need a Python IDE?

    Another thing I've used in direct command LEGO NXT via Bluetooth (with phone) via the application "S2 Terminal for Bluetooth" and sent and received all the commands and worked perfect, but I try the same with EV3 and could not.

    Would you help me?

    ReplyDelete
    Replies
    1. Hello Ebrahim,

      I think, you first need to install python3 on your windows computer. Then you can call python programs from the command prompt. You find information on the following link:

      https://automatetheboringstuff.com/appendixb/

      My blog is based on Unix machines. I should have mentioned it. A few sentences about Windows would be good. Maybe you can help me, I don't own a Windows computer.

      You find the python downloads at:

      https://www.python.org/downloads/windows/

      When you succeeded to run python3 programs on your machine, you need to get Bluetooth running.

      EV3 and NXT, both communicate via Bluetooth, but both use different types of commands. When you write "all the commands", what does it really mean?


      I hope, this information helps,

      Christoph

      Delete
  3. Hello :)
    Yes thank you so much ! it's really help me too.
    Nico

    ReplyDelete
  4. Thank's a lot for your blog and your knowledge !!! it is beautyfull for me ! (sorry for my english i'm french)
    without your blog i won't never be able to anderstand EV3's direct command.
    :)
    Thank's a lot !!!
    Nicolas

    ReplyDelete
    Replies
    1. Hi Nicolas,
      great to have readers like you!

      Delete
  5. Many many many thanks

    ReplyDelete