実験3 Boe-Botを思った場所に行かせる


ここで行うこと。

実験3-1 動作指令を変換する


前の実験では前後方向のみでしたが、この実験では全ての方向に移動できるようにします。ある量の回転するにはどのくらいのパルスを送るかを決めることによってさらに緻密な操縦ができるようになります。例えば、Boe-Botが正方形、三角形などを描くことができるようになります。

このレベルでのプログラムされた操縦性はとても良くなります。しかし、プログラムが長くなり、複雑な問題が生じてきます。C#ではプログラムメモリーの中に長い方向のリストを記録したりアクセスすることができます。これを単純にプログラムで行うと、方向を変えた時に不意に止まることがあります。このため、方向変換する時には減速し、方向を変えてから加速するコマンドが必要になります。この「ramping」処理(rampは傾斜の意味)は不意の停止を解決しサーボモータの寿命を長くします。
例えば車輪の円周は次のようになります

円周 = π × 車輪の直径
   = 3.141592 × 6.67cm ≒ 21cm

これで、一回転すると約21cm進むことが
分かります。
課題3-1-1

Boe-Botをいかに遠くまで移動させるかを決める一つの方法は、いかに速く移動できるかを決めることです。例えば、サーボモータが37.5rpm(revolution per minute)すなわち0.625回転/s で回っていたとすると、スピードは次のようになります。

21cm/回転×0.625回転/s = 13.125cm/s

50cm Boe-Botが移動するためにかかる時間は

ttravel=50cm ÷ 13.125cm/s ≒ 約3.81秒

車輪の回転速度を決定するのは難しいので、その代わりに移動距離を測定します。現実に起こる走行表面によるスリップも計算に入るのでこちらの方がいいかも知れません。次のプログラムでこれを行います。

実験2で作ったグラフより***のところに適切な数字を入れます。つまり、左右のサーボモータの前進のスピードが等しいと同様に後進のスピードも同じにしないと、旋回などが正常に動作しません。

車輪サーボモータの距離テストプログラム
namespace BasicWheelServoDistanceTest1
{
    public partial class Program
    {
        public void main()
        {
            BasicWheelServo leftWheel =
                new BasicWheelServo(
                  CPU.P13      // pin
                , ***         // forward
                , ***         // center
                , ***         // backward
                , 20000        // low
                );
            BasicWheelServo rightWheel =
              new BasicWheelServo(
                  CPU.P12      // pin
                , ***         // forward
                , ***         // center
                , ***         // backward
                , 20000        // low
                );

            leftWheel.move(100);
            rightWheel.move(100);

            CPU.delay(5000);   // 5秒間サーボを動かす

            leftWheel.stop();   // 車輪を止める
            rightWheel.stop();
        }
    }
}



BasicWheelServoDistanceTest1は5秒間前進します。測定は次のように行います。
  1. サーボモータへの電源は切っておき、BasicWheelServoDistanceTest1をBoe-Botにロードする。
  2. Boe-Botの位置にマーカーを置く。
  3. サーボモータの電源を入れる。
  4. 5秒間走ると止まります。
  5. メジャーで走行距離を測ります。
  6. 1cm当たりの時間を計算します。

Boe-Botが完全に直進しなくてもそのまま続けてください、大体真っ直ぐなら可です。サーボモータの設定でこれは修正できますが、大きな変更をする前にテストを終えてください。

プログラムは結果が安定するように、複数回データを取る必要がありますが。時間の節約のため1回のみとします。



課題3-1-2

BasicWheelServoDistanceTest1は前進の補正であるが、同様にして後進の補正を行いなさい。

実験3-2 操縦 - 回転する

Boe-Botは回転あるいは旋回することによって方向を変えます。回転は両輪を同じ方向に回転させますが、異なったスピードでサーボモータを回すことによって行います。回転は遅く動いている車輪の方向に行き、回転の直径は両輪のスピード差に基づきます。

旋回は両輪が逆方向に動く時に起こります。Boe-Botは両輪が同じスピードの時その場で旋回します。もし両輪のスピードが異なる時には回転と旋回の混合した動きになります。ここの目的としては、その場での旋回とします。

その場で旋回

課題3-2-1

  1. 実験3-1の車輪の回転方向を変えて45°の旋回になるように数字を決めなさい。

  2. 左回りに45°の旋回になるように数字を決めなさい。


旋回テストプログラム
/**
 * 旋回テストプログラム
 */

namespace BasicWheelServoPivotTest1
{
    public partial class Program
    {
        public void main()
        {
            BasicWheelServo leftWheel =
                new BasicWheelServo(
                  CPU.P13      // pin
                , ***         // forward
                , ***         // center
                , ***         // backward
                , 20000        // low
                );
            BasicWheelServo rightWheel =
                new BasicWheelServo(
                      CPU.P12   // pin
                    , ***      // forward
                    , ***      // center
                    , ***      // backward
                    , 20000     // low
                    );

            leftWheel.move(100);
            rightWheel.move(-100);
            CPU.delay(***);   // ***(ms)サーボを動かす

            leftWheel.stop();   // 車輪を止める
            rightWheel.stop();
        }
    }
}


回転させる

その場での旋回は手ごろですが、方向転換の方法としては必ずしも好ましいものではありません。特に、旋回は両輪を異なった方向に回転することが必要です。短い時間停止しなければBoe-Botは横滑りします。つまり急には変れないことを意味します。これに対して、回転はスピードの異なる同方向の動きを維持します。

左回転は右の車輪より左の車輪のスピードを遅くすることで行います。

課題3-2-2

  1. 回転スピードを変えて45°の回転になるように数字を決めなさい。

  2. 左回りに45°の回転になるように数字を決めなさい。
  3. 後ろ方向に回転するようにしなさい。(後ろの左右45°)

実験3-3 操縦 - 基本的なBoe-Botクラス

この点でのBoe-Botのコントロールは系統立てられています。個々の車輪のサーボモータのコントロールはBasicWheelServoクラスでカプセル化されています。このクラスを拡張して回転とか旋回のように動きを作るのが可能ですから、もしBoe-Botの動きをする他のクラスを定義するのならこれを使ったほうが簡単です。

新しいクラスの定義に入る前にどんなメソッドの組み合わせが使えるかを決める必要があります。決められた距離の前進と後進、左右の旋回、左右の回転、停止が必要になります。次のクラスがこれをします。

カプセル化 【encapsulation】
 オブジェクト指向プログラミングが持つ特徴の一つ。データとそれを操作する手続きを一体化して「オブジェクト」として定義し、オブジェクト内の細かい仕様や構造を外部から隠蔽すること。外部からは公開された手続きを利用することでしかデータを操作できないようにすることで、個々のオブジェクトの独立性が高まる。カプセル化を進めることによりオブジェクト内部の仕様変更が外部に影響しなくなり、ソフトウェアの保守性や開発効率が高まり、プログラムの部分的な再利用が容易になる。


Boe-Botの車輪コントロールインターフェース
/**
 * Boe-Botの車輪コントロールインターフェース
 *
 * Boe-Botの車輪コントロールクラスが備えていなければならない
 * メソッドを定義する
 */
namespace BoeBotLib
{
    public abstract class BoeBotInterface
    {
        protected eEvent startEvent;
        protected eEvent nextEvent = eEvent.nullEvent;
        protected eEvent oneTimeEvent = eEvent.nullEvent;

        public const int continuousForward = 32760;
        public const int continuousBackward = -32760;
        public const int continuousLeft = continuousForward;
        public const int continuousRight = continuousBackward;

        /**
         * 動作を開始した時に通知されるようにイベントを設定する
         *
         * 入力:Event event: イベント。nullに設定できる。
         */
        public BoeBotInterface(eEvent _event)
        {
            startEvent = eEvent.checkEvent(_event);
        }

        /**
         * 動作を終了するために現在のタスクを待機(wait)に設定する。
         *
         * 入力:int state: 次のタスク状態
         */

        public void wait(int state)
        {
            Task waitingTask = Task.getCurrentTask();
            waitingTask.nextState(state);

            if (waitingTask.taskStatus() == Task.taskRunning)
            {
                //System.out.println ( "BoeBotInterface" ) ;
                waitingTask.suspend();
                oneTimeEvent = waitingTask;
            }
        }

        /**
         * 動作を開始した時に通知されるようにイベントを設定する
         * 多分、null
         *
         * 入力:Event event: Event
         *
         * 戻り:直前のイベント
         */
        public eEvent setStartEvent(eEvent _event)
        {
            eEvent resultEvent = startEvent;
            startEvent = eEvent.checkEvent(_event);
            return resultEvent;
        }

        /**
         * 動作を開始した時に通知されるようにイベントを設定する
         * 多分、null
         *
         * 入力:Event event: Event
         *
         * 戻り:直前のイベント
         */
        public eEvent setNextEvent(eEvent _event)
        {
            eEvent resultEvent = nextEvent;
            nextEvent = eEvent.checkEvent(_event);

            if (movementDone())
            {
                causeNextEvent();
            }

            return resultEvent;
        }

        /**
         * 通常マッチング・マルチタスク・オブジェクトによって呼ばれる
         */
        protected void causeNextEvent()
        {
            // 次の動作が開始できるというイベントを通知する
            nextEvent.notify(this);

            // 次の動作が開始できるというイベントを通知する
            oneTimeEvent.notify(this);
            oneTimeEvent = eEvent.nullEvent;
        }

        /**
         * このメソッドは動作メソッドの始めに呼ばれるべきである。
         */
        protected void startMovement()
        {
            startEvent.notify(this);
        }

        /**
         * 動作が終わったかどうかチェックする。
         * trueを返すまで呼ばれるべきです。
         *
         * 戻り:もし動作の待機が終わったらtrueを返す
         */
        public abstract bool movementDone();

        /**
         * 動作を止める。
         */
        public virtual void stop()
        {
            move(0);
        }

        /**
         * 前進/後退(マイナスの値)するためにスピードを設定する。
         *
         * 入力:int cm:何センチ移動するか
         */
        public abstract void move(int cm);

        /**
         * 左旋回(プラス)右旋回(マイナス)するためにスピードを設定する。
         *
         * 入力:int steps:回転するステップの数
         */
        public abstract void pivot(int steps);
        /**
         * 左回転(プラス)右回転(マイナス)
         *
         * 入力:int staps:回転するためのステップ数
         */
        public abstract void turn(int steps);
    }
}


BoeBotInterfaceクラスで最初に注目することはabstract classです。これは抽象クラスと言い、オブジェクトを作ることはできませんが、抽象クラスでないクラスの基底クラス(スーパークラス)として使うことができることを意味します。2番目に気がつくことはeEventクラスの使用です。eEventクラスは比較的シンプルです。eEventオブジェクトと通信するのに用いられる「notify」(通知)と呼ばれるメソッドを持っています。この場合、3つのイベントがあります。: startEvent, nextEvent, oneTimeEvent 最初のは動作が始まった時に通知します。他の2つは動きが終了したときに通知します。これは、マルチタスク環境を作るためのイベントドリブン(駆動)システムにします。これらのイベントはこの後の実験で出てきます。

eEvent.nullEventは、通知された時に何もしないeEventクラスの唯一のインスタンスです。これは無意味に思えますが、nullの値をチェックせずに用いるnextEventのように参照できるようになります。他のイベントがこの変数によって通常参照されるから、nullよりもnullEventを用いるほうがより効果的です。notifyメソッドに応答することはいつも同じですから、1つのnullEventオブジェクトが必要になります。これは何もしません。

残りのBoeBotInterfaceクラスの定義を説明する前に、そのクラスの階層構造と後の実験で用いられるイベントを眺めてみましょう。始めにBoeBotInterfaceの階層を見てみましょう。

  BoeBotInterface
   BasicBoeBot
    RampingBoeBot
     WheelEncoderBoeBot


次に定義しますが、BasicBoeBotクラスは抽象クラスのBoeBotInterfaceを実装して、実際のオブジェクトを作れるようにします。この方法は、Boe-Botに使われるメソッドを決める簡単な方法です。

BasicBoeBotは前後の動作、旋回、回転をサポートします。これはサーボモータを急に動かしたり止めたりするので、追加のセンサーなどのモジュールを載せているような電池駆動での使用には向きません。

RampingBoeBotBasicBoeBotの上に作られます。急激なスピードの変化をなくし、スピードを滑らかに増減します。これは動作の開始停止時起こる電源のサージを最小化し電池駆動できるようにします。

WheelEncoderBoeBotRampingBoeBotへ閉じたフィードバックループを付け加えます。各々の車輪の内側で反射光の変化を見つけるためのIR検出器(赤外線検出器)を利用して車輪エンコーダとして用います。これは同じ時間内で各々の車輪の変化の数を確かめることによってBoe-Botを直進させることができます。

この実験ではBasicBoeBotRampingBoeBotについて行います。

Boe-Botをコントロールするための多くの仕事はmovementDoneメソッドで行われます。オブジェクトがサーボモータの行程を保つように繰り返し呼ばれなければなりません。サーボモータは実際に連続的に動きますが、もしBoe-Botが一方向に連続的に走るのではなければ、止めるかスピードを変える必要がでてきます。

movementDoneメソッドは色々の方法でポーリングされることができます。一番簡単な方法は、BoeBotオブジェクトを作るプログラム部分を持つことですが、むしろこれは長くなります。その代わりに2つの別の手があります。1つはシングルタスクを仮定することで、コントロールが呼んだプログラムに戻る前に各々の動作が完結している固定的な動作モードの運用です。2番目はBoe-Botが動いている間に他の事を呼んだプログラムができるようになるマルチタスク環境でのタスクを用いることです。これはマルチタスクで必要になるセンサーのサポートも含めることができます。マルチタスクについては次の実験で行いますが、センサーについてはさらにその後になります。

コントロールオブジェクトはBoeBotInterfaceクラス階層の中でコンストラクタにパラメータ(引数)として要求されます。このコントロールオブジェクトはそのイベントクラスに基づいており、オブジェクトが作られた時にstartEventに割り当てられます。これはオブジェクトを変更することができますが、一般的にはBoeBotオブジェクトが作られた時に一度だけ設定されます。そのコントロールオブジェクトの階層構造は下のようになります。

  eEvent
   FixedMovementBoeBot
    Task
     MultitaskingBoeBot


一般的に、固定した動作のコントロールオブジェクトのBasicBoeBotの作成は次のようになります。

BoebotInterface jbot = new BasicBoeBot ( new FixedMovementBoeBot ());
jbotBoebotInterfaceの型であることに注意してください。これはBoebotInterfaceBasicBoeBotの基底クラス(スーパークラス)だからこのようにできます。これを多態性といいます。BasicBoeBotは基底クラス(スーパークラス)としてBoebotInterfaceを持っているRampingBoeBotのようなクラスに置き換えることができます。movementDoneメソッドがjbotによって参照されるオブジェクトを作るプログラムによって明示的に呼ばれるならば、コンストラクタへのパラメータはeEvent.nullEventにすることができます。

FixedMovementBoeBotはこの実験で用います。MultitaskingBoeBotクラスは次の実験で定義します。

BoebotInterface抽象クラスに対する動作メソッドは移動、旋回、回転を持っています。各々1つの符号付整数のパラメータを持っています。正数値は前進、旋回と回転では左を意味します。負数値はその逆になります。ゼロの値はBoe-Botを停止させます。これはstopメソッドと同じです。

連続動作を開始させることも可能です。Boe-Botは新しい動作が指令されるまで指定された動作を行います。このモードは方向転換が必要な時かどうかをセンサーで決める時に用いられます。

abstractが付いている移動、旋回、回転のような動きのメソッドはサブクラスで定義されなければいけなく、3つのイベント、startEvent, nextEvent, oneTimeEventを考慮に入れる必要があります。startMovementメソッドはstartEventを通知(notify)します。全ての動作設定が完了した後で呼ばれなければいけません。普通、イベントは繰り返しmovementDoneを呼びます。startEventはコンストラクタの中で設定されますが、setStartEventを用いて変更することができます。このメソッドは直前のイベント参照を返します。nullの引数はイベントにeEvent.nullEventを設定します。

一度動作が終了したら、causeNextEventメソッドを呼ばなければいけません。このメソッドはnextEventoneTimeEventを通知します。前者はsetNextEventで設定されます。このイベントはいつも用いられることを意図しています。oneTimeEventwaitメソッドを用いて設定され、イベントが通知された後にリセットされます。そのwaitメソッドはシステムがマルチタスク環境を用いていることを仮定しています。waitメソッドはタスクから呼ばれなければなりません。イベント待ちについては次の実験のマルチタスクでさらに詳細に述べます。

BasicBoeBotクラスはBoeBotInterfaceを拡張して次のように定義されます。BasicBoeBotクラスはその基底クラス(スーパークラス)の全ての抽象クラスを実装してオブジェクトが作られます。これには2つのコンストラクタがあります。1つはデフォールトのサーボモータのオブジェクトを用いていて、もう1つはその代わりにユーザ定義したオブジェクトを用います。両方ともstartEventとして用いられるイベントを必要とします。

イベントドリブン 【event driven】
 イベント駆動ともいう。

 ユーザや他のプログラムが実行した操作(イベント)に対応して処理を行なう、プログラムの実行形式。

 ユーザが操作を行っていないときはプログラムは何もせず待機しているため、ユーザはそのプログラムを待たせた状態で他の操作を行なうことができる。

 イベントドリブンで動作するプログラムは必要以上にユーザを拘束しないため、マルチタスクOSとの親和性が高く、グラフィカルユーザインターフェースを持ったプログラムではイベントドリブン方式が広く採用されている。

 これに対し、コマンドライン入力で起動するプログラムはマルチタスク性をあまり意識する必要がないことが多く、プログラムが逐次ユーザに操作を要求するタイプのプログラムが主流である。
サージ【surge】
 日本では、100Vの交流電力が各家庭やオフィスに供給されるが、落雷などにより瞬間的に高電圧の電流が流れることがある。ごく短時間とはいえ、場合によって電圧は数万ボルトに達する場合もある。一般に定格以上の電圧がかかる電源異常は過電圧と呼ばれるが、その持続時間によって、スパイク(ナノ秒~マイクロナノ秒)とサージ(ミリ秒単位)に分類される。
 ここでは、交流ではなく直流の異常過電流のことをあらわしている。
ポーリング【Polling】
非同期に発生するイベントをチェックするための手法の1つ。プログラムや通信システムがこうした方式を利用する。

単純にいうと、何らかの変化が発生したかどうかをリモートシステムに確認しにいく、という方法である。たとえば、3つのスイッチを監視しているプログラムがあり、どれかのスイッチがオンになったらそれに対応する処理をするという例を考えよう。このとき、プログラムが無限ループを構成し、このループの中でスイッチがオンになっているかどうかをチェックするという構造が考えられる。これがポーリングである。逆に、スイッチがインテリジェントな構造になっており、オンになったときにプログラムに通知を送ることができるなら、ポーリングではなく単に通知が届くのを待つ、というプログラムにすることもできる。周辺機器からの入力を受け取るためにI/Oポートをチェックする場合などにも用いられる。

ポーリングは一見原始的な手法のようにも思えるが、変化を通知する機構がない場合にはよく使われる。新しいメールがメールサーバに届いているかどうか毎日チェックする、といった処理もポーリングだと考えられる。


基本的なBoe-Bot車輪コントロールクラス
using System;
using Microsoft.SPOT; // Smart Personal Objects Technology 
using System.Threading;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.NetduinoMini;
using BoeBotLib;

namespace BoeBotLib
{
    /**
     * 基本的なBoe-Bot車輪コントロールクラス
     * 
     * 車輪サーボモータの自由走行に対するPWMサポートを扱う
     * move(), pivot() or turn()を用いて動作を開始させる。
     * stop() or move(0)でBoe-Botを停止させる。
     */

    public class BasicBoeBot : BoeBotInterface
    {
        public BasicWheelServo leftWheel;
        public BasicWheelServo rightWheel;
        public _Timer timer;
        public int timeout;
        public int msecPerCm;
        public int msecPerPivot;
        public int msecPerTurn;

        protected const int movementNone = 0;
        protected const int movementMove = 1;
        protected const int movementPivot = 2;
        protected const int movementTurn = 3;

        protected int leftMovementSpeed;
        protected int rightMovementSpeed;
        protected int msecPerStep;
        protected bool Done;
        protected new bool wait;

        /**
         * 一般的な動きに対する車輪サーボモータを設定する。
         * 前進はcmを単位にする。
         * 旋回と回転は普通45゜ステップを単位にする。
         * デフォールトのサーボモータの設定を用いる。
         */

        public BasicBoeBot
            (
              eEvent _event
            , int msecPerCm
            , int msecPerPivot
            , int msecPerTurn
            , BasicWheelServo leftWheel
            , BasicWheelServo rightWheel
            )
            : base(_event)
        {
            BasicBot(msecPerCm, msecPerPivot, msecPerTurn, leftWheel, rightWheel);
        }

        /**
         * 一般的な動きに対する車輪サーボモータを設定する。
         * 前進はcmを単位にする。
         * 旋回と回転は普通45゜ステップを単位にする。
         *
         * 入力:int msecPerCm: 直進の時の1cmあたりのミリ秒
         * 入力:int msecPerPivot: 旋回単位あたりのミリ秒
         * 入力:int msecPerTurn: 回転単位あたりのミリ秒
         * 入力:BasicWheelServo leftWheel:左車輪に対するBasicWheelServo
         * 入力:BasicWheelServo rightWheel:右車輪に対するBasicWheelServo
         */

        public void BasicBot
            (
              int msecPerCm
            , int msecPerPivot
            , int msecPerTurn
            , BasicWheelServo leftWheel
            , BasicWheelServo rightWheel
            )
        {
            this.msecPerCm = msecPerCm;
            this.msecPerPivot = msecPerPivot;
            this.msecPerTurn = msecPerTurn;
            this.leftWheel = leftWheel;
            this.rightWheel = rightWheel;
            timer = new _Timer();
        }

        public BasicBoeBot(eEvent _event)
            : base(_event)
        {
            BasicBot(
                       ***   // 前進
                     , ***   // 旋回
                     , ***   // 回転
                     , new BasicWheelServo(
                           CPU.P13   // pin
                         , ***         // forward
                         , ***         // center
                         , ***         // backward
                         , 20000        // low
                         )
                     , new BasicWheelServo(
                           CPU.P12   // pin
                         , ***         // forward
                         , ***         // center
                         , ***         // backward
                         , 20000        // low
                         )
                     );
        }

        /**
         * 動作が完了したかどうかチェックする。
         * これは戻り値がtrueを返すまで呼ぶ。
         *
         * 戻り:もし動作が待ち状態を完了した時にtrueを返す。
         * BoeBotInterface に public abstract bool movementDone () ; がある
         */
        public override bool movementDone()
        {
            return timer.timeout ( timeout ) ;
            //return Done;
        }

        /**
         * 前進する車輪スピードを設定する。
         *
         * 入力:int steps: 何センチ進むか
         */
        public override void move(int steps)
        {
            movement(movementMove, steps);
        }

        /**
         * 左旋回(正数)あるいは右旋回(負数)に対する車輪スピードを設定する。
         *
         * 入力:int steps: 回転のステップ数
         */
        public override void pivot(int steps)
        {
            movement(movementPivot, steps);
        }

        /**
         * 左回転(正数)あるいは右回転(負数)に対する車輪スピードを設定する。
         *
         * 入力:int steps: 回転のステップ数
         */
        public override void turn(int steps)
        {
            movement(movementTurn, steps);
        }

        /**
         * 動作に対する車輪スピードを得る。
         * msecPerStep, leftMovementSpeed, rightMovementSpeedを設定する
         *
         * 入力:int movement: 動作のタイプ
         * 入力:int steps: 回転のステップ数
         */
        protected virtual void getMovementSpeed(int movement, int steps)
        {
            switch (movement)
            {
                default:
                case movementMove:
                    msecPerStep = msecPerCm;
// 注意:前進、後退、旋回、回転のスピードは固定になっています。
                    if (steps > 0)
                    {
                        leftMovementSpeed = 100;
                        rightMovementSpeed = 100;
                    }
                    else
                    {
                        leftMovementSpeed = -100;
                        rightMovementSpeed = -100;
                    }
                    break;

                case movementPivot:
                    msecPerStep = msecPerPivot;

                    if (steps > 0)
                    {
                        leftMovementSpeed = -100;
                        rightMovementSpeed = 100;
                    }
                    else
                    {
                        leftMovementSpeed = 100;
                        rightMovementSpeed = -100;
                    }
                    break;

                case movementTurn:
                    msecPerStep = msecPerTurn;

                    if (steps > 0)
                    {
                        leftMovementSpeed = 25;
                        rightMovementSpeed = 100;
                    }
                    else
                    {
                        leftMovementSpeed = 100;
                        rightMovementSpeed = 25;
                    }
                    break;
            }

            // Compute timeout when appropriate
            if ((steps != continuousForward)
                 || (steps != continuousBackward)
                 || (steps != 0))
            {
                timeout = msecPerStep * 10000* ((steps > 0) ? steps : -steps);
            }
        }

        /**
         * 動作に対する車輪スピードを設定する
         * 正数に対しては動作1を用いる
         * 負数に対しては動作2を用いる
         *
         * 入力:int movement: 動作タイプ
         * 入力:int steps: 動くステップ数
         */
        protected void movement(int movement, int steps)
        {
            // 引数を設定する
            getMovementSpeed(movement, steps);

            switch (steps)
            {
                case 0:
                    setSpeed(0, 0);
                    break;

                case continuousForward:
                case continuousBackward:
                    setSpeed(leftMovementSpeed, rightMovementSpeed);
                    break;

                default:
                    timer.mark () ;
                    setSpeed(leftMovementSpeed, rightMovementSpeed);
                    startMovement();
                    break;
            }
        }

        /**
         * 両輪に対するスピードを設定する。
         * 設定はパーセントである。
         * 正数は前進回転
         * 負数は後進回転
         *
         * 入力:int left: 左車輪に対するスピード
         * 入力:int right: 右車輪に対するスピード
         */
        protected virtual void setSpeed(int left, int right)
        {
            // Set real speed
            leftWheel.move(left);
            rightWheel.move(right);
        }
        public override void stop()
        {
            // Set real speed
            leftWheel.move(0);
            rightWheel.move(0);
        }
    }
}


プログラムの始めに変数の宣言があります。これらはpublicになっていますが、protectedの方がいいかもしません。

これらの変数は両輪のオブジェクト参照とTimerオブジェクトを含んでいます。以前のプログラムではタイミングはCPU.delayで行っていました。BasicBoeBotの場合では、タイミングは_Timerオブジェクトを用いてなされます。timeout変数は動作の持続時間を保持し、_Timer.timeoutメソッドと一緒に用いられます。

3つの変数:msecPerCm, msecPerPivot, msecPerTurnはこの実験の前のほうで行った実験に基づいています。例えばBasicWheelServoDistanceTest1.csプログラムは1cm移動する時間遅延を確かめるのに用いました。 この値は***(個々に異なる)ですが、これはCPU.delayが用いている100μsの単位です。_Timerは1msタイムアウトメソッドなので、10で割る必要があります。この値の整数型なので四捨五入などを行う必要があります。

旋回と回転の値は45゜ステップに設計されています。Boe-Botは細かい傾斜でプログラムできますが、その精度と信頼性はフィードバックの欠落のため制限されています。従って、45゜ステップは妥当な値です。

2つのコンストラクタの定義があります。最初のは、2番めのを用いて前もって定義された定数を用いてBasicWheelServoオブジェクトを割り当てます。これは1つのBoe-Botに対しては十分でしょう。2番めの定義はもしそのクラスが定数が僅かに異なっているような複数のBoe-Botに用いる場合に使われます。この場合、オブジェクトはBasicBoeBotクラスを修正する事無しに作ることができます。

次はpublicなメソッドの定義になります。その次はprotectedなメソッドの定義です。

movementDoneメソッドは動作を停止する時刻かどうかを決めるために_Timerオブジェクトをチェックします。もし停止の時だったらtrueの値を返します。

CPU.delayの代わりに何故これらのメソッドを使うのでしょうか? それには2つの理由があります。最初に、動作のタイミングを集中管理できること。2番めにこのタイプのポーリングアーキテクチャーは次の実験で説明するマルチタスクシステムで相性がよく合っています。マルチタスクシステムはタスクをポーリングします。車輪をコントロールするタスクはこの時点でmovementDoneメソッドを用いることができます。

性能に関しては、TimerCPU.delayを用いることはプログラムは何もしていないから同等です。事実、車輪の動作は両輪に対してPWMオブジェクトのバックグラウンド運転によってコントロールされています。

次のプログラムはBasicBoeBotクラスを使えるようにしました。このプログラムはBasicBoeBotで定義されているいくつかのメソッドのみを利用しているだけですが、非常に短くなっています。
main.csとします。

BasicBoeBotクラスをテストする
using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.NetduinoMini;
using BoeBotLib;

namespace BasicBoeBotTest1
{
    public partial class Program
    {
        public void main()
        {
            BasicBoeBot jbot = new BasicBoeBot(null);

            jbot.move(10);
            while (!jbot.movementDone()) ;

            jbot.pivot(-2);
            while (!jbot.movementDone()) ;

            jbot.turn(2);
            while (!jbot.movementDone()) ;

            jbot.stop();
            while (!jbot.movementDone()) ;
        }
    }
}
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 名前空間BasicBoeBotTest1
 9   
10 Programクラスの部分クラス
11 
12 Program.csから呼ばれる
13 
14 BasicBoeBotオブジェクトを作る
15
16 10cm前進
17 前進動作が終了するのを待つ
18
19 右に90°旋回
20 旋回動作が終了するのを待つ
21
22 左に90°回転
23 回転動作が終了するのを待つ
24
25 停止する
26 停止動作が終了するのを待つ
27
28
29


メインメソッドでは3つの動作が行われます。最初はBoe-Botは10cm前進し、90゜右に旋回し、左に90゜回転します。各々の動作呼び出しの後でmovementDoneが呼ばれるのに注意してください。これはstartEventが無いため必要です。BasicBoeBotコンストラクタのnullの引数はstartEventeEvent.nullEventを設定しています。

次のeEventクラスをeEvent.csとして入れてください。

eEvent クラス
using System;
/**
 * イベントが生じたことを示すためのインターフェース
 */
namespace BoeBotLib
{
    public class eEvent
    {
        public static readonly eEvent nullEvent = new eEvent();

        /**
         * null eventを作る。eEvent.nullEventを用いること。
         */
        protected eEvent()
        {

        }

        /**
         * イベント参照がnullかどうかチェックする。
         *
         * event:  チェックするevent
         *
         * returns:  もしnullだったらnullEventを返し、それ以外はeventを返す。
         */
        static public eEvent checkEvent(eEvent _event)
        {
            return (_event == null) ? nullEvent : _event;
        }

        /**
         * イベントを生じさせる。
         */
        public virtual void notify()
        {
            notify(null);
        }

        /**
         * イベントを生じさせる。
         */
        public virtual void notify(Object _object)
        {
        }

        /**
         * イベントの名前を得る。
         *
         * return: イベント名("Event")
         */
        public virtual String name()
        {
            return "Event";
        }
    }
}


次のTaskクラスのファイルをTask.csとして入れてください。

Taskクラス
/*
 * 協調的マルチタスク状態遷移マシンオペレーティングシステム
 * これはラウンドロビンタスクスケジューリングを用いた非常に
 * 基本的なOSです。
 * タスクリストは効率性の理由から循環的にリンクしています。
 *
 * 注意: もしTaskStatusクラスが用いないで、数バイトを付け加えれば
 *     コンストラクタの参照でtaskStatusListとnextTaskStatus
 *    を削除可能になる。
 */
using System;
using Microsoft.SPOT;
/**
 * フリーサイクルのときはいつでもタスクが走ります。
 * 注意: 外部タスクを終了させる良い方法はtaskToAbort.nextState(abort)
 * を用いることです。代替手段はtaskToAbort.stop()ですが、これはメモリ
 * の掃除をしません。
 */
namespace BoeBotLib
{
    public abstract class Task : eEvent
    {
        /**
         * taskState() result
         */
        public const int taskRunning = 0;

        /**
         * taskState() result
         */
        public const int taskSuspended = 1;

        /**
         * taskState() result
         */
        public const int taskSemaphoreWait = 2;

        /**
         * Initial value of state in execute()
         */
        public const int initialState = 0;
        public const int checkTimer = -1; // sleep()のサポート用
        public const int _interrupt = -2; // InterruptTaskを見よ
        public const int terminate = -3;
        public const int stopped = -4;

        /**
         * sleep()要求によるタスクを扱うためのTimerオブジェクト
         */
        public _Timer timer = new _Timer();

        /**
         * 次のアクティブタスクは循環リストの中
         */
        protected static Task taskStatusList = null;

        /**
         * taskStatusListに対するリンクされたリスト
         */
        protected Task nextTaskStatus = null;

        /**
         * taskState()によって戻される値
         */
        public int _taskStatus;

        /**
         * 循環リスト上の次のアクティブリスト
         */
        public Task nextTask = null;

        /**
         * execute()の中で用いられる状態変数
         */
        protected int state = initialState;

        /**
         * タイムアウトが起こった後の次の状態
         */
        protected int sleepState = checkTimer;

        /**
         * Timout値
         */
        protected int hi, lo;

        /**
         * タスクのセットアップ(設定)と開始
         */
        public Task()
        {
            addTask(this);

            // タスクをステータスリストに加える
            nextTaskStatus = taskStatusList;
            taskStatusList = this;
        }

        /**
         * タスク名を得る。
         *
         * 戻り: String の"Task"
         */
        public override String name()
        {
            return "Task";
        }

        /**
         * タスクのステータスを得る
         *
         * 戻り: タスクのステータス
         *        (taskRunning, taskSuspended, taskSemaphoreWait)
         */
        public int taskStatus()
        {
            return _taskStatus;
        }

        /**
         * 現在のタスクの状態を得る
         *
         * 戻り: int タスクの状態
         */
        public int getState()
        {
            return state;
        }

        /**
         * もし、タスクが実行中ならばタスクマネージャーのタスクリスト
         * からタスクを削除する。
         * 任意のタスクから呼ぶことが出来る。
         *
         * 戻り: boolean タスクがリストから削除されたときにtrue
         */
        public bool suspend()
        {
            if (_taskStatus == taskRunning)
            {
                // 注意: nextTaskがnullでなかったらマネージャーはnull
                // であってはいけない
                _taskStatus = taskSuspended;
                return removeTask(this);
            }
            return false;
        }

        /**
         * 停止するためにタスクを削除しstopの状態に設定する
         * 戻り: boolean  停止に成功したらtrue
         */
        public bool stop()
        {
            if (suspend())
            {
                state = stopped;
                return true;
            }

            return false;
        }

        /**
         * もしタスクが実行していなかったならば、タスクマネージャーの
         * リストにタスクを戻して加える。
         * 任意のタスクから呼ぶことができるが、そのタスクはマネージャーを
         * 持っていなければならない
         *
         */
        public bool resume()
        {
            if (_taskStatus == taskSuspended)
            {
                // 注意: マネージャーはnextTaskがnullでなかったならばnullで
                //       であってはいけない
                addTask(this);
                return true;
            }
            return false;
        }

        /**
         * resume()と同じ
         */
        public bool start()
        {
            return resume();
        }

        /**
         * resume()と同じ
        */
        public override void notify(Object _object)
        {
            resume();
        }

        /**
         * 次のexecute()呼び出しに対して状態を設定する
         *
         */
        public void nextState(int state)
        {
            this.state = state;
        }

        /**
         * 次のexecute()呼び出しに対して状態を設定する
         * タスクをSuspendする。他のタスクによってresumeされる
         */
        public void sleep(int state)
        {
            this.state = state;
            suspend();
        }

        /**
         * poll timerを仕掛けて設定単位時間を経過後に次の状態を続ける
         *
         * 入力: int nextState タイムアウトが起こったときに入る状態
         * 入力: int hi タイムアウト値。詳細はTimer.timeout(hi,lo)を見よ     * 入力: int lo  タイムアウト値
         */
        public void sleep(int nextState, int hi, int lo)
        {
            this.hi = hi;
            this.lo = lo;
            timer.mark();
            sleepState = nextState;
            state = checkTimer;
        }

        /**
         * poll timerを仕掛けて設定単位時間を経過後に次の状態を続ける
         * 入力: int nextState  タイムアウトが起こったときに入る状態
         * 入力: int msec ミリ秒単位でのタイムアウト値
         */
        public void sleep(int nextState, int timeMS)
        {
            //sleep ( nextState, timeMS/569, (timeMS%569)*119) ;
            this.hi = timeMS*10000; // tickの単位は0.1μ秒
            timer.mark();
            sleepState = nextState;
            state = checkTimer;
        }

        /**
         * poll timerを仕掛けて設定単位時間を経過後に次の状態を続ける
         * 入力: int nextState  タイムアウトが起こったときに入る状態
         * 入力: int timeS 秒単位でのタイムアウト値
         */
        public void sleepSec(int nextState, int timeS)
        {
            sleep(nextState, (timeS * 18) / 10, 0);
        }


        /**
         * 1期間タスクを実行する。全てのサブクラスはこのメソッドで
         * 実装されなければいけない
         * GUIがアイドルのときはいつでもアイドルタスクが実行される。
         * execute()の実装はインターフェースが応答可能にするために
         * 100ms以上かかってはいけない。
         */
        protected abstract void execute();


        // タスクマネージャーサポート

        /**
         * クラス変数: タスクリストの現在のタスク
         */
        static protected Task taskList = null;

        /**
         * 現在のアクティブタスク
         */
        public static Task currentTask = null;

        /**
         * 現在実行しているタスクオブジェクトを得る
         *
         * 戻り:Task 現在実行しているタスク
         */
        static public Task getCurrentTask()
        {
            return currentTask;
        }

        /**
         * タスクリストにタスクを加える
         *
         * 入力:Task aNewTask: タスクリストに加えるタスク
         */
        public static void addTask(Task aNewTask)
        {
            if (taskList == null)
            {
                aNewTask.nextTask = aNewTask;
            }
            else
            {
                aNewTask.nextTask = taskList.nextTask;
                taskList.nextTask = aNewTask;
            }

            aNewTask._taskStatus = taskRunning;
            taskList = aNewTask;
        }

        /**
         * タスクリストからタスクを削除する
         *
         * 入力:aTaskToRemove:タスクリストから削除するタスク
         *
         * 戻り:boolean リストからタスクが削除できたらtrue
         */
        public static bool removeTask(Task aTaskToRemove)
        {
            // 空のリストを始めに処理する

            if (taskList != null)
            {
                // 現在のタスクからリストを走査し見つかったらそれを削除する

                Task scanTask = taskList;

                do
                {
                    if (scanTask.nextTask == aTaskToRemove)
                    {
                        // 削除するタスクが見つかった

                        if (scanTask == aTaskToRemove)
                        {
                            // リストの中の一つだけのタスクを削除する
                            taskList = null;
                        }
                        else
                        {
                            // 現在のタスクが削除されたかどうかチェックする

                            if (taskList == aTaskToRemove)
                            {
                                taskList = aTaskToRemove.nextTask;
                            }

                            scanTask.nextTask = aTaskToRemove.nextTask;
                        }

                        aTaskToRemove.nextTask = null;
                        return true;
                    }

                    // リストの中の次のタスクをチェックする
                    scanTask = scanTask.nextTask;
                }
                while (scanTask != taskList); //全てのリストが走査されたら脱出
            }

            return false;
        }

        /**
         * ラウンドロビン法でタスクリストの中の各々のタスクを
         * 永久に実行する。実行するタスクが無くなった時に戻る
         */
        public static void TaskManager()
        {
            while (taskList != null)
            {
                // execute()がremoveTaskを通して変えることができるように
                // taskListを変える
                currentTask = taskList;
                taskList = taskList.nextTask;
                if (currentTask.state == checkTimer)
                {
                    // タスクがタイムアウトを待機している
                    if (currentTask.timer.timeout(currentTask.hi, currentTask.lo))
                    {
                        // タイムアウトが起こったら、次の状態を設定する
                        currentTask.state = currentTask.sleepState;

                        // もし、次の状態が不適切に設定されていたらば抜け出す
                        if (currentTask.state == checkTimer)
                        {
                            currentTask.state = terminate;
                        }
                    }
                }
                else
                {
                    // 次の状態を呼び出す
                    currentTask.execute();
                }
            }
        }
    }
}


課題3-3

  1. 正しく動作するか確かめるためにBasicBoeBotの全ての動作メソッドを試しなさい。(前後、左右の旋回、前後左右の回転)

  2. Boe-Botを10×15cmの四角で動かしなさい。次に50×75cmの四角でも動かしてみなさい。

  3. Boe-Botを円で一周動かしなさい。ヒント:turnを8回行う。

実験3-4 操縦 - 固定動作クラス

新しいプロジェクトBasicBoeBotTest2を作り、Program.csを次のリストに置き換えてください。

BasicBoeBotTest2
using System;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.NetduinoMini;
using BoeBotLib;

namespace BasicBoeBotTest2
{
    public partial class Program
    {
        public static void Main()
        {
            new Program().main();
        }
    }
}

BasicBoeBotは扱いやすいですが、テストプログラムは各々のアクションで複数のメソッドが必要になります。これらはmovementDoneメソッドを取り除けば半分になります。完全に新しい実装を持ってくる代わりにBasicBoeBotクラスのコンストラクタに異なったオブジェクトを単純に送ります。次のFixedMovementBoeBotクラスの定義はmovementDoneの呼び出しの代わりをします。

次のFixedMovementBoeBotクラスをFixedMovementBoeBot.csとして入れてください。

固定動作に対する車輪コントロールクラス
using System;

/**
 * 固定動作に対する車輪コントロールクラス
 *
 * 動作コントロールのためにBasicBoeBotクラスと組み合わせる
 */
namespace BoeBotLib
{
    public class FixedMovementBoeBot : eEvent
    {
        protected bool idle = true;

        /**
         * 動作が開始された時に呼ばれる
         */
        public override void notify(Object jbot)
        {
            if (idle && (jbot != null))
            {
                idle = false;
                while (!((BoeBotInterface)jbot).movementDone())
                {
                }
                idle = true;
            }
        }
    }
}


FixedMovementBoeBotクラスはイベントクラスを元にしています。これはnotifyメソッドを再定義する必要があります。 メソッドに送られるオブジェクトはBoeBotの動作をコントロールするBoeBotInterfaceでしょう。それはnullでは無いのでチェックが行われます。もし、参照がnullだったらそのメソッドは何もしません。そうでなければ、movementDoneがtrueを返すまで待ちます。

その結果、BoeBotInterfaceのmove,pivot,turnメソッドの呼び出しは、その動作が完了するまで戻りません。次のプログラムは新しいクラス定義の練習です。
次のリストをmain.csとして入れてください。

FixedMovementBoeBotクラスをテストする
using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.NetduinoMini;
using BoeBotLib;

namespace BasicBoeBotTest2
{
    public partial class Program
    {
        public void main()
        {
            BasicBoeBot jbot = new BasicBoeBot(new FixedMovementBoeBot());

            jbot.move(10);  // forward
            jbot.pivot(-2); // pivot right
            jbot.turn(2);   // turn left
            jbot.stop();
        }
    }
}


BasicBoeBotTest2のプログラムは以前のテストプログラムに比べてかなり短くなっています。また、読みやすくもなっています。jbot.moveのようにjbotの動作の呼び出しはFixedMovementBoeBotオブジェクトの使用のため動作が完了するまで戻りません。

実験3-5 操縦 - Ramping Servo クラス(BasicBoeBotTest3)

Rampingは急にサーボモータを逆方向に回す代わりにだんだんとスピードを変えていく方法です。このテクニックは電池とサーボモータの両方の寿命を長くします。Rampingを使わないとパワー0%から100%まで一気に行きます。これを車で行うと、滑った跡が残ります。Boe-Botは滑った跡は残しませんが予測できないおかしな動きになります。

Rampingでプログラムする

RampingBoeBotクラスはは内部的な利用のためいくつかのprotectedメソッドを付け加えますが、動作メソッドの付加的な変更はありません。その代わり、両輪の現在のスピードと望まれるスピードの間にある動作の増加をどのようにするかを決定します。これは前進の後すぐに後進ができます。この場合、Boe-Botは前進方向で停止するようにスピードを落とし、後進方向にフルスピードで走るまで加速します。

下に示すように、RampingはRamping期間にスピードダウンやスピードアップをするから同じ距離を移動するのに僅かに長くかかります。


Rampingの実装でのフルスピードの動作はBoe-BotはやはりRamping期間の間動いているから、Ramping無しのフルスピードより僅かに短くなります。Rampingは任意の数のステップを用いることができますが、非常に多くすると長い時間がかかるかNetduino miniの性能制限よりステップを短くしなければならなくなります。

BasicBoeBotTest3プロジェクトを作り、Program.csを次のリストに置き換えます。

BasicBoeBotTest3
using System;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.NetduinoMini;
using BoeBotLib;

namespace BasicBoeBotTest3
{
    public partial class Program
    {
        public static void Main()
        {
            new Program().main();
        }
    }
}

次のクラス定義はRampingをサポートします。

Rampingを用いて固定動作をする車輪コントロールクラス
using BoeBotLib;
/**
 * Rampingを用いて固定動作をする車輪コントロールクラス
 * 
 * 動作コントロールに対するBasicBoeBotクラスを用いる
 */

public class RampingBoeBot : BasicBoeBot {
  private const int rampTimeout = 5 ; // milliseconds
  private const int maxRampCount = 3 ;

  protected bool decelerate = false ;
  protected int rampCount = 0 ;
  protected int rampLeftStep ;
  protected int rampRightStep ;

  protected int leftSpeed = 0 ;
  protected int rightSpeed = 0 ;

  protected int currentMovement = movementNone ;
  protected int currentSteps ;
  protected int nextMovement    = movementNone ;
  protected int nextSteps ;

  /**
   * 一般的な動作のために車輪サーボを設定する
   * 前進はcmを単位にする。
   * 旋回と回転は普通45゜ステップを単位にする。
   * デフォールトのサーボモータの設定を用いる。
   *
   * 入力:Event event:動作が開始した時通知するイベント。nullにできる
   */
  public RampingBoeBot ( eEvent _event ) : base(_event)
  {
  }

  /**
   * 一般的な動きに対する車輪サーボモータを設定する。
   * 前進はcmを単位にする。
   * 旋回と回転は普通45゜ステップを単位にする。
   * デフォールトのサーボモータの設定を用いる。
   *
   * 入力:Event event:動作が開始した時通知するイベント。nullにできる
   * 入力:int msecPerCm: 直進の時の1cmあたりのミリ秒
   * 入力:int msecPerPivot: 旋回単位あたりのミリ秒
   * 入力:int msecPerTurn: 回転単位あたりのミリ秒
   * 入力:BasicWheelServo leftWheel:左車輪に対するBasicWheelServo
   * 入力:BasicWheelServo rightWheel:右車輪に対するBasicWheelServo
   */
  public RampingBoeBot
    ( eEvent _event
    , int msecPerCm
    , int msecPerPivot
    , int msecPerTurn
    , BasicWheelServo leftWheel
    , BasicWheelServo rightWheel
    ) :
    base ( _event, msecPerCm, msecPerPivot, msecPerTurn, leftWheel, rightWheel ) 
  {
  }

  /**
   * 次の動作が開始できるかチェックする。
   *
   * 戻り:もし次の動作メソッドが呼び出し可能のときtrue
   */
  public bool ready() {
    return nextMovement == movementNone ;
  }

  /**
   * 動作が完了したかどうかチェックする。
   * これは戻り値がtrueを返すまで呼ぶ。
   *
   * 戻り:もし動作が待ち状態を完了した時にtrueを返す。
   */
  public override bool movementDone () {
    if ( currentMovement == movementNone ) {
      if ( nextMovement == movementNone ) {
        // nothing left to do
        return true;
      } else {
        // 停止から新しい動作を開始する
        currentMovement = nextMovement ;
        currentSteps    = nextSteps ;
        nextMovement    = movementNone ;

        // Rampingを開始する
        startRamping () ;
      }
    } else {
      // currentMovement is active
      if ( rampCount > 0 ) {
        if ( timer.timeout ( rampTimeout )) {
          // Ramp timeout occurred. Adjust speed.
          -- rampCount ;

          if ( rampCount == 0 ) {
            if ( decelerate ) {
              // should be stopped, setup to accelerate
              decelerate = false ;
              startRamping () ;
            } else {
              // should be up to speed
              startFullSpeedMovement(true) ;
            }
          } else {
            // setup next ramp movement
            updateRampSpeed() ;
          }
        }
      } else {
        // Rampingはしない。動作ステータスをチェックする
        if ( base.movementDone ()) {
          getNextMovement ();
        }
      }
    }
    return false ;
  }

  /**
   * 次の動作の設定を得る
   */
  protected void getNextMovement () {
    // Get nextMovement
    if ( nextMovement == movementNone ) {
      // check if anything more to do
      if ( currentMovement == movementNone ) {
        return ;
      }

      if (    ( currentMovement == movementMove )
           && ( currentSteps == 0 )) {
        // Already stopped. Do nothing more
        currentMovement = movementNone ;
        return ;
      }

      // setup to stop if no movement acquired
      nextMovement = movementMove ;
      nextSteps    = 0 ;

      // Get next movement
      causeNextEvent () ;
    }

    // update steps, currentMovement must be updated later
    currentSteps = nextSteps ;

    // check if next movement or if need to decelerate
    if ( nextMovement == currentMovement ) {
      startFullSpeedMovement(false) ;
    } else {
      // decelerate first
      currentMovement = nextMovement ;
      rampDown();
    }
  }

  /**
   * currentMovementに対してRampingを開始する
   */
  protected void startRamping () {
    if ( currentSteps == 0 ) {
      base.move(0);  // stop servos
    } else {
      getMovementSpeed ( currentMovement, currentSteps ) ;

      rampTo ( leftMovementSpeed, rightMovementSpeed ) ;
    }
  }

  /**
   * 減速に対するRampingを開始する
   */
  protected void rampDown () {
    decelerate = true ;
    rampTo ( 0, 0 ) ;
  }

  /**
   * 動作を開始する。Rampingはすでに完了
   *
   * 入力:boolean ramping:Ramping時間が引かれるべき時にtrue
   */
  protected void startFullSpeedMovement ( bool ramping ) {
    int timeoutAdjust = 0 ;

    switch ( currentMovement ) {
    case movementMove:
      base.move ( currentSteps ) ;
      timeoutAdjust = msecPerStep - 2 ;
      break;

    case movementPivot:
      base.pivot ( currentSteps ) ;
      timeoutAdjust = msecPerStep - 2 ;
      break;

    case movementTurn:
      base.turn ( currentSteps ) ;
      timeoutAdjust =   ( currentSteps == 1 )
                      ? (( 3 * msecPerStep ) / 8 )
                      : ( msecPerStep / 2 ) ;
      break;
    }

    // adjust timeout for ramping
    if ( ramping ) {
      timeout -= timeoutAdjust ;
    }

    // Reset next movement
    nextMovement = movementNone ;
  }

  /**
   * 目的のスピードにRampingするための設定
   *
   * 入力:int left:最終的な左のスピード
   * 入力:int right:最終的な右のスピード
   */
  protected void rampTo ( int left, int right ) {
    rampCount      = maxRampCount - 1 ; // ramp down count less current step
    rampLeftStep   = ( left  - leftSpeed )  / maxRampCount ;
    rampRightStep  = ( right - rightSpeed ) / maxRampCount ;
    updateRampSpeed() ;
  }

  /**
   * rampパラメータを用いてスピードをアップデートする
   */
  protected void updateRampSpeed () {
    // set timeout mark for next ramp speed update
    timer.mark();

    // 新しいスピードに設定する
    setSpeed(leftSpeed+rampLeftStep,rightSpeed+rampRightStep) ;
  }

  /**
   * 前進するため車輪のスピードを設定する
   *
   * 入力:int cm: 移動する直線距離
   */
  public override void move ( int cm ) {
    setNextMovement ( movementMove, cm ) ;
  }

  /**
   * 左旋回するように車輪スピードを設定する
   *
   * 入力:int steps: 回転するステップ数
   */
  public override void pivot ( int steps ) {
    setNextMovement ( movementPivot, steps ) ;
  }

  /**
   * 左回転するように車輪スピードを設定する
   *
   * 入力:int steps: 回転するステップ数
   */
  public override void turn(int steps)
  {
    setNextMovement ( movementTurn, steps ) ;
  }

  /**
   * 次の動作が起こるための設定
   * 動作はmovementDone()を呼ぶことを通して開始される
   *
   * 入力:int movement: 行う次の動作
   * 入力:int steps: 回転するためのステップ
   */
  protected void setNextMovement ( int movement, int steps ) {
    nextMovement = movement ;
    nextSteps    = steps ;

    // 現在停止中ならば動作を開始する
    if ( currentMovement == movementNone ) {
      startMovement();
    }
  }

  /**
   * 両輪のスピードを設定する
   * 設定はパーセントである
   * 正の値は前進
   * 負の値は後進
   *
   * 入力:int left: 左車輪のスピード
   * 入力:int right: 右車輪のスピード
   */
  protected override void setSpeed(int left, int right)
  {
    // Set real speed
    base.setSpeed(left,right);
    leftSpeed  = left ;
    rightSpeed = right ;
  }

  /**
   * 両輪のスピードを調整する
   * 設定はパーセントである
   * 正の値は前進
   * 負の値は後進
   *
   * 入力:int left: 左車輪のスピード補正
   * 入力:int right: 右車輪のスピード補正
   */
  protected void adjustSpeed ( int left, int right ) {
    base.setSpeed
      ( leftSpeed  + (( leftSpeed  * -left  ) / 100 )
      , rightSpeed + (( rightSpeed * -right ) / 100 ));
  }
}


Rampingクラスがどのように動くか

RampingBoeBotクラスはBasicBoeBotによって備えられたサービスを使えるようにしていますが、movementDonesetSpeedメソッドの動作方法を補正します。オブジェクトはBasicBoeBotで起こるような大きなジャンプの代わりに小さな増加でスピードを変えることができます。例えば、move(1)メソッドが次にmove(-1)を続けるBasicBoeBotオブジェクトを呼ぶことはBoe-Botが1cm前進し、回転方向を逆方向にし1cm後退します。RampingBoeBotを使わなければ、Boe-Botの車輪は急に発進しそして逆に動作を開始します。後者の場合相対スピード変化は200%(100%- (-100%))です。これはBoe-Botが電池駆動のとき、重大なパワーサージが問題を引き起こします。

RampingBoeBotはBoe-Botの現在のスピードを見ています(setSpeedメソッドで保存しているのを思い出すこと)。目的のスピードと比較し、スピード変化が短いステップで起こるようにスピードを設定します。これは距離の僅かな変化を起こしますが、おかしな動作とパワーサージを取り除きます。そのクラスはどのくらい遠いところまで走るかを計算して距離の変化を計算に入れようともしています。

RampingBoeBotオブジェクトにはいくつかの一時的な変数が定義されています。rempCountはスピードの変化間を取るステップの数をコントロールします。rampLeftSteprampRightStepは両輪のパーセントのスピード変化です。2つののサーボモータは独立にコントロールされなければならないので2つの変数が必要です。最後に、finalは目的のスピードが保存されます。整数値が用いられ、丸め誤差が起こるのでステップする時に差異が生じるのでこれは必要になります。

変数currentMovementnextMovementcurrentStepsnextStepsと一緒に動作を維持します。次の動作を保存するのはおかしく思われるだろうが、動作の間の適切な推移ができるようにするのと、2つの同じ動きが1つに組み合わさるようにするのが必要です。例えば、前に5、それから10は前に15移動するのと同じでなければならない。

setSpeedメソッドはBasicBoeBotのsetSpeedを呼び、現在のスピードを保存します。adjustSpeedメソッドはそのスピードを変えることができます。このメソッドはBasicBoeBotオブジェクトでは使えません。それは後で出てくる車輪エンコーダフィードバックに基づくスピード調整を必要とするWheelEncoderBoeBotサブクラスがサポートしているからです。

仕事の塊はmovementDoneメソッドでなされます。このメソッドはcurrentMovementnextMovement変数を計算に入れています。もし両方がmovementNoneに設定されていたら、メソッドはtrueを持って終わります。もしcurrentMovementmovementNoneに設定されていたら、現在の動作は終了していて、nextMovementに推移する時点であることを示します。この推移は直前の動作がnextMovementと直前の動作の最後でBoeBotが僅かな時間停止したrampingとが異なった時に起こります。

動作の推移をチェックする時間が無いとしたら、rampCountによって管理されるrampingスタータスをチェックします。この変数は目的のスピードに上げていく時と停止するためにスピードダウンする時の両方に用いられます。RampingはrampCountがゼロになった時に完了します。decelerate変数はrampingはupかdownかを示しています。もしそれがdownの時はBoeBotは停止して、次の動作のためupにします。これはstartRampingメソッドを呼ぶことによってなされます。もしBoeBotがRamping upの時は、フルスピードで動作を開始する時です。これはstartFullSpeedMovementを呼ぶことによってなされます。

何の推移も無い時はrampingのスピードはupdateRampSpeedを呼ぶことよって更新されます。これはrampingサポートを行うオブジェクト変数を用います。全てのelseに一致しなかった場合、動作はスーパークラスBasicBoeBotmovementDoneメソッドによって操作されます。動作が完了した場合はgetNextMovementを呼ぶ時です。

次の図はgetNextMovementが1つの動作で呼ばれる時を示しています。

getNextMovementメソッドはちょっと複雑です。これは次の動作がすでに与えられているかを確かめます。もしそうでなかったら、causeNextEventが呼ばれます。このサポートはBoeBotInterfaceクラスに戻って行います。これは典型的にMultitaskingBoeBotサポートと一緒に用いられます。RampingBoeBotクラスをコントロールするタスクはcauseNextEvent呼び出しで通知されます。タスクは動作を開始し、nextMovement変数を設定して戻ります。もし、次の動作が直前のものと同じ時は、startFullSpeedMovementが呼ばれます。ramping時間は動作時間から減算しないことを示しているfalseをパラメータとして渡します。

rampingのメソッドとしてはstartRamping,rampDown, rampToがあります。これらはramping処理のupやdownを行うためにramping変数を操作します。

startFullSpeedMovementメソッドは、ソースファイルで出てくる順番で見ているので次にあります。これは実際にBasicBoeBotの動作メソッドが呼ばれるところにあります。BasicBoeBotは全ての動作はフルスピードで走ると仮定しています。startFullSpeedMovementメソッドはrampingの間か下に示すように2つの同じ動作が続けて起こるときに走ります。上のタイムチャートで、startFullSpeedMovementメソッドはgetNextMovementの点で二度目が呼ばれます。
スイッチ文は次に行う動作のタイプを選んでいます。各々の場合、フルスピードの動作を設定するためにBasicBoeBotのスーパークラスメソッドを呼んでいます。変数timeoutAdjustは下に示すように設定します。
    case movementMove:
      base.move ( currentSteps ) ;
      timeoutAdjust = msecPerStep - 2 ;
      break;
timeoutAdjustの値はramping upやdownを計算するために必要な変化です。もし、rampingが動作の一部の場合、この値はオブジェクトの動作のコントロールに用いられているtimeout値から減算されます。もし、同じタイプの動作が続けて2つ実行された場合、この補正は最初の動作のtimeoutを用います。

move, pivot, turnメソッドはBoeBotInterface基底クラス(スーパークラス)の中で定義されています。動作の取り扱い方法が異なるのでこのクラスで再定義されています。RampingBoeBotクラスでは、これらのメソッドはnextMovementnextSteps変数の中に要求される動作を単純に保存します。前に注意したように、これらの変数はBoeBotが1つの動作から他に推移する時に用いられます。

次のプログラムはRampingBoeBotクラスの練習です。

RampingBoeBotクラスをテストする
using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.NetduinoMini;
using BoeBotLib;

namespace BasicBoeBotTest3
{
    public partial class Program
    {
        public void main()
        {
            RampingBoeBot jbot = new RampingBoeBot(new FixedMovementBoeBot());

            jbot.move(10);  // forward
            jbot.pivot(-2); // pivot right
            jbot.turn(2);   // turn left
            jbot.stop();
        }
    }
}


このプログラムは最初に短い後進をしますが、後は以前のテストプログラムと同じ順番で動作します。このプログラムはRampingBoeBotに変更し最初に後進の動作を付けただけです。

実験3-6 Boe-Botを操作する

上ののテストプログラムはBoe-Botを他のパターンで動かすように変更するのは簡単にできますが、ソースコードが長く冗長になります。もし定義する事が動きそのものだけならば、パターンを作るのは簡単です。ここでは、単純なプログラムで動作をいかに符号化し復号化するかをみましょう。

下の例では、動作はバイト配列に格納されています。C#では次のサンプルプログラムで示すように、定数のバイト配列を定義するのは簡単です。

配列に格納されているコマンドを用いてのBoe-Botを動かす
using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.NetduinoMini;
using BoeBotLib;

namespace BasicBoeBotTest4
{
    public partial class Program
    {
        /**
         * 配列に格納されているコマンドを用いてのBoe-Botを動かす
         * 
         * RampingBoeBotクラスメソッドとマンド表を用いてBoe-Botを走らせる
         */
        const int move  = -1;
        const int pivot = -2;
        const int turn  = -3;

        RampingBoeBot jbot = new RampingBoeBot(new FixedMovementBoeBot());

        static int[] movements = { move, 10, pivot, -2, move, 20 };
        public void main()
        {
            performMovements(movements);
        }

        public void performMovements(int[] movements)
        {
            for (int i = 0; i < movements.Length; i += 2)
            {
                switch (movements[i])
                {
                    case move:
                        jbot.move(movements[i + 1]);
                        break;

                    case pivot:
                        jbot.pivot(movements[i + 1]);
                        break;

                    case turn:
                        jbot.turn(movements[i + 1]);
                        break;
                }
            }

            jbot.stop();
        }
    }
}


課題3-6

  1. Boe-Botが10×15cmの四角を描くようにmovements配列を変更しなさい。

  2. 現在のメソッドは100%のスピードで動作するようになっています。このスピードを変更するsetMovementSpeed()をつくり、これを用いて100%,50%,30%と動作スピードを変えて動作するように配列を変更し、動かしなさい。

  3. BasicBoeBotTest4クラスを2つに分離し、MovementBoeBotBasicBoeBotTest5にしなさい。このようにするとMovementBoeBotは再利用できるようになります。

  4. 楕円とか、星型とか今までと異なったパターンで動くようにmovementsの内容を変更しなさい。

実験3-7 距離センサーを用いて物体回避走行する

GP2Y0A21YK距離センサー
上記写真はコネクタから出ている配線の色を示しています。電源が赤、GNDが黒という常識的配色をしていないので、先端のみ配色を変えてあります。注意してください。電源の配線を間違えると壊れます。

distanceTest1プロジェクトを追加し、距離センサーのテストをします。

距離センサーテスト
/**
 * 距離センサーテスト
 * 
 * 距離センサーステータスを表示する
 */

namespace distanceTest1
{
    public class Program
    {
        public static void Main()
        {
            ADcon right = new ADcon(CPU.P0);
            ADcon left = new ADcon(CPU.P1);
            while (true)
            {
                Debug.Print("R:"+ right.Value + " L:"+left.Value);
                CPU.delay(500);
            }
        }
    }
}


distanceTest1クラスは距離センサーが正しく動作するかをテストするプログラムで、距離センサーから送られてくる電圧を表示します。センサーの前10cmに手とか紙などをかざしてみて値が変わるのを確認してください。値が変わらない場合は、配線が間違っていると思われます。その場合はすぐに電源を切ってください。

課題3-7-1

距離センサーが正しく機能しているか、残りの3つの状態について調べなさい。(右の距離センサーの前だけに紙を置いた場合、左の距離センサーの前だけに紙を置いた場合、両方の距離センサーの前に(中央に)紙を置いた場合に予想と同じかどうか調べる)

もし、予想と異なっていたら、回路、あるいはプログラムをチェックすること。


次は、この距離センサーの状態を出力ウィンドウで確認するのではなく、LEDを点灯させて確認するようにします。このためには、LEDを点灯させるI/Oピンを出力用に設定する必要があります。次の2行を先頭に入れてください。

CPU.setOutput(CPU.P9);
CPU.setOutput(CPU.P10);


状態の表示の代わりに次の文に変えてください。

CPU.writePin(CPU.P9, right.GetValue() > 400);
CPU.writePin(CPU.P10, left.GetValue() > 400);


課題3-7-2

再度、距離センサーが正しく機能するかチェックしなさい。もし、機能しない場合は回路とプログラムをチェックすること。
注: この実験でLEDを壊す人が最近増えています。正しい回路の作り方を実験1に戻って復習しましょう。特に、電源を入れながら配線をしてはいけない。

実験3-8 シングルタスクの物体回避ロボット

この実験では距離センサーを用いてBoe-Botをガイドするようにプログラムします。距離センサー前に何かが来たら方向転換するようなプログラムを作ります。

放浪するプログラムdistanceTest2クラスを作りましょう。このプログラムは障害物にぶつかるまで直進します。この場合、Boe-Botは距離センサーの片方あるいは両方の前に障害物に来たことを知ります。これを知ったらすぐに実験3-6で作ったバックしたり回転したりするルーチンを用いて回避します。そしてまた、障害物の前まで直進します。この距離センサーの状態をまとめると以下のようになります。

次のソースは距離センサーの状態からBoe-Botの動作に適切なルーチンをいかに選ぶかを示す例です。例えば、状態1はBoe-Botは直進、状態2は後進して右回転、状態3は後進して左回転、状態4は後進してUターンさせます。

状態
1truetrue障害物なし
2falsetrue左側に障害物
3truefalse右側に障害物
4falsefalse前方に壁のようなもの
物体回避テスト
/**
 * 物体回避テスト
 */
namespace whisker2
{
    public partial class Program
    {
        public void main()
        {
            BoeBotInterface jbot = new RampingBoeBot(new FixedMovementBoeBot());
            ADcon right = new ADcon(CPU.P0);
            ADcon left  = new ADcon(CPU.P1);

            while (true)
            {
                int LeftRight = ((right.GetValue() > 400) ? 0 : 2)+((left.GetValue() > 400) ? 0 : 1);
                switch (LeftRight)
                {
                    case 0:   // both low, 後進して右旋回
                        jbot.stop();
                        jbot.move(-3);
                        jbot.pivot(-4);
                        break;

                    case 1:   // P5 low, 後進して左旋回
                        jbot.stop();
                        jbot.move(-2);
                        jbot.pivot(2);
                        break;

                    case 2:   // P4 low, 後進して右旋回
                        jbot.stop();
                        jbot.move(-2);
                        jbot.pivot(-2);
                        break;

                    case 3:   // ぶつかっていない、直進
                        jbot.move(2);
                        break;
                }
            }
        }
    }
}


課題3-8

近くに物体を置いて実際に思ったとおりに動くかチェックしなさい、うまくいかなかった場合、対処できるようにプログラムなどを考えなさい。

実験2へ戻る   実験4へ

実験3終わり