Tilt Hydrometer (Part 12) CapSense

Summary

This article updates the user interface to have input as well as output by adding a CapSense GUI.

This series is broken up into the following 12 articles with a few additional possible articles. 

Tilt Hydrometer (Part 1) Overview & Out-of-Box

Tilt Hydrometer (Part 2) Architecture

Tilt Hydrometer (Part 3) Advertising Scanner

Tilt Hydrometer (Part 4) Advertising Packet Error?

Tilt Hydrometer (Part 5) Tilt Simulator & Multi Advertising iBeacons

Tilt Hydrometer (Part 6) Tilt Simulator an Upgrade

Tilt Hydrometer (Part 7) Advertising Database

Tilt Hydrometer (Part 8) Read the Database

Tilt Hydrometer (Part 9) An LCD Display

Tilt Hydrometer (Part 10) The Display State Machine

Tilt Hydrometer (Part 11) Draw the Display Screens

Tilt Hydrometer (Part 12) CapSense

Tilt Hydrometer: LittleFS & SPI Flash (Part ?)

Tilt Hydrometer: WiFi Introducer (Part ?)

Tilt Hydrometer: WebServer (Part ?)

Tilt Hydrometer: Amazon MQTT (Part ?)

Tilt Hydrometer: Printed Circuit Board (Part ?)

You can get the source code from git@github.com:iotexpert/Tilt2.git  This repository has tags for each of the articles which can be accessed with "git checkout part12"  You can find the Tilt Simulator at  git@github.com:iotexpert/TiltSimulator.git.

 

Story

If you look at the development kit you will notice on the right side that there are two CapSense buttons and one slider.  I know that this is going to come as a great shock to those of you who know me, but Im not very patient.  I don’t always want to wait for the system to page through screens every 5 seconds.  So let’s turn on those buttons to do something useful.  But what?  How about the left button will toggle “auto mode” and the right button will go to the next screen.

Board Support Package

We (Cypress/Infineon) created all of the setup stuff you need to make CapSense work on all of our development kits.  If you run “make config” you can look at the configuration of the Board Support Package for this development kit.  Notice that

  1. The CapSense is enabled
  2. The pins are setup for two buttons and a slider.

When you run the CapSense Configurator you can see that there is a slider and two buttons

And they are attached to these pins:

When you run the Library Manager you can also see that the CapSense middleware is already loaded into your project (notice MCU Middleware)

The Firmware

Let’s add the new capsenseManager.h, capsenseManager.c and update main.c.  The capsenseManager.h will just define the task:

#pragma once
void cpm_task();

main.c needs to start the task.

    xTaskCreate(cpm_task, "CapSense Manager", configMINIMAL_STACK_SIZE*2,0 /* args */ ,0 /* priority */, 0);

All of the action takes place in capsenseManager.c.  In the file there are really only two things that are even mildly complicated.  The CapSense block is a combination of a hardware block plus some firmware plus some middleware.  The hardware block does the CapSense to digital conversion then triggers an interrupt.  The interrupt firmware is responsible for managing the hardware block including sequencing the measurements, running the baseline etc.  When you trigger a scan, there is a back and forth between the hardware block and the firmware that must happen in an ISR.  Finally when things are done, you can ask for a callback.

The flow looks like this

  1. Initialize the hardware
  2. Initialize the Interrupt Service Routine
  3. Ask for a Callback
  4. Enable the CapSense block
  5. Start a scan
  6. Wait for the callback
  7. Process the results
  8. Start a scan (repeat)

Initialize the CapSense (steps 1-4)

To get this going you need to:

  1. Define the task
  2. Initialize a semaphore (which you will use in the callback)
  3. Initialize the hardware block
  4. Initialize the interrupt
  5. Register the callback
  6. Enable the CapSense block
void cpm_task()
{
    cpm_semaphore = xSemaphoreCreateCounting(10,0);

    static const cy_stc_sysint_t CapSense_ISR_cfg =
    {
        .intrSrc = csd_interrupt_IRQn, /* Interrupt source is the CSD interrupt */
        .intrPriority = 7u,            /* Interrupt priority is 7 */
    };

    Cy_CapSense_Init(&cy_capsense_context);
        
    Cy_SysInt_Init(&CapSense_ISR_cfg, &cpm_isr);
    NVIC_ClearPendingIRQ(CapSense_ISR_cfg.intrSrc);
    NVIC_EnableIRQ(CapSense_ISR_cfg.intrSrc);

    Cy_CapSense_RegisterCallback	(CY_CAPSENSE_END_OF_SCAN_E,cpm_callback, &cy_capsense_context); 
    Cy_CapSense_Enable (&cy_capsense_context);

The ISR & CallBack

The ISR is trivial.  All it does is run our interrupt handler.

The callback just gives a semaphore which has held the task in suspension while the CapSense is running.

static void cpm_isr(void)
{
    Cy_CapSense_InterruptHandler(CYBSP_CSD_HW, &cy_capsense_context);
}

static void cpm_callback(cy_stc_active_scan_sns_t *ptrActiveScan)
{
    xSemaphoreGiveFromISR(cpm_semaphore,portMAX_DELAY);
}

Main Task Loop

In the main loop I

  1. Scan all of the widgets… then wait until the scan is done by holding on the semaphore.
  2. After the scan is done I need to run all of the Cypress code which manages the data.
  3. Then I find out the state of the two buttons.
  4. Based on the state I call either the toggle auto mode or next screen command

Notice that I wait for 20ms after I get this done.  What this does is give me a GUI update rate of about 50hz.  Probably 10hz would be fine.

  int button0Prev = 0;
    int button1Prev = 0;
    int button0Curr = 0;
    int button1Curr = 0;
    while(1)
    {
        Cy_CapSense_ScanAllWidgets (&cy_capsense_context);
        xSemaphoreTake(cpm_semaphore,portMAX_DELAY);
        Cy_CapSense_ProcessAllWidgets(&cy_capsense_context);
        button0Curr = Cy_CapSense_IsWidgetActive(CY_CAPSENSE_BUTTON0_WDGT_ID,&cy_capsense_context);
        button1Curr = Cy_CapSense_IsWidgetActive(CY_CAPSENSE_BUTTON1_WDGT_ID,&cy_capsense_context);

        if(button0Curr != button0Prev && button0Curr == 1)
        {
            dm_submitAutoCmd();
        }
        if(button1Curr != button1Prev && button1Curr == 1)
        {
            dm_submitNextScreenCmd();
        }

        button0Prev = button0Curr;
        button1Prev = button1Curr;

        vTaskDelay(20); // 50 Hz Update Rate

    }

capSenseManager.c

Here is the whole file.

#include <stdio.h>

#include "cycfg_capsense.h"

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

#include "displayManager.h"

static SemaphoreHandle_t cpm_semaphore;

static void cpm_isr(void)
{
    Cy_CapSense_InterruptHandler(CYBSP_CSD_HW, &cy_capsense_context);
}

static void cpm_callback(cy_stc_active_scan_sns_t *ptrActiveScan)
{
    xSemaphoreGiveFromISR(cpm_semaphore,portMAX_DELAY);
}

void cpm_task()
{
    cpm_semaphore = xSemaphoreCreateCounting(10,0);

    static const cy_stc_sysint_t CapSense_ISR_cfg =
    {
        .intrSrc = csd_interrupt_IRQn, /* Interrupt source is the CSD interrupt */
        .intrPriority = 7u,            /* Interrupt priority is 7 */
    };

    Cy_CapSense_Init(&cy_capsense_context);
        
    Cy_SysInt_Init(&CapSense_ISR_cfg, &cpm_isr);
    NVIC_ClearPendingIRQ(CapSense_ISR_cfg.intrSrc);
    NVIC_EnableIRQ(CapSense_ISR_cfg.intrSrc);

    Cy_CapSense_RegisterCallback	(CY_CAPSENSE_END_OF_SCAN_E,cpm_callback, &cy_capsense_context); 
    Cy_CapSense_Enable (&cy_capsense_context);

    int button0Prev = 0;
    int button1Prev = 0;
    int button0Curr = 0;
    int button1Curr = 0;
    while(1)
    {
        Cy_CapSense_ScanAllWidgets (&cy_capsense_context);
        xSemaphoreTake(cpm_semaphore,portMAX_DELAY);
        Cy_CapSense_ProcessAllWidgets(&cy_capsense_context);
        button0Curr = Cy_CapSense_IsWidgetActive(CY_CAPSENSE_BUTTON0_WDGT_ID,&cy_capsense_context);
        button1Curr = Cy_CapSense_IsWidgetActive(CY_CAPSENSE_BUTTON1_WDGT_ID,&cy_capsense_context);

        if(button0Curr != button0Prev && button0Curr == 1)
        {
            dm_submitAutoCmd();
        }
        if(button1Curr != button1Prev && button1Curr == 1)
        {
            dm_submitNextScreenCmd();
        }

        button0Prev = button0Curr;
        button1Prev = button1Curr;

        vTaskDelay(20); // 50 Hz Update Rate

    }
}

 

Tilt Hydrometer (Part 11) Draw the Display Screens

Summary

This article updates the display system in my Tilt Hydrometer project to actually display useful information.

This series is broken up into the following 12 articles with a few additional possible articles. 

Tilt Hydrometer (Part 1) Overview & Out-of-Box

Tilt Hydrometer (Part 2) Architecture

Tilt Hydrometer (Part 3) Advertising Scanner

Tilt Hydrometer (Part 4) Advertising Packet Error?

Tilt Hydrometer (Part 5) Tilt Simulator & Multi Advertising iBeacons

Tilt Hydrometer (Part 6) Tilt Simulator an Upgrade

Tilt Hydrometer (Part 7) Advertising Database

Tilt Hydrometer (Part 8) Read the Database

Tilt Hydrometer (Part 9) An LCD Display

Tilt Hydrometer (Part 10) The Display State Machine

Tilt Hydrometer (Part 11) Draw the Display Screens

Tilt Hydrometer (Part 12) CapSense

Tilt Hydrometer: LittleFS & SPI Flash (Part ?)

Tilt Hydrometer: WiFi Introducer (Part ?)

Tilt Hydrometer: WebServer (Part ?)

Tilt Hydrometer: Amazon MQTT (Part ?)

Tilt Hydrometer: Printed Circuit Board (Part ?)

You can get the source code from git@github.com:iotexpert/Tilt2.git  This repository has tags for each of the articles which can be accessed with "git checkout part12"  You can find the Tilt Simulator at  git@github.com:iotexpert/TiltSimulator.git.

 

This article will continue to edit the display manager task

Story

In the last article I built all of the infrastructure to make the state machine work.  Now it is time to draw some actual graphics and get this puppy displaying some data.  Remember that I want three screens

  1. The Splash Screen with the IoT Expert Logo

2. The Table Screen with a table of Gravity and Temperature for the 8 Tilts

3. A detail screen.  One per Tilt with more information drawn in the same color as the Tilt.

From the last post you might remember that you need to write 4 functions to implement a new screen

  1. precheck – returns true if it is legal to come to that screen
  2. init – draw the initial data (like the table outline)
  3. update – to update the data on the screen
  4. sequence – up move to the next subscreen e.g. purple –> red

Splash Screen

First the splash screen.  The pre-check function returns true because you are always allowed to “go” to this screen.  The initialization function draws all of the screen (as it is static).  Basically sets the background to white, clears the screen (which actually turns it white) then draws the bitmap.  There is nothing to do in the update or the sequence functions.

////////////////////////////////////////////////////////////////////////////////
//
// Splash
// 
////////////////////////////////////////////////////////////////////////////////
static bool dm_displayScreenSplashPre()
{
    return true;
}

static void dm_displayScreenSplashInit()
{
    GUI_SetBkColor(GUI_WHITE);
    GUI_Clear();
    GUI_DrawBitmap(&bmIOTexpert_Logo_Vertical320x240,0,17);

}
static void dm_displayScreenSplashUpdate()
{
}

static bool dm_displayScreenSplashSeq()
{
    return true;
}

Table Screen

The table screen precheck and sequence functions say “true”, meaning it is always OK to come to this screen and it is always OK to leave this screen.

static bool dm_displayScreenTablePre()
{
    return true;
}

static bool dm_displayScreenTableSeq()
{
    return true;
}

The initialization function draws the header and the two lines.

First of all it sets the location of the graphics with #defines.  The actual function goes on to use that information and draw the picture.  Self explanatory.

#define TABLE_FONT (GUI_FONT_24B_ASCII)
#define TABLE_BGCOLOR (GUI_BLACK)
#define TABLE_HEAD_BGCOLOR (GUI_WHITE)

#define TABLE_NAME_LEFT_X (0)
#define TABLE_GRAV_LEFT_X (120)
#define TABLE_TEMP_LEFT_X (220)

#define TABLE_NAME_RIGHT_X (119)
#define TABLE_GRAV_RIGHT_X (219)
#define TABLE_TEMP_RIGHT_X (319)

#define TABLE_NAME_CENTER_X (TABLE_NAME_LEFT_X+(TABLE_NAME_RIGHT_X-TABLE_NAME_LEFT_X)/2)
#define TABLE_GRAV_CENTER_X (TABLE_GRAV_LEFT_X+(TABLE_GRAV_RIGHT_X-TABLE_GRAV_LEFT_X)/2)
#define TABLE_TEMP_CENTER_X (TABLE_TEMP_LEFT_X+(TABLE_TEMP_RIGHT_X-TABLE_TEMP_LEFT_X)/2)

static void dm_displayScreenTableInit()
{
    GUI_SetColor(TABLE_HEAD_BGCOLOR);
    GUI_SetBkColor(TABLE_BGCOLOR);
    GUI_SetFont(TABLE_FONT);
    GUI_Clear();

    GUI_FillRect(0,0,320,GUI_GetFontSizeY()+ TOP_MARGIN);

    GUI_SetTextMode(GUI_TM_REV);
    GUI_SetTextAlign(GUI_TA_CENTER);
    GUI_DispStringHCenterAt("Name",TABLE_NAME_CENTER_X,TOP_MARGIN);
    GUI_DispStringHCenterAt("Gravity",TABLE_GRAV_CENTER_X,TOP_MARGIN);
    GUI_DispStringHCenterAt("Temp",TABLE_TEMP_CENTER_X,TOP_MARGIN);

    GUI_DrawLine(TABLE_NAME_RIGHT_X,0,TABLE_NAME_RIGHT_X,240);
    GUI_DrawLine(TABLE_GRAV_RIGHT_X,0,TABLE_GRAV_RIGHT_X,240);
}

The update will:

  1. set the graphics configuration
  2. iterate through the list of Tilts
  3. If it is active then it will get the data
  4. sprintf it into a buffer than display it.
  5. If it is not active then display a “–“
static void dm_displayScreenTableUpdate()
{

    uint32_t activeTilts =tdm_getActiveTiltMask();

    char buff[64];
    
    GUI_SetColor(TABLE_HEAD_BGCOLOR);
    GUI_SetBkColor(TABLE_BGCOLOR);
    
    GUI_SetFont(TABLE_FONT);
    GUI_SetTextMode(GUI_TEXTMODE_NORMAL);
    GUI_SetTextAlign(GUI_TA_CENTER);

    int row;
    for(int i=0;i<tdm_getNumTilt();i++)
    {
        row = i+1;

        GUI_SetColor(tdm_colorGUI(i));
        GUI_DispStringHCenterAt(tdm_getColorString(i), TABLE_NAME_CENTER_X, ROW_Y(row));

        if(1<<i & activeTilts)
        {

            tdm_tiltData_t *response = tdm_getTiltData(i);

            sprintf(buff,"%1.3f",response->gravity);
            GUI_DispStringHCenterAt(buff, TABLE_GRAV_CENTER_X, ROW_Y(row));
            sprintf(buff,"%02d",response->temperature);
            GUI_DispStringHCenterAt(buff, TABLE_TEMP_CENTER_X, ROW_Y(row));
                        
            free(response);
        }
        else
        {
            GUI_DispStringHCenterAt("-----", TABLE_GRAV_CENTER_X, ROW_Y(row));
            GUI_DispStringHCenterAt("--", TABLE_TEMP_CENTER_X, ROW_Y(row));

        }
    }  
}

The only little bit of trickiness is that I defined a macro to calculate the y position on the screen.  Specifically I divided the screen into “rows” starting at 0 based on the height of the font plus the margin between the rows plus whatever the offset at the top of the screen (the top margin)

#define TOP_MARGIN (4)
#define LINE_MARGIN (2)
#define ROW_Y(row) (TOP_MARGIN + (row)*(LINE_MARGIN+GUI_GetFontSizeY()))

Single Screen

The single screen is a bit more interesting.  First of all you are only allowed to go to this screen if there is at least one Tilt that is active.  The next thing is that I want the first time you go to this screen to go to the first active tilt.

static tdm_tiltHandle_t currentSingle = 0xFF;

static bool dm_displaySinglePre()
{
    uint32_t activeTilts =tdm_getActiveTiltMask();
    if(activeTilts == 0)
        return false;

    for(int i=0;i<tdm_getNumTilt();i++)
    {
        if(1<<i & activeTilts)
        {
            currentSingle = i;
            break;
        }
    }

    return true;
}

The initialization function

  1. Clears the screen
  2. Asks for the color of the current Tilt (so that all of the text is drawn using that color)
  3. Draws the header
  4. Draws the labels
static void dm_displaySingleInit()
{
    GUI_SetBkColor(GUI_BLACK);
    GUI_SetFont(GUI_FONT_32B_ASCII);
    GUI_Clear();

    GUI_SetColor(tdm_colorGUI(currentSingle));

    GUI_FillRect(0,0,320,ROW_Y(1));
    
    GUI_SetTextAlign(GUI_TA_LEFT);
    GUI_SetTextMode(GUI_TM_REV);
    GUI_DispStringHCenterAt(tdm_getColorString(currentSingle), CENTER_X,ROW_Y(0) );

    GUI_SetTextMode(GUI_TM_NORMAL);
    GUI_SetTextAlign(GUI_TA_RIGHT | GUI_TA_VCENTER);

    GUI_DispStringAt("Gravity: ", SINGLE_LABEL_X,ROW_Y(SINGLE_GRAV_ROW) );
    GUI_SetTextAlign(GUI_TA_RIGHT | GUI_TA_VCENTER);
    GUI_DispStringAt("Temp: ",    SINGLE_LABEL_X,ROW_Y(SINGLE_TEMP_ROW) );
    GUI_SetTextAlign(GUI_TA_RIGHT | GUI_TA_VCENTER);
    GUI_DispStringAt("TxPower: ", SINGLE_LABEL_X,ROW_Y(SINGLE_TXPOWER_ROW) );
    GUI_SetTextAlign(GUI_TA_RIGHT | GUI_TA_VCENTER);
    GUI_DispStringAt("RSSI: ",    SINGLE_LABEL_X,ROW_Y(SINGLE_RSSI_ROW) );
    GUI_SetTextAlign(GUI_TA_RIGHT | GUI_TA_VCENTER);
    GUI_DispStringAt("Time: ",    SINGLE_LABEL_X,ROW_Y(SINGLE_TIME_ROW) );

}

The update function basically sets the font and color (just to make sure) then it gets the data, formats it into a string and displays it.  When I wrote this function originally I wasn’t sure if this would ever be called in a  situation where there is no data, so I handled that with the “—-“, but I don’t think that branch is actually ever used.

static void dm_displaySingleUpdate()
{

    uint32_t activeTilts =tdm_getActiveTiltMask();

    char gravString[10];
    char tempString[10];
    char txPowerString[10];
    char rssiString[10];
    char timeString[64];
       
    GUI_SetBkColor(GUI_BLACK);
    GUI_SetFont(GUI_FONT_32B_ASCII);
    GUI_SetColor(tdm_colorGUI(currentSingle));
    
    if(1<<currentSingle & activeTilts)
    {
        tdm_tiltData_t *response = tdm_getTiltData(currentSingle);
        sprintf(gravString,"%1.3f",response->gravity);
        sprintf(tempString,"%02d",response->temperature);
        sprintf(txPowerString,"%d",response->txPower);
        sprintf(rssiString,"%2d",response->rssi);

        int seconds = (xTaskGetTickCount()/1000- response->time);
        int days = seconds/(24*60*60);
        seconds = seconds - days*(24*60*60);
        int hours = seconds/(60*60);
        seconds = seconds - hours*(60*60);
        int minutes = seconds/60;
        seconds = seconds - (minutes * 60);
        
        sprintf(timeString,"%02d:%02d:%02d:%02d",days,hours,minutes,seconds);                       
        free(response);
    }

    else
    {
        sprintf(gravString,"-----");
        sprintf(tempString,"--");
        sprintf(txPowerString,"---");
        sprintf(rssiString,"--");
        sprintf(timeString,"---");                       
    }

    GUI_DispStringAtCEOL(gravString, SINGLE_VALUE_X,ROW_Y(SINGLE_GRAV_ROW) );
    GUI_SetTextAlign(GUI_TA_LEFT | GUI_TA_VCENTER);
    GUI_DispStringAtCEOL(tempString,    SINGLE_VALUE_X,ROW_Y(SINGLE_TEMP_ROW) );
    GUI_SetTextAlign(GUI_TA_LEFT | GUI_TA_VCENTER);
    GUI_DispStringAtCEOL(txPowerString, SINGLE_VALUE_X,ROW_Y(SINGLE_TXPOWER_ROW) );
    GUI_SetTextAlign(GUI_TA_LEFT | GUI_TA_VCENTER);
    GUI_DispStringAtCEOL(rssiString,    SINGLE_VALUE_X,ROW_Y(SINGLE_RSSI_ROW) );
    GUI_SetTextAlign(GUI_TA_LEFT | GUI_TA_VCENTER);
    GUI_DispStringAtCEOL(timeString,    SINGLE_VALUE_X,ROW_Y(SINGLE_TIME_ROW) );
}

The last bit of code is the sequence which will either

  1. Go to the next screen by returning True (if there are no active Tilts)
  2. Move to the next Tilt (if there are more active)
  3. Go to the next screen by returning True
static bool dm_displaySingleSeq()
{
    uint32_t activeTilts =tdm_getActiveTiltMask();
    if(activeTilts == 0)
        return true;

    for(int i=currentSingle+1;i<tdm_getNumTilt();i++)
    {
        if(1<<i & activeTilts)
        {
            currentSingle = i;
            dm_displaySingleInit();
            return false;
        }
    }
    return true;
}

That’s it for the display.  In the next article Ill add the User Input of the GUI by adding CapSense.

Tilt Hydrometer (Part 10) The Display State Machine

Summary

This article updates the display system in my Tilt Hydrometer project to include the state machine apparatus to move between screens.

This series is broken up into the following 12 articles with a few additional possible articles. 

Tilt Hydrometer (Part 1) Overview & Out-of-Box

Tilt Hydrometer (Part 2) Architecture

Tilt Hydrometer (Part 3) Advertising Scanner

Tilt Hydrometer (Part 4) Advertising Packet Error?

Tilt Hydrometer (Part 5) Tilt Simulator & Multi Advertising iBeacons

Tilt Hydrometer (Part 6) Tilt Simulator an Upgrade

Tilt Hydrometer (Part 7) Advertising Database

Tilt Hydrometer (Part 8) Read the Database

Tilt Hydrometer (Part 9) An LCD Display

Tilt Hydrometer (Part 10) The Display State Machine

Tilt Hydrometer (Part 11) Draw the Display Screens

Tilt Hydrometer (Part 12) CapSense

Tilt Hydrometer: LittleFS & SPI Flash (Part ?)

Tilt Hydrometer: WiFi Introducer (Part ?)

Tilt Hydrometer: WebServer (Part ?)

Tilt Hydrometer: Amazon MQTT (Part ?)

Tilt Hydrometer: Printed Circuit Board (Part ?)

You can get the source code from git@github.com:iotexpert/Tilt2.git  This repository has tags for each of the articles which can be accessed with "git checkout part12"  You can find the Tilt Simulator at  git@github.com:iotexpert/TiltSimulator.git.

 

We will continue to edit the Display Manager in this article:

Story

Things are a little unfair as I already know the end of this story because I wrote this code a few months ago.  Recently, as I came back to write these articles I looked at the code, actually Monday.  The code was pretty complicated and I have been really busy so I set it aside as I wasn’t sure how to explain it.  Then I looked again on Tuesday and contemplated re-writing it… then again on Wednesday then … and finally Saturday when I decided that what I had done originally was correct.  That means I just need to explain it.

My system is going to look work like this:

  1. A splash screen with the IoT Expert Logo
  2. A table of data screen with one row per tilt
  3. A series of details screens, one per tilt
  4. The ability to “skip” detail screens if there is no data
  5. An automated UI that moved through the screens every 5000ms
  6. Support for a manual UI so that the ntshell and the CapSense interface could send it commands to move to specific screens

Here is the picture:

Add Color to the Tilt Data Manager

Before I jump into the GUI, I need to add some color information to the database of Tilts.  This is a little bit of a smearing the line between the database and the display systems, but I felt that having all of the information about the Tilts in one place was better than having a split.  In order for the display manager to get the color information I add a new function to the tiltDataManager.h to return the specific GUI_COLOR (which is an emWin thing) for that specific Tilt handle.

GUI_COLOR tdm_colorGUI(tdm_tiltHandle_t handle)

Then I need to update the tiltDataManager. c to have the color in the database.  Notice that the emWin library doesn’t have purpose or pink so I create those colors.

typedef struct  {
    char *colorName;
    GUI_COLOR color;
    uint8_t uuid[20];
    tdm_tiltData_t *data;
    int numDataPoints;
    int numDataSeen;
} tilt_t;


#define IBEACON_HEADER 0x4C,0x00,0x02,0x15
#define GUI_PINK GUI_MAKE_COLOR(0x00CCCCFF)
#define GUI_PURPLE GUI_MAKE_COLOR(0x00800080)

static tilt_t tiltDB [] =
{
    {"Red",    GUI_RED,    {IBEACON_HEADER,0xA4,0x95,0xBB,0x10,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Green" , GUI_GREEN,  {IBEACON_HEADER,0xA4,0x95,0xBB,0x20,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Black" , GUI_GRAY,   {IBEACON_HEADER,0xA4,0x95,0xBB,0x30,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Purple", GUI_PURPLE, {IBEACON_HEADER,0xA4,0x95,0xBB,0x40,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Orange", GUI_ORANGE, {IBEACON_HEADER,0xA4,0x95,0xBB,0x50,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Blue"  , GUI_BLUE,   {IBEACON_HEADER,0xA4,0x95,0xBB,0x60,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Yellow", GUI_YELLOW, {IBEACON_HEADER,0xA4,0x95,0xBB,0x70,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Pink"  , GUI_PINK,   {IBEACON_HEADER,0xA4,0x95,0xBB,0x80,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
};
#define NUM_TILT (sizeof(tiltDB)/sizeof(tilt_t))

Then I add the actual function.

GUI_COLOR tdm_colorGUI(tdm_tiltHandle_t handle)
{
    return tiltDB[handle].color;
}

Display Manager State Machine

The picture above looks like a state machine, which I suppose makes sense as it is a state machine.  The parts of the state machine are

  1. Initialize the screen
  2. Update the data on the screen
  3. Go to the next screen
  4. Sequence the “subscreen” i.e. from the purple screen to the red screen
  5. Precheck (can you enter the screen)

To support this I created a structure of function pointers:

typedef struct {
    bool (*precheck)(void);   // return true if you can come to this screen
    void (*init)(void);       // draw the initial stuff
    void (*update)(void);     // update the data
    bool (*sequence)(void);   // sequence the data .. return true if you should go to the next screen
    dm_screenName_t next;
} dm_screenMgmt_t;

This means that each “screen” needs functions in a table that have:

  1. precheck – returns true if it is legal to come to that screen
  2. init – draw the initial data (like the table outline)
  3. update – to update the data on the screen
  4. sequence – up move to the next subscreen e.g. purple –> red

But what is the “next” member in the structure?  This is just an index into the state machine table that tells what is the next entry to go too.  I wish that there was a better way to do this in C as the enumerated value is critically mapped to the place in the array of screens (see below)

typedef enum {
    SPLASH,
    TABLE,
    SINGLE,
} dm_screenName_t;

With all of this setup I can now make a table to represent the states of my screens like this (more on the functions a bit later in this article).

dm_screenMgmt_t screenList[] = {
    {dm_displayScreenSplashPre, dm_displayScreenSplashInit, dm_displayScreenSplashUpdate, dm_displayScreenSplashSeq, TABLE},
    {dm_displayScreenTablePre , dm_displayScreenTableInit, dm_displayScreenTableUpdate, dm_displayScreenTableSeq, SINGLE},
    {dm_displaySinglePre, dm_displaySingleInit, dm_displaySingleUpdate, dm_displaySingleSeq, TABLE},
};

With that in place I can now create the code that actually runs the state machine, dm_nextScreen.  This function

  1. Runs the “sequence” function that will either move to the next subscreen OR it will return true (meaning go to the next screen)
  2. If the precheck returns true then it is legal to jump to this screen
  3. Jump to the next screen
  4. Finally update the data on the current screen
static void dm_nextScreen()
{
    if((*screenList[dm_currentScreen].sequence)())
    {
        if((*screenList[screenList[dm_currentScreen].next].precheck)())
        {
            dm_currentScreen = screenList[dm_currentScreen].next;
            (*screenList[dm_currentScreen].init)();
        }
    }
    (*screenList[dm_currentScreen].update)();
}

The Display Manager Task

The display manager task is now a bit tricky as well.  First of all I have a boolean variable called “autoRotate”.  When this variable is true it means that the screens should automatically switch to the next screen every 5 seconds.

The next part of the code initializes a command queue (so that other tasks can send a next screen or enable/disable of autorotate or a jump straight to the table.

The dm_currentScreen is global to this file and keep track of which screen you are currently on, which is what state the state machine is in.

The next part of the code waits for a message OR a timeout.  The timeout happens after 5000ms (5s) and tell the system to either go to the next screen, or update the data on the screen.

void dm_task(void *arg)
{
    dm_cmdMsg_t msg;
    bool autoRotate=true;
    dm_cmdQueue = xQueueCreate(10,sizeof(dm_cmdMsg_t));

    /* Initialize the display controller */
    mtb_st7789v_init8(&tft_pins);
    GUI_Init();

    dm_currentScreen = SPLASH;

    dm_displayScreenSplashInit();
    dm_displayScreenSplashUpdate();

    for(;;)
    {
        if(xQueueReceive(dm_cmdQueue,&msg,5000) == pdPASS) // Got a command
        {
            switch(msg.cmd)
            {
                case SCREEN_NEXT:
                dm_nextScreen();
                break;
                case SCREEN_AUTO:
                autoRotate = ! autoRotate;
                printf("AutoRotate =%s\n",autoRotate?"True":"False");
                break;
                case SCREEN_TABLE:
                dm_currentScreen = TABLE;
                (*screenList[dm_currentScreen].init)();
                (*screenList[dm_currentScreen].update)();
                break;

            }

        }
        else // otherwise just update the screen
        {
            if(autoRotate)
                dm_nextScreen();
            else
                (*screenList[dm_currentScreen].update)();
        }
    }
}

With the infrastructure in place, in the next article I actually draw the screens.