Friday 29 January 2016

Lesson 4 - Exact movements of vehicles with two drived wheels

EV3 Direct commands - Lesson 04

Introduction

Last lesson, we coded class TwoWheelVehicle, a subclass of EV3. Its methods are move and stop, but this is not more than a thin wrapper around the operations opOutput_Speed, opOutput_Start and opOutput_Stop. At the end of this lesson, class TwoWheelVehicle will have real substance. As most software, it grows step by step and this lesson will not be the last time, we work on this class.

Last lessons topic was a remote controlled vehicle. We coded unlimited movements, which were interrupted by new commands. We have seen the benefit of this design concept, it doesn't block the EV3 device. The first version of exact movements (the result of this lesson) will block the EV3 device and it will cost us further work to find a solution that doesn't block.

Synchronized motor movements

Hopefully last lessons vehicle still exists. We need it once more. As you remember, the right wheel was connected to port A, the left one to port D. Our solution of the remote control had a deficit: the slower the movement, the worse became the precision of turns. What we need, is an operation, where the speed of two motors can be set to a defined ratio. As you may expect, this kind of operation exists. Please send the following direct command to your vehicle:

------------------------------------------------------------- \ len \ cnt \ty\ hd \op\la\no\sp\ tu \ step \br\op\la\no\ ------------------------------------------------------------- 0x|12:00|2A:00|80|00:00|B0|00|09|3B|81:32|82:D0:02|00|A6|00|09| ------------------------------------------------------------- \ 18 \ 42 \no\ 0,0 \O \0 \A \-5\ 50 \ 720 \0 \O \0 \A \ \ \ \ \ \u \ \+ \ \ \ \ \u \ \+ \ \ \ \ \ \t \ \D \ \ \ \ \t \ \D \ \ \ \ \ \p \ \ \ \ \ \ \p \ \ \ \ \ \ \ \u \ \ \ \ \ \ \u \ \ \ \ \ \ \ \t \ \ \ \ \ \ \t \ \ \ \ \ \ \ \_ \ \ \ \ \ \ \_ \ \ \ \ \ \ \ \S \ \ \ \ \ \ \S \ \ \ \ \ \ \ \t \ \ \ \ \ \ \t \ \ \ \ \ \ \ \e \ \ \ \ \ \ \a \ \ \ \ \ \ \ \p \ \ \ \ \ \ \r \ \ \ \ \ \ \ \_ \ \ \ \ \ \ \t \ \ \ \ \ \ \ \S \ \ \ \ \ \ \ \ \ \ \ \ \ \ \y \ \ \ \ \ \ \ \ \ \ \ \ \ \ \n \ \ \ \ \ \ \ \ \ \ \ \ \ \ \c \ \ \ \ \ \ \ \ \ \ -------------------------------------------------------------
Motor A rotates 720°, motor D moves 360°. The vehicle turns left, both motors rotate with low speed and well synchronized. The new operation is:
  • opOutput_Step_Sync = 0xB0 with the arguments:
    • LAYER
    • NOS: Two output ports, the command is not symmetric, it distinguishes between the lower port and the higher. In our case, the lower port is PORT_A, the right wheel, the higher is PORT_D, the left wheel.
    • SPEED
    • TURN: Turn ratio, [-200 - 200]
    • STEP: Tacho pulses in degrees, value 0 stands for infinite movement (if TURN > 0, STEP limits the lower port, if TURN < 0, it limits the higher port). Positive, the sign of argument SPEED makes the direction
    • BRAKE

The argument TURN needs some explanations. But you are in a good position, you already know its meaning, because we used it in our remote control program of lesson 3. As you may remember, we calculated the speed of the two wheels as:


    if turn > 0:
        speed_right  = speed
        speed_left = round(speed * (1 - turn / 100))
    else:
        speed_right  = round(speed * (1 + turn / 100))
        speed_left = speed
      
The rounding to integer values was the cause of the bad precision at low speed! But now we become happy, operation opOutput_Step_Sync does the calculation without rounding:

    if turn > 0:
        speed_right  = speed
        speed_left = speed * (1 - turn / 100)
    else:
        speed_right  = speed * (1 + turn / 100)
        speed_left = speed
      
Speed and steps are proportional, we can also write:

    if turn > 0:
        step_right  = math.copysign(1, speed) * step
        step_left = math.copysign(1, speed) * step * (1 - turn / 100)
    else:
        step_right  = math.copysign(1, speed) * step * (1 + turn / 100)
        step_left = math.copysign(1, speed) * step
      
Operation opOutput_Step_Sync is perfect for vehicles with two drived wheels! It seems, as if it is made for it. Please improve your remote control program by replacing the two operations opOutput_Speed with a single opOutput_Step_Sync. You will see, it works better, especially at low speed. I changed my code of function move to:

    def move(self, speed: int, turn: int) -> None:
        assert self._sync_mode != ev3.SYNC, 'no unlimited operations allowed in sync_mode SYNC'
        assert isinstance(speed, int), "speed needs to be an integer value"
        assert -100 <= speed and speed <= 100, "speed needs to be in range [-100 - 100]"
        assert isinstance(turn, int), "turn needs to be an integer value"
        assert -200 <= turn and turn <= 200, "turn needs to be in range [-200 - 200]"
        if self._polarity == -1:
            speed *= -1
        if self._port_left < self._port_right:
            turn *= -1
        ops = b''.join([
            ev3.opOutput_Step_Sync,
            ev3.LCX(0),                                  # LAYER
            ev3.LCX(self._port_left + self._port_right), # NOS
            ev3.LCX(speed),
            ev3.LCX(turn),
            ev3.LCX(0),                                  # STEPS
            ev3.LCX(0),                                  # BRAKE
            ev3.opOutput_Start,
            ev3.LCX(0),                                  # LAYER
            ev3.LCX(self._port_left + self._port_right)  # NOS
        ])
        self.send_direct_cmd(ops)
      

A few remarks:

  • The vehicle follows the same turn, independent from speed. This is the main improvement of opOutput_Step_Sync compared with the usage of two operations opOutput_Speed.
  • If you used opOutput_Polarity, you will realize, that you can't combine it with opOutput_Step_Sync. You have to do it by hand. Changing the polarity of both motors is easy, just invert argument SPEED.
  • If you interchanged the connection of the motors, so that your left motor is connected to the lower port, it's easy too, invert TURN.
  • Changing the polarity of only one motor is tricky.

The following table may help to order the movements you have still seen. It describes the type of movement dependent from TURN, when the following conditions are given:

  • both motors have the same polarity,
  • the right wheel is connected to the lower port, the left to the higher.
TURN movement description
0 straight Both motors move in same direction and with same speed.
[0 - 100] turn left Both motors rotate in same direction, the left one moves with lower speed.
100 turn left around the left wheel Only the right motor moves.
[100 - 200] narrow turn left Both motors rotate in opposite direction, the left one rotates with lower speed.
200 circle left Both motors move in opposite direction, but same speed.
[0 - -100] turn right Both motors rotate in same direction, the right one moves with lower speed.
-100 turn right around the right wheel Only the left motor moves.
[-100 - -200] narrow turn right Both motors rotate in opposite direction, the right one moves with lower speed.
-200 circle right Both motors move in opposite direction, but same speed.

Please reflect the situation, when the ports are differently connected, when the left wheel is the lower port and the right one the higher.

You are also welcome, for further improvements of the remote control project. You can use a joystick instead of your keyboards arrow-keys. Or you can take the gyro sensor of your smartphone. But this is your project, we left the remote control behind (at least for the moment).

Well defined and predictable movements of vehicles with two drived wheels

We still focuse on vehicles with two drived wheels and the exactness of their movements. A remote control is a very special situation, where a human mind supervises the movement of the vehicle and immediately does some corrections, if this is needed. This situation does not need units like the radius of a turn. It's like driving a car, there is no need to know exactly how the radius of a turn depends from the position of the steering wheel. The corrections are relative and intuitive. Robots move without external control and their algorithm needs to know the exact dependencies of the parameters. We will write programs, which control the movement of a vehicle. We start with the hardest variant, where no mechanism of corrections exists. This means, we need functions, that predictable and precise describe the movement of the vehicle. I think of the following:

  • drive_straight(speed:int, distance: float=None), where
    • the sign of speed describes the direction (forward or backward).
    • the absolute value of speed describes the velocity. We would prefer the SI unit meter per second, but it's in percent.
    • the distance is None or positive, it is given in the SI unit meter. If it is None, the movement is unlimited.
  • drive_turn(speed:int, radius_turn:float, angle:float=None, right_turn:bool=False), where
    • the sign of speed describes the direction (forward or backward).
    • the absolute value of speed describes the velocity.
    • radius_turn is the radius of the turn in meter. We take the middle between the two drived wheels as our point of reference.
    • the sign of radius_turn decribes the direction of the turn. Positive values for left turns, which is the positive rotation direction, negative values stand for clockwise turns.
    • angle is None or positive, it is the circle segment in degrees (90° is a quarter circle, 720° are two whole circles). If it is None, an unlimited movement is meant.
    • right_turn is a flag for a very special situation. If we turn on place, attribute radius_turn is zero and has no sign. In this case, turning left is the default and attribute right_turn is the flag for the opposite direction.
As you may imagine, we are not so far away from that. We will use the operations opOutput_Ready, opOutput_Start and opOutput_Speed_Sync. This says we will not use interruption. From this point of view we fall back to the knowledge of lesson 2.

Determine the dimensions of your vehicle

We need some dimensions of the vehicle to translate the SI units of the above described functions into the arguments turn and step of operation opOutput_Speed_Sync. The dimensions of the vehicle are:

  • Radius of the drived wheels: radius_wheel.
  • The vehicles tread.

Using a yardstick gives (for my vehicle):

  • radius_wheel = 0.021 m
  • tread = 0.16 m

An alternative with better accuracy is the measurement of the vehicles movement. To get the radius of the drived wheels, you can use the following direct command:

---------------------------------------------------------- \ len \ cnt \ty\ hd \op\la\no\sp\tu\ step \br\op\la\no\ ---------------------------------------------------------- 0x|11:00|2A:00|80|00:00|B0|00|09|14|00|82:10:0E|01|A6|00|09| ---------------------------------------------------------- \ 17 \ 42 \no\ 0,0 \O \0 \A \20\0 \ 3600 \1 \O \0 \A \ \ \ \ \ \u \ \+ \ \ \ \ \u \ \+ \ \ \ \ \ \t \ \D \ \ \ \ \t \ \D \ \ \ \ \ \p \ \ \ \ \ \ \p \ \ \ \ \ \ \ \u \ \ \ \ \ \ \u \ \ \ \ \ \ \ \t \ \ \ \ \ \ \t \ \ \ \ \ \ \ \_ \ \ \ \ \ \ \_ \ \ \ \ \ \ \ \S \ \ \ \ \ \ \S \ \ \ \ \ \ \ \t \ \ \ \ \ \ \t \ \ \ \ \ \ \ \e \ \ \ \ \ \ \a \ \ \ \ \ \ \ \p \ \ \ \ \ \ \r \ \ \ \ \ \ \ \_ \ \ \ \ \ \ \t \ \ \ \ \ \ \ \S \ \ \ \ \ \ \ \ \ \ \ \ \ \ \y \ \ \ \ \ \ \ \ \ \ \ \ \ \ \n \ \ \ \ \ \ \ \ \ \ \ \ \ \ \c \ \ \ \ \ \ \ \ \ \ ----------------------------------------------------------
Take your yardstick and measure the distance of your vehicles movement (if your vehicle does not drive straight, look for the best combination of wheels, not everthing, that seems to be of identical size, really is). The distance of one full rotation of a wheel calculates as 2 * pi * radius_wheel. 3,600 degrees are 10 full rotations, so the following calculation gives the radius_wheel of your wheels:

   radius_wheel = distance / (20 * pi)
      
This corrected my wheels radius to radius_wheel = 0.02128 m. Next, we let the vehicle circle on place and count N, the number of the vehicles rotations. To do so, we send the direct command:
---------------------------------------------------------------- \ len \ cnt \ty\ hd \op\la\no\sp\ tu \ step \br\op\la\no\ ---------------------------------------------------------------- 0x|13:00|2A:00|80|00:00|B0|00|09|14|82:C8:00|82:50:46|01|A6|00|09| ---------------------------------------------------------------- \ 19 \ 42 \no\ 0,0 \O \0 \A \20\ 200 \ 18000 \1 \O \0 \A \ \ \ \ \ \u \ \+ \ \ \ \ \u \ \+ \ \ \ \ \ \t \ \D \ \ \ \ \t \ \D \ \ \ \ \ \p \ \ \ \ \ \ \p \ \ \ \ \ \ \ \u \ \ \ \ \ \ \u \ \ \ \ \ \ \ \t \ \ \ \ \ \ \t \ \ \ \ \ \ \ \_ \ \ \ \ \ \ \_ \ \ \ \ \ \ \ \S \ \ \ \ \ \ \S \ \ \ \ \ \ \ \t \ \ \ \ \ \ \t \ \ \ \ \ \ \ \e \ \ \ \ \ \ \a \ \ \ \ \ \ \ \p \ \ \ \ \ \ \r \ \ \ \ \ \ \ \_ \ \ \ \ \ \ \t \ \ \ \ \ \ \ \S \ \ \ \ \ \ \ \ \ \ \ \ \ \ \y \ \ \ \ \ \ \ \ \ \ \ \ \ \ \n \ \ \ \ \ \ \ \ \ \ \ \ \ \ \c \ \ \ \ \ \ \ \ \ \ ----------------------------------------------------------------
I counted N = 15.2 full rotations of my vehicle. Both wheels rotated 18.000°, which are 50 full rotations, or the distance 50 * 2 * pi * radius_wheel. The radius of the turn was 0.5 * tread (we defined the middle between the wheels as our point of reference). This says: N * 2 * pi * 0.5 * tread = 50 * 2 * pi * radius_wheel or:

   tread = radius_wheel * 100 / N
      
This corrects the dimension of tread (in my case: tread = 0.1346 m). Later we will do some additional movements, maybe this will correct the dimension tread once more.

Mathematical transformations to get the arguments STEP and TURN

Ok, we know our vehicles dimensions. Next we need to translate the arguments of our methods drive_straight and drive_turn into the arguments STEP and TURN of operation opOutput_Step_Sync. This needs some mathematics. If you are not interested in the details, you can just take the result from the bottom of this section. But most of you want to know the details, here they are.

Given is angle and radius_turn of the turn, our vehicle has to do. In a turn, the two wheels move different distances. The distance of the outer wheel is: 2 * pi * radius_wheel * STEP / 360. The same distance can be calculated from the geometrie of the turn and the knowledge of the vehicles tread: 2 * pi * (radius_turn + 0.5 * tread) * angle / 360. This two descriptions of the same distance give the following equation:


      STEP = angle * (radius_turn + 0.5 * tread) / radius_wheel
      
Ok, the first argument is calculated, but we still need the calculation of TURN. The crucial approach comes from the geometry of the turn and says, that the ratio between the two wheels speed speed_right / speed_left is the same as the ratio between the moved distances of the outer and the inner wheel:

      speed_right / speed_left = (radius_turn + 0.5 * tread) / (radius_turn - 0.5 * tread)
      
As you may remember, we already know the speeds of the two wheels: speed_right = SPEED and speed_left = SPEED * (1 - TURN / 100). This gives the equation:

      1 / (1 - TURN / 100) = (radius_turn + 0.5 * tread) / (radius_turn - 0.5 * tread)
      
The transformation of this equation results in:

      TURN  = 100 * (1 - (radius_turn - 0.5 * tread) / (radius_turn + 0.5 * tread))
      
This was it, if the dimensions of the turn movement (angle, radius_turn) and the dimensions of the vehicle (tread, radius_wheel) are given:
  1. variable STEP calculates as:
    
          STEP = angle * (radius_turn + 0.5 * tread) / radius_wheel
       
  2. variable TURN calculates as:
    
          TURN = 100 * (1 - (radius_turn - 0.5 * tread) / (radius_turn + 0.5 * tread))
       
In python, this reads as:

        rad_outer = radius_turn + 0.5 * self._tread
        rad_inner = radius_turn - 0.5 * self._tread
        step = round(angle*rad_outer / self._radius_wheel)
        turn = round(100*(1 - rad_inner / rad_outer))
        if angle < 0:
            step *= -1
            turn *= -1
        if self._polarity == -1:
            speed *= -1
      

Control of the mathematical transformations

Let's do some plausibility checks:

  • Turns with radius radius_turn = 0.5 * tread result in TURN = 100 or TURN = -100, this is correct.
  • Turns with radius radius_turn = 0 result in TURN = 200 or TURN = -200, also ok.

Now, we come to a real test. A turn with angle = 90° and radius_turn = 0.5 m. In my case the calculation above gives STEP = 2358 and TURN = 23, which results in the command:

---------------------------------------------------------- \ len \ cnt \ty\ hd \op\la\no\sp\tu\ step \br\op\la\no\ ---------------------------------------------------------- 0x|11:00|2A:00|80|00:00|B0|00|09|14|17|82:36:09|01|A6|00|09| ---------------------------------------------------------- \ 17 \ 42 \no\ 0,0 \O \0 \A \20\23\ 2358 \1 \O \0 \A \ \ \ \ \ \u \ \+ \ \ \ \ \u \ \+ \ \ \ \ \ \t \ \D \ \ \ \ \t \ \D \ \ \ \ \ \p \ \ \ \ \ \ \p \ \ \ \ \ \ \ \u \ \ \ \ \ \ \u \ \ \ \ \ \ \ \t \ \ \ \ \ \ \t \ \ \ \ \ \ \ \_ \ \ \ \ \ \ \_ \ \ \ \ \ \ \ \S \ \ \ \ \ \ \S \ \ \ \ \ \ \ \t \ \ \ \ \ \ \t \ \ \ \ \ \ \ \e \ \ \ \ \ \ \a \ \ \ \ \ \ \ \p \ \ \ \ \ \ \r \ \ \ \ \ \ \ \_ \ \ \ \ \ \ \t \ \ \ \ \ \ \ \S \ \ \ \ \ \ \ \ \ \ \ \ \ \ \y \ \ \ \ \ \ \ \ \ \ \ \ \ \ \n \ \ \ \ \ \ \ \ \ \ \ \ \ \ \c \ \ \ \ \ \ \ \ \ \ ----------------------------------------------------------
Indeed, my vehicle moved a nearly perfect quarter of a circle with a radius of 0.5 m!

Enhance class TwoWheelVehicle

Enough mathematics, at least for the moment, let's code now! As our first task, we modify the constructor of class TwoWheelVehicle. We add the two dimensions radius_wheel and tread, which were identified as required:


    def __init__(
            self,
            radius_wheel: float,
            tread: float,
            protocol: str=None,
            host: str=None,
            ev3_obj: ev3.EV3=None
    ):
        super().__init__(protocol=protocol, host=host, ev3_obj=ev3_obj)
        self._radius_wheel = radius_wheel
        self._tread = tread
        self._polarity = 1
        self._port_left = ev3.PORT_D
        self._port_right = ev3.PORT_A
      

Next we code method _drive, which is very close to method move. It's called with the arguments speed, turn and step. The outside world thinks in radius_turn and angle, which must be translated into the internal arguments turn and step. This says, methods drive_straight and drive_turn do this translation, then they call the internal method _drive:


    def _drive(self, speed: int, turn: int, step: int) -> bytes:
        assert isinstance(speed, int), "speed needs to be an integer value"
        assert -100 <= speed and speed <= 100, "speed needs to be in range [-100 - 100]"
        if self._polarity == -1:
            speed *= -1
        if self._port_left < self._port_right:
            turn *= -1
        ops_ready = b''.join([
            ev3.opOutput_Ready,
            ev3.LCX(0),                                  # LAYER
            ev3.LCX(self._port_left + self._port_right)  # NOS
        ])
        ops_start = b''.join([
            ev3.opOutput_Step_Sync,
            ev3.LCX(0),                                  # LAYER
            ev3.LCX(self._port_left + self._port_right), # NOS
            ev3.LCX(speed),
            ev3.LCX(turn),
            ev3.LCX(step),
            ev3.LCX(0),                                  # BRAKE
            ev3.opOutput_Start,
            ev3.LCX(0),                                  # LAYER
            ev3.LCX(self._port_left + self._port_right)  # NOS
        ])
        if self._sync_mode == ev3.SYNC:
            return self.send_direct_cmd(ops_start + ops_ready)
        else:
            return self.send_direct_cmd(ops_ready + ops_start)
      
We distinguish between SYNC and ASYNC or STD. In case of ASYNC or STD, we wait before the movement starts, in case of SYNC until it's finished. If you did download module ev3_vehicle.py from ev3-python3, you will not find a method _drive. This is a hint, that we will come back to class TwoWheelVehicle to implement interruption. We code method drive_turn:

    def drive_turn(
            self,
            speed: int,
            radius_turn: float,
            angle: float=None,
            right_turn: bool=False
    ) -> None:
        assert isinstance(radius_turn, numbers.Number), "radius_turn needs to be a number"
        assert angle is None or isinstance(angle, numbers.Number), "angle needs to be a number"
        assert angle is None or angle > 0, "angle needs to be positive"
        assert isinstance(right_turn, bool), "right_turn needs to be a boolean"
        rad_right = radius_turn + 0.5 * self._tread
        rad_left = radius_turn - 0.5 * self._tread
        if radius_turn >= 0 and not right_turn:
            turn = round(100*(1 - rad_left / rad_right))
        else:
            turn = - round(100*(1 - rad_right / rad_left))
        if turn == 0:
            raise ValueError("radius_turn is too large")
        if angle is None:
            self.move(speed, turn)
        else:
            if turn > 0:
                step = round(angle*rad_right / self._radius_wheel)
            else:
                step = - round(angle*rad_left / self._radius_wheel)
            self._drive(speed, turn, step)
      
Very large values of radius_turn cause problems. After rounding to integers, they result in straight movements. In this case we raise an error.

Method drive_straight:


    def drive_straight(self, speed: int, distance: float=None) -> None:
        assert distance is None or isinstance(distance, numbers.Number), \
            "distance needs to be a number"
        assert distance is None or distance > 0, \
            "distance needs to be positive"
        if distance is None:
            self.move(speed, 0)
        else:
            step = round(distance * 360 / (2 * math.pi * self._radius_wheel))
            self._drive(speed, 0, step)
      
Relax, we have realized a tool, that drives vehicles predictable. Please do some tests!

Knowledge of the vehicles position and orientation

Sorry, a few sentences ago i wrote enough mathematics, and now I come with trigonometry. But we are in a situation, where it's really worth the effort. Imagine, you drive your vehicle and after a series of movements, you need to know its position and its orientation. We will do that without any usage of sensors. Instead we use pure mathematics and no magic.

Let's make some assumptions:

  • The position, where your vehicle is placed, when you create the class TwoWheelVehicle will be the origin of your coordinate system (0, 0).
  • The direction, which in this moment points straight forward, is the direction of the x-axis.
  • The y-axis directs to the left hand side of your vehicle.
  • The position of your vehicle is described as x- and y-coordinates. It's in meter.
  • The orientation of your vehicle is the difference between the original orientation of the vehicle and its actual orientation. It's in degrees. Left turns increase the orientation, right turns decrease it.

This time, I will not present the mathematics. Take it as it is, or take it as a riddle, you have to solve. But please add the following logic to your class TwoWheelVehicle:

  • constructor:
    
            self._orientation = 0.0
            self._pos_x = 0.0
            self._pos_y = 0.0
       
  • drive_straight(speed, distance)
    
            diff_x = distance * math.cos(math.radians(self._orientation))
            diff_y = distance * math.sin(math.radians(self._orientation))
            if speed > 0:
                self._pos_x += diff_x
                self._pos_y += diff_y
            else:
                self._pos_x -= diff_x
                self._pos_y -= diff_y
       
  • drive_turn(speed, radius_turn, angle)
    
                angle += 180
                angle %= 360
                angle -= 180
                fact = 2.0 * radius_turn * math.sin(math.radians(0.5 * angle))
                self._orientation += 0.5 * angle
                self._pos_x += fact * math.cos(math.radians(self._orientation))
                self._pos_y += fact * math.sin(math.radians(self._orientation))
                self._orientation += 0.5 * angle
                self._orientation += 180
                self._orientation %= 360
                self._orientation -= 180
       

Complete class TwoWheelVehicle

We complete class TwoWheelVehicle with some more functionality:

  • We add a method rotate_to(speed: int, o: float), that does the following:
    • calculates the distance between the actual and the new orientation.
    • calls drive_turn with radius_turn = 0 to rotate the vehicle, so that it gets the new orientation.
  • We add a method drive_to(self, speed: int, x: float, y: float), that does the following:
    • calculates the distance between the actual position and the new position:
      
              diff_x = pos_x - self._pos_x
              diff_y = pos_y - self._pos_y
             
      We need it in coordinates and as an absolute value:
      
              distance = math.sqrt(diff_x**2 + diff_y**2)
             
    • calculates the direction to the new position. This is tricky, you need a thorough understanding of trigonometry, because you have to use atan, the inverse function of tan. I give you a hint:
      
              if abs(diff_x) > abs(diff_y):
                  direction = math.degrees(math.atan(diff_y/diff_x))
              else:
                  fract = diff_x / diff_y
                  sign = math.copysign(1.0, fract)
                  direction = sign * 90 - math.degrees(math.atan(fract))
              if diff_x < 0:
                  direction += 180
              direction %= 360
             
    • calls rotate_to, so that the orientation points to direction.
    • calls drive_straight to move the vehicle to the new position.

We do some tests:

  • We send the vehicle to some circular trips and code series of drive_to, which end at position = (0,0). We add a final rotate_to with orientation = 0. Please evaluate, if the vehicle really returns to its original position and orientation.
  • We add some drive_turn to the circular trip and evaluate, if the error of drive_turn is larger or smaller than that of drive_to.

Turns with large radius_turn have a bad precision. Again this is a result of rounding. In this case, the rounding of TURN to an integer value creates the error.

If you downloaded module ev3_vehicle from ev3-python3, you need to add a final call of method stop when you call its methods drive_straight, drive_turn, rotate_to or drive_to!

Asynchronous and synchronous movements

Let's take a closer look to the driving of our vehicle. Here's my program with a circular trip:


#!/usr/bin/env python3

import ev3, ev3_vehicle

my_vehicle = ev3_vehicle.TwoWheelVehicle(0.02128, 0.1346, protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
my_vehicle.verbosity = 1
speed = 25
my_vehicle.drive_straight(speed, 0.05)
my_vehicle.drive_turn(speed, -0.07, 65)
my_vehicle.drive_straight(speed, 0.35)
my_vehicle.drive_turn(speed, 0.20, 140)
my_vehicle.drive_straight(speed, 0.15)
my_vehicle.drive_turn(speed, -1.10, 55)
my_vehicle.drive_turn(speed, 0.35, 160)
my_vehicle.drive_to(speed, 0.0, 0.0)
my_vehicle.rotate_to(speed, 0.0)
      
The output of this program:

15:42:05.989592 Sent 0x|14:00|2A:00|80|00:00|AA:00:09:B0:00:09:19:00:82:87:00:00:A6:00:09|
15:42:05.990443 Sent 0x|15:00|2B:00|80|00:00|AA:00:09:B0:00:09:19:81:9E:82:A3:01:00:A6:00:09|
15:42:05.990925 Sent 0x|14:00|2C:00|80|00:00|AA:00:09:B0:00:09:19:00:82:AE:03:00:A6:00:09|
15:42:05.991453 Sent 0x|15:00|2D:00|80|00:00|AA:00:09:B0:00:09:19:81:32:82:DF:06:00:A6:00:09|
15:42:05.991882 Sent 0x|14:00|2E:00|80|00:00|AA:00:09:B0:00:09:19:00:82:94:01:00:A6:00:09|
15:42:05.992330 Sent 0x|14:00|2F:00|80|00:00|AA:00:09:B0:00:09:19:34:82:C9:0B:00:A6:00:09|
15:42:05.992766 Sent 0x|15:00|30:00|80|00:00|AA:00:09:B0:00:09:19:81:20:82:42:0C:00:A6:00:09|
15:42:05.993306 Sent 0x|16:00|31:00|80|00:00|AA:00:09:B0:00:09:19:82:38:FF:82:D0:01:00:A6:00:09|
15:42:05.993714 Sent 0x|14:00|32:00|80|00:00|AA:00:09:B0:00:09:19:00:82:3F:03:00:A6:00:09|
15:42:05.994202 Sent 0x|15:00|33:00|80|00:00|AA:00:09:B0:00:09:19:82:38:FF:81:69:00:A6:00:09|
      
Within five milliseconds the program sends all direct commands to the EV3 device, where they are queued and wait to be operated. This is simple to code, but blocks the EV3 device until the last of the commands is started. Please follow the code and reflect, which values the properties pos_x, pos_y and orientation have and if they correspond to the real position and orientation of our vehicle.

This is asynchronous behaviour! The program and the EV3 device act on different timescales.

Now we change to synchronous mode and compare the behaviour:


#!/usr/bin/env python3

import ev3, ev3_vehicle

my_vehicle = ev3_vehicle.TwoWheelVehicle(0.02128, 0.1346, protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
my_vehicle.verbosity = 1
speed = 25
my_vehicle.sync_mode = ev3.SYNC
my_vehicle.drive_straight(speed, 0.05)
my_vehicle.drive_turn(speed, -0.07, 65)
my_vehicle.drive_straight(speed, 0.35)
my_vehicle.drive_turn(speed, 0.20, 140)
my_vehicle.drive_straight(speed, 0.15)
my_vehicle.drive_turn(speed, -1.10, 55)
my_vehicle.drive_turn(speed, 0.35, 160)
my_vehicle.drive_to(speed, 0.0, 0.0)
my_vehicle.rotate_to(speed, 0.0)
      
This version produced the following output:

15:46:19.859532 Sent 0x|14:00|2A:00|00|00:00|B0:00:09:19:00:82:87:00:00:A6:00:09:AA:00:09|
15:46:20.307045 Recv 0x|03:00|2A:00|02|
15:46:20.307760 Sent 0x|15:00|2B:00|00|00:00|B0:00:09:19:81:9E:82:A3:01:00:A6:00:09:AA:00:09|
15:46:22.100007 Recv 0x|03:00|2B:00|02|
15:46:22.100612 Sent 0x|14:00|2C:00|00|00:00|B0:00:09:19:00:82:AE:03:00:A6:00:09:AA:00:09|
15:46:24.597018 Recv 0x|03:00|2C:00|02|
15:46:24.597646 Sent 0x|15:00|2D:00|00|00:00|B0:00:09:19:81:32:82:DF:06:00:A6:00:09:AA:00:09|
15:46:29.141999 Recv 0x|03:00|2D:00|02|
15:46:29.142609 Sent 0x|14:00|2E:00|00|00:00|B0:00:09:19:00:82:94:01:00:A6:00:09:AA:00:09|
15:46:30.221994 Recv 0x|03:00|2E:00|02|
15:46:30.222626 Sent 0x|14:00|2F:00|00|00:00|B0:00:09:19:34:82:C9:0B:00:A6:00:09:AA:00:09|
15:46:44.779922 Recv 0x|03:00|2F:00|02|
15:46:44.780364 Sent 0x|15:00|30:00|00|00:00|B0:00:09:19:81:20:82:42:0C:00:A6:00:09:AA:00:09|
15:46:46.938901 Recv 0x|03:00|30:00|02|
15:46:46.939701 Sent 0x|16:00|31:00|00|00:00|B0:00:09:19:82:38:FF:82:D0:01:00:A6:00:09:AA:00:09|
15:46:55.695901 Recv 0x|03:00|31:00|02|
15:46:55.696508 Sent 0x|14:00|32:00|00|00:00|B0:00:09:19:00:82:3F:03:00:A6:00:09:AA:00:09|
15:47:05.061856 Recv 0x|03:00|32:00|02|
15:47:05.062504 Sent 0x|15:00|33:00|00|00:00|B0:00:09:19:82:38:FF:81:69:00:A6:00:09:AA:00:09|
15:47:05.097692 Recv 0x|03:00|33:00|02|
      
The movement of the vehicle is the same, but now the program sends one direct command and waits until it's finished, then it sends the next one. sync_mode = SYNC works as designed, it synchronizes the timescales of the program and the EV3 device. Another benefit is, that it controls the success per direct command and directly reacts, if something unexpected happens.

Both versions, asynchronous and synchronous, block the EV3 device.

Conclusion

Class TwoWheelVehicle controls the movement of a vehicle with two drived wheels. If we know the geometry of a course, we can code a program, that drives our vehicle through it.

We have seen the difference between synchronous and asynchronous mode. For the moment, we prefer SYNC. But what we target is a solution, which drives the vehicle synchronously and doesn't block the EV3 device. This would open the door to multitasking.

My class EV3TwoWheelVehicle actually has the following state:


Help on module ev3_vehicle:

NAME
    ev3_vehicle - EV3 vehicle

CLASSES
    ev3.EV3(builtins.object)
        TwoWheelVehicle
    
    class TwoWheelVehicle(ev3.EV3)
     |  ev3.EV3 vehicle with two drived Wheels
     |  
     |  Method resolution order:
     |      TwoWheelVehicle
     |      ev3.EV3
     |      builtins.object
     |  
     |  Methods defined here:
     |  
     |  __init__(self, radius_wheel:float, tread:float, protocol:str=None, host:str=None, ev3_obj:ev3.EV3=None)
     |      Establish a connection to a LEGO EV3 device
     |      
     |      Arguments:
     |      radius_wheel: radius of the wheels im meter
     |      tread: the vehicles tread in meter
     |      
     |      Keyword Arguments (either protocol and host or ev3_obj):
     |      protocol
     |        BLUETOOTH == 'Bluetooth'
     |        USB == 'Usb'
     |        WIFI == 'Wifi'
     |      host: mac-address of the LEGO EV3 (f.i. '00:16:53:42:2B:99')
     |      ev3_obj: an existing EV3 object (its connections will be used)
     |  
     |  drive_straight(self, speed:int, distance:float=None) -> None
     |      Drive the vehicle straight forward or backward.
     |      
     |      Attributes:
     |      speed: in percent [-100 - 100] (direction depends on its sign)
     |          positive sign: forwards
     |          negative sign: backwards
     |      
     |      Keyword Attributes:
     |      distance: in meter, needs to be positive
     |                if None, unlimited movement
     |  
     |  drive_to(self, speed:int, pos_x:float, pos_y:float) -> None
     |      Drive the vehicle to the given position.
     |      
     |      Attributes:
     |      speed: in percent [-100 - 100] (direction depends on its sign)
     |          positive sign: forwards
     |          negative sign: backwards
     |      x: x-coordinate of target position
     |      y: y-coordinate of target position
     |  
     |  drive_turn(self, speed:int, radius_turn:float, angle:float=None, right_turn:bool=False) -> None
     |      Drive the vehicle a turn with given radius.
     |      
     |      Attributes:
     |      speed: in percent [-100 - 100] (direction depends on its sign)
     |          positive sign: forwards
     |          negative sign: backwards
     |      radius_turn: in meter
     |          positive sign: turn to the left side
     |          negative sign: turn to the right side
     |      
     |      Keyword Attributes:
     |      angle: absolute angle (needs to be positive)
     |             if None, unlimited movement
     |      right_turn: flag of turn right (only in case of radius_turn == 0)
     |  
     |  move(self, speed:int, turn:int) -> None
     |      Start unlimited movement of the vehicle
     |      
     |      Arguments:
     |      speed: speed in percent [-100 - 100]
     |        > 0: forward
     |        < 0: backward
     |      turn: type of turn [-200 - 200]
     |        -200: circle right on place
     |        -100: turn right with unmoved right wheel
     |         0  : straight
     |         100: turn left with unmoved left wheel
     |         200: circle left on place
     |  
     |  rotate_to(self, speed:int, orientation:float) -> None
     |      Rotate the vehicle to the given orientation.
     |      Chooses the direction with the smaller movement.
     |      
     |      Attributes:
     |      speed: in percent [-100 - 100] (direction depends on its sign)
     |      orientation: in degrees [-180 - 180]
     |  
     |  stop(self, brake:bool=False) -> None
     |      Stop movement of the vehicle
     |      
     |      Arguments:
     |      brake: flag if activating brake
     |  
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |  
     |  orientation
     |      actual orientation of the vehicle in degree, range [-180 - 180]
     |  
     |  polarity
     |      polarity of motor rotation (values: -1, 1, default: 1)
     |  
     |  port_left
     |      port of left wheel (default: PORT_D)
     |  
     |  port_right
     |      port of right wheel (default: PORT_A)
     |  
     |  pos_x
     |      actual x-component of the position in meter
     |  
     |  pos_y
     |      actual y-component of the position in meter
     |  
     |  ----------------------------------------------------------------------
     |  Methods inherited from ev3.EV3:
     |  
     |  __del__(self)
     |      closes the connection to the LEGO EV3
     |  
     |  send_direct_cmd(self, ops:bytes, local_mem:int=0, global_mem:int=0) -> bytes
     |      Send a direct command to the LEGO EV3
     |      
     |      Arguments:
     |      ops: holds netto data only (operations), the following fields are added:
     |        length: 2 bytes, little endian
     |        counter: 2 bytes, little endian
     |        type: 1 byte, DIRECT_COMMAND_REPLY or DIRECT_COMMAND_NO_REPLY
     |        header: 2 bytes, holds sizes of local and global memory
     |      
     |      Keyword Arguments:
     |      local_mem: size of the local memory
     |      global_mem: size of the global memory
     |      
     |      Returns: 
     |        sync_mode is STD: reply (if global_mem > 0) or message counter
     |        sync_mode is ASYNC: message counter
     |        sync_mode is SYNC: reply of the LEGO EV3
     |  
     |  wait_for_reply(self, counter:bytes) -> bytes
     |      Ask the LEGO EV3 for a reply and wait until it is received
     |      
     |      Arguments:
     |      counter: is the message counter of the corresponding send_direct_cmd
     |      
     |      Returns:
     |      reply to the direct command
     |  
     |  ----------------------------------------------------------------------
     |  Data descriptors inherited from ev3.EV3:
     |  
     |  __dict__
     |      dictionary for instance variables (if defined)
     |  
     |  __weakref__
     |      list of weak references to the object (if defined)
     |  
     |  sync_mode
     |      sync mode (standard, asynchronous, synchronous)
     |      
     |      STD:   Use DIRECT_COMMAND_REPLY if global_mem > 0,
     |             wait for reply if there is one.
     |      ASYNC: Use DIRECT_COMMAND_REPLY if global_mem > 0,
     |             never wait for reply (it's the task of the calling program).
     |      SYNC:  Always use DIRECT_COMMAND_REPLY and wait for reply.
     |      
     |      The general idea is:
     |      ASYNC: Interruption or EV3 device queues direct commands,
     |             control directly comes back.
     |      SYNC:  EV3 device is blocked until direct command is finished,
     |             control comes back, when direct command is finished.               
     |      STD:   NO_REPLY like ASYNC with interruption or EV3 queuing,
     |             REPLY like SYNC, synchronicity of program and EV3 device.
     |  
     |  verbosity
     |      level of verbosity (prints on stdout).
      

I hope, you got the feeling, that we are doing real things now. For me, it was a highlight. Only in rare cases, one can earn so much with so little effort.

Friday 22 January 2016

Lesson 3 - Remote controlled vehicle

EV3 Direct commands - Lesson 03

Moving motors is the heart of the EV3. A robot needs the ability to move, therefore you need to know, how it's done. Take a look at part 4.9 in the document EV3 Firmware Developer Kit to get a first impression about the operations that correspond to the motors. Nearly all of them start with the same two arguments:

  • LAYER: If you combine more than one EV3 bricks to act as a single machine, you declare one of them to be the master and up to 3 additional bricks to be the masters slaves. Then you send your operations to the master brick and the master sends it to the slaves. This is the meaning of LAYER. Layer 0x|00| is the master, layers 0x|01|, 0x|02| and 0x|03| are its optional slaves. In the standard case, where no slaves exist, we will set LAYER = 0x|00|. Our coding with direct commands allows to connect multiple EV3 devices. Therefore I see no need for different values of argument LAYER.
  • NO or NOS: The motor ports are alphabetically identified with A, B, C and D. At the level of direct commands you name them by numbers, A = 0x|01|, B = 0x|02|, C = 0x|04| and D = 0x|08|. If you write these numbers as binaries, A = 0b 0000 0001, B = 0b 0000 0010, C = 0b 0000 0100 and C = 0b 0000 1000, you see, that the last half byte can be interpreted as a series of four flags. You can combine these flags. If you address an operation to the motors at ports A and C, you set NOS = 0x|05| resp. 0b 0000 0101. If an arguments name is NO, you can address only a single motor port. NOS says, that you can address one or multiple ports.

Start and stop motors

Plug in some motors to the ports of your choice, then start them with full power. This is done by the following direct command:

---------------------------------------------- \ len \ cnt \ty\ hd \op\la\no\power\op\la\no\ ---------------------------------------------- 0x|0D:00|2A:00|80|00:00|A4|00|0F|81:64|A6|00|0F| ---------------------------------------------- \ 13 \ 42 \no\ 0,0 \O \0 \a \ 100 \O \0 \a \ \ \ \ \ \u \ \l \ \u \ \l \ \ \ \ \ \t \ \l \ \t \ \l \ \ \ \ \ \p \ \ \ \p \ \ \ \ \ \ \ \u \ \ \ \u \ \ \ \ \ \ \ \t \ \ \ \t \ \ \ \ \ \ \ \_ \ \ \ \_ \ \ \ \ \ \ \ \P \ \ \ \S \ \ \ \ \ \ \ \o \ \ \ \t \ \ \ \ \ \ \ \w \ \ \ \a \ \ \ \ \ \ \ \e \ \ \ \r \ \ \ \ \ \ \ \r \ \ \ \t \ \ \ ----------------------------------------------

After some time, you, your parents, siblings or children become nerved of the sound of full powered motors. If you are a peace loving character, you should send the following direct command:

---------------------------------- \ len \ cnt \ty\ hd \op\la\no\br\ ---------------------------------- 0x|09:00|2A:00|00|00:00|A3|00|0F|00| ---------------------------------- \ 9 \ 42 \re\ 0,0 \O \0 \a \no\ \ \ \ \ \u \ \l \ \ \ \ \ \ \t \ \l \ \ \ \ \ \ \p \ \ \ \ \ \ \ \ \u \ \ \ \ \ \ \ \ \t \ \ \ \ \ \ \ \ \_ \ \ \ \ \ \ \ \ \S \ \ \ \ \ \ \ \ \t \ \ \ \ \ \ \ \ \o \ \ \ \ \ \ \ \ \p \ \ \ \ ----------------------------------

We have seen three new operations:

  • opOutput_Power = 0x|A4| with the arguments:
    • LAYER
    • NOS
    • POWER: specifies output power [-100 – 100 %] (negative values mean opposite direction).
  • opOutput_Start = 0x|A6| with the arguments:
    • LAYER
    • NOS
  • opOutput_Stop = 0x|A3| with the arguments:
    • LAYER
    • NOS
    • BRAKE: Specify break level [0: Float, 1: Break]. This is not a passive brake, where the motor is fixed at its position. It is an active one, where the motor tries to move back to its original position and this costs energy!
With these three operations, we are able to start and stop our motors. There is another one, we should talk about, which sets the polarity. Normally the movement of a motor is described in terms of forward and backward direction and one uses positive and negative numbers. If the construction of our machine results in opposite polarity, there is an operation, to change it:
  • opOutput_Polarity = 0x|A7| with the arguments:
    • LAYER
    • NOS
    • POLARITY: specifies polarity [-1, 0, 1]
      • -1: Motor will run backward
      • 0: Motor will run opposite direction
      • +1: Motor will run forward

Defining speed instead of power often is the better choice. Luckily the LEGO motors are regulated ones and we can easily define constant speed for motor movement with the following operation:

  • opOutput_Speed = 0x|A5| with the arguments:
    • LAYER
    • NOS
    • SPEED: specifies output speed [-100 – 100 %] (negative values mean opposite direction).

Please construct a vehicle, that is powered by two motors, one for the left side, the other for the right one. Well designed examples are:

but a simple one also will serve our needs. Connect the right motor to port A, the left one to port D and send the following direct command:
------------------------------------------- \ len \ cnt \ty\ hd \op\la\no\sp\op\la\no\ ------------------------------------------- 0x|0C:00|2A:00|80|00:00|A5|00|09|3D|A6|00|09| ------------------------------------------- \ 12 \ 42 \no\ 0,0 \O \0 \A \-3\O \0 \A \ \ \ \ \ \u \ \+ \ \u \ \+ \ \ \ \ \ \t \ \D \ \t \ \D \ \ \ \ \ \p \ \ \ \p \ \ \ \ \ \ \ \u \ \ \ \u \ \ \ \ \ \ \ \t \ \ \ \t \ \ \ \ \ \ \ \_ \ \ \ \_ \ \ \ \ \ \ \ \S \ \ \ \S \ \ \ \ \ \ \ \p \ \ \ \t \ \ \ \ \ \ \ \e \ \ \ \a \ \ \ \ \ \ \ \e \ \ \ \r \ \ \ \ \ \ \ \d \ \ \ \t \ \ \ -------------------------------------------
Hopefully your vehicle moves straight with low velocity. Use the direct command above (opOutput_Stop) to stop it.

Remote controlled vehicles

Now it's time to code a real application. Impress your friends or children with a remote controlled EV3 vehicle. We start with a simple remote control and use the keyboards arrow-keys to change speed and direction. First add the new operations to your class EV3, then write your remote-control-program. As usual I have done it in python3:


#!/usr/bin/env python3

import curses
import ev3_dc as ev3

def move(speed: int, turn: int) -> None:
    global myEV3, stdscr
    stdscr.addstr(5, 0, 'speed: {}, turn: {}      '.format(speed, turn))
    if turn > 0:
        speed_right = speed
        speed_left  = round(speed * (1 - turn / 100))
    else:
        speed_right = round(speed * (1 + turn / 100))
        speed_left  = speed
    ops = b''.join([
        ev3.opOutput_Speed,
        ev3.LCX(0),                       # LAYER
        ev3.LCX(ev3.PORT_A),              # NOS
        ev3.LCX(speed_right),             # SPEED
        ev3.opOutput_Speed,
        ev3.LCX(0),                       # LAYER
        ev3.LCX(ev3.PORT_D),              # NOS
        ev3.LCX(speed_left),              # SPEED
        ev3.opOutput_Start,
        ev3.LCX(0),                       # LAYER
        ev3.LCX(ev3.PORT_A + ev3.PORT_D)  # NOS
    ])
    myEV3.send_direct_cmd(ops)

def stop() -> None:
    global myEV3, stdscr
    stdscr.addstr(5, 0, 'vehicle stopped                         ')
    ops = b''.join([
        ev3.opOutput_Stop,
        ev3.LCX(0),                       # LAYER
        ev3.LCX(ev3.PORT_A + ev3.PORT_D), # NOS
        ev3.LCX(0)                        # BRAKE
    ])
    myEV3.send_direct_cmd(ops)

def react(c):
    global speed, turn
    if c in [ord('q'), 27, ord('p')]:
        stop()
        return
    elif c == curses.KEY_LEFT:
        turn += 5
        turn = min(turn, 200)
    elif c == curses.KEY_RIGHT:
        turn -= 5
        turn = max(turn, -200)
    elif c == curses.KEY_UP:
        speed += 5
        speed = min(speed, 100)
    elif c == curses.KEY_DOWN:
        speed -= 5
        speed = max(speed, -100)
    move(speed, turn)

def main(window) -> None:
    global stdscr
    stdscr = window
    stdscr.clear()      # print introduction
    stdscr.refresh()
    stdscr.addstr(0, 0, 'Use Arrows to navigate your EV3-vehicle')
    stdscr.addstr(1, 0, 'Pause your vehicle with key <p>')
    stdscr.addstr(2, 0, 'Terminate with key <q>')

    while True:
        c = stdscr.getch()
        if c in [ord('q'), 27]:
            react(c)
            break
        elif c in [ord('p'),
                   curses.KEY_RIGHT, curses.KEY_LEFT, curses.KEY_UP, curses.KEY_DOWN]:
            react(c)

speed = 0
turn  = 0   
myEV3 = ev3.EV3(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
stdscr = None

# ops = opOutput_Polarity + b'\x00' + LCX(PORT_A + PORT_D) + LCX(-1)
# myEV3.send_direct_cmd(ops)

curses.wrapper(main)
      
Some remarks:
  • I used the module curses to realize an event handler for keyboard events.
  • Method wrapper takes control over the resources keyboard and terminal. It changes the behavior of the terminal, creates a window object and calls function main.
  • Function main writes some text to the terminal and then waits for key events (method getch).
  • If one of the relevant keys is hit, function react is called, keys <q> or <Ctrl-c> break the loop.
  • Variable speed defines the speed of the faster wheel.
  • Variable turn defines the direction. Value 0 means straight on, positive values result in left turns, negative values in right turns. Large absolute values of turn mean small radius of the turn. Maximum value +200 and minimum value -200 mean circling on place. In the next lesson, we will come back to this definition of driving a turn.
  • I changed speed and turn in steps of 5. This seems to be a good compromize beween precision and speed of reaction.
  • Maybe, the construction of your vehicle needs the change of polarity for both wheels. This is done by the commented code.
  • The right wheel is connected to port A, the left one to port D.
  • For small velocities, the precision of the turns radius becomes worse, because of the rounding to integer values.

Interruption as a design concept

Last lesson, we compared interruption with an impatient and badly behaving character. This lesson shows, that exactly this behaviour can be a practicable design concept.

The remote control program sends unlimited commands, which would move the motors forever, if not interrupted by the next one. A human mind, that controls the driving, wants her corrections immediately take place. It's absolutely correct, that the newer command interrupts the older one.

Another positive aspect of this design concept is the short execution time of the direct commands. They are not time consuming. The movement of the motors lasts long, but the execution of the direct command is finished, when the motors got their new parameters. Control is back immediately and the program is free, to do the next task. The resource EV3 device isn't blocked, it's available for the next direct command.

You have seen, how the interruption process of the remote control is coded. The central part is a loop, that asks for key events. If a relevant key event occurs, a function is called, which changes the parameters of the movement and sends a direct command to the EV3 device.

When we played the triad in c'' (last lesson), we avoided interruption. We had a fixed schedule of all the tones to play. Our opinion was, that every interruption disturbs the plan and needs to be prevented. When drawing the triangle, we already discussed two alternatives for the timing, by the direct command or by the local program. Playing a triad can also be done with all the scheduling in the local program and using interruption. This is an alternative to get the correct timing:


#!/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_Write,
    ev3.LED,
    ev3.LED_RED,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(262),
    ev3.LCX(0)
])
my_ev3.send_direct_cmd(ops)
time.sleep(0.5)
ops = b''.join([
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_GREEN,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(330),
    ev3.LCX(0)
])
my_ev3.send_direct_cmd(ops)
time.sleep(0.5)
ops = b''.join([
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_RED,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(392),
    ev3.LCX(0)
])
my_ev3.send_direct_cmd(ops)
time.sleep(0.5)
ops = b''.join([
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_RED_FLASH,
    ev3.opSound,
    ev3.TONE,
    ev3.LCX(1),
    ev3.LCX(523),
    ev3.LCX(0)
])
my_ev3.send_direct_cmd(ops)
time.sleep(2)
ops = b''.join([
    ev3.opUI_Write,
    ev3.LED,
    ev3.LED_GREEN,
    ev3.opSound,
    ev3.BREAK
])
my_ev3.send_direct_cmd(ops)
      
This version also realizes a fixed schedule, but the timing happens in the program and not on the EV3 device (resp. in the direct command). Again this has the advantage, not to block the EV3 device. As long, as we execute only one task at a time, blocking does not matter. If we try to combine f.i. sound and driving in independent and parallel executed tasks, local timing and interruption are a must.

If we would set sync_mode == SYNC in the program above, the data traffic would grow but we wouldn't hear any difference. It's worth to reflect, that this program runs synchronously because none of the direct commands is time consuming. sync_mode == ASYNC or sync_mode == STD are designed for asynchronous execution, but we also can code synchronous execution with these settings. Asynchronous execution only happens when the direct command is time consuming (resp. the timing is done by the direct command) and the local program does not wait until the direct command ends. Interruption helps to avoid time consuming direct commands and is synchronous execution.

Subclassing EV3

Up to now, we composed the operations directly in our programs. This cries for encapsulation and abstraction. I think of specialized classes, one for vehicles with two drived wheels, one for tones and music and so on. The design should allow to use them parallel. My solution is a number of subclasses of class EV3, which all communicate with the same EV3 device. This says, they have to share resources. To realize this, I have changed the constructor of class EV3:


     |  __init__(self, protocol:str=None, host:str=None, ev3_obj=None)
     |      Establish a connection to a LEGO EV3 device
     |      
     |      Keyword Arguments (either protocol and host or ev3_obj):
     |      protocol: None, 'Bluetooth', 'Usb' or 'Wifi'
     |      host: None or mac-address of the LEGO EV3 (f.i. '00:16:53:42:2B:99')
     |      ev3_obj: None or an existing EV3 object (its connections will be used)
      
The code:

class EV3:

    def __init__(self, protocol: str=None, host: str=None, ev3_obj=None):
        assert ev3_obj or protocol, \
            'Either protocol or ev3_obj needs to be given'
        if ev3_obj:
            assert isinstance(ev3_obj, EV3), \
                'ev3_obj needs to be instance of EV3'
            self._protocol = ev3_obj._protocol
            self._device = ev3_obj._device
            self._socket = ev3_obj._socket
        elif protocol:
            assert protocol in [BLUETOOTH, WIFI, USB], \
                'Protocol ' + protocol + 'is not valid'
            self._protocol = None
            self._device = None
            self._socket = None
            if protocol == BLUETOOTH:
                assert host, 'Protocol ' + protocol + 'needs host-id'
                self._connect_bluetooth(host)
            elif protocol == WIFI:
                self._connect_wifi()
            elif protocol == USB:
                self._connect_usb()
        self._verbosity = 0
        self._sync_mode = STD
        self._msg_cnt = 41
      
A few annotations:
  • There are two ways to create a new instance of class EV3:
    • As before, call the constructor with the arguments protocol and host for a new connection.
    • The alternative is calling it with an existing EV3 object as argument ev3_obj, from which the connections are used.
  • This is not limited to connections. For future extensions, we can share any of the resources.
  • Every class has its own _verbosity and _sync_mode. This is o.k., but we make the message counter _msg_cnt a class attribute.
    
    class EV3:
        _msg_cnt = 41
       
With this in mind, I coded class TwoWheelVehicle, here its constructor:

class TwoWheelVehicle(ev3.EV3):

    def __init__(
            self,
            protocol: str=None,
            host: str=None,
            ev3_obj: ev3.EV3=None
    ):
        super().__init__(protocol=protocol, host=host, ev3_obj=ev3_obj)
        self._polarity = 1
        self._port_left = ev3.PORT_D
        self._port_right = ev3.PORT_A
      
I added two methods:

    def move(self, speed: int, turn: int) -> None:
        assert self._sync_mode != ev3.SYNC, "no unlimited operations allowed in sync_mode SYNC"
        assert isinstance(speed, int), "speed needs to be an integer value"
        assert -100 <= speed and speed <= 100, "speed needs to be in range [-100 - 100]"
        assert isinstance(turn, int), "turn needs to be an integer value"
        assert -200 <= turn and turn <= 200, "turn needs to be in range [-200 - 200]"
        if self._polarity == -1:
            speed *= -1
        if self._port_left < self._port_right:
            turn *= -1
        if turn > 0:
            speed_right = speed
            speed_left  = round(speed * (1 - turn / 100))
        else:
            speed_right = round(speed * (1 + turn / 100))
            speed_left  = speed
        ops = b''.join([
            ev3.opOutput_Speed,
            ev3.LCX(0),              # LAYER
            ev3.LCX(PORT_A),         # NOS
            ev3.LCX(speed_right),    # SPEED
            ev3.opOutput_Speed,
            ev3.LCX(0),              # LAYER
            ev3.LCX(PORT_D),         # NOS
            ev3.LCX(speed_left),     # SPEED
            ev3.opOutput_Start,
            ev3.LCX(0),              # LAYER
            ev3.LCX(PORT_A + PORT_D) # NOS
        ])
        self.send_direct_cmd(ops)
      
and

    def stop(self, brake: bool=False) -> None:
        assert isinstance(brake, bool), "brake needs to be a boolean value"
        if brake:
            br = 1
        else:
            br = 0
        ops_stop = b''.join([
            ev3.opOutput_Stop,
            ev3.LCX(0),                                  # LAYER
            ev3.LCX(self._port_left + self._port_right), # NOS
            ev3.LCX(br)                                  # BRAKE
        ])
        self.send_direct_cmd(ops)
      
We need three properties:

    @property
    def polarity(self):
        return self._polarity
    @polarity.setter
    def polarity(self, value: int):
        assert isinstance(value, int), "polarity needs to be of type int"
        assert value in [1, -1], "allowed polarity values are: -1 or 1"
        self._polarity = value

    @property
    def port_right(self):
        return self._port_right
    @port_right.setter
    def port_right(self, value: int):
        assert isinstance(value, int), "port needs to be of type int"
        assert value in [ev3.PORT_A, ev3.PORT_B, ev3.PORT_C, ev3.PORT_D], "value is not an allowed port"
        self._port_right = value

    @property
    def port_left(self):
        return self._port_left
    @port_left.setter
    def port_left(self, value: int):
        assert isinstance(value, int), "port needs to be of type int"
        assert value in [ev3.PORT_A, ev3.PORT_B, ev3.PORT_C, ev3.PORT_D], "value is not an allowed port"
        self._port_left = value
      
That's it, we have a new class TwoWheelVehicle with this API:

Help on module ev3_vehicle:

NAME
    ev3_vehicle - EV3 vehicle

CLASSES
    ev3.EV3(builtins.object)
        TwoWheelVehicle
    
    class TwoWheelVehicle(ev3.EV3)
     |  ev3.EV3 vehicle with two drived Wheels
     |  
     |  Method resolution order:
     |      TwoWheelVehicle
     |      ev3.EV3
     |      builtins.object
     |  
     |  Methods defined here:
     |  
     |  __init__(self, protocol:str=None, host:str=None, ev3_obj:ev3.EV3=None)
     |      Establish a connection to a LEGO EV3 device
     |      
     |      Keyword Arguments (either protocol and host or ev3_obj):
     |      protocol
     |        BLUETOOTH == 'Bluetooth'
     |        USB == 'Usb'
     |        WIFI == 'Wifi'
     |      host: mac-address of the LEGO EV3 (f.i. '00:16:53:42:2B:99')
     |      ev3_obj: an existing EV3 object (its connections will be used)
     |  
     |  move(self, speed:int, turn:int) -> None
     |      Start unlimited movement of the vehicle
     |      
     |      Arguments:
     |      speed: speed in percent [-100 - 100]
     |        > 0: forward
     |        < 0: backward
     |      turn: type of turn [-200 - 200]
     |        -200: circle right on place
     |        -100: turn right with unmoved right wheel
     |         0  : straight
     |         100: turn left with unmoved left wheel
     |         200: circle left on place
     |  
     |  stop(self, brake:bool=False) -> None
     |      Stop movement of the vehicle
     |      
     |      Arguments:
     |      brake: flag if activating brake
     |  
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |  
     |  polarity
     |      polarity of motor rotation (values: -1, 1, default: 1)
     |  
     |  port_left
     |      port of left wheel (default: PORT_D)
     |  
     |  port_right
     |      port of right wheel (default: PORT_A)
     |  
     |  ----------------------------------------------------------------------
     |  Methods inherited from ev3.EV3:
     |  
     |  __del__(self)
     |      closes the connection to the LEGO EV3
     |  
     |  send_direct_cmd(self, ops:bytes, local_mem:int=0, global_mem:int=0) -> bytes
     |      Send a direct command to the LEGO EV3
     |      
     |      Arguments:
     |      ops: holds netto data only (operations), the following fields are added:
     |        length: 2 bytes, little endian
     |        counter: 2 bytes, little endian
     |        type: 1 byte, DIRECT_COMMAND_REPLY or DIRECT_COMMAND_NO_REPLY
     |        header: 2 bytes, holds sizes of local and global memory
     |      
     |      Keyword Arguments:
     |      local_mem: size of the local memory
     |      global_mem: size of the global memory
     |      
     |      Returns: 
     |        sync_mode is STD: reply (if global_mem > 0) or message counter
     |        sync_mode is ASYNC: message counter
     |        sync_mode is SYNC: reply of the LEGO EV3
     |  
     |  wait_for_reply(self, counter:bytes) -> bytes
     |      Ask the LEGO EV3 for a reply and wait until it is received
     |      
     |      Arguments:
     |      counter: is the message counter of the corresponding send_direct_cmd
     |      
     |      Returns:
     |      reply to the direct command
     |  
     |  ----------------------------------------------------------------------
     |  Data descriptors inherited from ev3.EV3:
     |  
     |  __dict__
     |      dictionary for instance variables (if defined)
     |  
     |  __weakref__
     |      list of weak references to the object (if defined)
     |  
     |  sync_mode
     |      sync mode (standard, asynchronous, synchronous)
     |      
     |      STD:   Use DIRECT_COMMAND_REPLY if global_mem > 0,
     |             wait for reply if there is one.
     |      ASYNC: Use DIRECT_COMMAND_REPLY if global_mem > 0,
     |             never wait for reply (it's the task of the calling program).
     |      SYNC:  Always use DIRECT_COMMAND_REPLY and wait for reply.
     |      
     |      The general idea is:
     |      ASYNC: Interruption or EV3 device queues direct commands,
     |             control directly comes back.
     |      SYNC:  EV3 device is blocked until direct command is finished,
     |             control comes back, when direct command is finished.               
     |      STD:   NO_REPLY like ASYNC with interruption or EV3 queuing,
     |             REPLY like SYNC, synchronicity of program and EV3 device.
     |  
     |  verbosity
     |      level of verbosity (prints on stdout).
      
From now on, we will use class TwoWheelVehicle, when we move a vehicle with two drived wheels. Actually this was more an exercise, than a real thing, but next lesson, we will add functionality to this class. For the moment you may ask, if effort and benefit really are balanced. But look, our program has become shorter:

#!/usr/bin/env python3

import curses
import ev3_dc as ev3

def react(c):
    global speed, turn, my_vehicle
    if c in [ord('q'), 27, ord('p')]:
        my_vehicle.stop()
        return
    elif c == curses.KEY_LEFT:
        turn += 5
        turn = min(turn, 200)
    elif c == curses.KEY_RIGHT:
        turn -= 5
        turn = max(turn, -200)
    elif c == curses.KEY_UP:
        speed += 5
        speed = min(speed, 100)
    elif c == curses.KEY_DOWN:
        speed -= 5
        speed = max(speed, -100)
    stdscr.addstr(5, 0, 'speed: {}, turn: {}      '.format(speed, turn))
    my_vehicle.move(speed, turn)

def main(window) -> None:
    global stdscr
    stdscr = window
    stdscr.clear()      # print introduction
    stdscr.refresh()
    stdscr.addstr(0, 0, 'Use Arrows to navigate your EV3-vehicle')
    stdscr.addstr(1, 0, 'Pause your vehicle with key <p>')
    stdscr.addstr(2, 0, 'Terminate with key <q>')

    while True:
        c = stdscr.getch()
        if c in [ord('q'), 27]:
            react(c)
            break
        elif c in [ord('p'),
                   curses.KEY_RIGHT, curses.KEY_LEFT, curses.KEY_UP, curses.KEY_DOWN]:
            react(c)

speed = 0
turn  = 0   
my_vehicle = ev3.TwoWheelVehicle(protocol=ev3.BLUETOOTH, host='00:16:53:42:2B:99')
stdscr = None

curses.wrapper(main)
      
If you did download module ev3_vehicle.py from ev3-python3, you need to call the constructor of TwoWheelVehicle with two additional arguments radius_wheel and tread. For the moment you may set them to any positive value.

Conclusion

Now we know, how to move motors and we got experience with two important design concepts, interruption and subclassing. Beside we coded something like a real app, a remote control for vehicles.

I hope, that your remote control works and made you a technic-star. We are at the end of this lesson. A glance at the document EV3 Firmware Developer Kit tells you, that there exists a number of additional operations to move motors. Most of them are for precise, synchronized and smooth movements. This will be the topic of our next lesson. I hope to see you again.

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.