Platform Integration: Main Loop

This article describes the integration of an Embedded Wizard generated GUI application into your main application. It covers the necessary steps to initialize and de-initialize the entire system and it describes the details about launching the GUI application and providing the necessary user or system events.

IMPORTANT

Please be aware that every Embedded Wizard GUI application requires the execution in a single GUI task!

If you are working with an operating system and your software is using several threads/tasks, please take care to access your GUI application only within the context of your GUI thread/task. Use operating system services to exchange data or events between the GUI thread/task and other worker threads/tasks.

Includes

First of all, some header files from the appropriate Platform Package directory have to be included into your main.c file to get access to the Runtime Environment (RTE), the Graphics Engine (GFX) and some generated header files from Mosaic class library:

#include "ewrte.h"

#include "ewgfx.h"
#include "ewextgfx.h"
#include "ewgfxdefs.h"

#include "Core.h"
#include "Graphics.h"

In order to find all of the above header files during the compilation of your main.c file, the include path has to be set correctly within your makefile. In case of a GCC compiler, this is typically done by using the -I compiler flag.

Initialization of the GUI Application

Before the Embedded Wizard generated GUI application can be executed on the target platform, the framebuffer and the Graphics Engine have to be initialized.

The initialization of the framebuffer depends completely on your target system and the underlying graphics API. For example in case of DirectFB, a DirectFB surface needs to be created, in case of OpenGL ES 2.0 the frame buffer is typically created via EGL.

In order to simplify this task, a template of the main.c file including the necessary initialization is provided as so called Build Environment together with a Platform Package.

Nevertheless, the following code snippet illustrates the necessary steps to initialize the Embedded Wizard generated GUI application:

int main( int argc, char **argv )
{
  CoreRoot        rootObject;
  XViewport*      viewport;
  YourFramebuffer framebuffer;
  int             w, h;

  /* Startup your subsystem and obtain the frame buffer or display device */
  framebuffer = YourSubsystem_Initialize();

  /* Query the size of your frame buffer or display device */
  YourSubsystem_GetFramebufferSize( framebuffer, &w, &h );

  /* Initialize the Graphics Engine and the Runtime Environment */
  EwInitGraphicsEngine( &framebuffer );

  /* Create the applications root object ... */
  rootObject = (CoreRoot)EwNewObjectIndirect( EwApplicationClass, 0 );
  EwLockObject( rootObject );

  /* ... and initialize the application object. */
  CoreRoot__Initialize( rootObject, EwScreenSize );

  /* Create the viewport object to provide access to the framebuffer */
  viewport = EwInitViewport( EwScreenSize, EwNewRect( 0, 0, w, h ), 0, 255, framebuffer, 0, 0, 0 );

  /* Initialize your device driver(s) that provide data for your GUI */
  YourDevice_Initialize();
  ...

The parameter EwScreenSize is an automatically generated constant in the module Core.c of type XPoint. This value contains the size of the screen as it was defined in the attribute ScreenSize within the profile for that the code was generated.

The parameter EwApplicationClass, contains the root object of the GUI application. This application class is also defined within the attribute ApplicationClass of the profile.

The initialization of your device driver is needed in case that your GUI application contains a device class that exchanges data, commands or events with an underlying system. For more details, please have look to the article Device class and device driver.

Implementing the Main Loop

Embedded Wizard generated UI applications are running in an (endless) loop, which drives the UI application. This main loop is responsible to provide all user inputs to the UI application, to start the processing of timers and signals, to take care for the update of the display and finally to start the garbage collection.

The main loop can be implemented within your main() function or it can be implemented as a separate OS task. However, it is absolutely important, that the complete Embedded Wizard generated UI is running within one task! Splitting the different actions in several tasks (e.g. to start the garbage collection from another task) will cause unpredictable results…

The following code snipped shows the typical implementation of a main loop:

  ...
  XEnum cmd = CoreKeyCodeNoKey;

  /* Endless loop, until the 'power' key is pressed... */
  while ( cmd != CoreKeyCodePower )
  {
    int    events  = 0;
    int    timers  = 0;
    int    signals = 0;
    int    devices = 0;
    int    touch;
    XPoint point;

    /* Step 1: Process data of your device driver(s) and update the GUI
    application by setting properties or by triggering events */
    devices = YourDevice_ProcessData();

    /* Step 2: Receive user inputs (key events)... */
    cmd = GetKeyCommand();

    /* ...and provide it to the application. */
    if ( cmd != CoreKeyCodeNoKey )
    {
      /* feed the application with a 'press' and 'release' event */
      events |= CoreRoot__DriveKeyboardHitting( rootObject, cmd, 0, 1 );
      events |= CoreRoot__DriveKeyboardHitting( rootObject, cmd, 0, 0 );
    }

    /* Step 3: Receive cursor inputs (mouse/touch events)... */
    touch = GetTouchPosition( &point );

    /* ...and provide it to the application. */
    if ( touch > 0 )
    {
      /* Begin of touch cycle */
      if ( touch == 1 )
        events |= CoreRoot__DriveCursorHitting( rootObject, 1, 0, pos );

      /* Movement during touch cycle */
      else if ( touch == 2 )
        events |= CoreRoot__DriveCursorMovement( rootObject, pos );

      /* End of touch cycle */
      else if ( touch == 3 )
        events |= CoreRoot__DriveCursorHitting( rootObject, 0, 0, pos );
    }

    /* Step 4: Process expired timers */
    timers = EwProcessTimers();

    /* Step 5: Process the pending signals */
    signals = EwProcessSignals();

    /* Update screen and memory, only if something has changed */
    if ( devices || timers || signals || events )
    {
      /* Step 6: Refresh the screen and draw its content */
      if ( CoreRoot__DoesNeedUpdate( rootObject ))
        Update( viewport, rootObject );

      /* Step 7: Start the garbage collection */
      EwReclaimMemory();
    }

    /* Otherwise suspend for a while */
    else
      usleep( 1 );
  }
  ...

In the following, we want to have a closer look into the single steps of this main loop.

Step 1: Processing Data from your Device Driver(s)

Typically, every GUI application exchanges data with an underlying system in order to get data from a certain hardware device or to start a certain action. The interface between the GUI application and the underlying system is implemented by a device class and a device driver: The device class is accessed by the GUI application. The device driver is the counterpart that communicates with the real devices (e.g. by calling BSP functions, by setting registers, by calling other drivers,...).

The function YourDevice_ProcessData() is called from the main UI loop, in order to process data and events from your particular device. This function is responsible to update properties within the device class if the corresponding state or value of the real device has changed. This function is also responsible to trigger system events if necessary. The return value of the function indicates, that properties have been updated or events have been triggered.

For more details, please have look to the article 'Device Class and Device Driver'.

Step 2: Processing Key Events

Depending on the operating system or generally speaking the application runtime environment, the input device drivers (keyboard, remote control, hardware buttons, etc.) will generate messages when the input device state changes (e. g. a remote control button is pressed). Typically, the input task will have to write the input data into a message queue and the main loop can then process these events out of this queue and feed them to the GUI application.

If an event is in the message queue (e.g. a remote control key pressed event) the main loop first has to examine the event. This means, the event information has to be translated into a Mosaic key code. This has to be done within the function GetKeyCommand(): all key events which are needed within your UI application, need to be translated into a Mosaic key code. Typically this function is part of the main.c template.

For this purpose, the Mosaic class library contains a set of predefined keys in the enumeration KeyCode of unit Core. For example, the identifier CoreKeyCodeOk corresponds to the Ok key on the remote control or the Enter key on a keyboard.

As soon as the incoming key event is translated into an appropriate Mosaic key code, it can be passed to the GUI application by using the method DriveKeyboardHitting().

The above description assumes that you are familiar with concepts how the native code (in particular the implementation of the main loop) and the generated code do interact together. If this is not the case, please see the sections Invoke GUI application code from device and Data types. To see the documentation of the described method DriveKeyboardHitting() please use the links above.

Step 3: Processing Cursor or Touch Screen Events

Some devices are cursor operated (e.g. via the touch panel, a joystick or the mouse device). In this case a device driver will generate cursor information and write it into a message queue. Similar to key events, the main loop have then to read the mouse or touch events from this message queue and feed them to the application.

The Mosaic class Core::Root (the base class for every GUI application) provides the both methods DriveCursorHitting() and DriveCursorMovement(). These serve as interface you call from the main loop to feed the GUI application with events generated by a mouse device or a touch screen. These both methods are limited to handle a single interaction at the same time. They are thus ideal when your device is not using any multi-touch capable touch screen.

If the touch screen in your device is able to distinguish between simultaneous touch interactions you can adapt the above source code to call the methods DriveMultiTouchHitting() and DriveMultiTouchMovement() instead of DriveCursorHitting() and DriveCursorMovement(). The unique difference to the single-touch version of the methods is, that they expect a number in range 0 .. 9 identifying the finger, which has caused the event.

Usually, multi-touch screen drivers do track every movement of every finger touching actually the screen and are so able to identify the fingers individually. You have to translate this finger identification in a number lying in range 0 .. 9 and pass this value to the method DriveMultiTouchHitting() and DriveMultiTouchMovement() when calling it.

For example, the user touches the screen with one finger, then you call the methods with the number 0 since this number identifies the first finger. When the user presses then a second finger you have to use the value 0 for all events associated with the first finger and the value 1 for all events caused by the second finger. Similarly, when the user presses third finger on the screen, all events generated by this third finger should be passed with the value 2 when calling the methods DriveMultiTouchHitting() and DriveMultiTouchMovement().

The above description assumes that you are familiar with concepts how the native code (in particular the implementation of the main loop) and the generated code do interact together. If this is not the case, please see the sections Invoke GUI application code from device and Data types. To see the documentation of the described methods please use the links above.

Step 4: Processing Timers

Within an Embedded Wizard generated UI application all timer objects, that are currently running, are stored in a timer list. In this step, the timer list is traversed and all expired timers are handled. The number of processed timers is returned by this function. In order to activate the timer processing, the function EwProcessTimers() of the Runtime Environment has to be called.

Step 5: Processing Signals

In an Embedded Wizard generated UI application, signals can be sent in order to trigger slot methods with certain behavior. Beside the regular signal statement, which is processed immediately, special postsignal and idlesignal statements are collected and executed right before, respectively after a screen update is performed. The processing of the deferred signals is done by calling EwProcessSignals().

Step 6: Updating the Screen

In reaction to any system/key/cursor event, any signal or any expired timer the UI application can change its look and for example open a new dialog box or move a bitmap across the screen. In that case, all affected objects are marked as invalid. During the update of the root object, all invalid areas are examined and changes to the visible objects are processed. Finally, the resulting display is shown within the visible framebuffer.

The following function contains the complete display update cycle:

static void Update( XViewport* aViewport, CoreRoot aApplication )
{
  XBitmap*       bitmap     = EwBeginUpdate( aViewport );
  GraphicsCanvas canvas     = EwNewObject( GraphicsCanvas, 0 );
  XRect          updateRect = {{ 0, 0 }, { 0, 0 }};

  /* Let's redraw the dirty area of the screen. Cover the returned bitmap
  objects within a canvas, so Mosaic can draw to it. */
  if ( bitmap && canvas )
  {
    GraphicsCanvas__AttachBitmap( canvas, (XUInt32)bitmap );
    updateRect = CoreRoot__UpdateGE20( aApplication, canvas );
    GraphicsCanvas__DetachBitmap( canvas );
  }

  /* Complete the update */
  if ( bitmap )
    EwEndUpdate( aViewport, updateRect );
  }

Step 7: Triggering the Garbage Collection

In the last step of the main loop the garbage collection is only triggered if any processing has happened. The function EwReclaimMemory() of the Runtime Environment is called.

De-Initialization of the Embedded Wizard GUI Application

When the application is finished, the unused resources have to be released using the following commands:

  /* De-initialize your device driver(s) */
  YourDevice_Deinitialize();

  /* Release unused resources / memory... */
  EwDoneViewport( viewport );
  EwUnlockObject( rootObject );
  EwReclaimMemory();

  /* ... and de-initialize the Graphics Engine */
  EwDoneGraphicsEngine();

  /* Finally, shutdown your graphics subsystem */
  YourSubsystem_Deinitialize();

  return 0;
}