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
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, becauseopCom_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 CMDSET_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 send0x|84:6D:79:45:56:33:00|
as the value of the argumentNAME
. This needs some explanations:0x|6D:79:45:56:33|
is the ascii-code of the character string myEV3 with0x|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.
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 bits00
. - LC1:
0b 1000 0001 SVVV VVVV
, 8-bit integer value, range: -127 - 127, length: 2 byte, identified by leading byte0x|81|
. Value0x|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 byte0x|82|
. Value0x|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 leading0x|83|
. Value0x|80:00:00:00|
is NaN. - LCS:
0b 1000 0100 VVVV VVVV ... 0000 0000
, zero-terminated string, length: variable, identified by leading0x|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 CMDPLAY = 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")
#!/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 CMDREPEAT = 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 CMDBREAK = 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 CMDTONE = 0x|01|
with the arguments:- VOLUME: in percent [0 - 100]
- FREQUENCY: in Hz, [250 - 10000]
- DURATION: in milliseconds (0 stands for unlimited)
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)
#!/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|
#!/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 CMDLED = 0x|1B|
and the argument:- PATTERN:
GREEN = 0x|01|
,RED = 0x|02|
, etc.
- PATTERN:
#!/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 CMDUPDATE = 0x|00|
without argumentsopUI_Draw = 0x|84|
with CMDTOPLINE = 0x|12|
and the argument:- (Data8) ENABLE: Enable or disable top status line, [0: Disable, 1: Enable]
opUI_Draw = 0x|84|
with CMDFILLWINDOW = 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 CMDBMPFILE = 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.
#!/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 argumentsY0
andY1
are zero, the whole display is meant. - Setting argument
COLOR
of CMDBMPFILE
to value 0 inverts the colors of the image. - Operation
opUI_Draw
allows to store and restore images (CMD
sSTORE
andRESTORE
). But the stored images are lost when the execution of the actual direct command ends.
CMD
s 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 bits010
. - LV1:
0b 1100 0001 VVVV VVVV
, 8-bit address, range: 0 - 255, length: 2 byte, identified by leading byte0x|C1|
. - LV2:
0b 1100 0010 VVVV VVVV VVVV VVVV
, 16-bit address, range: 0 – 65.536, length: 3 byte, identified by leading byte0x|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 byte0x|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
opTimer_Ready = 0x|86|
with the argument:- (Data32) TIMER: Variable used for timing
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 CMDLINE = 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
#!/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 CMDLOAD_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")
- (Data32) SIZE: Image size in bytes
- (Data32) *IP: Address of image
- (Data16) PRGID: Slot, where the program has to
run. Value
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:
BACK_BUTTON = 0x|06|
RIGHT_BUTTON = 0x|04|
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 CMDPRESS = 0x|05|
and argument:- BUTTON:
Up Button = 0x|01|
,Enter Button = 0x|02|
, etc.
- BUTTON:
opUI_Button = 0x|83|
with CMDWAIT_FOR_PRESS = 0x|03|
-------------------------------------------------------------
\ 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.
Hi
ReplyDeleteI 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
Hi Sam,
Deletegreat, 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
Humm
DeleteSorry 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
Hi Sam,
Deletethe 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
Hi Christoph,
ReplyDeleteYou 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
Hi Jose Enrique,
Deletethanks 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
Hi Christoph,
Deletesorry 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.
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.
DeleteI 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
Sorry...the problem of copy and paste.
ReplyDeleteGreetings,
Jose Enrique.
Hi Jose Enrique,
ReplyDeletetake 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
Hi Christoph,
DeleteThanks 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
I have solve the problem. I don't know what was happening but after a restart of the brick It began to work properly.
DeleteGreetings,
Jose Enrique
Hi Christoph,
ReplyDeleteYour 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
Hallo,
ReplyDeletethank 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
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
ReplyDeleteHi,
Deletethanks for your very positive feedback. From now on, whenever I feel unhappy, I will read your comment.
kind regards,
Christoph
Thank You and that i have a nifty present: How Much Is A Complete House Renovation home renovation cost
ReplyDelete