バックエンド通信
ほとんどのアプリケーションでは、UIを何らかの方法でシステムのその他のコンポーネントに接続し、データを送受信する必要があります。 このために、ハードウェア・ペリフェラル(センサ・データ、A/D変換、シリアル通信など)とのインタフェースや、他のソフトウェア・モジュールとのインタフェースを行うことが考えられます。
この記事では、この接続を実装するために推奨されるソリューションについて説明します。
最初の方法はクイック・アンド・ダーティなアプローチで、主にプロトタイプを目的とするものです。一方で、2番目の方法は、現実世界のアプリケーションでUIをその他のコンポーネントと正しく接続するための堅実なアーキテクチャーに基づくものです。
この記事の最後には、両方の方法を紹介するサンプルへのリンクがあります。
モデル・クラス
すべてのTouchGFXアプリケーションにはモデル・クラスがあります。これは、UIの状態情報を保存するのとは別に、周辺システムへのインタフェースとして機能する目的も持っています。 モデル・クラスによって、両方のハードウェア・ペリフェラルを参照するだけでなく、システム内の他のOSタスクとも通信します。 通常、他のソフトウェア・モジュールやハードウェアに、個別のビュー・クラスでアクセスするのは良い設計ではありません。
Further reading
以下の理由から、モデル・クラスはこうしたインタフェース・コードの配置によく適しています。
- モデル・クラスには
tick
関数があります。これは毎フレームに自動的に呼び出され、他のサブモジュールからのイベントを探して反応するために実装されます。 - モデル・クラスには現在アクティブなプレゼンタへのポインタがあります。これにより、受信するイベントをUIに通知することができます。
システムとのインタフェース
周辺システムとのインタフェースには2つの方法があります。GUIタスクから直接サンプリングする方法と、セカンダリ・タスクからサンプリングする方法です。
GUIタスクからのサンプリング
周辺システムとのインタフェースのために最適な方法は、サンプリングが必要とされる頻度、それにかかる時間の長さ、かかる時間がどれほど重要かによって決まります。
これらの点での要件が緩やかな場合、最も単純なアプローチは、Model::tick
関数で周囲のシステムを直接サンプリングすることです。
サンプリングの実行頻度がフレーム・レート(通常は60Hz前後)よりも低い場合は、単純にカウンタを追加し、N番目のティックごとにサンプリングを実行します。 この方法で実行する場合、サンプリング操作をある程度高速(通常は1ms以下)で行う必要があり、そうでないとフレーム・レートに支障が出始めます。このサンプリングはGUIタスクのコンテキストで実行され、フレームの描画を遅延させるからです。
セカンダリ・タスクからのサンプリング
あるいは、GUIタスクのコンテキスト内で周辺システムと直接インタラクションを実行するのが望ましくない場合、サンプリングを実行する新しいOSタスクを作成することができます。
このタスクは、特定のシナリオで必要とされる正確な時間間隔で実行するように設定できます。 また、ユーザのニーズに応じて、この新しいタスクの優先順位をGUIタスクよりも低く、または高く設定することもできます。
優先順位を高くすると、実行中のGUIタスクの内容に関わらず、指定した正確な時間にこちらのタスクが実行されることが保証されます。 ただし欠点として、それがCPUの負荷が高いプロセスである場合、UIのフレーム・レートに影響する恐れがあります。
一方、サンプリングが時間的に重要でない場合は、GUIタスクより低い優先度を割り当て、UIフレームレートが周囲のシステムのサンプリングに影響されないようにできます。 描画中は、GUIタスクのスリープ時間が長く(DMAベースのピクセル転送の完了を待つ場合など)、優先度の低いタスクでもかなりの頻度で実行可能になるため、大半のアプリケーションにとってはこれで十分です。
セカンダリ・タスクのアプローチを使用する場合には、RTOSでサポートされるタスク間メッセージング・システムを利用することをお勧めします。 すべてではありませんが、ほとんどのRTOSはキュー / メール・メカニズムを備えているので、タスク間でデータ(通常はユーザ定義のC構造体、バイト配列、または単純な整数)の送信が可能です。 新しいデータをGUIタスクに伝送するには、UIタスク用のメールボックスまたはメッセージ・キューをセットアップし、このメッセージング・システムを使用してデータをGUIタスクに送信します。 次に、Model::tick
でGUIタスクのメールボックスをポーリングして、新着データがないかチェックします。 その場合、データを読み取り、それに応じてUIを更新します。
UIへのデータの伝播
GUIタスクからのサンプリングとセカンダリ・タスクからのサンプリングのどちらを使用する場合でも、Model::tick
関数によって、GUIタスクはUIに表示すべき新しいデータを認識することになります。 周辺システムへのインタフェースとして機能するのとは別に、モデル・クラスには状態データを保存する役目もあることを思い出してください。つまり、いくつか状態変数を更新する必要もあるかもしれないということです。
温度センサがシステムに接続されているシンプルな例を考えてみましょう。ここでは現在の温度がUIに表示されます。 準備として、以下をサポートするようにモデル・クラスを拡張します。
Model.hpp
class Model
{
public:
// Function that allow your Presenters to read current temperature.
int getCurrentTemperature() const { return currentTemperature; }
// Called automatically by framework every tick.
void tick();
...
private:
// Variable storing last received temperature;
int currentTemperature;
...
};
上記によって、Presenters
が現在の温度をモデルに要求できるようになります。これで、温度を表示するスクリーンに入ると、プレゼンタがこの値をUI(ビュー)に設定できます。 次に必要なのは、新しい温度情報を受信したときに再びUIを更新できるようにすることです。 このためには、モデルが現在アクティブなプレゼンタへのポインタを持っていることを利用します。 このポインタのタイプはインタフェース(ModelListener
)で、適用可能なアプリケーション固有のイベントを反映するように変更することができます。
ModelListener.hpp
class ModelListener
{
public:
// Call this function to notify that temperature has changed.
// Per default, use an empty implementation so that only those
// Presenters interested in this specific event need to
// override this function.
virtual void notifyTemperatureChanged(int newTemperature) {}
};
これで、このインタフェースに接続できるようになりました。あとは、受信する"新しい温度"のイベントの実際のサンプリングを行います。
Model.cpp
void Model::tick()
{
// Pseudo-code for sampling data
if (OS_Poll(GuiTaskMBox))
{
// Here we assume that you have defined a "Message" struct containing type and data,
// along with some event definitions.
struct Message msg = OS_Read(GuiTaskMBox);
if (msg.eventType == EVT_TEMP_CHANGED)
{
// We received information that temperature has changed.
// First, update Model state variable
currentTemperature = msg.data;
// Second, notify the currently active Presenter that temperature has changed.
// The modelListener pointer points to the currently active Presenter.
if (modelListener != 0)
{
modelListener->notifyTemperatureChanged(currentTemperature);
}
}
}
}
上記のアプローチでは次の2つが確実になります。
currentTemperature
変数は常に最新状態なので、プレゼンタはいつでも現在の温度を取得できます。Presenter
は温度の変化が即座に通知され、適切なアクションを実行できます。
MVPパターンの1つの利点は、現在のスクリーンに応じて通知を個別に処理できることです。 たとえば、何かの設定メニューを表示しているとき(MainMenuPresenter/MainMenuViewがアクティブな場合など)に温度変化のイベントが発生した場合、そこに現在の温度は関係しません。
notifyTemperatureChanged
関数はデフォルトで空実装なので、この通知はMainMenuPresenter
によって単に無視されます。 一方、TemperatureControlPresenter
がある場合は、このプレゼンタでnotifyTemperatureChanged
関数をオーバーライドし、更新後の温度を表示するようにビューに通知することができます。
TemperatureControlPresenter.hpp
class TemperatureControlPresenter : public ModelListener
{
public:
// override the empty function.
virtual void notifyTemperatureChanged(int newTemperature) {
view.setTemp(newTemperature);
}
};
ビュー・クラスのTemperatureControlView
では、当然ながらsetTemp
メソッドを実装する必要があります。
UIから周辺システムへのデータの伝送
UIから周辺システムへのデータ / イベントの反対方向の伝送も、モデルを通してほぼ同じ方法で実行されます。 新しいターゲット温度を設定する機能を追加する必要が生じた場合、これまでの例を続行すると、モデルに以下を追加することになります。
Model.hpp
void setNewTargetTemperature(int newTargetTemp)
{
// Pseudo-code for sending an event to a task responsible for controlling temperature.
struct Message msg;
msg.eventType = EVT_SET_TARGET_TEMP;
msg.data = newTargetTemp;
OS_Send(SystemTaskMBox, &msg);
}
ユーザがUIで新しいターゲット温度を設定した場合には、ビューが、モデル・オブジェクトへのポインタを保持するプレゼンタに通知できるようにし、その結果setNewTargetTemperature
関数を呼び出すことができます。
例
以下の例は、特定のデモ・ボード向けに設定された完全なデモですが、ここに示されているコードの多くは、他のデモ・ボードやカスタム・ハードウェアで再利用できます。
GUIタスクから
STM32F746向けの実施例は、モデル・クラスでボタンをサンプリングし、LEDを直接制御する方法を示しています。 この例では、MVPアーキテクチャを使用して、2つのビューとモデル・クラス間で値とイベントを転送しています。 モデル・クラスはボタンをサンプリングし、アプリケーションの状態と一致するようにLEDを更新します。
STM32F429向けの実施例は、モデル・クラスでボタンをサンプリングする方法を示しています。 この例では、MVPアーキテクチャを使用してボタン・イベントをビューに転送しています。
他のタスクから
STM32F469向けの実施例は、別個のスレッドでアナログ入力をサンプリングする方法を示しています。 この例では、MVPアーキテクチャを使用してアナログ値をビューに転送しています。
この実施例は、タスク間の通信とUIとの間の伝播を示しています。 独自のセットアップの際に、ヒントとして使用できます。 この例では、Cコードで実装されたバックエンド・システムとC++のTouchGFX GUIの間で通信が行われています。 この例はSTM32F746G-DISCOボードで、FreeRTOS上で実行されています。
複数タスクから
この実施例は、2018年5月28日のTouchGFXウェビナー「Integration with your hardware」で示されたものです。
このアプリケーションはSTM32F769-DISCOボード向けに設計されており、LEDおよびUSER BUTTONとのインタラクションによって、Cコードとハードウェア・ペリフェラルの両方をTouchGFXアプリケーションに統合する方法を示しています。
このアプリケーションではGPIOモードでボタンを設定します。 ボタンの状態をbtntask.cでサンプリングし、ボタンが押されたら、GUIメッセージ・キューを通してメッセージが渡されるという動作になっています。 これにより、ボタンを押したままアプリケーション内のアニメーションを進めることができます。
このアプリケーションでは3つのFreeRTOSタスクを使用しています。 1つはGUI用、残り2つは各ペリフェラル(LEDとUSER Button)用です。
タスクおよび外部の割込みラインから
この実施例は、2018年5月28日のTouchGFXウェビナー「Integration with your hardware」で示されたものです。
このアプリケーションはSTM32F769-DISCOボード向けに設計されており、LEDおよびUSER BUTTONとのインタラクションによって、Cコードとハードウェア・ペリフェラルの両方をTouchGFXアプリケーションに統合する方法を示しています。
このアプリケーションでは、EXTIモードでボタンを設定します(外部割込みライン0)。 ボタンが押下されると割込みを受信し、その後割込みが消去されるという動作になっています。 GPIOと同じ動作はできず、代わりに1ステップずつアニメーションを進めることになります。割込みを受信するたびに、GUIメッセージ・キューを通してメッセージが送信されるのみだからです。
このアプリケーションでは2つのFreeRTOSタスクを使用しています。 1つはGUI用、もう1つはLED用です。 (このアプリケーションでは複数タスクのデモのボタン・タスクがアクティブなままになっており、ペリフェラル・インタラクションのコードが割込みハンドラに移動した例を示しています。)