🆓

【Wio Terminal】FreeRTOSで画面遷移(求助言)

2024/04/07に公開

Wio Terminalで画面遷移の練習

世にはM5Stackを使う方が多いようですが、Wio Terminalも亦、画面を有するArduino機であります。Wio TerminalRaspberry Piと同じ40ピンを持つことから、Raspberry Piに慣れた者としては親しみやすい機器です。更に、標準搭載しているセンサー等の周辺機器も比較的豊富ですから、これ一つでも電子工作は完結します。

とは言え、実態はやはりArduinoRaspberry Piとは勝手が全く異なるため、基本的な扱いを覚えるのにも逐一時間を要しています。検索しても前例が少ないか、全くない。やりたいことはあっても、その実現方法が分からないということが屢屢で、試行錯誤の連続です。本記事はその一環として、このWio Terminalで画面遷移を実装できないか、試行した結果を述べるものです。

概要

本記事で作成したプログラムの概要を述べます。

画面遷移
画面遷移の概要

Wio Terminal本体には、ABC三つのボタンが設けられています。今回作成したプログラムは、このボタンと画面を連動させるという単純なものです。

Wio Terminal外観
Wio Terminalの概観(スイッチサイエンスのページから引用)

横一列に並んでいるUser Programmable Buttonsが、機体中央から端に向かってそれぞれABCボタンとなっています。

プログラム

プログラムは私のGitHubにも共有しています。

https://github.com/amenaruya/WioTerminal_ScreenTransition_FreeRTOS/tree/main

一応、本記事でも記載しておきます。Wio TerminalArduino IDEで扱うための初期設定は済んでいる前提です。設定手順は公式のWikiから御覧下さい。

https://wiki.seeedstudio.com/Wio-Terminal-Getting-Started/

プログラム

プログラム概説

簡単ながらプログラムについて説明しています。

inoファイル

本プログラムではFreeRTOSを利用しています。但し、このやり方が正しいのかはよくわかりません。「斯く訂正するが宜しい」との御意見は歓迎しております。

/*
画面遷移
トップ画面からA画面とB画面の孰れかに遷移できる
Aボタンを押せば画面Aへ
Bボタンを押せば画面Bへ遷移する
遷移先でCボタンを押すことでトップ画面に復帰する
*/

#include <Seeed_Arduino_FreeRTOS.h>
#include "queue.h"

#include "enumerates.h"
#include "readButton.h"
#include "TFT_Display.h"

/* タスクの許容領域 */
#define STACK_SIZE  256
/* キューの容量 */
#define QUEUE_SIZE  16

TaskHandle_t        taskHandlerLCD,
                    taskHandlerButton;

QueueHandle_t       queueButton;

/*
TFT LCDの初期化
クラスメンバーにそのまま含めたり函数内で初期化したりすると
正しく動作しない
*/
TFT_eSPI LIQUID_CRYSTAL_DISPLAY;

static void ThreadButton(void* pvParameters);
static void ThreadLCD(void* pvParameters);

void setup() {
    /* キューを作る */
    queueButton = xQueueCreate(
        QUEUE_SIZE,
        sizeof(Wio3Button)
    );
    /* タスクを作る */
    xTaskCreate(
        ThreadLCD,
        "TFT LCD",
        STACK_SIZE,
        NULL,
        tskIDLE_PRIORITY + 1,
        &taskHandlerLCD
    );
    xTaskCreate(
        ThreadButton,
        "Button",
        STACK_SIZE,
        NULL,
        tskIDLE_PRIORITY,
        &taskHandlerButton
    );
    /* タスクを開始する */
    vTaskStartScheduler();
}
void loop() {}

/* ボタン入力処理 */
static void ThreadButton(void* pvParameters) {
    (void) pvParameters;

    /* 初期化 */
    ButtonReader buttonReader;
    /* 入力検知 */
    ButtonInputDetection resultCode;

    Wio3Button* (ButtonReader::*pmFpW3BGetInput)()
        = &ButtonReader::mFpW3BGetInput;
    void (ButtonReader::*pmFReadButtonInput)(
        ButtonInputDetection* const pResult
    )   = &ButtonReader::mFReadButtonInput;

    while (1) {
        (buttonReader.*pmFReadButtonInput)(&resultCode);
        if (resultCode == INPUT_DETECTED) {
            /* 入力を検知した場合ボタンの種類をqueueに送信する */
            xQueueSend(
                queueButton,
                (buttonReader.*pmFpW3BGetInput)(),
                portMAX_DELAY
            );
        }
        delay(100);
    }
}

/* 画面処理 */
static void ThreadLCD(void* pvParameters) {
    (void) pvParameters;

    /* 初期化 */
    Displayer displayer(&LIQUID_CRYSTAL_DISPLAY);
    /* ボタン入力を受信する変数 */
    Wio3Button receivedInputButton;

    void (Displayer::*pmFChangeDisplay)(
        const Wio3Button* const pButtonInput
    )   = &Displayer::mFChangeDisplay;

    while (1) {
        /* ボタン入力を受信する */
        xQueueReceive(
            queueButton,
            &receivedInputButton,
            portMAX_DELAY
        );
        (displayer.*pmFChangeDisplay)(&receivedInputButton);
        delay(150);
    }
}

機能分離

具体的な機能は分離しています。ファイルとしては整理されましたが、結合に四苦八苦した末、このような形に落ち着いています。なお、Arduino特有の関数等は、通常のC/C++プログラムは対応していないため、必要なものだけincludeしています。

列挙体

種々の状態管理に用いている値を集約しています。

enumerates.h
#ifndef __ENUMERATES__
#define __ENUMERATES__

typedef enum {
    BUTTON_A,
    BUTTON_B,
    BUTTON_C
} Wio3Button;

typedef enum {
    NO_INPUT,
    INPUT_A,
    INPUT_B,
    INPUT_C
} LatestInputState;

typedef enum {
    MAIN_SCREEN,
    DISPLAY_A,
    DISPLAY_B
} DisplayState;

typedef enum {
    NO_INPUT_DETECTED,
    INPUT_DETECTED
} ButtonInputDetection;

#endif

ボタン処理

ボタン入力に関する処理をクラスにしています。長押しによって処理が連発しないよう、直前にボタンが押されていたか否かを変数に記録するようにしています。

readButton.h
#ifndef __READ_BUTTON__
#define __READ_BUTTON__

#include "enumerates.h"

class ButtonReader {
private:
    /*
    enumの値をqueueで扱うには変数を設ける
    この変数の参照を送ればよい
    */
    Wio3Button mInputButton;
    /*
    一度のボタン操作に対して処理は一度きりとするために、
    直前の入力の有無を管理する
    */
    LatestInputState mLatestInput;

public:
    ButtonReader();
    // ~ButtonReader();
    /* 入力検知 */
    void mFReadButtonInput(ButtonInputDetection* const pResult);
    /* getter */
    Wio3Button* mFpW3BGetInput();
};

#endif

readButton.cpp
/* WIO_KEY_A WIO_KEY_B WIO_KEY_C */
#include <variant.h>
/* pinMode() */
#include <wiring_digital.h>
/* HIGH LOW */
#include <wiring_constants.h>
/* delay() */
#include <delay.h>

#include "readButton.h"
#include "enumerates.h"

ButtonReader::ButtonReader() {
    /* ボタンA・B・Cピンを入力に設定する */
    pinMode(WIO_KEY_A, INPUT);
    pinMode(WIO_KEY_B, INPUT);
    pinMode(WIO_KEY_C, INPUT);
    /* 無入力を初期状態とする */
    this -> mLatestInput = NO_INPUT;
}
// ButtonReader::~ButtonReader() {}

void ButtonReader::mFReadButtonInput(ButtonInputDetection* const pResult) {
    /* 入力検知の有無を記録する */
    *pResult = NO_INPUT_DETECTED;
    if (this -> mLatestInput == NO_INPUT) {
        /* 直前に入力が無かった場合 */
        if (digitalRead(WIO_KEY_A) == LOW) {
            this -> mInputButton = BUTTON_A;
            this -> mLatestInput = INPUT_A;
            *pResult = INPUT_DETECTED;
        }
        else if (digitalRead(WIO_KEY_B) == LOW) {
            this -> mInputButton = BUTTON_B;
            this -> mLatestInput = INPUT_B;
            *pResult = INPUT_DETECTED;
        }
        else if (digitalRead(WIO_KEY_C) == LOW) {
            this -> mInputButton = BUTTON_C;
            this -> mLatestInput = INPUT_C;
            *pResult = INPUT_DETECTED;
        }
    }
    else {
        /*
        直前に入力があった場合
        現在孰れのボタンも押されていない場合のみ
        NO_INPUTに更新する
        */
        if (
            digitalRead(WIO_KEY_A) == HIGH &&
            digitalRead(WIO_KEY_B) == HIGH &&
            digitalRead(WIO_KEY_C) == HIGH
        ) {
            this -> mLatestInput = NO_INPUT;
        }
    }
}

Wio3Button* ButtonReader::mFpW3BGetInput() {
    return &(this -> mInputButton);
}

画面処理

画面表示に関する処理をクラス化しています。TFT_eSPIインスタンスはinoファイルにて定義しています。この定義を関数の中で行う場合、あるいはクラスメンバーとしてポインターを使わずに記述した場合、画面処理が正常に行われません

TFT_Display.h
#ifndef __TFT_DISPLAY__
#define __TFT_DISPLAY__

#include "TFT_eSPI.h"
#include "enumerates.h"

/* 黒紅 */
#define TFT_KUROBENI    0x2926
/* 鴇鼠 */
#define TFT_TOKINEZU    0xDE7A

class Displayer {
private:
    TFT_eSPI* pTft;
    /* 画面表示の状態 */
    DisplayState currentDisplayState;
    void privateFShowMainScreen();
    void privateFChangeScreen(const DisplayState newState);
    void privateFChangeDisplayState(const DisplayState newState);

public:
    Displayer(TFT_eSPI* const pTftLcd);
    // ~Displayer();
    void mFChangeDisplay(const Wio3Button* const pButtonInput);
};

#endif
TFT_Display.cpp
#include "TFT_Display.h"
#include "enumerates.h"

Displayer::Displayer(TFT_eSPI* const pTftLcd):
pTft(pTftLcd) {
    /* TFT LCDの準備 */
    (this -> pTft) -> begin();
    (this -> pTft) -> setRotation(3);
    (this -> pTft) -> setTextSize(3);
    (this -> pTft) -> setTextColor(TFT_KUROBENI);
    /* トップ画面を表示する */
    this -> privateFShowMainScreen();
}
// Displayer::~Displayer() {}

void Displayer::privateFShowMainScreen() {
    /* 背景初期化 */
    (this -> pTft) -> fillScreen(TFT_TOKINEZU);
    /* ボタン指示 */
    (this -> pTft) -> drawString("A", 166, 2);
    (this -> pTft) -> drawString("B", 82, 2);
    /* メッセージ */
    (this -> pTft) -> drawString("Push button", 60, 110);
    (this -> pTft) -> drawString("A or B", 110, 135);
    this -> privateFChangeDisplayState(MAIN_SCREEN);
}

void Displayer::privateFChangeScreen(const DisplayState newState) {
    if (this -> currentDisplayState != newState) {
        char* message;
        switch (newState) {
            case DISPLAY_A:
                message = "Button A";
                break;

            case DISPLAY_B:
                message = "Button B";
                break;
            
            default:
                message = "hello?";
                break;
        }
        (this -> pTft) -> fillScreen(TFT_TOKINEZU);
        (this -> pTft) -> drawString(message, 85, 110);
        (this -> pTft) -> drawString("C", 10, 2);
        (this -> pTft) -> drawString("Push C to back.", 25, 205);
        this -> privateFChangeDisplayState(newState);
    }
}

void Displayer::privateFChangeDisplayState(const DisplayState newState) {
    if (this -> currentDisplayState != newState)
        this -> currentDisplayState = newState;
}

void Displayer::mFChangeDisplay(const Wio3Button* const pButtonInput) {
    switch (*pButtonInput) {
        case BUTTON_A:
            /* トップ画面ならA画面に遷移する */
            if (this -> currentDisplayState == MAIN_SCREEN) {
                this -> privateFChangeScreen(DISPLAY_A);
            }
            break;

        case BUTTON_B:
            /* トップ画面ならB画面に遷移する */
            if (this -> currentDisplayState == MAIN_SCREEN) {
                this -> privateFChangeScreen(DISPLAY_B);
            }
            break;

        case BUTTON_C:
            /* トップ画面でないならトップ画面に戻る */
            if (this -> currentDisplayState != MAIN_SCREEN) {
                this -> privateFShowMainScreen();
            }
            break;

        default:
            break;
    }
}
旧プログラム

旧プログラム概説

こちらのプログラムは、TFT_eSPIインスタンスの扱いが気に食わなかったので、後に修正しました。これに関係ない箇所はそのままです。

inoファイル

#include <Seeed_Arduino_FreeRTOS.h>
#include "queue.h"

#include "enumerates.h"
#include "readButton.h"
#include "TFT_Display.h"

/* タスクの許容領域 */
#define STACK_SIZE  256
/* キューの容量 */
#define QUEUE_SIZE  16

TaskHandle_t        taskHandlerLCD,
                    taskHandlerButton;

QueueHandle_t       queueButton;

/*
TFT LCDの初期化
クラスメンバーに含めたり函数内で初期化したりすると
正しく動作しない
*/
TFT_eSPI LIQUID_CRYSTAL_DISPLAY;

static void ThreadButton(void* pvParameters);
static void ThreadLCD(void* pvParameters);

void setup() {
    /* キューを作る */
    queueButton = xQueueCreate(
        QUEUE_SIZE,
        sizeof(Wio3Button)
    );
    /* タスクを作る */
    xTaskCreate(
        ThreadLCD,
        "TFT LCD",
        STACK_SIZE,
        NULL,
        tskIDLE_PRIORITY + 1,
        &taskHandlerLCD
    );
    xTaskCreate(
        ThreadButton,
        "Button",
        STACK_SIZE,
        NULL,
        tskIDLE_PRIORITY,
        &taskHandlerButton
    );
    /* タスクを開始する */
    vTaskStartScheduler();
}
void loop() {}

/* ボタン入力処理 */
static void ThreadButton(void* pvParameters) {
    (void) pvParameters;

    /* 初期化 */
    ButtonReader buttonReader;
    /* 入力検知 */
    ButtonInputDetection resultCode;

    Wio3Button* (ButtonReader::*pmFpW3BGetInput)()
        = &ButtonReader::mFpW3BGetInput;
    void (ButtonReader::*pmFReadButtonInput)(
        ButtonInputDetection* const pResult
    )   = &ButtonReader::mFReadButtonInput;

    while (1) {
        (buttonReader.*pmFReadButtonInput)(&resultCode);
        if (resultCode == INPUT_DETECTED) {
            /* 入力を検知した場合ボタンの種類をqueueに送信する */
            xQueueSend(
                queueButton,
                (buttonReader.*pmFpW3BGetInput)(),
                portMAX_DELAY
            );
        }
        delay(100);
    }
}

/* 画面処理 */
static void ThreadLCD(void* pvParameters) {
    (void) pvParameters;

    /* 初期化 */
    Displayer displayer(&LIQUID_CRYSTAL_DISPLAY);
    /* ボタン入力を受信する変数 */
    Wio3Button receivedInputButton;

    void (Displayer::*pmFChangeDisplay)(
        const Wio3Button* const pButtonInput,
        TFT_eSPI* const pTftLcd
    )   = &Displayer::mFChangeDisplay;

    while (1) {
        /* ボタン入力を受信する */
        xQueueReceive(
            queueButton,
            &receivedInputButton,
            portMAX_DELAY
        );
        (displayer.*pmFChangeDisplay)(&receivedInputButton, &LIQUID_CRYSTAL_DISPLAY);
        delay(150);
    }
}

画面処理

これを書いた時は、TFT_eSPIインスタンスのポインターをクラスメンバー変数として保有するという発想がなかったため、関数の引数からポインターを受けています。

TFT_Display.h
#ifndef __TFT_DISPLAY__
#define __TFT_DISPLAY__

#include "TFT_eSPI.h"
#include "enumerates.h"

/* 黒紅 */
#define TFT_KUROBENI    0x2926
/* 鴇鼠 */
#define TFT_TOKINEZU    0xDE7A

class Displayer {
private:
    /* 画面表示の状態 */
    DisplayState currentDisplayState;
    void privateFShowMainScreen(TFT_eSPI* const pTftLcd);
    void privateFChangeScreen(const DisplayState newState, TFT_eSPI* const pTftLcd);
    void privateFChangeDisplayState(const DisplayState newState);

public:
    Displayer(TFT_eSPI* const pTftLcd);
    // ~Displayer();
    void mFChangeDisplay(const Wio3Button* const pButtonInput, TFT_eSPI* const pTftLcd);
};

#endif
TFT_Display.cpp
#include "TFT_Display.h"
#include "enumerates.h"

Displayer::Displayer(TFT_eSPI* const pTftLcd) {
    /* TFT LCDの準備 */
    pTftLcd -> begin();
    pTftLcd -> setRotation(3);
    pTftLcd -> setTextSize(3);
    pTftLcd -> setTextColor(TFT_KUROBENI);
    /* トップ画面を表示する */
    this -> privateFShowMainScreen(pTftLcd);
}
// Displayer::~Displayer() {}

void Displayer::privateFShowMainScreen(TFT_eSPI* const pTftLcd) {
    /* 背景初期化 */
    pTftLcd -> fillScreen(TFT_TOKINEZU);
    /* ボタン指示 */
    pTftLcd -> drawString("A", 166, 2);
    pTftLcd -> drawString("B", 82, 2);
    /* メッセージ */
    pTftLcd -> drawString("Push button", 60, 110);
    pTftLcd -> drawString("A or B", 110, 135);
    this -> privateFChangeDisplayState(MAIN_SCREEN);
}

void Displayer::privateFChangeScreen(const DisplayState newState, TFT_eSPI* const pTftLcd) {
    if (this -> currentDisplayState != newState) {
        char* message;
        switch (newState) {
            case DISPLAY_A:
                message = "Button A";
                break;

            case DISPLAY_B:
                message = "Button B";
                break;
            
            default:
                message = "hello?";
                break;
        }
        pTftLcd -> fillScreen(TFT_TOKINEZU);
        pTftLcd -> drawString(message, 85, 110);
        pTftLcd -> drawString("C", 10, 2);
        pTftLcd -> drawString("Push C to back.", 25, 205);
        this -> privateFChangeDisplayState(newState);
    }
}

void Displayer::privateFChangeDisplayState(const DisplayState newState) {
    if (this -> currentDisplayState != newState)
        this -> currentDisplayState = newState;
}

void Displayer::mFChangeDisplay(const Wio3Button* const pButtonInput, TFT_eSPI* const pTftLcd) {
    switch (*pButtonInput) {
        case BUTTON_A:
            /* トップ画面ならA画面に遷移する */
            if (this -> currentDisplayState == MAIN_SCREEN) {
                this -> privateFChangeScreen(DISPLAY_A, pTftLcd);
            }
            break;

        case BUTTON_B:
            /* トップ画面ならB画面に遷移する */
            if (this -> currentDisplayState == MAIN_SCREEN) {
                this -> privateFChangeScreen(DISPLAY_B, pTftLcd);
            }
            break;

        case BUTTON_C:
            /* トップ画面でないならトップ画面に戻る */
            if (this -> currentDisplayState != MAIN_SCREEN) {
                this -> privateFShowMainScreen(pTftLcd);
            }
            break;

        default:
            break;
    }
}

実行結果

以下に画面の様子を示します。

トップ画面

AボタンまたはBボタンを押すと、それに応じて各画面に移ります。Cボタンを押しても何も起こらないようにしています。

トップ画面
トップ画面の表示

画面A

Cボタンを押すと、トップ画面に戻ります。その他のボタンを押しても、何も起こらないようにしています。

画面A
画面Aの表示

メッセージの文法が普通に間違っています。恥ずかしい。プログラムは修正してあります。

画面B

画面A同様、Cボタンを押すと、トップ画面に戻ります。その他のボタンを押しても、何も起こらないようにしています。

画面B
画面Bの表示

画面が遷移するだけのプログラムですから、これだけでは無味乾燥というものです。しかし実現することはできたので、これを応用して何か作れましょう。個人的にも幾らか案があるので、いずれ実現しようと思います。

Discussion