googletest/googlemockを使って、組込みLinux上で動作するソフトウェアをTDDで開発しています。
実際にTDDで開発している手順をリアルタイムでそのまま書いているため、行き当たりばったりな部分があります
過程も楽しんで頂ければ幸いです。
不明な点、間違っている点があれば、コメント下さい。励みになります。
これまでの経緯を詳しく知りたい方は、前回までの記事をご覧下さい。
組込みに近いものをTDDで開発してみる〜準備編〜
組込みに近いものをTDDで開発してみる〜問題提起編〜
組込みに近いものをTDDで開発してみる〜file open編〜
組込みに近いものをTDDで開発してみる〜libevdev初期化/終了処理編〜
組込みに近いものをTDDで開発してみる〜キー入力検知編〜
組込みに近いものをTDDで開発してみる〜中間振り返り編〜
組込みに近いものをTDDで開発してみる〜インタフェース継承編〜
いよいよ大詰めです。
前回、次の仕様を実装しました。
- キーボードの「A」ボタンを押すと、caps lockのLEDが点灯し、5秒後に消灯する。消灯するまでは、「A」キー押下を無視する。
キー入力検出とタイマ検出は、EventDetectorインタフェースを共通の基底クラスとすることで、同一インタフェースで使えるようになりました。
今回は、LEDへの異なる操作を、同一インタフェースで使えるようにして、さらにプログラムの拡張性を高めます。
LED操作をどうしたいか
今のLEDドライバには、LEDを操作する3つの関数がある。
void TurnOnLed(LedDriver self);
void TurnOffLed(LedDriver self);
void ToggleLed(LedDriver self);
これらの3つを同じインタフェースで使用し、main関数を誤魔化しのない状態にしたい。
for(int i = 0; detectors[i] != NULL; i++) {
StartEventDetector(detectors[i]);
// イベント検出まで待つ
while(CheckEvent(detectors[i]) != DETECTOR_EVENT_DETECTED) {}
ToggleLed(caps_led); // ここで共通インタフェースを呼び出し、On/Offを正しく切り替えたい
// 仕様を満たすためには、本当は下のように書かなければならない。
// if (i == 0) TurnOnLed(caps_led);
// else if (i == 1) TurnOffLed(caps_led);
}
Commandパターン
そこで、LEDの操作をCommandパターンとして実装する。
Commandパターンは、『命令』をインスタンスとして表現できるデザインパターンだ。
Commandパターンは笑ってしまうほど単純だ(と言ってもC言語だとぼちぼちの量になる)。
typedef struct CommandInterfaceStruct *CommandInterface;
typedef struct CommandStruct *Command;
typedef struct CommandStruct {
CommandInterface vtable;
} CommandStruct;
// インタフェースはExecute()だけ
typedef struct CommandInterfaceStruct {
void (*Execute)(Command);
} CommandInterfaceStruct;
void CommandExecute(Command cmd);
Commandインタフェースを継承するクラスは、Execute()だけを実装していれば良い。
ただ、この単純な実装に、びっくりするほどの柔軟性がある。
LED操作Commandの実装
いつも通り、テストから考えていこう。
// LEDをOnするCommandを生成、実行するテスト
TEST_F(LedOperatorTest, LedTrunOnOperationCallsIoWriteWithOne) {
// driver_はLedDriverのインスタンス
Command command = LedOperatorFactory(driver_, OP_LED_TURN_ON);
EXPECT_CALL(*mock_io, IO_WRITE(kFd, StrEq("1\n"), 2)).Times(1);
CommandExecute(command);
}
少なくとも、On/Off/Toggleの3つのLED操作Commandが必要なことが分かっている。
そこで、FactoryMethodパターンを用意して、引数で生成インスタンスを切り替えることにした。
まず、LED操作の派生クラスは次のようにした。
Commandインタフェースを継承する以外は、LedDriverのインスタンスがあるだけだ。
typedef struct LedOperatorStruct {
CommandStruct base;
LedDriver driver;
} LedOperatorStruct;
typedef struct LedOperatorStruct *LedOperator;
LEDをOnするExecute()の実装は、次のようになる。
static void LedTurnOn(Command super) {
LedOperator self = (LedOperator)super;
TurnOnLed(self->driver);
}
static CommandInterfaceStruct interface = {
.Execute = LedTurnOn
};
後は、インスタンスを作ってあげるだけだ。
この時点では、LED操作を切り替える引数は使っていないが、この後すぐ実装する。
Command LedOperatorFactory(LedDriver driver, int32_t op_id) {
if (driver == NULL) return NULL;
LedOperator command = calloc(1, sizeof(LedOperatorStruct));
command->base.vtable = &interface;
command->driver = driver;
return (Command)command;
}
ここまで作れば、LEDをOnするテストが通るようになる。
少し説明が雑になるが、順次、次のテストを用意し、実装を進めていった。
// OffするCommandを作生成、実行するテスト
TEST_F(LedOperatorTest, LedTrunOffOperationCallsIoWriteWithZero) {
Command command = LedOperatorFactory(driver_, OP_LED_TURN_OFF);
EXPECT_CALL(*mock_io, IO_WRITE(kFd, StrEq("0\n"), 2)).Times(1);
CommandExecute(command);
}
// ToggleするCommandを生成、実行するテスト
TEST_F(LedOperatorTest, LedToggleOperationTogglesIoWrite) {
Command command = LedOperatorFactory(driver_, OP_LED_TOGGLE);
InSequence s;
EXPECT_CALL(*mock_io, IO_WRITE(kFd, StrEq("1\n"), 2)).Times(1);
EXPECT_CALL(*mock_io, IO_WRITE(kFd, StrEq("0\n"), 2)).Times(1);
EXPECT_CALL(*mock_io, IO_WRITE(kFd, StrEq("1\n"), 2)).Times(1);
CommandExecute(command);
CommandExecute(command);
CommandExecute(command);
}
// FactoryがCommand生成に失敗するテスト
TEST_F(LedOperatorTest, FatoryReturnsNullIfFailed) {
Command command = LedOperatorFactory(nullptr, OP_LED_TURN_ON);
EXPECT_EQ(nullptr, command);
command = LedOperatorFactory(driver_, -1);
EXPECT_EQ(nullptr, command);
command = LedOperatorFactory(driver_, OP_MAX_FACTORY_ID);
EXPECT_EQ(nullptr, command);
}
最終的なプロダクトコードは次のようになる。
static void LedTurnOn(Command super) {
LedOperator self = (LedOperator)super;
TurnOnLed(self->driver);
}
static void LedTurnOff(Command super) {
LedOperator self = (LedOperator)super;
TurnOffLed(self->driver);
}
static void LedToggle(Command super) {
LedOperator self = (LedOperator)super;
ToggleLed(self->driver);
}
static CommandInterfaceStruct interface[OP_MAX_FACTORY_ID] = {
{ .Execute = LedTurnOn },
{ .Execute = LedTurnOff },
{ .Execute = LedToggle }
};
Command LedOperatorFactory(LedDriver driver, int32_t op_id) {
if (driver == NULL) return NULL;
if (op_id <= -1 || OP_MAX_FACTORY_ID <= op_id) return NULL;
LedOperator command = calloc(1, sizeof(LedOperatorStruct));
command->base.vtable = &interface[op_id];
command->driver = driver;
return (Command)command;
}
これで、LEDのOnだろうが、Offだろうが、Toggleだろうが、全て、Commandインタフェースから実行できるようになった。
プロダクトコード
最終的なプロダクトコードは次の通りだ。
int main(void) {
struct timeval kTime = {};
const struct input_event kPressA = {kTime, EV_KEY, KEY_A, INPUT_KEY_PRESSED};
EventDetector detectors[NUM_OPERATION_ON_DETECTION];
detectors[0] = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressA);
detectors[1] = CreateTimeOutDetector(5000, TIMER_ONE_SHOT);
LedDriver caps_led = CreateLedDriver();
if (InitLedDriver(caps_led, LED_DEVICE) != LED_DRIVER_SUCCESS) {
DEBUG_LOG("Fail to init led device\n");
exit(1);
}
Command operators[NUM_OPERATION_ON_DETECTION];
operators[0] = LedOperatorFactory(caps_led, OP_LED_TURN_ON);
operators[1] = LedOperatorFactory(caps_led, OP_LED_TURN_OFF);
// EventDetectorとCommandインタフェースだけで、仕様を実現している
for(int i = 0; i < NUM_OPERATION_ON_DETECTION; i++) {
StartEventDetector(detectors[i]);
while(CheckEvent(detectors[i]) != DETECTOR_EVENT_DETECTED) {}
CommandExecute(operators[i]);
}
// 終了処理は省略
}
ここまでできれば、様々な種類のイベント検出し、何らかの操作をする、という機能が、全て同じインタフェースで実装できる。
2秒のあいだにボタンが3回押されたことを検出する
次の問題を実装する(ただし、レギュレーションはけっこう無視する)。
(問題) 2秒のあいだにボタンが3回押されたかで、真・偽を返す関数を実装してください。
上記の問題では、2秒経過をタイマで計りながら、ボタン押下を検出する。
個々の要素は大したことないが、2つのことを同時にやる必要がある。
今回は、簡易な並列処理のデザインパターンであるActiveObjectパターンを使って、この問題を解いてみる。
話が前後してしまうため、テストの記載は省略しますが、これ以降も全てTDDで開発しています。
ActiveObjectに関するテストはこちら。
active_object_test.c
ActiveObjectパターン
このパターンは、APIとコアとなる実装を見た方が、理解しやすいと思う。
大雑把にいうと、実行したいCommandを複数追加しておいて、Run()で一気に実行するだけだ。
// engineにCommandを追加する
void FuelEngine(ActiveObjectEngine engine, Command cmd);
// engineに搭載されているCommandを実行する
void EngineRuns(ActiveObjectEngine engine);
typedef struct ActiveObjectEngineStruct {
// glibのリストを拝借。FuelEngineで追加されたCommandリストを保持する。
GSList *commands;
} ActiveObjectEngineStruct;
void FuelEngine(ActiveObjectEngine engine, Command cmd) {
if ((engine == NULL) || (cmd == NULL)) return;
// Commandをengineに追加する
engine->commands = g_slist_append(engine->commands, (gpointer)cmd);
}
void EngineRuns(ActiveObjectEngine engine) {
while(g_slist_length(engine->commands) > 0) {
// engineにCommandが存在する間、Commandを実行し続ける。
Command cmd = (Command)g_slist_nth_data(engine->commands, 0);
CommandExecute(cmd);
// 一度実行したコマンドはengineから除く
engine->commands = g_slist_remove(engine->commands, (gpointer)cmd);
}
}
ここまで見る限りは、なんてことないように見える。
ActiveObjectの本領は、Commandがengineに自分自身を追加し始めてからだ。
// このDetectChainは、後程コナミコマンドを検出するために使用する。
// イベントを検出するとwakeup Commandを実行するCommand
Command CreateDetectChain(EventDetector detector,
// 自分を実行するengineを引数に取る
ActiveObjectEngine engine,
// イベント検出時にengineに追加するコマンド
Command wakeup) {
DetectChain self = calloc(1, sizeof(DetectChainStruct));
self->base.vtable = &interface;
self->detector = detector;
self->wakeup = wakeup;
}
static void DetectChainExecute(Command super) {
DetectChain self = (DetectChain)super;
if (CheckEvent(self->detector) == DETECTOR_EVENT_DETECTED) {
// イベントが検出されると、wakeup Commandをengineに追加する
FuelEngine(self->engine, self->wakeup);
self->detector_started = false;
CleanupEventDetector(self->detector);
} else {
// イベントが検出されるまでは自分自身をengineに戻す
FuelEngine(self->engine, (Command)self);
}
}
static CommandInterfaceStruct interface = {
.Execute = DetectChainExecute
};
なんとなく見えてきただろうか?
つまり、2秒経過検出でengineを止めるCommandと、ボタン押下検出をカウントするCommandをengineに追加してあげれば、2つのCommandが同時に動き続ける。
engineが停止した段階で、ボタン押下カウンタの値を取り出してあげれば完成だ。
ActiveObjectは、同時に動くオブジェクトは1つだけ、という並列処理のパターンだ。
個人的には、かなりお気に入りのパターンだが、本当にマルチスレッドが必要とされるような場合には使えない。
フットプリントが軽いので、貧弱な環境では十分使えるはずだ。
2秒のあいだにボタンが3回押されたかで、真・偽を返す実装
プログラム実行直後から、2秒の間に、「A」ボタンが何回押されたか、を出力するようにした。
sudo led_controller/three_times_in_two_sec
Press A key 3 times in two seconds
以下、重要な部分のみ抜粋。
int main(void) {
struct timeval kTime = {};
const struct input_event kPressA = {kTime, EV_KEY, KEY_A, INPUT_KEY_PRESSED};
ActiveObjectEngine engine = CreateActiveObjectEngine();
// Create components.
EventDetector press_a = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressA);
EventDetector two_sec = CreateTimeOutDetector(2000, TIMER_ONE_SHOT);
Command total = CreateCountTotal();
Command halt = CreateHaltEngine(engine);
TriggerActionPair one = CreateTriggerActionPair(press_a, total);
TriggerActionPair two = CreateTriggerActionPair(two_sec, halt);
Command cmd_one = CreateActionOnTriggerChain(one, engine, LOOP_CHAIN);
Command cmd_two = CreateActionOnTriggerChain(two, engine, ONE_SHOT_CHAIN);
FuelEngine(engine, cmd_one);
FuelEngine(engine, cmd_two);
EngineRuns(engine);
// 3と比較すれば、真偽が返る
printf("Press A key %d times in two seconds\n.", TotalIs(total));
// 終了処理
return 0;
}
// イベント検出時、所定のCommandを実行するCommand
typedef struct TriggerActionPairStruct {
EventDetector detector;
Command command;
} TriggerActionPairStruct;
typedef struct ActionOnTriggerChainStruct {
CommandStruct base;
TriggerActionPair chain;
ActiveObjectEngine engine;
int32_t loop_flag;
bool started;
} ActionOnTriggerChainStruct;
typedef struct ActionOnTriggerChainStruct *ActionOnTriggerChain;
static void ExecuteActionOnTrigger(Command super) {
ActionOnTriggerChain self = (ActionOnTriggerChain)super;
if (!self->started) {
StartEventDetector(self->chain->detector);
self->started = true;
}
// イベント検出時
if (CheckEvent(self->chain->detector) == DETECTOR_EVENT_DETECTED) {
FuelEngine(self->engine, self->chain->command);
CleanupEventDetector(self->chain->detector);
self->started = false;
if (self->loop_flag == ONE_SHOT_CHAIN)
return; // Finish this chain by avoiding FuelEngine().
}
FuelEngine(self->engine, (Command)self);
}
static CommandInterfaceStruct interface = {
.Execute = ExecuteActionOnTrigger
};
TriggerActionPair CreateTriggerActionPair(EventDetector detector, Command command) {
TriggerActionPair pair = calloc(1, sizeof(TriggerActionPairStruct));
pair->detector = detector;
pair->command = command;
return pair;
}
// Should give ActiveObjectEngine.
Command CreateActionOnTriggerChain(TriggerActionPair chain,
ActiveObjectEngine engine,
int32_t loop_flag) {
ActionOnTriggerChain self = calloc(1, sizeof(ActionOnTriggerChainStruct));
self->base.vtable = &interface;
self->chain = chain;
self->engine = engine;
self->loop_flag = loop_flag;
self->started = false;
return (Command)self;
}
残りの細かい部分は、GitHubを見てほしい。
ソースコード全体
three_times_in_two_sec.cがmainを含むソースコード。
コナミコマンドを検出する
先ほど紹介した、DetectChainを使って、コナミコマンドを検出する。
コマンドを検出すると、caps lockのLEDを点灯して終了する。途中で間違えるとプログラムは何もせず終了する。
検出するイベントが多いので、少し下準備が長くなっている。
マトリョーシカのようにDetectChain Commandを作っていけば、あとはActiveObjectEngineを動かすだけだ。
int main(void) {
struct timeval kTime = {};
const struct input_event kPressUp = {kTime, EV_KEY, KEY_UP, INPUT_KEY_PRESSED};
const struct input_event kPressDown = {kTime, EV_KEY, KEY_DOWN, INPUT_KEY_PRESSED};
const struct input_event kPressLeft = {kTime, EV_KEY, KEY_LEFT, INPUT_KEY_PRESSED};
const struct input_event kPressRight = {kTime, EV_KEY, KEY_RIGHT, INPUT_KEY_PRESSED};
const struct input_event kPressA = {kTime, EV_KEY, KEY_A, INPUT_KEY_PRESSED};
const struct input_event kPressB = {kTime, EV_KEY, KEY_B, INPUT_KEY_PRESSED};
LedDriver caps_led = CreateLedDriver();
if (InitLedDriver(caps_led, LED_DEVICE) != LED_DRIVER_SUCCESS) {
DEBUG_LOG("Fail to init led device\n");
exit(1);
}
// Create components.
EventDetector press_up = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressUp);
EventDetector press_down = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressDown);
EventDetector press_left = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressLeft);
EventDetector press_right = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressRight);
EventDetector press_a = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressA);
EventDetector press_b = CreateKeyInputDetector(KEYBOARD_DEVICE, &kPressB);
Command caps_on = LedOperatorFactory(caps_led, OP_LED_TURN_ON);
ActiveObjectEngine engine = CreateActiveObjectEngine();
Command acceptted = CreateDetectChain(press_a, engine, caps_on);
Command uuddlrlrb = CreateDetectChain(press_b, engine, acceptted);
Command uuddlrlr = CreateDetectChain(press_right, engine, uuddlrlrb);
Command uuddlrl = CreateDetectChain(press_left, engine, uuddlrlr);
Command uuddlr = CreateDetectChain(press_right, engine, uuddlrl);
Command uuddl = CreateDetectChain(press_left, engine, uuddlr);
Command uudd = CreateDetectChain(press_down, engine, uuddl);
Command uud = CreateDetectChain(press_down, engine, uudd);
Command uu = CreateDetectChain(press_up, engine, uud);
Command start = CreateDetectChain(press_up, engine, uu);
FuelEngine(engine, start);
EngineRuns(engine);
return 0;
}
2秒で3回ボタン押下検出にせよ、コナミコマンド検出にせよ、これまでに作ったキー入力検出、タイマ、LED操作のモジュールには一切変更を加えていない。
これが大きなポイントとなる。
最後に
@mt08 さん、おもしろい問題を(タイムリーに)提供して頂き、ありがとうございました。
だいぶレギュレーションを無視していますが、かっこいい実装になっていますでしょうか?
組込みにちかいものをTDDで開発してみる、は一旦ここまでとさせて頂きます。
モチベーションが回復すれば、ラズパイあたりに移植して動かしてみます。
最後の方、細かいことが説明しきれなくなって、色々端折ってしまいました…。
後は、コメントで質問やマサカリ頂けば嬉しいです。