M5Paper の FactoryTest に HelloWorld アプリを追加する

M5PaperM5Stack より発売されたスマホ型 e-Ink 搭載 IoT デバイスである。

M5Paper には非常にリッチなサンプルプログラムである FactoryTest が存在する。

FactoryTest の GUI は EPDGUI, Frame というフレームワークからなる。これらは簡単に拡張できる構造となっており、実際に他のサンプルプログラムである Todo, Calculator でも利用されているが、非常に残念なことに Documentation が存在せず、Web で検索してもこれらを活用している例は少なく、各人が独自の UI を制作して利用している状態である。

この記事の目的は、そのような状態に一石を投じるべく、FactoryTest に HelloWorld アプリを実装する方法を示すことで、EPDGUI, Frame の使用方法を解説することである。

実際に動いているところの写真を載せる。

f:id:KainokiKaede:20210417175519j:plain f:id:KainokiKaede:20210417175527j:plain

実装後のプロジェクトは GitHub に掲載したFactoryTest からの diff を見ることで、新規機能の実装の際に必要な要素を概観することができる。ある程度プログラミングに慣れている人であれば記事を読むまでもなくこの diff を見ればよいだろう。

フレームワークの構造

EPDGUI フレームワークにおいて一つ一つの画面は Frame という名で管理される。新たな画面はこの Frame_Base クラスを継承して作成する。

EPDGUI フレームワークにおいて、画面に表示されるボタンや画像などの要素は EPDGUI_Base を継承して作成される。このクラスには位置・サイズ・タッチの判定などの機能が含まれている。たとえばこれを継承して作成された EPDGUI_Button クラスには、タッチされたときの挙動などが定義されている。

フレームの起動から終了までの流れ

このフレームワークがどのように用いられるかを、FactoryTest のコードを追うことによって見ていく。

起動

起動後、まず setup() 内から SysInit_start() が呼ばれる。

ここでは、はじめに表示する Frame である Frame_Main のインスタンスを作成し、それを EPDGUI_PushFrame を用いて frame_stack へ push している。

    // Frame_Main の新規インスタンスを作成。
    Frame_Main *frame_main = new Frame_Main();
    // EPDGUI_PushFrame でそのインスタンスを frame_stack へ Push。
    EPDGUI_PushFrame(frame_main);

このように、EPDGUI では Frame を frame_stack に push することで実行することができる。

続いて実行予定のフレームの全てを EPDGUI_AddFrame を用いて frame_map へ追加していく。これにより、名称を指定することで EPDGUI_GetFrame を用いて Frame を得ることができるようになる。ぶっちゃけこの構造はいらないんじゃないかと思う(実行したい Frame をその都度 frame_stack に push するのではだめなのか?)が、FactoryTest で用いられている方法なのでそれに従っておく。

    // 実行予定のフレームのインスタンスを作成。
    Frame_FactoryTest *frame_factorytest = new Frame_FactoryTest();
    // EPDGUI_AddFrame でそのインスタンスを名前付きで frame_map へ Add。
    EPDGUI_AddFrame("Frame_FactoryTest", frame_factorytest);
    // ...
    // ほかにも実行予定のフレームを frame_map へ登録していく。
    /// ...

実行

setup() が終了すると、loop() 内の EPDGUI_MainLoop() が実行される。

この中では、frame_stack の一番上にある Frame を frame_stack.top() で取ってきたのちに、そのフレームを初期化(frame->init(args) を実行)し、EPDGUI_Run(frame) を実行している。この EPDGUI_Run は frame が終了する際に終了する。つまり各 Frame が実行〜終了のプロセスを終えるごとに EPDGUI_MainLoop() は1回実行されるということになる。

EPDGUI_MainLoop() から実行された EPDGUI_Run の中にも while loop がある。

ここでは、まずその Frame が実行状態にあることを確認したのちに、frame->fun() を1回実行する。ゆえに各 Frame でループ実行したい処理がある場合は1ループぶんを frame->run() 内に実装すればよい。run() の戻り値が 0 のときその Frame は終了させられるので、終了したいとき以外は 0 以外を返さなければならない。

frame->run() が実行されたのち、タッチ状態が判定され、変化があれば EPDGUI_Process(x,y) が実行される。

EPDGUI_Process(x,y) では、epdgui_object_list (表示されているボタンなどの要素が全て入っている list)内の全てに対して UpdateState(x,y) を実行する。ゆえに、タッチイベントに反応させたい要素を作るには、UpdateState を実装したのちに要素のインスタンスを EPDGUI_AddObject を用いて epdgui_object_list 内に登録すればよい。

ここまでが EPDGUI_Run のループである。ループが終了するとまたループの頭に戻る。

終了

EPDGUI_Run のループ内で、PopFrame() を実行したのちに _is_run を 0 にすると、次の EPDGUI_Run 内のループ中に frame->exit() が呼ばれてフレームは終了される。ゆえにフレーム終了時に行いたい処理は frame->exit() 内に実装すればよい。

フレーム終了後は EPDGUI_MainLoop() が再度頭から実行され、frame_stack.top() にある次のフレームが EPDGUI_Run(frame) される。

HelloWorld アプリを実装する

いよいよアプリの実装に入る。

まずは座標などを決めるために、何らかのお絵かきアプリを用いて目標とする画面を作ってみる。私は下のような画面を作成した。

f:id:KainokiKaede:20210417183601j:plain

続いて frame_helloworld.cpp, frame_helloworld.h を作る。ヘッダファイルでは変数定義のみを行っている。frame_helloworld.cpp について解説する。

前述したように、新しい Frame を作成するにあたって実装する必要があるのは、コンストラクタ・デストラクタ・init・run・exit である。ただし run, exit の際に何もしなくていいなら、空の関数が基底クラスで定義されているので再定義は不要である。

#include "frame_helloworld.h"

Frame_HelloWorld::Frame_HelloWorld(void)
{
    // コンストラクタ。

    // _frame_name を定義する。
    _frame_name = "Frame_HelloWorld";

    // Frame(アプリ)名を上段に表示する。
    _canvas_title->drawString("HelloWorld", 270, 34);

    // 左上に表示される、ホーム画面に戻るボタンを作成する。
    // exitbtn 関数 は Frame_Base に定義されている。実行すると _key_exit 変数に戻るボタンのインスタンスが作成される。
    exitbtn("Home");
    // ボタンから指が離れたときに、コールバック関数に渡す 0 番目の変数を定義する。ここでは _is_run を渡している。
    _key_exit->AddArgs(EPDGUI_Button::EVENT_RELEASED, 0, (void*)(&_is_run));
    // ボタンから指が離れたときに実行されるコールバック関数 exit_cb(Frame_Base で定義されている)をバインドしている。
    _key_exit->Bind(EPDGUI_Button::EVENT_RELEASED, &Frame_Base::exit_cb);

    // Hello World と表示するボタンのインスタンスを作成している。位置は前述のお絵かきアプリで作成した画面から算出する。
    _key_helloworld = new EPDGUI_Button("Hello M5Paper World!", 30, 137, 480, 190, EPDGUI_Button::STYLE_DEFAULT);
}

Frame_HelloWorld::~Frame_HelloWorld(void)
{
    // デストラクタ。とくに何もしない。
}

int Frame_HelloWorld::init(epdgui_args_vector_t &args)
{
    // init.

    // まずこの Frame が実行中であるフラグを立てる。_is_run = 0 の状態で次のループが始まると Frame は終了される。
    _is_run = 1;
    // 画面を全消しする。
    M5.EPD.Clear();
    // アプリ名を上段に表示する。
    _canvas_title->pushCanvas(0, 8, UPDATE_MODE_NONE);
    // epdgui_object_list に Hello World を表示しているボタンを登録している。こうしなければボタンは表示されない。
    EPDGUI_AddObject(_key_helloworld);
    // 同様に、epdgui_object_list に戻るボタンを登録している。
    EPDGUI_AddObject(_key_exit);
    // FactoryTest の Frame は 3 とか 6 とか 9 とかを返している。この値を参照するコードはないのでテキトーでよさそう。
    return 3;
}

// run, exit のときには何もしなくてよいので定義していない。

ここまでで HelloWorld フレームは作成できた。

続いて FactoryTest からこのフレームを実行できるようにする。変更点は GitHub の diff のほうが見やすいのでそちらを見ていただいてもよい。

frame/frame.h に frame_helloworld.h を追加。これはもしかしたら不要かもしれない。

#include "frame_helloworld.h"

systeminit.cpp に次を追加。これで frame_map に "Frame_HelloWorld" のキーで HelloWorld フレームのインスタンスを作成できた。

        Frame_HelloWorld *frame_helloworld = new Frame_HelloWorld();
        EPDGUI_AddFrame("Frame_HelloWorld", frame_helloworld);

92x92 のアイコンを作成して、

f:id:KainokiKaede:20210417183532p:plain

M5Paper のリポジトリに付属しているツールである image2gray.py でアイコンを  ImageResource_HelloWorld.h に変換。変換先のファイルを一度開いて、ifndef, define の行を書き換えないとファイルが読み込まれないので注意。

#ifndef IMAGERESOURCE_HELLOWORLD_H
#define IMAGERESOURCE_HELLOWORLD_H
// ...

frame_main.cpp に新しいボタンを追加して、それを押すと HelloWorld フレーム(アプリ)が開くように変更。

#include "frame_helloworld.h"
#include "../resources/ImageResource_HelloWorld.h"

enum{
    kKeyHelloWorld
}

// HelloWorld アプリのボタンを押したときに呼ばれるコールバック関数。
// Frame_HelloWorld のインスタンスを frame_stack へ push している。
void key_helloworld_cb(epdgui_args_vector_t &args)
{
    Frame_Base *frame = EPDGUI_GetFrame("Frame_HelloWorld");
    if(frame == NULL)
    {
        frame = new Frame_HelloWorld();
        EPDGUI_AddFrame("Frame_HelloWorld", frame);
    }
    EPDGUI_PushFrame(frame);
    *((int*)(args[0])) = 0;
}

// ...

_key[8] = new EPDGUI_Button("HelloWorld", 20, 390, KEY_W, KEY_H);

// ...
    _key[kKeyHelloWorld]->CanvasNormal()->pushImage(0, 0, 92, 92, ImageResource_helloworld_icon_92x92);
    *(_key[kKeyHelloWorld]->CanvasPressed()) = *(_key[kKeyHelloWorld]->CanvasNormal());
    _key[kKeyHelloWorld]->CanvasPressed()->ReverseColor();
    _key[kKeyHelloWorld]->AddArgs(EPDGUI_Button::EVENT_RELEASED, 0, (void*)(&_is_run));
    _key[kKeyHelloWorld]->Bind(EPDGUI_Button::EVENT_RELEASED, key_helloworld_cb);

    _names->fillCanvas(0);
    _names->drawString("HelloWorld", 20 + 46, 16);
    _names->pushCanvas(0, 337+151, mode);

int Frame_Main::init(epdgui_args_vector_t &args)
    for(int i = 0; i < 9; i++)
    {
        EPDGUI_AddObject(_key[i]);
    }

これにて終了。コンパイルして M5Paper に送ると HelloWorld アプリが出現しているはずである。

結語

この記事によって M5Paper の GUI 開発がさかんになれば幸いである。