2013年12月15日 星期日

如何使用"行為控制模式"撰寫JAVA程式來控制你的NXT機器人

測試環境: eclipse、lejos、Windows 7、Lego NXT



大多數的人在開始撰寫控制機器人的程式時,第一個想到的程式是使用一堆的 if-then 語法來控制你的機器人,使用這種方式是很容易入門,但很快的會發現,在附於機器人更多功能時(如更多的感應器、更多的馬達),單純 if 判斷的寫法漸漸的讓你的程式失去控制。因為不管是程式的可閱讀性或執行的效率上都會呈線性式的下滑,它不僅會造成程式在要多加機器人新功能時很難下手而且因為過多的條件判斷讓它的執行效率大打折扣。為了解決這個問題,leJOS提供了一個叫 "Behavior(行為)控制模式" 的程式撰寫方式,它可以讓我們撰寫出易於維護和擴充的機器人Java程式。

在這個模式下,當你開始撰寫控制程式前必須要先計畫一番,但好處是由於機器人每個動作都封裝的很好,所以程式相當容易閱讀及修改。在幫機器人加入新功能時,不會去變動至原有的程式,在移除某個機器人動作時也不至於影響其他動作的控制程式。甚至在為每個動作去分別進行測試及除錯都變得可行。

leJOS提供了一個稱為 "Behavior(行為)控制模式" 它的觀念是很簡單的:
  • 任何一個時間只有一個行為者(behaviors)可以被啟動並控制機器人
  • 每個行為者(behaviors)都有一個特定的優先順序
  • 每個行為者(behaviors)都有決定是否要控制機器人的機制
  • 被觸動的行為者(behaviors)若它的優先順序是高於目前正在運作中的行為者,那它就會接手機器人的操作。


The Behavior API

"Behavior(行為)控制模式"是由一個Interface(介面) 及一個class(類別)所組成。Interface 是用來定義各個行為類別 (behavior class)的,它其實很簡單,共提供了三個公開的方法(public method):takeControl()、action()、suppress()。每一個機器人的任務(task)都必須利用Interface 去實現(implement) 一個行為者類別(behavior class)。由於每個任務都是獨立的類別,所以整個程式看起來非常簡潔易懂。一旦所有的行為者都建立完成後,可透過仲裁者物件來管理這些行為者,它可以去控制那個時間點應該啟動那個行為者。Arbitrator class 與 Behavior interface 是由lejos.subsumption 這個package所提供的。

lejos.subsumption.Behavior
行為者提供有三個方法:

boolean takeControl()
回傳值是一個用來表示: 此一行為者是否應該變成啟動狀態的布林值。舉例: 如果碰撞感應器感測到已經碰撞到一個物體時,takeControl 這個方法就應該回一個 True值。特別提醒,在這個方法中的程式應該儘可能的簡潔,不要在過多的運算後才去執行回應的動作。

void action()
這個方法是在一個行為者變成為啟動後會去執行的程式碼,也是實際控制機器人行動的程式碼所在。舉例: 當碰撞感應器感測到已經碰撞到一個物體時,機器人應該後退後再轉彎,這樣的動作控制程式碼就是放在 action() 這個方法中。
action 這個method是控制機器人行動的中心,所以要特別留意正確的結束這個method的執行,在離開這個方法時要讓你的機器人保留在一個安全的狀態中,包含由 suppress method 來觸發你 action結束也是一樣。

void suppress()
這個方法是提供給仲裁者用來呼叫以停止正在執行的這個行為者的程式碼。舉例:如果機器人正在執行直行這個行為者,當仲裁者接收到碰撞感應器(也就是負責執行後退轉彎的這個行為者)感測到已碰撞到某個物體時,仲裁者必須停止繼續前行的動作,這時仲裁者所要呼叫的方法就是正在啟動中的這個行動者所提供的 suppress 方法。

如你上述所見的,提供三個方法的這個介面(interface)程式相關簡單。如果你的機器人有三個不同的任務,那你的程式就必須建立三個class (繼承相同的這個介面interface)。行為者程式一旦完成,接下來就是去撰寫仲裁者程式了。


lejos.subsumption.Arbitrator
仲裁者是用來協調所有行為者的執行動作,它有一個建構子(constructor)及一個方法(methor):

public Arbitrator(Behavior[] behaviors, boolean returnWhenInactive)

建構子傳入的參數有二個: 一是行為者陣列(集合),每個行為者都有一個優先順序,優先順序最高者放在陣列的最後面。所以Behavior[0] 是最低優先順序者。第二個參數是一個布林值,為Ture時表示若在無何任行為者去取得控制權時程式就自動停止。若為Fasle,則程式就不會自動停止,除非你按下NXT上的離開按鈕(Escape)。

public start()
開始執行仲裁者程式

其實仲裁者這個類別(class)是很容易了解的。當仲裁者物件被初始時(instantiated)時會傳入一個行為者陣列。有了這個陣列,當仲裁者被啟動時(start),它就開始進行協調工作,來決定那一個行為者該被啟始(active)。仲裁者會依照優先順序(由陣列最大索引子者開始)來呼叫每一個行為者的 takeControl() 方法,若tackeControl回傳是ture表示這個行為者的事件被觸發了,此時仲裁者會檢查這個行為者的陣列索引子(即表優先順序碼)是否大於目前正在執行中的行為者的優先順序,如果優先順序比較高,就會呼叫目前執行中行為者的 supress方法來停止它的作業,然後再呼叫新行為者的action方法。在這個邏輯下,若同一時間有多個行為者被觸動,只有一個優先順序最高會變成是啟動中。

開始撰寫行為者的程式

為了確保行為控制模式確實可行,當suppress 方法被呼叫時要立刻會執行的這個要求一定要達成。有一種方法可以達成這個要求,就是設定一個控制旗標(flag),這個旗標在supress 被呼叫時會被設定成true,而且這個旗標會先被設為false,並且在每次action執行循環中都會被檢查,當旗標為true時,action這個方法中主要控制機器人行動的程式碼就不會被執行。

現在我們已經熟悉leJOS中Behavior API的功能了,讓我們實際進行一個簡單的範例吧。我們要針對機器人不同的任務設計不同的行為者類別(behavior class)。首先,讓機器人向前前進的最主要任務,而它擁有的是最低優先的執行順序。這個任務會持續進行,除非它碰撞到某個物體。當它碰撞到物體時,另一個比較高執行順序的任務會被啟動,所以機器人後退並旋轉90度。

如我們前面所介紹的,當你繼承Behavior interface 時,必須實作三個方法(method):action、suppress、takeControl。向前行的動作是被實作在action這個方法內,很簡單地,你只要讓馬達B及C向前轉動,然後當停止轉動後或supress 方法被呼叫時就離開action這個方法。這個方法程式如下:
public void action() {
  suppressed = false;
  Motor.A.forward();
  Motor.C.forward();
  while( !suppressed )
     Thread.yield();
  Motor.A.stop(); // clean up
  Motor.C.stop();
}

suppress() 方法的程式如下,當它被呼叫時,是用來停止 action 方法的執行。

public void suppress() {
  suppressed = true;
}

接下來我們要去實現一個方法去告訴仲裁者何時應該讓這個行為者變成是啟動中。如前面所述,我們的機器人是保持一直前進,除非它碰撞到某個物體。所以必須讓這個行為者保持在"活動中"(這是有點怪的控制方式),為了達到這個目的,我們必須讓 takeControl()這個方法不管發生任何事永遠都回傳true值。這個作法可以我們機器人在被高優先順序的行為者中斷後,可以再持續持行往前進的任務。
public boolean takeControl() {
  return true;
}

所以我們機器的第一個行為者的完整程式碼如下所示:

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

public class DriveForward  implements Behavior {
  private boolean suppressed = false;
  
  public boolean takeControl() {
     return true;
  }

  public void suppress() {
     suppressed = true;
  }

  public void action() {
    suppressed = false;
    Motor.A.forward();
    Motor.C.forward();
    while( !suppressed )
       Thread.yield();
    Motor.A.stop(); // clean up
    Motor.C.stop();
  }
}

第二個行為者比第一個稍微複雜些,但依舊非常相似。主要的行動任務是當撞上某物件或非常接近某物時後退並轉彎。這個行為者在取得主導權的作法上與第一個不同,它只有在撞上某物件或非常接近某物情況下才會去取得程式主導權(即讓takeControl方法回傳true值)。takeControl()方法程式如下:
public boolean takeControl() {
 return touch.isPressed() || sonar.getDistance() < 25;
}
touch是一個觸控感應器(Tourch Sensor) 物件變數,使用前一定要先建立,相同的 sonar 也是一個物件變數,它是超音波感應器(Ultra Sonic Sensor),當然使用前也要先建立好。
至於acion 方法中,我們要機器人在偵測到物體後先後退再轉彎,程式碼如下:
public void action()
{
   // Back up and turn
   suppressed = false;
   Motor.A.rotate(-180, true);
   Motor.C.rotate(-360, true);

   while(Motor.C.isRotating() && !suppressed)
       Thread.yield();  // wait till turn is complete or suppressed is called
   
   Motor.A.stop();
   Motor.C.stop();
}

完整程式碼如下:

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

public class HitWall implements Behavior {
  
 private TouchSensor touch;
   private UltrasonicSensor sonar;

   private boolean suppressed = false;
   
   public HitWall(SensorPort port )
   {
      sonar = new UltrasonicSensor( port );
   }

   public boolean takeControl() {
      return touch.isPressed() || sonar.getDistance() < 25;
   }

   public void suppress() {
      suppressed = true;
   }

   public void action() {
      suppressed = false;
      Motor.A.rotate(-180, true);
      Motor.C.rotate(-360, true);

      while( Motor.C.isMoving() && !suppressed )
        Thread.yield();

      Motor.A.stop();
      Motor.C.stop();
   }
}

完成了上述二個行為者程式後,我們要開始建立讓程式起頭的程式碼- main() method。在這個方法中我們會使用上述完成的二個行為者類別(class)來建立二個行為者物件,再將這二個物件加入陣列中,再將這個陣列當成參數傳給仲裁者物件:

import lejos.nxt.SensorPort;
import lejos.robotics.subsumption.Arbitrator;
import lejos.robotics.subsumption.Behavior;

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

我們已完成了我們機器人的所有程式碼了,現在它具備有: 前進、偵測(碰撞到物體或接近某物體)、後退轉彎、再前進的功能了。接下來要完成的第三個行為者顯得有些多餘,它主要的用意是讓你了解,當你要再幫機器人加入新功能時,在我們使用"Behavior(行為)控制模式"下是多麼簡單易行。在增加新功能時,我們不會去變動到先前完成的二個行為者的程式碼,在仲裁者主程式中也僅須多一個新行動者的物件,並把它也加入陣列中即可。新的行動者是用來監控電池使用狀況,在它低於某個水平時去播放一段音樂及顯示訊息在LCD上來提醒你。程式如下:

import lejos.nxt.Battery;
import lejos.nxt.Sound;
import lejos.robotics.subsumption.Behavior;

public class BatteryLow implements Behavior {
  private float LOW_LEVEL;
  private boolean suppressed = false;
  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() {
     suppressed = true;
  }

  public void action() {
     suppressed = false;
     play();
     System.exit(0);
  }

  public void play() {
     for(int i=0; i<note.length; i+=2) {
        final short w = note[i+1];
        Sound.playTone(note[i], w);
        Sound.pause(w*10);
        if (suppressed)
        return; // exit this method if suppress is called
     }
  }
}


再來我們看看在main 方法中要如何修改:

import lejos.nxt.SensorPort;
import lejos.robotics.subsumption.Arbitrator;
import lejos.robotics.subsumption.Behavior;

public class BumperCar {
  public static void main(String [] args) {
     Behavior b1 = new DriveForward();
     Behavior b2 = new BatteryLow(6.5f);
    
 Behavior b3 = new HitWall(SensorPort.S2);      Behavior [] bArray = {b1, b2, b3};
     Arbitrator arby = new Arbitrator(bArray);
     arby.start();
  }
}
你發現僅須多加一條指令外加修改一條原有的指令就完成了,cool!

提醒您: 在新行為中我們是去監控電壓值,這個值有可能會因為你使用的電池種類及使用的狀況而有所不同,所以在電壓監測值上請依你實際的狀況再作調整。

這個展示程式碼很漂亮的呈現行為控制模式的程式模式的實質好處。在加入新行為者功能時在整體程式的撰寫上是多麼的簡潔易懂。這要歸功於使用這種模式,完全發揮了物件導向優點,每個行為者是封裝得獨立、完整漂亮。

技巧提示: 在程式開發的過程中若你將所有行為者全部整合再來測試,可能在除錯的過程中會遇到困難,所以應該利用這個模式的優點,採個個擊破的方式來測試你的程式碼,也就是一次只測一個行為者,等所有行為者皆完成各別除錯任務後,再來整合所有行為者功能。

為了了解為何會建議使用這個設計模式(pattern),更深入了解仲裁者(arbitrator)這個類別是有需要的。仲裁者(arbitrator)類別中包含了一個監視(monitor)執行緒,它會循序的去呼叫個各行動者(behaviors),檢查它們的 takeControl()方法是否應該要把該行動者設定成為"執行中"(active)。呼叫的順序是依行動者(behaviors)陣列索引由大至小來進行。檢查過程中若發現有某個行動者的Take Control方法被觸動且它的執行優先順序高於目前正在執行中的行動者(behaviors),則仲裁者中的監視(monitor)會去執行suppress()方法來停止活動中的行動者,並重頭再一一檢查行動者(behaviors)陣列。仲裁者(arbitrator)主執行緒是很簡單的邏輯,它會去呼叫享有最高優先序的行動者(behaviors)的 Action() Method,然後將這個行動者(behaviors)變成啟動中(Active)。當行動者的 Action Method結束時(不管是這個方法執行完畢或是因為 monitor 去呼叫了行動者的 suppress 方法)這個行動者就不再是啟動中(active)。整個程式的執行邏輯就又回到了監視者(monitor)依序的去呼叫行動者陣列中的各個行動者(behaviors),檢查它們的 takeControl()方法是否應該要把該行動者設定成為"執行中"。


其中你會發現 Behavior.java 只是個 Interface 的宣告,繼承這個介面的程式一定得要Implement其中的三個方法(Method),分別是 takeControl()、action()、suppress()。至於整個行為模式的控制中心是在 Arbitrator.java 仲裁者)這支程式中。

沒有留言:

張貼留言