行为编程
感谢网友圈圈妈 的翻译


行为编程

用LeJOS NXJ的编程行为

大多数人开始做一个编程机器人时,他们认为这就是可以结构化的一系列的if-thens的程序流(见图1)。这一类型的编程可以非常容易的开始,并且在着手前不需要有什么想法和设计。一个程序员可以直接坐到电脑前开始敲代码。这个程序,这些代码最后会变为套管程序;都包含在一起,很难扩展。相比之下,行为控制模型,在开始前需要一些计划,但结果是每一个行为都可以很好的总结到一个容易理解的结构里。这在理论上会使其他熟悉行为控制模型的人可以很容易的理解你的代码,但更重要的是,它能非常容易的从整体结构里添加或删除特定的行为,而不会对其他编码产生坏的后果。让我们对如何在LeJOS NXJ里这样操作进行测试。

图 1: 形象化的结构编程

API行为

API行为非常简单,只由一个接口和一个类组成。这个行为接口用来定义行为。这个行为接口非常普通,它使用起来很好,因为一个行为的独立执行非常宽广。一旦所有的行为被定义了,它们都会被交给一个仲裁来管理哪个行为应该被激活。所有行为控制的类和接口都在lejos.subsumption包内。API行为接口列举如下:

lejos.subsumption.Behavior (行为类)

  • boolean takeControl()

如果这个行为应当被激活,那么就返回一个布尔值。例如,一个触觉传感器指出机器人撞上了一个物体,用这个方法就会返回真。

  • void action()

在这个方法中编码表明了行为被激活的动作。例如,如果takeControl()检测到机器人碰撞到一个物体,action()编码就会让机器人后退并离开这个物体。

  • void suppress()

在suppress()方法中的编码会立刻终止在action()方法中运行的编码。suppress()方法也可以在行为结束前用来升级任何数据。

正如你所见,行为接口的三个方法都非常简单。如果一个机器人有三个慎重的行为,那么程序员需要创建3个类,每一个类都执行行为接口。一旦这些类结束,编码应该把行为物体关闭交给仲裁去处理。

lejos.subsumption.Arbitrator (仲裁类)

结构如下:

  • public Arbitrator(Behavior [] behaviors)

    创建一个仲裁物,在每个行为将被激活时起管理作用。一个行为的数组索引数字越高,那它的优先级越高。

    参数: 一个行为数组

公开的方法

  • public void start()

    启动仲裁系统

仲裁类甚至比行为更好理解。当用一个仲裁对象作为示例,它会给出一个行为对象的数组。一旦它拥有了这些,start()方法会被调用,它开始仲裁;决定哪种行为会被激活。仲裁会从数组中索引数字最高的对象开始,在每个行为对象上调用takeControl()方法。它通过每个行为对象进行它的方法,直到它遇到一个想接手控制的行为。当它遇到一个时,它会执行那个行为的action()方法一次,并且只执行一次。如果两个行为都想接手控制,那么只有高级别的行为才会被允许。(图2)

图 2: 高级别行为阻止低级别行为

编程行为

现在我们已经熟悉了leJOS的API行为,让我妈看一个用三种行为的简单例子。在这个例子中,我们将会对一个有不同转向装置的简单机器人进行一些行为编程。这个机器人将会往前走,这是它的一个初级的低级别行为。当它触碰到一个物体时,一个高级别的行为将被激活,让机器人后退和转90度。在这两个行为之后,我们也会插入第三个行为。当我们开始第一个行为。

正如我们所看到的行为接口,我们需要执行方法action(), suppress(), 和takeControl()。向前走的行为将会在action()方法中发生。它只需要用到马达A和C轮流前进。


public void action() {
  Motor.A.forward();
  Motor.C.forward();
}
				

那个太简单了!现在当它被调用时,suppress()方法将会用来停止这个动作,如下:


public void suppress() {
  Motor.A.stop();
  Motor.C.stop);
}
				

到目前为止,一切都还好。现在,我们需要执行一个方法告诉仲裁,什么时候这个行为需要被激活。当我们提早画出轮廓时,这个机器人就会一直前进,知道有什么东西阻止了它,所以这个行为应该希望一直可以控制(它有一点可以控制反常的事)。不管发生什么,takeControl()方法都应该可以返回真。这似乎与凭直觉判断相反,但确信无疑的是当需求发生时,高级别的行为可以切断这个行为。方法如下:


public boolean takeControl() {
  return true;
}
				

这是所有我们用来定义驱动机器人前进的第一个行为。这个类中所有用到的编码列表如下:


import lejos.subsumption.*;
import lejos.nxt.*;

public class DriveForward implements Behavior {
   public boolean takeControl() {
      return true;
   }

   public void suppress() {
      Motor.A.stop();
      Motor.C.stop();
   }

   public void action() {
      Motor.A.forward();
      Motor.C.forward();
   }
}
				

第二个行为比第一个要稍微复杂一些,但仍然很相似。这个行为的主要动作是当机器人撞上物体时,可以反转和返回。在这个例子中,我们只需要当触觉传感器撞到一个物体时才起作用,所以定义takeControl()如下:


public boolean takeControl() {
  return touch.isPressed();
}
				

假设在这个例子中一个触觉传感器对象创建了一个实例变量叫做触觉。对于动作,我们希望当机器人撞到物体时能够返回和旋转,所以我们定义action()方法如下:


public void action() {
   // Back up:
   Motor.A.backward();
   Motor.C.backward();
   try{Thread.sleep(1000);}catch(Exception e) {}

   // Rotate by causing one wheel to stop:
   Motor.A.stop();
   try{Thread.sleep(300);}catch(Exception e) {}
   Motor.C.stop();
}			
				

在这个例子中,为这个动作定义suppress()方法非常简单。上面提到的action()方法运行的非常快(1.3秒),而且优先级很高。我们可以通过停下电机运动来忽然停下它,或者等待它完成向后的动作。为了让它变得简单,我们就停止电机旋转:


public void suppress {
  Motor.A.stop();
  Motor.C.stop();
}
				

这个动作的完整列表如下:


import lejos.subsumption.*;
import lejos.nxt.*;

public class HitWall implements Behavior {
   public TouchSensor touch = new TouchSensor(SensorPort.S!);
   
   public boolean takeControl() {
      return touch.isPressed();
   }

   public void suppress() {
      Motor.A.stop();
      Motor.C.stop();
   }

   public void action() {
      // Back up:
      Motor.A.backward();
      Motor.C.backward();
      try{Thread.sleep(1000);}catch(Exception e) {}
      
      // Rotate by causing only one wheel to stop:
      Motor.A.stop();
      try{Thread.sleep(300);}catch(Exception e) {}
      Motor.C.stop();
   }
}
				

我们现在对两个动作进行了定义,而且它只简单的用main()方法建立类,然后开始。所有我们要做的就是创建一个我们行为对象的数组,和示例,启动如下编码列表中所显示的仲裁


import lejos.subsumption.*;

public class BumperCar {
   public static void main(String [] args) {
      Behavior b1 = new DriveForward();
      Behavior b2 = new HitWall();
      Behavior [] bArray = {b1, b2};
      Arbitrator arby = new Arbitrator(bArray);
      arby.start();
   }
}
				

上面的代码时非常容易理解的。main()方法中的头两行创建了我们行为的示例。第三行将它们放入数组,最低优先级的行为有着最低的数组索引。第四行创建了仲裁。第五行开始运行仲裁过程。当这个程序运行的时候,机器人会急匆匆的前进直到撞到一个物体,然后它会退却,旋转,并继续它前进的运动直到电源关闭。

这看起来好像给2个简单的动作增加了很多额外的工作,但是让我们看看它是怎么样的简单,可以在其他类里不改变任何编码就能加入第三个行为。这就是通过机器人技术编程让行为控制系统非常具有吸引力的部分。我们的第三个行为可以是任何事情。我们可以让这个新行为监控电源水平和当它掉到某个水平线下时能发出声音。


import lejos.subsumption.*;
import lejos.nxt.*;

public class BatteryLow implements Behavior {
   private float LOW_LEVEL;

   private static final short [] note = {
      2349,115, 0,5, 1760,165, 0,35, 1760,28, 0,13, 1976,23,
      0,18, 1760,18, 0,23, 1568,15, 0,25, 1480,103, 0,18,
      1175,180, 0,20, 1760,18, 0,23, 1976,20, 0,20, 1760,15,
      0,25, 1568,15, 0,25, 2217,98, 0,23, 1760,88, 0,33, 1760,
      75, 0,5, 1760,20, 0,20, 1760,20, 0,20, 1976,18, 0,23,
      1760,18, 0,23, 2217,225, 0,15, 2217,218};

   public BatteryLow(float volts) {
      LOW_LEVEL = volts;
   }

   public boolean takeControl() {
      float voltLevel = Battery.getVoltage();
      System.out.println("Voltage " + voltLevel);

      return voltLevel < LOW_LEVEL;
   }

   public void suppress() {
      // Nothing to suppress
   }

   public void action() {
      play();
      try{Thread.sleep(3000);}catch(Exception e) {}
      System.exit(0);
   }

   public static void play() {
      for(int i=0;i <note.length; i+=2) {
         final short w = note[i+1];
         Sound.playTone(note[i], w);
         try {
            Thread.sleep(w*10);
         } catch (InterruptedException e) {}
      }
   }
}
				

完整的曲调存在注释数组的第6行,发出这个曲调的方法在30行。这个行为只在当前电源水平低于构造器中规定的电压时才会启动。takeControl()方法看起来有点膨胀,这是因为它也需要在LCD显示屏上展示电源充电器。action()和suppress()相对来说比较容易。动作会制造一些噪音,然后当它被调用的时候退出程序。正因为这个行为停止了程序,所以也没有必要创建一个suppress()方法。

把这个行为插入我们的方案是没有意义的。我们可以用如下的方法简单的改变主类中的代码:


import lejos.subsumption.*;

public class BumperCar {
   public static void main(String [] args) {
      Behavior b1 = new DriveForward();
      Behavior b2 = new BatteryLow(6.5f);
      Behavior b3 = new HitWall();
      Behavior [] bArray = {b1, b2, b3};
      Arbitrator arby = new Arbitrator(bArray);
      arby.start();
   }
}
				

注意:NXT的电压显示是静止的,这与动作中的电压不同。静止时的电压也许为7.8V,但是当马达运行的时候,电压会下降。保证在低电压构造器中使用的电压足够低。

这个例子完美的演示了一个很棒的行为控制编码。无论其他编码看起来如何,插入一个新行为是简单的。这样做是为了以设计为目标的对象打基础;每个行为都是自我包含,独立的对象。

提示:当创建一个行为控制系统时,最好一次编写一个行为并且独立测试它们。如果你写好所有行为的编码,然后向NXT中一次加载,那么很容易在这些行为中出现bug,而且很难知道bug出在哪里。如果每次编写和测试一个行为,那么它将很容易找到问题。

行为编程对自主机器人来说很有用,激起人们可以根据它们自身的情况独立工作。一个由人类控制的机器臂看起来没有使用行为编程,虽然它可以。例如,一个有四个操纵杆的机器臂可以进行每个方向的运动。但是如果重复调用,被定为高级别的行为会比低级别的行为有优先权。也就是说,向左推操纵杆会取代推上去?换言之,无论什么东西除了自主机器人的行为控制都在很大程度上有不必要的过度行动。

返回顶部

进阶行为编程

如果所有的行为都想上面给出的例子那样简单就好了。但是在一些更复杂的编码中,有时可以介绍一些无法预料的结果。例如,线程,很难从suppress()方法中停下来,这使得2个不同的线程争夺同一资源,通常是同一个电机。在这个章节中,我们会检测一些意想不到的困难。让我们从这三个要执行的行为方法中最不复杂的看起。takeControl()方法

注意:LeJOS NXJ的API行为控制是根据Rodney Brooks提议的模型修改的版本。他的版本用于最低级别的可能——电机们。这阻止了高级别类在行为中的使用。例如,导航类直接进入NXT的电机,通过原始行为控制模型,导航不能使用。同样,如果两个电机都前进,一个高级别行为有一个它不是很清楚的命令,所有的低级别电机的行动会被停止。那么如果高级别行为只用了其中一个电机呢?其他的应该保持前进吗?这是否会引起奇怪的行为?这些事LeJOS NXJ的API控制想要寻址。

防轰炸无需操心的编码方法

对takeControl()方法来说,在行为控制系统中反应灵敏是非常重要的。当一个缓冲器与一个物体相撞时,机器人必须马上停下或者转变方向,否则它会继续前进撞击这个物体。有时,当事情发生时,如一个触觉传感器按下时,程序因为NXT在执行另一个线程儿错过了这个事件。到它得到takeControl()方法时,传感器已经被释放,并且程序错过了激活相应行为动作的机会。在这一章节中,我们将学习如何使用傻瓜验证法takeControl()。

在上面的例子中,我们只用到了是否需要控制的方法。例如,用读一个传感器类去检查触觉传感器是否撞上物体。takeControl()方法还可以根据一个数据或不同的数值来判断是否去控制。当它面向东方,光线超过60,温度低于20度时,可以指示一个行为。


public boolean takeControl() {
   boolean pass = false;

   if(direction == EAST)
      if(light.readValue() > 60)
         if(temperature.getCelcius() < 20)
            pass = true;

   return pass;
}
				

同样的,一个不同的行为,只需要根据不同的数值表现不同,就可以在同样的数据上轻松得到检查。例如,;当机器人面向西方,光线小于60,温度大于20度,它可以指示另一个行为。所以一个机器人可以只根据一些传感器数据不同的排列,指示无穷多的结果。这也指出了关于执行takeControl()方法的另一点。

仲裁要在所有的takeControl()方法里循环,这就在检查条件中产生了一个重要的延时,如一个触觉传感器是否被触发。这是我们发现一点小瑕疵,当机器人撞上一个物体,触觉传感器未必一直保持被触发。有时机器人撞上某个物体,又被反弹回来,而缓冲器未必能一直按在触觉传感器上。你也许注意到在这个例子中,它依赖于要非常频繁的检查触觉传感器。那么当触觉传感器在某个时刻被按下,而仲裁又错过这一时刻,该怎么办呢?解决方法就是使用一个传感器监听器,并且通过它设置一个标志位,记录事件发生过。让我们做个撞墙实验,通过使用传感器监听器来改进它:


import lejos.subsumption.*;
import lejos.nxt.*;

public class HitWall implements Behavior, SensorPortListener {
   boolean hasCollided;
   TouchSensor bumper = new TouchSensor(SensorPort.S1);

   // Constructor:
   public HitWall() {
      hasCollided = false;
      SensorPort.S2.addSensorPortListener(this);
   }

   public void stateChanged(SensorPort port, int oldValue, int newValue) {
      if(bumper.isPressed())
         hasCollided = true;
   }

   public boolean takeControl() {
      if(hasCollided) {
         hasCollided = false; // reset value
         return true;
      } else
         return false;
   }

   public void suppress() {
      Motor.A.stop();
      Motor.C.stop();
   }

   public void action() {
      // Back up:
      
      Motor.A.backward();
      Motor.C.backward();
      try{Thread.sleep(1000);}catch(Exception e) {}

      // Rotate by causing only one wheel to stop:
      Motor.A.stop();
      try{Thread.sleep(300);}catch(Exception e) {}
      Motor.C.stop();
   }
} 
				

上面的编码使用了一个传感器端口监听器,然后执行了stateChanged()方法。如11行所示,在传感器端口S2上增加一个传感器监听器非常重要。注意下,stateChanged()方法并不是简单的返回一个缓冲传感器的数值,更进一步的是,如果传感器数值为真,它将hasCollided置为真。如果,在下一步,传感器数值是假,则hasCollided仍保留为真,直到takeControl()看到hasCollided的数值。一旦takeControl()看到发生了一起碰撞,那么hasCollided会被重置为假(见20行)。用这个新编码,机器人就能够不错过任何一次碰撞了。

稳定的功能和查禁的编码方法

要编写一组action()和suppress()功能,就需要理解仲裁是如何工作的。仲裁在它的行为里一直循环,检查takeControl()方法,看行为的action()是否被执行。它从优先级最高的方法一直检查到优先级最低的行为。一旦它遇到一个想拥有控制权的行为,它就会对前一个行为执行suppress()(假设它不是一个更高级别的线程) ,然后对当前的行为执行action()方法。当action()执行完毕后,它又会重新进行循环,检查每一个行为。如果前一个行为的takeControl()仍为真,它就不会再次运行action()。这很重要,在一个队列中,单一的行为不会被执行两次。如果它可以,那它就必须一直中断它自己。如果仲裁运行到另一个行为,当那个行为完成时,它就会再调用低级别的行为。

注意:如果你想深入了解仲裁类的问题,可以看看src/classes/lejos/subsumption/Arbitrator.java中的一些源代码。

要对一些个别的行为进行编程,理解不同行为间的基本不同是非常重要的。行为动作来自两个基本的种类:

  • 能很快完成的不相关的行为 (如后退和转动)

  • 开始运行和保持一个连续时间的动作,除非它们被中断 (如前进,沿着墙)

最后一点建议。不相关的动作只执行一次,而且只有当它完成它的行为后,才能通过action()方法调用返回。这些类型的行为通常不需要在suppress()方法中进行任何编码,因为一旦这动作开始,没有什么可以中断它。第二种类型的动作有时但不是全部,会在单独的线程中运行。例如,电机A的forward()方法通过一个线程调用动作,因为电机会在返回前一直转动。实际上,这并不是一个线程,RCX只是打开了一个内部开关来激活电机。一个真正线程的例子是复杂的行为,如沿着墙走。action()方法可以打开一个线程开始沿着墙走,直到suppress()方法被调用。要小心死循环!如果在action()中出现一个,这个程序就不动了。

摘要

那么为什么要使用API行为呢?最好的理由就是,在一个程序中,即使它占用多一点的时间,我们也力求创建一个最简单,功能最强大的解决办法。可再利用,可维持的编码的重要性,已经重复演示过了,尤其是在不止一个人的项目里。如果你离开你的编码,几个月之后再来看它,以前很熟悉的编码也许看起来什么都不是。如果用行为控制,即使在这个程序中有10个或以上的行为,你也可以甚至不用看其他的代码就能添加或删除行为。关于行为控制的另一个优势是,程序员之间可以互相交换行为,这可以促进编码的再利用。大量的有趣的,通用的行为可以上传到互联网上,你可以很容易的找到需要的加载到你的机器人里(假设你的机器人是整齐点类型)。这些编码的再利用,通过标准的LeJOS NXJ类,可以直接用,如API导航。

返回顶部