EV3 直接命令 - 第 4 课 用两个驱动轮精确地移动小车

简介

上一课,我们编写了 TwoWheelVehicle 类,一个 EV3 的子类。它的方法是 movestop,但它不仅仅是围绕操作 opOutput_SpeedopOutput_StartopOutput_Stop 的薄薄的封装。本节课的最后,类 TwoWheelVehicle 将具有实质的内容。如大多数软件那样,它一步步增长,本节课不会是最后一次,我们将继续使用这个类。

上一课的主题是一个遥控小车。我们编写了无限移动的代码,它们由新命令中断。我们已经看到了这一设计理念的好处,它不阻塞 EV3 设备。精确移动的第一个版本(本节课的结果)将阻塞 EV3 设备,且它将耗费我们进一步的工作来找到一个不阻塞的方案。

同步的电机移动

希望上一节课的车辆仍然存在。我们再次需要它。你还记得,右轮连接到端口 A,左边连接到端口 D。我们的遥控解决方案存在缺陷:移动越慢,转向的精度越差。我们需要的是一个操作,两个电机的速度可以被设置为定义的比率。如你可能期待的那样,这种操作是存在的。请给你的小车发送如下的直接命令:

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

点击 A 旋转 720°,点击 D 移动 360°。小车左转,两个电机都以低速转动且良好的同步。新的操作是:

  • opOutput_Step_Sync = 0xB0,参数是:
    • LAYER
    • NOS:两个输出端口,命令不是对称的,它区别了低端口和高端口。在我们的情况中,低端口是 PORT_A,右轮,高端口是 PORT_D,左轮。
    • SPEED
    • TURN:转向比率,[-200 - 200]
    • STEP:转速脉冲以度为单位,值 0 代表无限运动(如果TURN > 0,则 STEP 限制低端口,如果 TURN < 0,则限制较高端口)。正值,参数 SPEED 的符号确定方向。
    • BRAKE

参数 TURN 需要一些解释。但你处在一个很好的位置,你已经知道了它的含义,因为我们在第 3 课的远程控制程序中使用了它。如你可能记得的那样,我们计算两轮的速度:

    if turn > 0:
        speed_right  = speed
        speed_left = round(speed * (1 - turn / 100))
    else:
        speed_right  = round(speed * (1 + turn / 100))
        speed_left = speed

到整数值的舍入是低速下糟糕的精度的原因!但现在我们很高兴,操作 opOutput_Step_Sync 计算时不执行舍入:

    if turn > 0:
        speed_right  = speed
        speed_left = speed * (1 - turn / 100)
    else:
        speed_right  = speed * (1 + turn / 100)
        speed_left = speed

速度和步数是成比例的,我们也可以写为:

    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

操作 opOutput_Step_Sync 对于具有两个驱动轮的小车是完美的!就像是专门为它设计的一样。请通过把两个 opOutput_Speed 操作替换为一个 opOutput_Step_Sync 来提升你的远程控制程序。你将看到,它工作的更好,特别是低速下。我把我的 move 函数的代码修改为了:

    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)

一些说明:

  • 车辆遵从相同的转弯,与速度无关。这是 opOutput_Step_Sync 相对于使用两个 opOutput_Speed 操作的主要提升。
  • 如果你使用了 opOutput_Polarity,你将发现 ,你无法把它和 opOutput_Step_Sync 结合。你不得不手动完成。修改两个电机的极性很简单,只是反转参数 SPEED 即可。
  • 如果你交换了电机的连接,以使你的左边电机连接在较低端口上,它也很简单,反转 TURN。
  • 仅更改一个电机的极性非常棘手。

下表可能有助于理清你已经看到的移动。当给出以下条件时,它描述了依赖于 TURN 的运动类型:

  • 两个电机具有相同的极性,
  • 右轮连接到更低的端口,左边的更高。
TURN移动描述
0直行两个电机以相同的速度朝相同的方向移动
[0 - 100]左转两个电机朝相同的方向旋转,左边的以更低的速度
100绕左轮左转只有右边的电机移动
[100 - 200]向左转弯两个电机朝相反的方向旋转,左边的以更低的速度旋转
200向左转圈两个电机朝相同的方向旋转,但速度相同
[0 - -100]右转两个电机朝相同的方向旋转,右边的以更低的速度
-100绕右轮右转只有左边的电机移动
[-100 - -200]向右转弯两个电机朝相反的方向旋转,右边的以更低的速度旋转
-200向右转圈两个电机朝相同的方向旋转,但速度相同

当端口连接不同时,左轮为较低端口,右轮为较高端口时,请反映情况。

欢迎你进一步提升远程控制工程。你可以使用游戏杆而不是你的键盘的方向键。或者你可以使用智能手机的陀螺仪传感器。但这是你的项目,我们将远程控制留在了后面(至少目前为止)。

具有两个驱动轮的车辆的定义明确且可预测的运动

我们仍然专注于具有两个驱动轮的车辆及其运动的准确性。远程控制是一种非常特殊的情况,如果需要,人类的头脑会监督车辆的运动并立即进行一些修正。这种情况不需要像转弯半径那样的单位。这就像驾驶汽车一样,没有必要确切知道转弯半径是如何从方向盘的位置决定的。修正是相对和直观的。机器人在没有外部控制的情况下移动,它们的算法需要知道参数的确切依赖性。我们将编写控制车辆运动的程序。我们从最难的变体开始,不存在任何校正机制。这意味着,我们需要能够预测和精确描述车辆运动的函数。我想到以下几点:

  • drive_straight(speed:int, distance: float=None),其中

    • speed 的符号描述了方向(向前或向后)
    • speed 的绝对值描述了速度。我们更喜欢每秒 SI 单位计,但它是百分比。
    • distance 为 None 或正的,它以 SI 单位 meter 给出。如果它是 None,则运动是无限的。
  • drive_turn(speed:int, radius_turn:float, angle:float=None, right_turn:bool=False),其中

    • speed 的符号描述了方向(向前或向后)
    • speed 的绝对值描述了速度。
    • radius_turn 是转弯的半径,单位为 meter。我们取两个驱动轮的中间作为我们的参考点。
    • radius_turn 的符号描述了转弯的方向。正值表示左转,正转向,负值代表顺时针转动。
    • angle 为 None 或正的,它是以度为单位的圆弧段 (90° 为四分之一圆,720° 为两个完整的圆)。如果它是 None,则表示无限移动。
    • right_turn 是一个用于特殊情景的标记。如果我们打开 place,属性radius_turn 为零并且没有符号。在这种情况下,左转是默认值,属性right_turn 是相反方向的标志。

正如你可能想象的那样,我们并没有走太远。我们将使用操作 opOutput_ReadyopOutput_StartopOutput_Speed_Sync。这是说我们将不使用中断。从这个角度来看,我们可以回到第二课的知识。

确定车辆的尺寸

们需要车辆的某些尺寸来将上述功能的 SI 单位转换为参数 turn 和操作opOutput_Speed_Sync 的步骤。 车辆的尺寸为:

  • 驱动轮的半径:radius_wheel
  • 车辆 tread

使用尺子量出(对于我的车辆):

  • radius_wheel = 0.021 m
  • tread = 0.16 m

具有更高精度的替代方案是测量车辆的运动。要获得驱动轮的半径,可以使用以下直接命令:

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

拿出尺子并测量你的小车移动的距离(如果你的小车没有直行,寻找轮子的最佳结合,而不是外观,这似乎是相同的尺寸,真的是)。轮子的一个完整旋转的距离计算公式为 2 * pi * radius_wheel。3,600 度为 10 个完整旋转,因此下面的计算给出了你的轮子的 radius_wheel

   radius_wheel = distance / (20 * pi)

这将我的轮子的半径修正为 radius_wheel = 0.02128 m。接下来,我们让车辆旋转,并计数 N,车辆转数。为此,我们发送直接命令:

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

我计算了我的车辆 N = 15.2 完整旋转。两个轮子都旋转了 18, 000°,这是50 个完整的旋转,或者距离 50 * 2 * pi * radius_wheel。旋转的半径为 0.5 * tread(我们定义轮子的中间为我们的参考点)。这就是说 N * 2 * pi * 0.5 * tread = 50 * 2 * pi * radius_wheel 或者:

   tread = radius_wheel * 100 / N

这修正了 tread 的尺寸(在我的情况中:tread = 0.1346 m)。稍后我们将执行一些额外的运动,也许这将再次修正 tread 的尺寸。

获取参数 STEP 和 TURN 的数学转换

OK,我们知道了车的尺寸。接下来我们需要把我们的方法 drive_straightdrive_turn 的参数转为操作 opOutput_Step_Sync 的参数 STEP 和 TURN。这需要一些数学的东西。如果你对细节不感兴趣,你可以只看本小节最后的结果。但是大多数人都想知道细节,细节在这里。

给定我们的小车需要旋转的 angleradius_turn。在转弯中,两个轮子移动不同的距离。外面的轮子的距离为:2 * pi * radius_wheel * STEP / 360。可以根据转弯的几何形状和车辆踏板的知识计算相同的距离:2 * pi * (radius_turn + 0.5 * tread) * angle / 360。这两个相同距离的描述给出了以下等式:

      STEP = angle * (radius_turn + 0.5 * tread) / radius_wheel

OK,第一个参数计算出来了,但是我们仍然需要计算 TURN。关键的方法来自转弯的几何学,是说,两个车轮之间的速度比 speed_right / speed_left 与外面和里面的车轮的距离比相同:

      speed_right / speed_left = (radius_turn + 0.5 * tread) / (radius_turn - 0.5 * tread)

你可能还记得,我们已经知道了两个轮子的速度:speed_right = SPEED 和 speed_left = SPEED * (1 - TURN / 100)。这给出等式:

      1 / (1 - TURN / 100) = (radius_turn + 0.5 * tread) / (radius_turn - 0.5 * tread)

转换这个等式得到:

      TURN  = 100 * (1 - (radius_turn - 0.5 * tread) / (radius_turn + 0.5 * tread))

就是这样,果给出转弯运动的尺寸 (angle, radius_turn) 和车辆的尺寸 (tread, radius_wheel):

  1. 变量 STEP 的计算方法为:
      STEP = angle * (radius_turn + 0.5 * tread) / radius_wheel
  1. 变量 TURN 的计算方法为:
      TURN = 100 * (1 - (radius_turn - 0.5 * tread) / (radius_turn + 0.5 * tread))

在 Python 中,这可以写作:

        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

数学转换的控制

我们做一些合理性检查:

  • 以半径 radius_turn = 0.5 * tread 旋转得到 TURN = 100 或 TURN = -100,这是对的。
  • 以半径 radius_turn = 0 旋转得到 TURN = 200 或 TURN = -200,这也是OK 的。

现在,我们回到真正的测试。以 angle = 90° 和 radius_turn = 0.5 m 旋转。在我的情况中,由上面的计算给出 STEP = 2358 和 TURN = 23,这将得到命令:

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

确实,我的小车几乎以半径 0.5 m 移动了完美的四分之圆。

增强类 TwoWheelVehicle

数学已经足够了,至少在目前,让我们的编码吧!作为我们的第一个任务,我们修改类 TwoWheelVehicle 的构造函数。我们添加两个尺寸 radius_wheeltread,它们被确定为必要的:

    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

接下来我们编写方法 _drive,它与方法 move 非常接近。调用者传入参数 speedturnstep 调用它。外部的世界用 radius_turnangle 来思考,但它们必须被转为内部的参数 turnstep。这是说,方法 drive_straightdrive_turn 执行转换,然后它们调用内部的方法 _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)

我们区分 SYNC 和 ASYNC 或 STD。在 ASYNC 或 STD 的情况中,我们在移动开始前等待,在 SYNC 的情况中直到它结束。如果你从 ev3-python3 下载了模块 ev3_vehicle.py,你将找不到方法 _drive。这是一个提示,我们将回到类 TwoWheelVehicle 实现中断。我们编写方法 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)

非常大的 radius_turn 值将导致一些问题。舍入到整数之后,它们导致直行。在这种情况下我们抛出一个错误。

方法 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)

放松,我们已经实现了一种可以预测车辆的工具。 请做一些测试!

了解车辆的位置和方向

抱歉,之前的一些内容我写了 足够的数学知识,但现在我来到了三角学。但我们处于一种真的值得努力的情境。想象一下,你驾驶你的车并经过了一系列的移动,你需要知道它的位置和方向。我们无需使用传感器就可以做到。相反,我们使用纯数学而不是魔法。

让我们做一些假设:

  • 当你创建类 TwoWheelVehicle 时你的车放置的位置将是你的坐标系统的原点 (0, 0)。
  • 此时指向正前方的方向是 x 轴的方向。
  • y 轴指向你的车的左手边。
  • 你的车的位置由 x 和 y 坐标描述。以米为单位。
  • 你的车的 orientation 是车的原始方向和它的实际方向的差值。以度数为单位。左转增加 orientation,右转减小它。

这次,我不会介绍数学。把它当作原样,或者把它作为一个谜语,你必须解决。但是请把如下的逻辑添加到你的类 TwoWheelVehicle 中:

  • 构造函数:
        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

完成类 TwoWheelVehicle

我们通过添加更多的功能来完成类 TwoWheelVehicle

  • 我们添加一个方法 rotate_to(speed: int, o: float),它做如下的事情:

    • 计算实际的和新的方向的距离
    • radius_turn = 0 调用 drive_turn 旋转小车,以使其获得新方向。
  • 我们添加一个方法 drive_to(self, speed: int, x: float, y: float),它做如下的事情:

    • 计算实际的位置和新位置的距离:
        diff_x = pos_x - self._pos_x
        diff_y = pos_y - self._pos_y

我们需要坐标和绝对值:

        distance = math.sqrt(diff_x**2 + diff_y**2)
- 计算新位置的方向。这很棘手,你需要彻底了解三角学,因为你必须使用 `atan`,`tan` 的反函数。我给你一个提示:
        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
- 调用 `rotate_to`,以使 `orientation` 指向 `direction`。
- 调用 `drive_straight` 移动小车到新的位置。

我们做一些测试:

  • 我们把小车发送到一些循环行程中,并编码一系列 drive_to,最终在 position = (0,0) 结束。我们最后添加一个以 orientation = 0 对 rotate_to 的调用。请评估,小车是否真的回到了它最初的位置和方向。
  • 我们给循环行程添加一些 drive_turn 并评估,drive_turn 的错误是大于还是小于 drive_to 的。

以大 radius_turn 旋转将有一个糟糕的精度。这也是舍入的结果。在这种情况中,将 TURN 舍入为整数值创造了错误。

如果你从 ev3-python3 下载了 ev3_vehicle 模块,当你调用它的方法 drive_straightdrive_turnrotate_todrive_to 时,你最后需要添加一个对 stop 的方法调用!

异步和同步运动

让我们仔细看看我们的车辆驾驶情况。这是我的循环行程的程序:

#!/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)

这个程序的输出:

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|

在五毫秒内这个程序给 EV3 设备发送了所有的直接命令,它们被放入队列并等待执行。这个代码很简单,但是会阻塞 EV3 设备直到开始执行最后的命令。请遵循代码并反映出属性 pos_x,pos_y 和 orientation 的值,以及它们是否与我们车辆的实际位置和方向相对应。

这是异步行为! 程序和 EV3 设备在不同的时间尺度上运行。

现在我们改为同步模式并比较行为:

#!/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)

此版本产生以下输出:

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|

车辆的运动是相同的,但现在程序发送一个直接命令并等待它完成,然后它发送下一个。sync_mode = SYNC 如它设计那样工作,它同步程序和EV3 设备的时间尺度。另一个好处是,它控制每个直接命令的成功并直接作出反应,如果出现意外情况。

这两个版本(异步和同步)都会阻塞 EV3 设备。

结论

TwoWheelVehicle 通过两个驱动轮控制小车的运动。如果我们知道课程的几何形状,我们就可以编写一个程序,通过它来驱动我们的车辆。

我们已经看到了同步和异步模式之间的区别。目前,我们更倾向于 SYNC。但我们的目标是一个解决方案,它可以同步驱动车辆并且不会阻塞EV3 设备。这将打开多任务的大门。

我的 EV3TwoWheelVehicle 类实际上有以下状态:

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).

我希望,你感觉到,我们现在正在做实际的事情。 对我来说,这是一个亮点。 只有在极少数情况下,人们才能用这么少的努力获得这么多。

原文

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值