Arduinoでイベント駆動型プログラミング
Arduino(に限らず、OSの無いマイコンで)マルチタスク的な処理をさせようとすると結構面倒です。次に作ろうと思っていた自動水やり機では、各センサーの値を読み、ポンプを制御し、そして人間からのコマンドを処理し…など、いろいろな処理する必要があります。そこでイベント駆動型のプログラミングができるよう、基本的な部分を作成しました。この上でプログラムを作成すればそれぞれの動作を独立してプログラミングすることができ、とてもすっきりします。まぁ、誰もがやっているだろう事ではありますが、車輪の再発明でも自分で作るから楽しいというものです。
基本
イベントが発行された時、それがそれぞれの処理単位にどうやって伝わるようにするかを設計しなければなりません。とりあえずその処理単位をC++のオブジェクト指向機能を利用することにしました。イベントを受ける処理単位を「レスポンダ」と呼ぶ事にします。Responderクラスにイベントを受けるための基本的な処理を書き、実際に動作する処理はResponderクラスを継承したクラスで書く事にしました。
動作開始前に、まず各レスポンダをイベント受信対象として「登録」します。イベントが発行された場合、登録されたレスポンダに対し、次々にイベントを送信していきます。各レスポンダは、受信したイベントが自分が処理すべきイベントである場合、それに対応した動作をし、それ以外の場合は無視します。イベントに対する処理はなるべく速く実行する必要があります(原則としてdelay関数は使わない)。
なお、本格的なイベント駆動では、イベント発行時にはそのイベントは一旦イベントキューに登録されるかと思います。しかし本ソフトはイベントキューを持たず、イベントが発行された時点でそのイベントに対する処理を各レスポンダが行うようになってます。このため、イベントの処理中に別のイベントを発行する際は、いわゆる無限再起コールにならないよう注意する必要があります。
Responderクラスは非常に簡単です。以下、抜粋です。
(以下、表示されているソースはあくまでも抜粋で、全てではありません。全ソースは最後にあるリンクよりダウンロードしてください)。
class Responder { public: Responder *prevResponder; public: static void init(); static void addResponder(Responder *); Responder* getPrevResponder(); void setPrevResponder(Responder *resp); virtual bool procEvent(Event_t event, EventParam_t param); };
#include "EventMgr.h" Responder* g_currentResponder; void Responder::init() { g_currentResponder = 0; } void Responder::addResponder(Responder *resp) { resp->setPrevResponder(g_currentResponder); g_currentResponder = resp; } Responder* Responder::getPrevResponder() { return prevResponder; } void Responder::setPrevResponder(Responder *resp) { prevResponder = resp; } bool Responder::procEvent(Event_t event, EventParam_t param) { return true; }
複数のレスポンダが「登録」されるわけですが、登録されたものの一覧を用意するのではなく、各レスポンダがひとつ前のレスポンダのポインタを保持するようにします。そしてg_currentResponderが、登録が完了した時点で最も「上」に位置するレスポンダです。イベントの送信は、この最上位のレスポンダのprocEventメソッドを呼び出し、さらにそのレスポンダのgetPrevResponderで得られるひとつ下のレスポンダのprocEventメソッドを呼び出し…という流れになります。これを、procEventの戻り値がtrueになるか、またはgetPrevResponderの値がnull(これ以上レスポンダは無い)まで繰り返します。
各レスポンダはprocEventの結果としてtrueまたはfalseを返します。イベントを処理し、それ以下のレスポンダにイベントを伝える必要が無い場合にtrueを返し、イベントを処理しなかった場合、または処理したけれども他のレスポンダにも同じイベントを伝える場合にはfalseを返すようにします。これにより、動作状況によってレスポンダを追加する事で、イベントを「キャプチャ」できるようにしてあります。これは例えば、あるボタンに対する動作(ボタンが押されたというイベントに対する動作)が、その時点の動作モード(メニュー表示とか)によって変わる…という時に役立ちます。
以下が、イベントを発行する、sendEvent関数です。各レスポンダにはイベントのID(Event_t型…実際にはunsigned int等、任意)と、任意のパラメーターが渡されます。
void sendEvent(Event_t event, EventParam_t param) {
Responder *r;
r = g_currentResponder;
while(r) {
if(r->procEvent(event,param)) break;
r = r->getPrevResponder();
}
}
イベントの発行元は様々ですが、たぶん必ず必要になるであろう物がタイマーでしょう。これは各処理単位から、イベントを発行してもらいたい時間をセットし、その時刻になると登録したイベントが発行されるようにします。以下、抜粋です。
#define MAX_TIMER 10 typedef unsigned long TimeSpanParam_t; void initTimer(); void setTimer(byte timerId,TimeSpanParam_t span, bool f_repeat=false, Event_t event= EVENT_TIMEOUT); void procTimer();
void initTimer() { byte i; for(i=0; i<MAX_TIMER; i++) { g_timerTime[i] = g_timerSpan[i] = 0; } } void _timer_setTimer(byte timerId,TimeSpanParam_t span) { g_timerTime[timerId] = millis() + span; } void setTimer(byte timerId, TimeSpanParam_t span, bool f_repeat, Event_t event) { if(span == 0) { event = 0; } _timer_setTimer(timerId,span); if(!f_repeat) span = 0; g_timerSpan[timerId] = span; g_timerEvent[timerId] = event; } void procTimer() { unsigned long m; byte i; m = millis(); for(i=0; i<MAX_TIMER; i++) { if(g_timerEvent[i]) { if((long)(g_timerTime[i] - m) <= 0) { sendEvent(g_timerEvent[i],i); if(g_timerSpan[i]) { _timer_setTimer(i,g_timerSpan[i]); } else { g_timerEvent[i] = 0; } } } } }
setTimerが登録ルーチンです。タイマーの識別子、イベント発行までの間隔、リピートの有無、発行するイベントを指定します。procTimerは、登録されている時刻に達したかどうかをチェックし、達していればイベントを発行するというルーチンです。基本的にこれはloop()関数の中から呼びます。
実際の処理
例として、複数のLEDを別々の間隔で点滅させるスケッチ「MultiBlink」を作成しました。
まずはひとつのLEDを点滅させるための処理単位、Blinkerクラスです。
#ifndef _Blinker.h #define _Blinker.h #include "EventMgr.h" class Blinker : public Responder { byte m_id; byte m_pin; byte m_state; public: void init(byte id, byte pin); void start(word time); bool procEvent(Event_t event, EventParam_t param); }; #endif
#include "Blinker.h" void Blinker::init(byte id, byte pin) { m_id = id; m_pin = pin; m_state = 0; pinMode(m_pin,OUTPUT); digitalWrite(m_pin,LOW); } void Blinker::start(word time) { setTimer(m_id,time,true); } bool Blinker::procEvent(Event_t event, word param) { switch(event) { case EVENT_TIMEOUT : if(param == m_id) { if(m_state) { digitalWrite(m_pin,HIGH); m_state = 0; } else { digitalWrite(m_pin,LOW); m_state = 1; } return true; } break; } return false; }
まずはinitメソッドにて、タイマーのための識別子、出力端子の番号を設定します。startメソッドにて、指定した間隔でイベントが発行されるよう設定します。
BlinkerクラスのprocEventメソッドにて、EVENT_TIMEOUTというイベントを受信した時に、そのイベントが自分が処理すべきイベントであれば、出力端子のHIGH/LOWを切り替えるようにしています。
(caseがひとつだけなのでswitchではなくifで充分ですが、複数のイベントを処理するように追加する事を考慮してswitchを使ってます。)
あとはメインルーチン、Arduinoお決まりのsetup()とloop()です。
#include "EventMgr.h" #include "Blinker.h" Blinker blinker0; Blinker blinker1; Blinker blinker2; void setup() { initTimer(); Responder::addResponder(&blinker0); Responder::addResponder(&blinker1); Responder::addResponder(&blinker2); blinker0.init(0,11); blinker1.init(1,12); blinker2.init(2,13); blinker0.start(300); blinker1.start(500); blinker2.start(700); } void loop() { procTimer(); }
例として3個のLEDを点滅させることにしました。blinker1, blinker2, blinker3の3個のインスタンスを準備し、それらにイベントが発行されるよう登録します(Responder::addResponderメソッド)。次に各インスタンスに対して識別子とピン番号を設定し、それぞれの時間間隔(この例ではそれぞれ300, 500, 700ms間隔)でスタートさせます(ピン番号は上記ではそれぞれDigital11,12,13としています。これは#defineで定義すべきですね…)。
loop()の中ではprocTimer()を呼ぶだけです。これで設定した時間になれば3個のインスタンスにイベントが発行され、各インスタンスはそのイベントが自分が処理すべきイベントであればLEDを点灯・消灯させます。
これだけだとちょっと複雑に見えますが、例えばLEDを4個、5個と増やした場合でも、実際にLEDを点滅させる処理系であるBlinkerクラスは全くいじる必要が無く、Blinkerのインスタンスを増やすだけで済みます。
さらにこれをベースに、点滅以外の処理を簡単に追加できるようになります。
全ソースコードのダウンロード:MultiBlink.zip