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

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

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, 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

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

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

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

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, 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

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, 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

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

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

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.

4 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