Backend Communication
In most applications, the UI needs to be connected to the rest of your system somehow, and send and receive data. This could be interfacing with hardware peripherals (sensor data, A/D conversions, serial communication, ...) or interfacing with other software modules.
This article describes the recommended solutions for implementing this connection.
The first method is a "quick-and-dirty" approach, primarily intended for prototyping, whereas the second method is an architecturally sound way of properly connecting the UI with the remaining components in a real world application.
In the end of this article, we link to examples of using both methods.
The Model Class
All TouchGFX applications have a Model class, which apart from storing UI state information is also intended to function as the interface to your surrounding system. By this we are referring to both hardware peripherals but also communicating with other OS tasks in your system. It is normally not a good design to access other software modules or hardware in the individual View classes.
Further reading
The Model class is well suited for placing any such interface code because:
- The Model class has a
tick()
function which is automatically called every frame and can be implemented to look for and react to events from other sub-modules. - The Model class has a pointer to your currently active Presenter, in order to be able to notify the UI of incoming events.
System Interfacing
There are two ways of interfacing with the surrounding system, either by sampling directly from the GUI Task, or by sampling from a Secondary Task
Sampling from GUI Task
The best way of interfacing with surrounding system depends on how frequently you need to sample, how time consuming it is and how time critical it is.
If your requirements in those regards are lenient, the simplest approach is to just sample the surrounding system directly in the Model::tick
function.
If the sampling occurs less frequently than your frame rate (typically around 60Hz), you can just add a counter and only do the sampling every Nth tick. When done this way, your sampling operation must be somewhat fast (typically 1ms or less), otherwise your frame rate will begin to suffer, since the sampling is done in the context of the GUI task and will delay drawing the frame.
Sampling from Secondary Task
Alternatively, if it is not desirable to place the interaction with the surrounding system directly within the context of the GUI task, you can create a new OS task responsible for doing the sampling.
You can configure this task to run at the exact time intervals you need for your specific scenario. Also depending on your needs this new task can have a lower or higher priority than the GUI task.
If it has a higher priority, then you are guaranteed that it runs at exactly the times you have specified, regardless of what the GUI task is doing. This has the drawback that if it is a CPU consuming process it might impact the frame rate of the UI.
If on the other hand the sampling is not time critical, you can assign the task a lower priority than the GUI task, such that the UI frame rate is never affected by the sampling of the surrounding system. The GUI task will sleep a lot while rendering (e.g. when waiting for a DMA-based pixel transfer to complete) so lower priority tasks will be allowed to run quite frequently and this is therefore sufficient for the vast majority of applications.
If you use the secondary task approach, we recommend that you take advantage of the inter-task messaging system that is provided by your RTOS. Most, if not all, RTOSes have a queue/mail mechanism which allows you to send data (typically user-defined C structs, byte arrays or simple integers) from one task to another. In order to communicate new data to the GUI task, setup a mailbox or message queue for the UI task, and send the data to the GUI task using this messaging system. You can then Model::tick
poll the mailbox of the GUI task to check if any new data has arrived. In case, read the data and update the UI accordingly.
Propagating Data to UI
Regardless of whether you use Sampling from GUI Task or Sampling from Secondary Task, the Model::tick
function is the place where the GUI Task becomes aware of the new data to be shown in the UI. Apart from acting as an interface to your surrounding system, recall from earlier that the Model class is also responsible for holding state data, so there might be some state variables that needs to be updated too.
Let us consider a simple example where a temperature sensor is attached to the system, and that the current temperature is to be shown in the UI. In preparation, we will augment the Model class to support this:
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;
...
};
With the above, your Presenters
are able to ask the model about the current temperature, allowing a Presenter to set this value in the UI (the View) when entering a screen that displays the temperature. What we need to do now is to be able to update the UI again when new temperature information is received. To do this we take advantage of the fact that the Model has a pointer to your currently active Presenter. The type of this pointer is an interface (ModelListener
) which you can modify to reflect the application-specific events that are appropriate:
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) {}
};
Now that we have this interface hooked up, the remaining this is to do the actual sampling of incoming "new temperature" events Model::tick
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);
}
}
}
}
The approach above ensures two things:
- The
currentTemperature
variable is always up-to-date so that your Presenter can at any time obtain the current temperature. - The
Presenter
is immediately notified of temperature changes and can take appropriate action.
One advantage of the MVP pattern is that you achieve a separated handling of notifications depending on what screen you are currently on. Assume for instance that a temperature changed event occurs while displaying some kind of settings menu (e.g. MainMenuPresenter/MainMenuView is active) where the current temperature is of no relevance.
Since the notifyTemperatureChanged
function has a default empty implementation, this notification is simply disregarded by the MainMenuPresenter
. On the other hand, if you have a TemperatureControlPresenter
you can in this Presenter override the notifyTemperatureChanged
function and inform the View that it should display an updated temperature:
TemperatureControlPresenter.hpp
class TemperatureControlPresenter : public ModelListener
{
public:
// override the empty function.
virtual void notifyTemperatureChanged(int newTemperature) {
view.setTemp(newTemperature);
}
};
The View class TemperatureControlView
, must ofcourse implement the setTemp
method.
Transmitting Data from UI to Surrounding System
The reverse direction where data/events are transferred from the UI to the surrounding system, is done through the Model in much the same way. Continuing the example from before if we need to add the ability to configure a new target temperature, we would add the following to the Model:
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);
}
In case the user sets a new target temperature in the UI, the View can inform the Presenter which holds a pointer to the Model object and is therefore able to call the setNewTargetTemperature
function.
Examples
The following examples are full demos configured for specific demo boards, however much of the code demonstrated can be reused for other demo boards and custom hardware.
From GUI Task
A working example for STM32F746 showing how to sample a button and controlling a LED directly in the Model class. The example uses the MVP architecture to transfer values and events between the two views and the Model class. The Model class samples a button and updates the LED to match the state of the application.
A working example for STM32F429 showing how to sample a button in the Model class. The example uses the MVP architecture to transfer the button event to the View.
From Other Task
A working example for STM32F469 showing how to sample an analog input in separate thread. The example uses the MVP architecture to transfer the analog value to the View.
A working example showing intertask communication and propagation to and from the UI has been implemented. Use this as inspiration for your own setup. The example communicates between the backend system implemented in C code and the C++ TouchGFX GUI. The example runs on the STM32F746G-DISCO board on top of FreeRTOS.
From Multiple tasks
This working example was demonstrated at the TouchGFX webinar "Integration with your hardware" from the 28th of May 2018.
The application was designed for the STM32F769-DISCO board and interacts with an LED and the USER BUTTON to show how to integrate both C code and hardware peripherals into your TouchGFX application.
The application configures the button in GPIO mode. Behavior is to sample the state of the button in btntask.c and pass a message through the GUI message queue if the button is pressed down. This allows us to advance the animation in the application by keeping the button pressed.
The application uses three FreeRTOS tasks. One for the GUI, one for each peripheral (LED and USER Button).
From Task and External Interrupt Line
This working example was demonstrated at the TouchGFX webinar "Integration with your hardware" from the 28th of May 2018.
The application was designed for the STM32F769-DISCO board and interacts with an LED and the USER BUTTON to show how to integrate both C code and hardware peripherals into your TouchGFX application.
This application configures the button in EXTI mode (external interrupt line 0). Behavior is to receive an interrupt when the button is pressed down after which the interrupt is cleared. This does not allow the same behavior as in GPIO, but instead we'll be single stepping the animation because a message is only sent through the gui message queue whenever an interrupt is received.
The application uses two FreeRTOS tasks. One for the GUI, one for the LED. (The Button task from Multiple tasks demo remains active in this application to exemplify that the peripheral interaction code has been moved into an interrupt handler).