Friday 15 January 2016

Lesson 2 - Tell your EV3, what to do

EV3 Direct commands - Lesson 02

Introduction

Last lesson we coded class EV3, which allows to communicate with a LEGO EV3 device. We tested it with operation opNop, doing nothing. This lesson is about real instructions with arguments. This will make your EV3 device an active part of your programs. For the moment, we will not receive data from our EV3. This topic has to wait for some later lessons. We pick the following kinds of operations:

  • set EV3's brickname
  • play sound and tones
  • control its LEDs
  • display images
  • timers
  • start programs
  • simulate button actions
Please take document EV3 Firmware Developer Kit, LEGO's official documentation of EV3's operation set and read it parallel. Document EV3 Communication Developer Kit from LEGO's official documentation also contains some examples of direct commands.

If you didn't code class EV3 but you want to run the programs of this lesson, you are free to download module ev3 from ev3-python3. The only thing you have to modify in your programs is the mac-address. Replace 00:16:53:42:2B:99 by the value of your EV3 device.

Setting EV3's brickname

An important part in the art of programming is selecting good names. The way, we think about something is strongly dependent from the name, we use for it. Therefore we start with setting the brickname. To change your EV3's name to myEV3, you have to send the following direct command:


-------------------------------------------------                
 \ len \ cnt \ty\ hd  \op\cd\ Name               \               
  -------------------------------------------------              
0x|0E:00|2A:00|00|00:00|D4|08|84:6D:79:45:56:33:00|              
  -------------------------------------------------              
   \ 14  \ 42  \Re\ 0,0 \C \S \ "myEV3"            \             
    \     \     \  \     \o \E \                    \            
     \     \     \  \     \m \T \                    \           
      \     \     \  \     \_ \_ \                    \          
       \     \     \  \     \S \B \                    \         
        \     \     \  \     \e \R \                    \        
         \     \     \  \     \t \I \                    \       
          \     \     \  \     \  \C \                    \      
           \     \     \  \     \  \K \                    \     
            \     \     \  \     \  \N \                    \    
             \     \     \  \     \  \A \                    \   
              \     \     \  \     \  \M \                    \  
               \     \     \  \     \  \E \                    \ 
                -------------------------------------------------
    

The reply is:


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

which tells, that the direct command was successfully operated. You can control it with a look at your bricks display, in it's first line it should show the new name. Additionally, if some bluetooth device does a search and finds your EV3, it will be shown under the new name. A few remarks:

  • We used a new operation, that does some settings: opCom_Set = 0x|D4|
  • The operation opCom_Set always is followed by a CMD, that specifies the operation, because opCom_Set is used for very different settings. The CMD tells, which one is meant. You can think of both as of a two byte operation. But in terms of EV3, it is an operation (resp. instruction) and its CMD.
  • opCom_Set with CMD SET_BRICKNAME = 0x|08| needs one argument: NAME. In LEGOs description of the operation, you can read: (DATA8) NAME – First character in character string. But in fact, we send 0x|84:6D:79:45:56:33:00| as the value of the argument NAME. This needs some explanations:
    • 0x|6D:79:45:56:33| is the ascii-code of the character string myEV3 with 0x|6D| = “m”, 0x|79| = “y” and so on.
    • 0x|00| terminates the character string (this is known as zero terminated string).
    • 0x|84| is the leading identification byte of LCS character strings (in binary notation, it is: 0b 1000 0100).

The conclusion is, that every character string, you send to your EV3 as an operations constant argument, must be completed by a leading 0x|84| and a trailing 0x|00|. In my case, the concatenation results in LCS("myEV3") = 0x|84:6D:79:45:56:33:00| as the value of argument NAME.

Please add a static class-method to your class EV3 (in case of python, a module level function):

  • LCS(value: str) returns a byte-array in the format of LCS, that represents the value.
then add two constants opCom_Set = 0x|D4| and SET_BRICKNAME = 0x|08|.

This allows to write a little program that changes the brickname. I did it with the following code:


#!/usr/bin/env python3

import ev3_dc as ev3

my_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
my_ev3.verbosity = 1
ops = b''.join([
    ev3.opCom_Set,
    ev3.SET_BRICKNAME,
    ev3.LCS("myEV3")
])
my_ev3.send_direct_cmd(ops)
      
its output:

09:12:31.011558 Sent 0x|0E:00|2A:00|80|00:00|D4:08:84:6D:79:45:56:33:00|
      
Please look at the display of your EV3, if its name changed.

Constant integer arguments

Strings are one type of argument, there are others too. Common to all is, that the type of the argument is identified by the leading byte, the identification byte. In this lesson, we concentrate on constant arguments and local variables. The terminus constant argument is not very precise but it means arguments which have two characteristics:

  • They are arguments of operations.
  • They always hold values and never addresses.

There are the following formats of constant arguments:

  • character strings (one of them you have already studied)
  • integer values

Character strings are of variable length, integer values are signed and can hold values of 5 bits, 8 bits, 16 bits and 32 bits. Maybe you miss floats, but you will see, that no operation needs floats as arguments. In fact, there are only 5 kinds of constant arguments.

You should especially concentrate on the first byte, the identification byte, which defines the type and length of the variables. Bit 0 of the identification byte stands for short or long format:

  • 0b 0... .... short format (only one byte, the identification byte includes the value),
  • 0b 1... .... long format (the identification byte does not contain any bit of the value).

Bit 5 (in case of long format) stands for the length type:

  • 0b .... .0.. means fixed length,
  • 0b .... .1.. means zero terminated string.

Bits 6 and 7 (long format only) stand for the length of the following integer:

  • 0b .... ..00 means variable length,
  • 0b .... ..01 means one byte follows,
  • 0b .... ..10 says, two bytes follow,
  • 0b .... ..11 says, four bytes follow.

Now we write the 5 constants as binary masks, where S stands for the sign (0 is positive, 1 is negative), V stands for one bit of the value.

  • LC0: 0b 00SV VVVV, 5-bit integer value, range: -32 - 31, length: 1 byte, identified by 2 leading bits 00.
  • LC1: 0b 1000 0001 SVVV VVVV, 8-bit integer value, range: -127 - 127, length: 2 byte, identified by leading byte 0x|81|. Value 0x|80| is NaN.
  • LC2: 0b 1000 0010 VVVV VVVV SVVV VVVV, 16-bit integer value, range: -32,767 – 32,767, length: 3 byte, identified by leading byte 0x|82|. Value 0x|80:00| is NaN.
  • LC4: 0b 1000 0011 VVVV VVVV VVVV VVVV VVVV VVVV SVVV VVVV, 32-bit integer value, range: -2,147,483,647 – 2,147,483,647, length: 5 byte, identified by leading 0x|83|. Value 0x|80:00:00:00| is NaN.
  • LCS: 0b 1000 0100 VVVV VVVV ... 0000 0000, zero-terminated string, length: variable, identified by leading 0x|84|.

The byte sequence of LC2 and LC4 is little endian. That means, as you hopefully remember from lesson 1, that the identification byte is the head and the following bytes are in opposite sequence as you are used to. If an operation has integer constants as arguments, you can choose between LC0, LC1, LC2 or LC4. For small values (range -32 to 31), take LC0, for very large ones, take LC4. The direct command will be red from left to right. When the first byte of an argument is interpreted, then it's clear, which additional bytes belong to it and where to find the value. Always using the shortest possible variant reduces communication traffic and therefore accelerates the operation of the direct command, but this effect is small. More details about the identification byte of arguments can be found in LEGO's EV3 Firmware Developer Kit at part 3.4.

Please add another static class-method (or module function) to your class EV3:

  • LCX(value: int) returns a byte-array in the format of LC0, LC1, LC2 or LC4, dependent from the range of value.

Playing Sound Files

We want our EV3 brick to play the sound file /home/root/lms2012/sys/ui/DownloadSucces.rsf. This is done by the operation:

  • opSound = 0x|94| with CMD PLAY = 0x|02| with the arguments:
    • VOLUME: in percent [0 - 100]
    • NAME: sound file with absolute path, or relative to /home/root/lms2012/sys/ (without extension ".rsf")
The program:

#!/usr/bin/env python3

import ev3_dc as ev3

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

ops = b''.join([
    ev3.opSound,
    ev3.PLAY,
    ev3.LCX(100),                  # VOLUME
    ev3.LCS('./ui/DownloadSucces') # NAME
])
my_ev3.send_direct_cmd(ops)
      
The output:

09:42:03.575103 Sent 0x|1E:00|2A:00|80|00:00|94:02:81:64:84:2E:2F:75:69:2F:44:6F:77:6E:6C:6F:61:64:53:75:63:63:65:73:00|
      
The filesystem of the EV3 brick is not the topic of this lesson. For further information: Folder Structure

Playing sound files repeatedly

Operation opSound has a CMD REPEAT, that plays the sound file in an endless loop, which can be interrupted by operation opSound with CMD BREAK. These are two additional operations:

  • opSound = 0x|94| with CMD REPEAT = 0x|03| with the arguments:
    • VOLUME: in percent [0 - 100]
    • NAME: sound file with absolute path, or relative to /home/root/lms2012/sys/ (without extension ".rsf")
  • opSound = 0x|94| with CMD BREAK = 0x|00| without arguments.

We test it with this program:


#!/usr/bin/env python3

import ev3_dc as ev3
import time

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

ops = b''.join([
    ev3.opSound,
    ev3.REPEAT,
    ev3.LCX(100),                  # VOLUME
    ev3.LCS('./ui/DownloadSucces') # NAME
])
my_ev3.send_direct_cmd(ops)
time.sleep(5)
ops = b''.join([
    ev3.opSound,
    ev3.BREAK
])
my_ev3.send_direct_cmd(ops)
      
It plays the sound file for 5 sec, then stops the playing. The output:

09:55:28.814320 Sent 0x|1E:00|2A:00|80|00:00|94:03:81:64:84:2E:2F:75:69:2F:44:6F:77:6E:6C:6F:61:64:53:75:63:63:65:73:00|
09:55:33.822352 Sent 0x|07:00|2B:00|80|00:00|94:00|
      

Playing Tones

We want our EV3 brick to play tones. This is done by the operation:

  • opSound = 0x|94| with CMD TONE = 0x|01| with the arguments:
    • VOLUME: in percent [0 - 100]
    • FREQUENCY: in Hz, [250 - 10000]
    • DURATION: in milliseconds (0 stands for unlimited)
The direct command to play an a' for one second:

-------------------------------------------------        
 \ len \ cnt \ty\ hd  \op\cd\vo\ fr     \ du     \       
  -------------------------------------------------      
0x|0E:00|2A:00|80|00:00|94|01|01|82:B8:01|82:E8:03|      
  -------------------------------------------------      
   \ 14  \ 42  \no\ 0,0 \S \T \1 \ 440    \ 1000   \      
    \     \     \  \     \o \O \  \        \        \     
     \     \     \  \     \u \N \  \        \        \    
      \     \     \  \     \n \E \  \        \        \   
       \     \     \  \     \d \  \  \        \        \  
        -------------------------------------------------
      
and the program, to send it:

#!/usr/bin/env python3

import ev3_dc as ev3

my_ev3 = ev3.EV3(protocol=ev3.USB, host='00:16:53:42:2B:99')
ops = b''.join([
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),    # VOLUME
    ev3.LCX(440),  # FREQUENCY
    ev3.LCX(1000), # DURATION
])
my_ev3.send_direct_cmd(ops)
      

Proudly as we are, we want our EV3 to play the triad in c':

  • c' (262 Hz)
  • e' (330 Hz)
  • g' (392 Hz)
  • c'' (523 Hz)
We change our program to:

#!/usr/bin/env python3

import ev3_dc as ev3

my_ev3 = ev3.EV3(protocol=ev3.USB, host='00:16:53:42:2B:99')
ops = b''.join([
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(262),
    ev3.LCX(500),
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(330),
    ev3.LCX(500),
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(392),
    ev3.LCX(500),
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(2),
    ev3.LCX(523),
    ev3.LCX(1000)
])
my_ev3.send_direct_cmd(ops)
      
but we listen only to one tone, the last one (c''). Why that?
This is because the operations interrupt each other. You have to think of the operations as of impatient and badly behaving characters. Interuption is their standard. If you want to prevent that, you have to tell it explicitly. In case of sound, this is done by the operation:
  • opSound_Ready = 0x|96|
which waits until the sound has finished. Once more we change the program:

#!/usr/bin/env python3

import ev3_dc as ev3

my_ev3 = ev3.EV3(protocol=ev3.USB, host='00:16:53:42:2B:99')
ops = b''.join([
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(262),
    ev3.LCX(500),
    ev3.opSound_Ready,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(330),
    ev3.LCX(500),
    ev3.opSound_Ready,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(392),
    ev3.LCX(500),
    ev3.opSound_Ready,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(2),
    ev3.LCX(523),
    ev3.LCX(1000)
])
my_ev3.send_direct_cmd(ops)
      
Now we hear what we expected!

Changing the color of the LEDs

Our EV3 will never reach the quality of a real jukebox, but why not adding some light effects? This needs a new operation:

  • opUI_Write = 0x|82| with CMD LED = 0x|1B| and the argument:
    • PATTERN: GREEN = 0x|01|, RED = 0x|02|, etc.
Again we add some code to our program:

#!/usr/bin/env python3

import ev3_dc as ev3

my_ev3 = ev3.EV3(protocol=ev3.USB, host='00:16:53:42:2B:99')
my_ev3.verbosity = 1
ops = b''.join([
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_RED,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(262),
    ev3.LCX(500),
    ev3.opSound_Ready,
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_GREEN,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(330),
    ev3.LCX(500),
    ev3.opSound_Ready,
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_RED,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(392),
    ev3.LCX(500),
    ev3.opSound_Ready,
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_RED_FLASH,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(2),
    ev3.LCX(523),
    ev3.LCX(2000),
    ev3.opSound_Ready,
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_GREEN
])
my_ev3.send_direct_cmd(ops)
      
What we send is a direct command with 60 bytes length:

11:39:49.039902 Sent 0x|3C:00|2A:00|80|00:00|82:1B:02:94:01:01:82:06:01:82:F4:01:96:82:1B:01:94:01:01:82:4A:01:82:F4:01:96:82:1B:02:...
      
This is less than 6% of its maximum length.

Displaying Images

EV3's display is monochrome and has a resolution of 180 x 128 pixels. This sounds and is somewhat out of time but allows to show icons and emoticons or draw pictures. Operation opUI_Draw has a large number of different CMDs which operate on the display. Here we use four of them:

  • opUI_Draw = 0x|84| with CMD UPDATE = 0x|00| without arguments
  • opUI_Draw = 0x|84| with CMD TOPLINE = 0x|12| and the argument:
    • (Data8) ENABLE: Enable or disable top status line, [0: Disable, 1: Enable]
  • opUI_Draw = 0x|84| with CMD FILLWINDOW = 0x|13| and the arguments:
    • (Data8) COLOR: Specify either black or white, [0: White, 1: Black]
    • (Data16) Y0: Specify Y start point, [0 - 127]
    • (Data16) Y1: Specify Y size
  • opUI_Draw = 0x|84| with CMD BMPFILE = 0x|1C| and the arguments:
    • (Data8) COLOR: Specify either black or white, [0: White, 1: Black]
    • (Data16) X0: Specify X start point, [0 - 177]
    • (Data16) Y0: Specify X start point, [0 - 127]
    • (Data8) NAME: sound file with absolute path, or relative to /home/root/lms2012/sys/ (with extension ".rgf"). The name of this command is misleading. The file must be a file with extension .rgf (stands for robot graphic format) and not a bmp-image.
We run this program:

#!/usr/bin/env python3

import ev3_dc as ev3
import time

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

ops = b''.join([
    ev3.opUI_Draw,
    ev3.TOPLINE,
    ev3.LCX(0),                                      # ENABLE
    ev3.opUI_Draw,
    ev3.BMPFILE,
    ev3.LCX(1),                                      # COLOR
    ev3.LCX(0),                                      # X0
    ev3.LCX(0),                                      # Y0
    ev3.LCS("../apps/Motor Control/MotorCtlAD.rgf"), # NAME
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops)
time.sleep(5)
ops = b''.join([
    ev3.opUI_Draw,
    ev3.TOPLINE,
    ev3.LCX(1),     # ENABLE
    ev3.opUI_Draw,
    ev3.FILLWINDOW,
    ev3.LCX(0),     # COLOR
    ev3.LCX(0),     # Y0
    ev3.LCX(0),     # Y1
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops)
      
The output:

12:01:00.253855 Sent 0x|35:00|2A:00|80|00:00|84:12:00:84:1C:01:00:00:84:2E:2E:2F:61:70:70:...
12:01:05.265584 Sent 0x|0F:00|2B:00|80|00:00|84:12:01:84:13:00:00:00:84:00|
      
For five sec. the display shows the image MotorCtlAD.rgf, then the display becomes empty except for the topline. Some annotations:
  • Drawing something needs a canvas. This is the actual image of the display. We add some elements, then we call an UPDATE to make the canvas visible. If you prefer to start with an empty canvas, you have to explicitly erase the canvas' content.
  • CMD TOPLINE allows to switch the topline on or off.
  • CMD FILLWINDOW allows to fill or erase a part of the window. If both arguments Y0 and Y1 are zero, the whole display is meant.
  • Setting argument COLOR of CMD BMPFILE to value 0 inverts the colors of the image.
  • Operation opUI_Draw allows to store and restore images (CMDs STORE and RESTORE). But the stored images are lost when the execution of the actual direct command ends.
You are welcome to test a few more CMDs of operation opUI_Draw.

The Local Memory

In lesson 1 we red, that the local memory is the address space to hold intermediate information. Now we learn how to use it and again we talk about the identification byte, which defines the type and length of variables. We will code another function LVX, which returns addresses of the local memory. As you already know, bit 0 of the identification byte stands for short or long format:

  • 0b 0... .... short format (only one byte, the identification byte includes the value),
  • 0b 1... .... long format (the identification byte does not contain any bit of the value).

If bits 1 and 2 are 0b .10. ...., they stand for local variables, which are addresses of the local memory.

Bits 6 and 7 stand for the length of the following value:

  • 0b .... ..00 means variable length,
  • 0b .... ..01 means one byte follows,
  • 0b .... ..10 says two bytes follow,
  • 0b .... ..11 says four bytes follow.

This allows to write the 4 local variables as binary masks, we don't need signs because addresses are always positive numbers. V stands for one bit of the address (value).

  • LV0: 0b 010V VVVV, 5-bit address, range: 0 - 31, length: 1 byte, identified by three leading bits 010.
  • LV1: 0b 1100 0001 VVVV VVVV, 8-bit address, range: 0 - 255, length: 2 byte, identified by leading byte 0x|C1|.
  • LV2: 0b 1100 0010 VVVV VVVV VVVV VVVV, 16-bit address, range: 0 – 65.536, length: 3 byte, identified by leading byte 0x|C2|.
  • LV4: 0b 1100 0011 VVVV VVVV VVVV VVVV VVVV VVVV VVVV VVVV, 32-bit address, range: 0 – 4,294,967,296, length: 5 byte, identified by leading byte 0x|C3|.

A few remarks:

  • In direct commands, there is no need for LV2 and LV4! You remember that the local memory has a maximum of 63 bytes.
  • Addresses of the local memory must be placed correctly. If you write a 4-byte value into the local memory, its address needs to be 0, 4, 8, ... (a multiple of 4). The same with 2-byte values, their address must be multiples of 2.
  • You need to split the local memory into segments of the needed lengths, then use the addresses of the first byte of every segment.
  • The header bytes contain the total length of the local memory (for details read lesson 1). Don't forget to send correct header bytes!

A new module function: LVX

Please add a function LVX(value) to your module ev3, that returns the shortest of the types LV0, LV1, LV2 or LV4, dependent from the value. I have done it, now the documentation of my module ev3 says:


FUNCTIONS
    LCS(value:str) -> bytes
        pack a string into a LCS
    
    LCX(value:int) -> bytes
        create a LC0, LC1, LC2, LC4, dependent from the value
    
    LVX(value:int) -> bytes
        create a LV0, LV1, LV2, LV4, dependent from the value
      

Timers

Contolling time is an important aspect in real time programs. We have seen how to wait until a tone ended and we waited in our local program until we stopped the repeated playing of a sound file. The operation set of the EV3 includes timer operations which allow to wait in the execution of a direct command. We use the following two operations:

  • opTimer_Wait = 0x|85| with the arguments:
    • (Data32) TIME: Time to wait (in milliseconds)
    • (Data32) TIMER: Variable used for timing
    This operation writes a 4-bytes timestamp into the local or global memory.
  • opTimer_Ready = 0x|86| with the argument:
    • (Data32) TIMER: Variable used for timing
    This operation reads a timestamp and waits until the actual time reaches the value of this timestamp.

We test the timer operations with a program that draws a triangle. This needs another CMD of operation opUI_Draw:

  • opUI_Draw = 0x|84| with CMD LINE = 0x|03| and the arguments:
    • (Data8) COLOR: Specify either black or white, [0: White, 1: Black]
    • (Data16) X0: Specify X start point, [0 - 177]
    • (Data16) Y0: Specify Y start point, [0 - 127]
    • (Data16) X1: Specify X end point
    • (Data16) Y1: Specify Y end point
The program:

#!/usr/bin/env python3

import ev3_dc as ev3

my_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
ops = b''.join([
    ev3.opUI_Draw,
    ev3.TOPLINE,
    ev3.LCX(0),     # ENABLE
    ev3.opUI_Draw,
    ev3.FILLWINDOW,
    ev3.LCX(0),     # COLOR
    ev3.LCX(0),     # Y0
    ev3.LCX(0),     # Y1
    ev3.opUI_Draw,
    ev3.UPDATE,
    ev3.opTimer_Wait,
    ev3.LCX(1000),
    ev3.LVX(0),
    ev3.opTimer_Ready,
    ev3.LVX(0),
    ev3.opUI_Draw,
    ev3.LINE,
    ev3.LCX(1),     # COLOR
    ev3.LCX(2),     # X0
    ev3.LCX(125),   # Y0
    ev3.LCX(88),    # X1
    ev3.LCX(2),     # Y1
    ev3.opUI_Draw,
    ev3.UPDATE,
    ev3.opTimer_Wait,
    ev3.LCX(500),
    ev3.LVX(0),
    ev3.opTimer_Ready,
    ev3.LVX(0),
    ev3.opUI_Draw,
    ev3.LINE,
    ev3.LCX(1),     # COLOR
    ev3.LCX(88),    # X0
    ev3.LCX(2),     # Y0
    ev3.LCX(175),   # X1
    ev3.LCX(125),   # Y1
    ev3.opUI_Draw,
    ev3.UPDATE,
    ev3.opTimer_Wait,
    ev3.LCX(500),
    ev3.LVX(0),
    ev3.opTimer_Ready,
    ev3.LVX(0),
    ev3.opUI_Draw,
    ev3.LINE,
    ev3.LCX(1),     # COLOR
    ev3.LCX(175),   # X0
    ev3.LCX(125),   # Y0
    ev3.LCX(2),     # X1
    ev3.LCX(125),   # Y1
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops, local_mem=4)
      
This program cleans the display, then waits for one sec., draws a line, waits for half a sec., draws a 2nd line, waits and finally draws a 3rd line. It needs 4 bytes of local memory, which are multiple times written in and red out.

Obviously the timing can be done in the local program or in the direct command. We change the program:


#!/usr/bin/env python3

import ev3_dc as ev3
import time

my_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
ops = b''.join([
    ev3.opUI_Draw,
    ev3.TOPLINE,
    ev3.LCX(0),     # ENABLE
    ev3.opUI_Draw,
    ev3.FILLWINDOW,
    ev3.LCX(0),     # COLOR
    ev3.LCX(0),     # Y0
    ev3.LCX(0),     # Y1
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops)
time.sleep(1)
ops = b''.join([
    ev3.opUI_Draw,
    ev3.LINE,
    ev3.LCX(1),     # COLOR
    ev3.LCX(2),     # X0
    ev3.LCX(125),   # Y0
    ev3.LCX(88),    # X1
    ev3.LCX(2),     # Y1
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops)
time.sleep(0.5)
ops = b''.join([
    ev3.opUI_Draw,
    ev3.LINE,
    ev3.LCX(1),     # COLOR
    ev3.LCX(88),    # X0
    ev3.LCX(2),     # Y0
    ev3.LCX(175),   # X1
    ev3.LCX(125),   # Y1
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops)
time.sleep(0.5)
ops = b''.join([
    ev3.opUI_Draw,
    ev3.LINE,
    ev3.LCX(1),     # COLOR
    ev3.LCX(175),   # X0
    ev3.LCX(125),   # Y0
    ev3.LCX(2),     # X1
    ev3.LCX(125),   # Y1
    ev3.opUI_Draw,
    ev3.UPDATE
])
my_ev3.send_direct_cmd(ops)
      
Both alternatives result in the same behaviour of the display but are different. The 1st version needs less communication but blocks the EV3 device until the direct command ends its execution. The 2nd version needs four direct commands but allows to send other direct commands while the drawing sleeps.

Starting programs

Direct commands allow to start programs. You normally do it by pressing buttons of the EV3 device. A program is a file with the extension ".rbf", that exists in the filesystem of the EV3. We will start the program /home/root/lms2012/apps/Motor Control/Motor Control.rbf. This needs two new operations:

  • opFile = 0x|C0| with CMD LOAD_IMAGE = 0x|08| and the arguments:
    • (Data16) PRGID: Slot, where the program has to run. Value 0x|01| is used for executing user projects, apps and tools.
    • (Data8) NAME: executable file with absolute path, or relative to /home/root/lms2012/sys/ (with extension ".rbf")
    Returns:
    • (Data32) SIZE: Image size in bytes
    • (Data32) *IP: Address of image
    This operation is the loader. It places a program into memory and prepares it for execution.
  • opProgram_Start = 0x|03| with the arguments:
    • (Data16) PRGID: Slot, where the program has to run.
    • (Data32) SIZE: Image size in bytes
    • (Data32) *IP: Address of image
    • (Data8) DEBUG: Debug mode, value 0 stands for normal mode.

The program:


#!/usr/bin/env python3

import ev3_dc as ev3

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

ops = b''.join([
    ev3.opFile,
    ev3.LOAD_IMAGE,
    ev3.LCX(1),                                         # SLOT
    ev3.LCS('../apps/Motor Control/Motor Control.rbf'), # NAME
    ev3.LVX(0),                                         # SIZE
    ev3.LVX(4),                                         # IP*
    ev3.opProgram_Start,
    ev3.LCX(1),                                         # SLOT
    ev3.LVX(0),                                         # SIZE
    ev3.LVX(4),                                         # IP*
    ev3.LCX(0)                                          # DEBUG
])
my_ev3.send_direct_cmd(ops, local_mem=8)
      
The return values of the first operation are SIZE and IP*. We write them to the local memory at addresses 0 and 4. The second operation reads its arguments SIZE and IP* from the local memory. It's arguments SLOT and DEBUG are given as constant values. The output of the program:

12:50:45.332826 Sent 0x|38:00|2A:00|80|00:20|C0:08:01:84:2E:2E:2F:61:70:70:73:2F:4D:6F:74:6F:...
      
This really starts the program /home/root/lms2012/apps/Motor Control/Motor Control.rbf.

Simulating Button presses

In this example, we shutdown the EV3 brick by simulating the following button presses:

  1. BACK_BUTTON = 0x|06|
  2. RIGHT_BUTTON = 0x|04|
  3. ENTER_BUTTON = 0x|02|

We need to wait until the initiated operations are finished. This can be done by the operation opUI_Button with CMD WAIT_FOR_PRESS, which once more prevents interruption. The following new operations are used:

  • opUI_Button = 0x|83| with CMD PRESS = 0x|05| and argument:
    • BUTTON: Up Button = 0x|01|, Enter Button = 0x|02|, etc.
  • opUI_Button = 0x|83| with CMD WAIT_FOR_PRESS = 0x|03|
The direct command has the following structure:

-------------------------------------------------------------                 
 \ len \ cnt \ty\ hd  \op\cd\bu\op\cd\op\cd\bu\op\cd\op\cd\bu\                
  -------------------------------------------------------------               
0x|12:00|2A:00|80|00:00|83|05|06|83|03|83|05|04|83|03|83|05|02|               
  -------------------------------------------------------------               
   \ 18  \ 42  \no\ 0,0 \U \P \B \U \W \U \P \R \U \W \U \P \E \              
    \     \     \  \     \I \R \A \I \A \I \R \I \I \A \I \R \N \             
     \     \     \  \     \_ \E \C \_ \I \_ \E \G \_ \I \_ \E \T \            
      \     \     \  \     \B \S \K \B \T \B \S \H \B \T \B \S \E \           
       \     \     \  \     \U \S \_ \U \_ \U \S \T \U \_ \U \S \R \          
        \     \     \  \     \T \  \B \T \F \T \  \_ \T \F \T \  \_ \         
         \     \     \  \     \T \  \U \T \O \T \  \B \T \O \T \  \B \        
          \     \     \  \     \O \  \T \O \R \O \  \U \O \R \O \  \U \       
           \     \     \  \     \N \  \T \N \_ \N \  \T \N \_ \N \  \T \      
            \     \     \  \     \  \  \O \  \P \  \  \T \  \P \  \  \T \     
             \     \     \  \     \  \  \N \  \R \  \  \O \  \R \  \  \O \    
              \     \     \  \     \  \  \  \  \E \  \  \N \  \E \  \  \N \   
               \     \     \  \     \  \  \  \  \S \  \  \  \  \S \  \  \  \  
                \     \     \  \     \  \  \  \  \S \  \  \  \  \S \  \  \  \ 
                 -------------------------------------------------------------
      
My corresponding program:

#!/usr/bin/env python3

import ev3_dc as ev3

my_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
ops = b''.join([
    ev3.opUI_Button,
    ev3.PRESS,
    ev3.BACK_BUTTON,
    ev3.opUI_Button,
    ev3.WAIT_FOR_PRESS,
    ev3.opUI_Button,
    ev3.PRESS,
    ev3.RIGHT_BUTTON,
    ev3.opUI_Button,
    ev3.WAIT_FOR_PRESS,
    ev3.opUI_Button,
    ev3.PRESS,
    ev3.ENTER_BUTTON
])
my_ev3.send_direct_cmd(ops)
      
This really shuts down the EV3 device!

There was no need for any reply, but I'm a curious person. My question: will the EV3 reply before it shuts down or will it not?


#!/usr/bin/env python3

import ev3_dc as ev3

my_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
my_ev3.verbosity = 1
my_ev3.sync_mode = ev3.SYNC
ops = b''.join([
    ev3.opUI_Button,
    ev3.PRESS,
    ev3.BACK_BUTTON,
    ev3.opUI_Button,
    ev3.WAIT_FOR_PRESS,
    ev3.opUI_Button,
    ev3.PRESS,
    ev3.RIGHT_BUTTON,
    ev3.opUI_Button,
    ev3.WAIT_FOR_PRESS,
    ev3.opUI_Button,
    ev3.PRESS,
    ev3.ENTER_BUTTON
])
my_ev3.send_direct_cmd(ops)
      
Nothing happened until I pressed another button, then it replied and shut down. It's not astonishing, that this is not consistent. Shutdown and replying don't fit together, the EV3 device can't finish the command and then send a reply!

What we have learned

  • Direct commands consist of a sequence of operations. When we send a direct command to the brick, one operation after the other is executed. But they interrupt each other and it needs special operations, if we want them to wait.
  • Most of the operations need arguments, which can be sent in the formats LC0, LC1, LC2 and LC4, which all include signed integers, but have different ranges.
  • Another format is LCS, used for strings. It starts with 0x|84|, then follows the zero terminated ascii code of the string.
  • Local variables (LV0, LV1, LV2 and LV4) allow to address the local memory which holds intermediate data.
  • Some of the operations have a number of CMDs, which define different tasks with different sets of arguments.
  • We have seen a number of operations and know about the meaning of their arguments, but this is only a very small part of EV3's operation set.

Conclusion

Our knowledge about direct commands has grown, so did our class EV3. It costs some patience, to add all the constants we need. With the growing number of operations, the reference document of direct commands EV3 Firmware Developer Kit needs to be red intensively. Here is the actual state of my functions and data:


Help on module ev3:

NAME
    ev3 - LEGO EV3 direct commands

CLASSES
    builtins.object
        EV3
    
    class EV3(builtins.object)
     ...

FUNCTIONS
    LCS(value:str) -> bytes
        pack a string into a LCS
    
    LCX(value:int) -> bytes
        create a LC0, LC1, LC2, LC4, dependent from the value
    
    LVX(value:int) -> bytes
        create a LV0, LV1, LV2, LV4, dependent from the value

DATA
    ASYNC = 'ASYNC'
    BACK_BUTTON = b'\x06'
    BLUETOOTH = 'Bluetooth'
    BMPFILE = b'\x1c'
    BREAK = b'\x00'
    ENTER_BUTTON = b'\x02'
    FILLWINDOW = b'\x13'
    LED = b'\x1b'
    LED_OFF = b'\x00'
    LED_GREEN = b'\x01'
    LED_GREEN_FLASH = b'\x04'
    LED_GREEN_PULSE = b'\x07'
    LED_ORANGE = b'\x03'
    LED_ORANGE_FLASH = b'\x06'
    LED_ORANGE_PULSE = b'\t'
    LED_RED = b'\x02'
    LED_RED_FLASH = b'\x05'
    LED_RED_PULSE = b'\x08'
    LINE = b'\x03'
    LOAD_IMAGE = b'\x08'
    PLAY = b'\x02'
    PRESS = b'\x53'
    REPEAT = b'\x02'
    RIGHT_BUTTON = b'\x04'
    SET_BRICKNAME = b'\x08'
    STD = 'STD'
    SYNC = 'SYNC'
    TONE = b'\x01'
    TOPLINE = b'\x12'
    USB = 'Usb'
    UPDATE = b'\x00'
    WAIT_FOR_PRESS = b'\x03'
    WIFI = 'Wifi'
    opCom_Set = b'\xd4'
    opFile = b'\xc0'
    opNop = b'\x01'
    opProgram_Start = b'\x03'
    opSound = b'\x94'
    opSound_Ready = b'\x96'
    opTimer_Wait = b'\x85'
    opTimer_Ready = b'\x86'
    opUI_Button = b'\x83'
    opUI_Draw = b'\x84'
    opUI_Write = b'\x82'
      

A real robot reads data from its sensors and does movements with its motors. For the moment our EV3 device does neither. I also know, that there exist electronic devices with cooler sound- or light-effects. Now it's on you to test some of the other operations you find in the EV3 Firmware Developer Kit.

Please keep in contact, the next lesson will be about motors. I hope, we will come closer to the topics of your real interest.

16 comments:

  1. Hi
    I tried to run the change of name code!

    but I have the following error (generated by one of the imported ev3.py):

    File "D:\ev3.py", line 408, in send_direct_cmd
    self._socket.send(cmd)
    AttributeError: 'NoneType' object has no attribute 'send'

    I appreciate your comment!

    Sam

    ReplyDelete
    Replies
    1. Hi Sam,

      great, that you try to use my class and sorry, that it doesn't work as expected. For the moment, I don't know, what happened and I need some more information.

      First: I just downloaded EV3.py and tried to reproduce your error. In the original version of class EV3 the above mentioned line of code is line number 418, your error message found it at line 408. I think, you changed the code of module EV3.py and then tried to run the program, which is fine! All these code examples are thought to be manipulated.

      Second: self._socket is a 'None Type' object when you try to call its method send. This says, that your version of class EV3 did not successfully establish a bluetooth-socket.

      Did you sucessfully run the bluetooth python version of doing nothing from lesson 1? Please try this and then tell me what happens.

      Best regards
      Christoph

      Delete
    2. Humm

      Sorry but I don't get it!

      Why do I need to worry about Bluetooth connection if I am not using Bluetooth. I am only trying the code using the code to check if it works with me (using usb cable), then later worry about any wireless connection.

      Sam

      Delete
    3. Hi Sam,

      the program executes the following line of code:

      my_ev3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='your mac-address')

      This tries to connect the EV3 device and establish a socket for communication. There are three options for communication protocols: WIFI, BLUETOOTH and USB. The program above chooses BLUETOOTH. If you try to communicate via USB, you may modify this line into:

      my_ev3 = ev3.EV3(protocol=ev3.USB)

      connect your computer and your EV3 device with an USB wire and again run the program. My assumption was, that you started the program with protocol=ev3.BLUETOOTH and I couldn't know, that you tried it with USB. I hope, this clarifies the background of my question.

      Best regards
      Christoph

      Delete
  2. Hi Christoph,

    You have done a great job. Thanks for sharing.
    I am learning robotics with ROS (Robot Operating System) and at the moment I am developing a node that uses the class that you have programmed and allows publishing data from Lego sensors in ROS and receiving commands.
    I have discovered that there is a bug in the function "send_direct_cmd", in the line "struct.pack ('<hh', len (ops) + 5, msg_cnt)". When the counter exceeds the value 32767, an error occurs because the instruction expects a value in short form. I have corrected this by modifying the reset value of the variable "_msg_cnt".

    Greetings,
    Jose Enrique Cabrera

    ReplyDelete
    Replies
    1. Hi Jose Enrique,
      thanks for your comment.
      It's good to know, that my code is usefull for other projects too.

      I expect you want this at line 395:

      if self._msg_cnt < 32767:
      self._msg_cnt += 1
      else:
      self._msg_cnt = 1

      I guess, you implement class EV3 (without the task stuff) into ROS. Is this correct?

      Greetings,
      Christoph

      Delete
    2. Hi Christoph,
      sorry for my English. What do you mean "without the task stuff"?
      Basically what I'm doing is using your ev3.send_direct_cmd function to get the data from all the sensors and motors as well as to send orders to them.
      Then I package the information in a structure and publish it to make it available to ROS nodes.
      Since the refresh rate is important to be as high as possible I have had to send the read commands to all the devices in a single order instead of asking for the data one by one in a loop. With this I have managed to go from times of the order of 1s to times less than 20 ms.

      Delete
    3. By the way, I am trying to use opOutput_Read command to read velocity value and tacho counter value but I am only able to get velocity value.
      I am using the following code:

      def opOutput_Read(self, port, layer=0):
      ops = b''.join([
      ev3.opOutput_Read,
      ev3.LCX(layer), # LAYER [0-3]
      ev3.LCX(port), # PORT [0-3]
      ev3.GVX(4), # SPEED %
      ev3.GVX(0), # TACHO COUNT
      ])
      reply = self.ev3Obj.send_direct_cmd(ops, global_mem=5)
      (tacho,speed) = struct.unpack('<Ib', reply[5:])
      return (tacho, speed)

      What am I doing wrong?

      Greetings,
      Christoph

      Delete
  3. Sorry...the problem of copy and paste.

    Greetings,

    Jose Enrique.

    ReplyDelete
  4. Hi Jose Enrique,

    take a look into class TwoWheelVehicle (current filename is ev3_vehicle.py, filename will change soon). There you find an internal method _ops_pos(self), that reads the current positions of two motors. Good luck!

    Your question about task stuff: I use thread tasks to organize parallel execution, but as far as I know, ROS is based on subscritions. Therefore I guessed, that you are interested in class EV3 only.

    Greetings,
    Christoph

    ReplyDelete
    Replies
    1. Hi Christoph,

      Thanks for your answer. I have looked at the function. I see you use the command "opInput_Device" that read the motor as it was a sensor. But I am trying to use opOutput_Read that it is supposed to return two values: speed and tacho counter.
      I get no errors and the speed value is right but tacho counter not. Can you see any error on my code? I have taken into account that tacho value must be adressed first in the global memory on the response but I don't know what else to do.
      And you are right, I dont need to use thread task because as you says communications between ROS nodes are bases on subscriptions, services and actions.

      Greetings,
      Jose Enrique

      Delete
    2. I have solve the problem. I don't know what was happening but after a restart of the brick It began to work properly.

      Greetings,
      Jose Enrique

      Delete
  5. Hi Christoph,

    Your work is very useful to simplify the use and your explanations are very clear. I use this module in a university context and my goal is to create autonomous forklifts. The architecture that is imposed on me requires me to run internal programs in the robots that are able to receive commands from a pc.

    With this system, I encounter a misunderstanding with the communication protocol of LEGO.
    Is it possible, using your module, to send a command containing only an integer or a character and not an action such as "change the color of a led", "run an engine"?
    In this way, the sent integer would be received by a messaging block in a lego program. So when receiving "1" or "s", the robot would understand that it has to stop.

    That's a lot of context, but a quite simple request. Feel free to make me rephrase it. Thank you very much in advance! Merry christmas

    ReplyDelete
  6. Hallo,

    thank you for your positive feedback.

    To answer your quite simple question: I need to confess, that I never did use the mailbox of my ev3-device, but there is a system command, to write messages: WRITEMAILBOX

    You find the documentation in LEGO's Communication Developer Kit, section 3.3.9 at page 21.
    The mailbox is not really a mailbox, it is a messaging service. Messages are byte-strings and messages have names. The above mentioned system command allows to send a message to the ev3 device, where it is stored under the name, you gave it.

    Alternatively you can use direct commands. Commands opMailbox_Open, opMailbox_Write, opMailbox_Read, opMailbox_Test, opMailbox_Ready and opMailbox_Close allow to read and write messages. This seems to be the preferable option.

    I do not really know, what you try to do, but the second option allows to start programs on the ev3 device with arguments. Your host program first writes the argument values as messages into the mailbox, then it starts the program on the ev3 device, which reads the argument values from the messages and writes its return values as messages into the mailbox. Finally your host program reads the return messages from the mailbox.

    This says, the local program on the ev3 device acts as a function does. Or, more precisely see it as a service on a server. The host program becomes the client, which calls the service.

    I hope, this is detailed enough. Tell me about your success. It seems to be worth to document it as an alternative option to communicate with an ev3 device. Maybe, you can send me some details of your code, then I will extend the documentation.

    Kind regards,
    Christoph

    ReplyDelete
  7. You have done a great job on this article. It’s very readable and highly intelligent. You have even managed to make it understandable and easy to read. You have some real writing talent. Thank you. Direct Response Copywriter

    ReplyDelete
    Replies
    1. Hi,
      thanks for your very positive feedback. From now on, whenever I feel unhappy, I will read your comment.

      kind regards,
      Christoph

      Delete