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.

Tilt Hydrometer (Part 9) An LCD Display

Summary

This article updates the Tilt Hydrometer project to include an LCD display using the Segger emWin library.  This includes a discussion of a “new” library in the suite of IoT Expert awesome sauce.  Specifically the IoT Expert Graphics library.

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.

 

Here is the architecture with the blocks we are editing marked green.

Story

I knew that I wanted to use the TFT Display so that I could have this Tilt Hydrometer system sitting in my “brewery”.  Notice in the picture below you can see that the screen is currently displaying the data from the “black” tilt.  Also notice that you can see two fermenters in the background in the brewery.  Who needs a guest bathroom in the era of Covid 🙂

Also notice that there is a Raspberry Pi (more on that in a future article).  And for all of you married people out there, yes I have a very frustrated but forgiving wife.

Add Display Libraries & Fix the Makefile

The first step in building this is to add the new libraries.  Specifically the display-tft-st7789v library which knows how to talk to the lcd and the emWin library.  AND the new IoT Expert Graphics Library.

In order to use the emWin library you need to fix the Makefile to include the emWin component.  Notice that I chose OS  meaning an RTOS and NTS meaning no touch screen.

COMPONENTS=FREERTOS WICED_BLE EMWIN_OSNTS

IoT Expert Graphics Library

I wanted to have a splash screen on this project with my Logo.  So I started digging around on my computer to find the graphics, which I found in the file IOTexpert_Logo_Vertical.png.  Unfortunately this is a 1091×739 portable network graphics file, not exactly what emWin wants to draw with.  To fix this I opened it in GIMP

Then I resized it to my screen size (320×240)

Now it looks like this:

Then I saved it as a PNG.  Still doesn’t work with emWin.  But, if you look in the emWin library you will notice that there is a new directory called “Tool”

This directory contains the Segger tools for supporting their graphics library.  The one that I want is called “Bin2C.exe” (unfortunately it is a Windows only tool).  I run it and open the logo.

Then I click save and pick “C” bitmap file (*.c)

For my display I am using 16-bit color, also known as High Color with Alpha 565.

The tool nicely makes me a file with the right stuff in it.  First, the array of unsigned char (representing the bitmap) and the GUI_BITMAP structure.

Unfortunately it didn’t create a header file which I do like this (notice I renamed the data)

#pragma once
#include <stdlib.h>

#include "GUI.h"

extern GUI_CONST_STORAGE GUI_BITMAP bmIOTexpert_Logo_Vertical320x240;

And, if you have been paying attention you might have noticed that I created a new IoT Expert Library – Graphics that contains these new files. (I have explained this process several times in the past)

TFT Pins Discussion

I am planning on making a custom circuit board for this whole project.  So, I didn’t want to use the whole CY8CKIT-028-TFT library.  In order to start the TFT you need to call the function “mtb_st7789v()” with an argument of type “mtb_st7789v_pins_t”.  When Cypress created the BSP for the TFT we defined a function like this in the header file.

/**
 * Initializes GPIOs for the software i8080 interface.
 * @return CY_RSLT_SUCCESS if successfully initialized, else an error about
 * what went wrong
 */
static inline cy_rslt_t cy_tft_io_init(void)
{
    static const mtb_st7789v_pins_t PINS =
    {
        .db08 = CY8CKIT_028_TFT_PIN_DISPLAY_DB8,
        .db09 = CY8CKIT_028_TFT_PIN_DISPLAY_DB9,
        .db10 = CY8CKIT_028_TFT_PIN_DISPLAY_DB10,
        .db11 = CY8CKIT_028_TFT_PIN_DISPLAY_DB11,
        .db12 = CY8CKIT_028_TFT_PIN_DISPLAY_DB12,
        .db13 = CY8CKIT_028_TFT_PIN_DISPLAY_DB13,
        .db14 = CY8CKIT_028_TFT_PIN_DISPLAY_DB14,
        .db15 = CY8CKIT_028_TFT_PIN_DISPLAY_DB15,
        .nrd = CY8CKIT_028_TFT_PIN_DISPLAY_NRD,
        .nwr = CY8CKIT_028_TFT_PIN_DISPLAY_NWR,
        .dc = CY8CKIT_028_TFT_PIN_DISPLAY_DC,
        .rst = CY8CKIT_028_TFT_PIN_DISPLAY_RST,
    };
    return mtb_st7789v_init8(&PINS);
}

Curiously we also made a static definition inside of the .c file.

static const mtb_st7789v_pins_t tft_pins =
{
    .db08 = CY8CKIT_028_TFT_PIN_DISPLAY_DB8,
    .db09 = CY8CKIT_028_TFT_PIN_DISPLAY_DB9,
    .db10 = CY8CKIT_028_TFT_PIN_DISPLAY_DB10,
    .db11 = CY8CKIT_028_TFT_PIN_DISPLAY_DB11,
    .db12 = CY8CKIT_028_TFT_PIN_DISPLAY_DB12,
    .db13 = CY8CKIT_028_TFT_PIN_DISPLAY_DB13,
    .db14 = CY8CKIT_028_TFT_PIN_DISPLAY_DB14,
    .db15 = CY8CKIT_028_TFT_PIN_DISPLAY_DB15,
    .nrd = CY8CKIT_028_TFT_PIN_DISPLAY_NRD,
    .nwr = CY8CKIT_028_TFT_PIN_DISPLAY_NWR,
    .dc = CY8CKIT_028_TFT_PIN_DISPLAY_DC,
    .rst = CY8CKIT_028_TFT_PIN_DISPLAY_RST,
};

What I know is that I need those pin definitions, so I will copy them into my project (a bit later in this article)

Make a New Display Task

To get this going, make a file called displayManager.h to contain the public definition of the task.

#pragma once

void dm_task(void *arg);

Then add a file displayManager.c.  This file will be the focus of the rest of this article.  Before we get too far down the road building all of the functionality of the display, first lets setup a test task to verify the functionality of the libraries and the screen.  As I talked about earlier, define the pins for the driver.  These the pin names come out of the BSP and “should” work for all of the Cypress development kits (that have Arduino headers).

#include <stdio.h>
#include <stdlib.h>

#include "GUI.h"
#include "mtb_st7789v.h"
#include "cybsp.h"

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

#include "displayManager.h"

#include "IOTexpert_Logo_Vertical320x240.h"

const mtb_st7789v_pins_t tft_pins =
{
    .db08 = (CYBSP_J2_2),
    .db09 = (CYBSP_J2_4),
    .db10 = (CYBSP_J2_6),
    .db11 = (CYBSP_J2_10),
    .db12 = (CYBSP_J2_12),
    .db13 = (CYBSP_D7),
    .db14 = (CYBSP_D8),
    .db15 = (CYBSP_D9),
    .nrd  = (CYBSP_D10),
    .nwr  = (CYBSP_D11),
    .dc   = (CYBSP_D12),
    .rst  = (CYBSP_D13)
};

Then make a simple task that

  1. Initializes the driver
  2. Initialize emWin
  3. Sets the background color
  4. Clear the screen
  5. Draw the IoT Expert Logo
void dm_task(void *arg)
{
    /* Initialize the display controller */
    mtb_st7789v_init8(&tft_pins);
    GUI_Init();
    GUI_SetBkColor(GUI_WHITE);
    GUI_Clear();
    GUI_DrawBitmap(&bmIOTexpert_Logo_Vertical320x240,0,11);

    while(1)
        vTaskDelay(portMAX_DELAY);
}

Test

When I program this project I get a nice white background screen with my beautiful logo on it. Sweet.  In the next article I’ll do some work on displaying useful information on the screen.

 

Tilt Hydrometer (Part 8) Read the Database

Summary

This article expands the functionality of the Tilt Database by adding functions to get the status of Tilts from other threads in the system.  It demonstrates the FreeRTOS queue’s to provide a generic mechanism for communication between threads.

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

At this point the Tilt Data Manager has all of the knowledge of the status of Tilts.  But, we have other threads which need access to that data.  How are they going to get it in a thread safe way?  Moreover, how am I going to provide access to this data without hardcoding specific information about the threads into the Tilt Data Manager?  In this article I add functionality to the Tilt Data Manager Thread to provide Tilt status.  Then I will test the functionality by adding test code into the ntshell.  The green boxes are the functional blocks I will touch in this update.

 

Update the Public Interface to tiltDataManager.h

As I thought about what the other threads in the system would need I decided the best thing to do would be to write their public APIs.  Here it is:

  • What Color is the “Tilt” i.e. “Pink”
  • How many Tilts are in the system (so that you can loop through them)
  • What Tilts are currently active – a bitmask
  • How many events have been seen for each Tilt
  • Please give me a copy of the current data.
/////////////// Generally callable threadsafe - non blocking
char *tdm_colorString(tdm_tiltHandle_t handle);       // Return a char * to the color string for the tilt handle
int tdm_getNumTilt();                                 // Returns the number of possible tilts (probably always 8)
uint32_t tdm_getActiveTiltMask();                     // Return a bitmask of the active handles
uint32_t tdm_getNumDataSeen(tdm_tiltHandle_t handle); // Return number of data points seen
tdm_tiltData_t *tdm_getTiltData(tdm_tiltHandle_t handle);

Create the Thread Unsafe Functions

Obviously you need to do things thread safely.  However, I decided to have a few functions which couldn’t hurt anything, but are unsafe.  (Sorry Butch… comment below if you don’t agree and Ill fix it).  To make this a bit safer, I set the data as volatile to make the compiler at least write it back into memory when things are done.

typedef struct  {
    char *colorName;
    uint8_t uuid[TILT_IBEACON_HEADER_LEN];
    volatile tdm_tiltData_t *data;
    volatile int numDataPoints;
    volatile int numDataSeen;
} tilt_t;

The functions are very straight forward.

char *tdm_colorString(tdm_tiltHandle_t handle)
{
    return tiltDB[handle].colorName;
}

int tdm_getNumTilt()
{
    return NUM_TILT;
}

uint32_t tdm_getActiveTiltMask()
{
    uint32_t mask=0;
    for(int i=0;i<NUM_TILT;i++)
    {
        if(tiltDB[i].data)
            mask |= 1<<i;
    }
    return mask;
}

uint32_t tdm_getNumDataSeen(tdm_tiltHandle_t handle)
{
    return tiltDB[handle].numDataSeen;
}

Thread Safe Communication

Now, a much better answer for thread safety.  If you have multiple tasks that need access to the same data it is very tempting to just communicate between the threads with global variables (see above).  You are just asking for trouble because one task might be partially through updating data, when it is interrupted by the RTOS scheduler.  Then another threads starts reading/writing from global variables that were incompletely updated by the suspended thread.  BAD!

So, what is a person to do?

What I typically do is use an RTOS queue to communicate between the two tasks.  In the picture below you can see that the ntshell will send a message to the Tilt Data Manager that it wants to know the data from a Tilt (using the handle) and it wants the response sent back to the “response queue”.  When the Tilt Data Manager gets this command, it will build a response and push it back into the “response queue”.  With this scheme the Tilt Data Manager does not know which thread that it is talking to, just the handle of the queue.

Create the Thread Safe Function

Inside of my code, the first step in creating the thread safe communication is to add a command to the Tilt Commands, TDM_CMD_GET_DATAPOINT

typedef enum {
    TDM_CMD_ADD_DATAPOINT,
    TDM_CMD_PROCESS_DATA,
    TDM_CMD_GET_DATAPOINT,
} tdm_cmd_t;

The response to all questions about Tilts will be in the form of a “tdm_tiltData_t” structure.  Specifically one that has been malloc’d on the heap.  To support this I will create a function that:

  1. Mallocs a new structure
  2. Copys the data
  3. Fix the averaging
  4. Returns a pointer to the new data
// This function returns a malloc'd copy of the front of the most recent datapoint ... this function should only be used 
// internally because it is not thread safe.
static tdm_tiltData_t *tdm_getDataPointCopy(tdm_tiltHandle_t handle)
{
    tdm_tiltData_t *dp;
    dp = malloc(sizeof(tdm_tiltData_t));
    memcpy(dp,(tdm_tiltData_t *)tiltDB[handle].data,sizeof(tdm_tiltData_t));

    dp->gravity = dp->gravity / tiltDB[handle].numDataPoints;
    dp->temperature = dp->temperature / tiltDB[handle].numDataPoints;

    return dp;
}

Then I update the command queue to deal with the get data message.

            case TDM_CMD_GET_DATAPOINT:
                dp = tdm_getDataPointCopy(msg.tilt);
                xQueueSend(msg.msg,&dp,10);
            break;

Then I create the public function for other threads to call the send the request for the data command.  This function is called in the context of the calling thread NOT the tiltDataManager.  It will create a temporary queue for the transaction, send the command, then sit and wait for the response.

tdm_tiltData_t *tdm_getTiltData(tdm_tiltHandle_t handle)
{
    QueueHandle_t respqueue;
    tdm_tiltData_t *rsp;

    if(handle<0 || handle>=NUM_TILT || tiltDB[handle].data == 0 )
        return 0;

    respqueue = xQueueCreate(1,sizeof(tdm_tiltData_t *));
    if(respqueue == 0)
        return 0;

    tdm_cmdMsg_t msg;
    msg.msg = respqueue;
    msg.tilt = handle;
    msg.cmd =  TDM_CMD_GET_DATAPOINT;
    if(xQueueSend(tdm_cmdQueue,&msg,0) != pdTRUE)
    {
        printf("failed to send to dmQueue\n");
    }
    xQueueReceive(respqueue,(void *)&rsp,portMAX_DELAY);
    vQueueDelete(respqueue);
    return rsp;
}

Test with the ntshell

To test things whole thing out I add a new command to ntshell.  This command just try all of the public APIs

static int usrcmd_testdm(int argc, char **argv)
{
    tdm_tiltData_t *msg;

    printf("NumTilt = %d\n",tdm_getNumTilt());
    printf("Active = %X\n",(unsigned int)tdm_getActiveTiltMask());
    
    for(int i =0;i<tdm_getNumTilt();i++)
    {
        printf("Color = %s\t#Seen=%d\t",
            tdm_getColorString(i),
            (int)tdm_getNumDataSeen(i));

        if(tdm_getActiveTiltMask() & 1<<i)
        {
            msg = tdm_getTiltData(i);
            if(msg)
            {
                printf("G=%f\tT=%d",
                    msg->gravity,
                    (int)msg->temperature);
                free(msg);
            }
        }
        printf("\n");
    }
    return 0;
}

Here it is running.  Notice that I typed the command “testdm”  And that my test setup has heard two Tilts (remember I have a test broadcaster)

In the next article Ill add the TFT Display.

Tilt Hydrometer (Part 7) Advertising Database

Summary

In part7 of the Tilt Hydrometer series I will create a new task called the “Tilt Data Manager” which will manage the current state of all of the Tilt Hydrometers.

Story

In the design of my system I want the Bluetooth Manager to get a high priority so that it can react to all of the advertising reports without dropping data.  In addition I want other tasks in the system to be able to find out about the state of “Tilts” without distracting the Bluetooth subsystem from its job, collecting data.  To that end I will create a new task called the “Tilt Data Manager” which have a command queue where the Bluetooth Manager can submit advertising reports and other tasks can ask for data. For example the future Display Manager task can ask for the current state of the Pink Tilt.  In addition this task will filter down the blast of data to a more reasonable amount.  In the picture below, you can see the current state of the architecture in blue, and the new updates in green.

 

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.

 

Tilt Database

The first step is to build the data structures that will represent the database.  The question is, what do I want to save and how do I want to save it?  The answer to the first question is:

  1. Gravity
  2. Temperature
  3. RSSI
  4. TxPower
  5. Time (when the report was made)

The answer to how is, a “struct” that represents one data point with the current data, and a pointer which can be used to make a linked list.  In future articles I will send this data structure around to the other tasks (like the display or filesystem) so I will put this structure into the public interface file “tiltDataManager.h”

#pragma once

#include <stdint.h>
#include "FreeRTOS.h"
#include "queue.h"
#include "wiced_bt_ble.h"

typedef struct {
    float gravity;
    int temperature;
    int8_t rssi;
    int8_t txPower;
	uint32_t time;
    struct tdm_tiltData_t *next;
} tdm_tiltData_t;

You might recall from the previous article that I have an array of structures that hold information about the Tilts, one for Red, Green, Black, etc. I decided that rather than other tasks asking for a “int” that they should ask for a handle which is an alias for that int.  So, I define that next.

typedef int tdm_tiltHandle_t;

The next step in building the database is to yank the struct and array out of the BluetoothManager.c and put it where it belongs, in the tiltDataManager.c.  In the list of devices I add a pointer to the “data” which is just a list of the datapoint defined in the structure tdb_tiltData_t.  In addition I add “numDataPoints” because I am going to implement a running average.  And a “numDataSeen” which is the total number that I have ever seen.  Here is the data structure:

#define TILT_IBEACON_HEADER_LEN 20
#define TILT_IBEACON_DATA_LEN 5
#define TILT_IBEACON_LEN (TILT_IBEACON_HEADER_LEN + TILT_IBEACON_DATA_LEN)
typedef struct  {
    char *colorName;
    uint8_t uuid[TILT_IBEACON_HEADER_LEN];
    tdm_tiltData_t *data;
    int numDataPoints;
    int numDataSeen;
} tilt_t;

#define IBEACON_HEADER 0x4C,0x00,0x02,0x15

static tilt_t tiltDB [] =
{
    {"Red",      {IBEACON_HEADER,0xA4,0x95,0xBB,0x10,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Green" ,   {IBEACON_HEADER,0xA4,0x95,0xBB,0x20,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Black" ,   {IBEACON_HEADER,0xA4,0x95,0xBB,0x30,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Purple",   {IBEACON_HEADER,0xA4,0x95,0xBB,0x40,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Orange",   {IBEACON_HEADER,0xA4,0x95,0xBB,0x50,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Blue"  ,   {IBEACON_HEADER,0xA4,0x95,0xBB,0x60,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Yellow",   {IBEACON_HEADER,0xA4,0x95,0xBB,0x70,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"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))

Update the Public Interface to the tiltDataManager

Back in the tiltDataManager.h I will add two public function definitions

  1. The task (so that it can be started in main)
  2. A function tdm_processIbeacon which the BluetoothManager can call to consume a datapoint
void tdm_task(void *arg);
void tdm_processIbeacon(uint8_t *mfgAdvField,int len,wiced_bt_ble_scan_results_t *p_scan_result);

Modify the Bluetooth Manager to Submit

The next step is to yank out the old scanning callback in the Bluetooth Manager and replace it with a call to the new public interface to the tiltDataManager.  I spent a good bit of time thinking about where exactly I wanted the partition to exist between the Bluetooth Manager and the Tilt Data Manager.  I decided that the Tilt Data Manager would need to know how to decode the advertising packet, but only in that one function.

case BTM_ENABLED_EVT:
            if (WICED_BT_SUCCESS == p_event_data->enabled.status)
            {
				wiced_bt_ble_observe(WICED_TRUE, 0,tdm_processIbeacon);
            }

Build the Command Queue

The Tilt Data Manager will just “sit on a queue”, specifically a command queue.  When it receives messages from various sources it will do the needful.  For now there are two messages, TDM_CMD_ADD_DATA_POINT and TDM_CMD_PROCESS_DATA.  The message is a structure with

  1. What is the command
  2. Which Tilt are we talking about
  3. A generic void pointer
typedef enum {
    TDM_CMD_ADD_DATAPOINT,
    TDM_CMD_PROCESS_DATA,
} tdm_cmd_t;

typedef struct {
    tdm_cmd_t cmd;
    tdm_tiltHandle_t tilt;
    void *msg;
} tdm_cmdMsg_t;

static QueueHandle_t tdm_cmdQueue;

Process the Command Queue & tdm_task

The main Tilt Data Manager task has two things going on

  1. A queue where messages can be sent
  2. A periodic timer which will insert the message “TDM_CMD_PROCESS_DATA”

The main tasks just waits for messages to be pushed into the command queue.

void tdm_task(void *arg)
{
    tdm_cmdQueue = xQueueCreate(10,sizeof(tdm_cmdMsg_t));
    tdm_cmdMsg_t msg;

    TimerHandle_t tdm_processDataTimer;

    // Call the process data function once per hour to filter the input data
    tdm_processDataTimer=xTimerCreate("process",1000*30,pdTRUE,0,tdm_submitProcessData);
    xTimerStart(tdm_processDataTimer,0);

    while(1)
    {
        xQueueReceive(tdm_cmdQueue,&msg,portMAX_DELAY);
        switch(msg.cmd)
        {
            case TDM_CMD_ADD_DATAPOINT:
                tdm_addData(msg.tilt,msg.msg);
            break;

            case TDM_CMD_PROCESS_DATA:
                tdm_processData();
            break;
        }
    }
}

Process the Data

Every 30 seconds a Process Data message is pushed into the command queue.  When that happens it calls the function tdm_processData which will:

  1. Iterate through the list of tilts
  2. If there is data
  3. Calculate the running average
  4. Print out the current value
// There is a bunch of data coming out of the tilts... every second on 3 channels
// So you may endup with a boatload of data
// This function will take an average of all of the data that has been saved

static void tdm_processData()
{
    for(int i=0;i<NUM_TILT;i++)
    {
        if(tiltDB[i].data == 0)
        {
            continue;
        }

        tiltDB[i].data->gravity /= tiltDB[i].numDataPoints;
        tiltDB[i].data->temperature /= tiltDB[i].numDataPoints;
        tiltDB[i].numDataPoints = 1;
        printf("Tilt %s Temperature = %d Gravity =%f\n",tiltDB[i].colorName,tiltDB[i].data->temperature,tiltDB[i].data->gravity);
    }
}

Add Data

The add data function is called when a data point is submitted into the command queue.  When this happens there are two possible scenarios

  1. There is no history, in which case just setup the data pointer to this message
  2. There is history, in which case just add to the running total.
static void tdm_addData(tdm_tiltHandle_t handle, tdm_tiltData_t *data)
{
    if(tiltDB[handle].data == 0)
    {
        tiltDB[handle].data = data; 
        tiltDB[handle].numDataPoints = 1;
        tiltDB[handle].data->next=0;
    }
    else
    {
        tiltDB[handle].data->gravity     += data->gravity;
        tiltDB[handle].data->temperature += data->temperature;
        tiltDB[handle].numDataPoints += 1;

        free(data);
    }

    tiltDB[handle].numDataSeen += 1;
    tiltDB[handle].data->time    = data->time;
    tiltDB[handle].data->rssi    = data->rssi;
    tiltDB[handle].data->txPower = data->txPower;
}

Process the iBeacon

The process iBeacon function is actually called in the context of the Bluetooth Manager.  But, if that is true, why did I put it into the Tilt Data Manager file?  Answer: because it needs to know all about what Tilts look like.  This function is essentially the same as the one from the previous article.  It looks for an iBeacon in the manufacturer specific data.  If it find it, then it decodes the Gravity, Temperature and txPower.  Then puts that data into a structure and submits it to the Tilt Data Manager command queue.

//
// This function is called in the Bluetooth Manager Context
// Specifically it is the advertising observer callback
//
void tdm_processIbeacon(wiced_bt_ble_scan_results_t *p_scan_result,uint8_t *p_adv_data)
{

	uint8_t mfgFieldLen;
	uint8_t *mfgAdvField;
	mfgAdvField = wiced_bt_ble_check_advertising_data(p_adv_data,BTM_BLE_ADVERT_TYPE_MANUFACTURER,&mfgFieldLen);
    
	if(!mfgAdvField)
        return;

    if(mfgFieldLen != TILT_IBEACON_LEN)
        return;

    for(int i=0;i<NUM_TILT;i++)
    {
        if(memcmp(mfgAdvField,tiltDB[i].uuid,TILT_IBEACON_HEADER_LEN) == 0)
        {
            uint32_t timerTime = xTaskGetTickCount() / 1000;
		    int8_t txPower = mfgAdvField[24];
		    float gravity = ((float)((uint16_t)mfgAdvField[22] << 8 | (uint16_t)mfgAdvField[23]))/1000;
		    int temperature = mfgAdvField[20] << 8 | mfgAdvField[21];

            // The tilt repeater will send out 0's if it hasnt heard anything ... and they send out some crazy stuff
            // when they first startup 
            if(gravity>2.0 || gravity<0.95 || temperature > 110 || gravity == 0 || temperature == 0)
                return;

            tdm_tiltData_t *data;
            data = malloc(sizeof(tdm_tiltData_t));
                
            data->gravity     = gravity;
            data->temperature = temperature;
            data->txPower     = txPower;
            data->time        = timerTime;
            data->rssi        = p_scan_result->rssi;

            tdm_submitNewData(i,data);
            
        }
    }
}

Submit Commands

To simply submitting commands, I create two helper functions:

void tdm_submitNewData(tdm_tiltHandle_t handle,tdm_tiltData_t *data)
{
    tdm_cmdMsg_t msg;
    msg.msg = data;
    msg.tilt = handle;
    msg.cmd =  TDM_CMD_ADD_DATAPOINT;
    if(xQueueSend(tdm_cmdQueue,&msg,0) != pdTRUE)
    {
        printf("Failed to send to dmQueue\n");
        free(data);
    }
}

static void tdm_submitProcessData()
{
    tdm_cmdMsg_t msg;
    msg.cmd = TDM_CMD_PROCESS_DATA;
    xQueueSend(tdm_cmdQueue,&msg,0);
}

Update main.c

In main.c I need to start the tdm_task

    xTaskCreate(tdm_task, "Tilt Data Manager", configMINIMAL_STACK_SIZE*2,0 /* args */ ,0 /* priority */, 0);

Program and Test

Now lets test this thing and see if I have it working.  Now I am glad that I did the work to build a Tilt Simulator.  You can see the window on top is the simulator.  I tell it to start broadcasting “Red” with Temperature of 70, Gravity of 1010 and TxPower of 99.  Then on the bottom window you can see that is what is coming out of the system every 30 seconds.

In the next article I will add the LCD display.

tiltDataManager.h

#pragma once

#include <stdint.h>
#include "FreeRTOS.h"
#include "queue.h"
#include "wiced_bt_ble.h"

typedef struct {
    float gravity;
    int temperature;
    int8_t rssi;
    int8_t txPower;
	uint32_t time;
    struct tdm_tiltData_t *next;
} tdm_tiltData_t;

typedef int tdm_tiltHandle_t;

void tdm_task(void *arg);
void tdm_processIbeacon(wiced_bt_ble_scan_results_t *p_scan_result,uint8_t *p_adv_data);

tiltDataManager.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "wiced_bt_ble.h"

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "timers.h"
#include "tiltDataManager.h"


/*********************************************************************************
*
* Tilt Database Definition
* 
*********************************************************************************/

#define TILT_IBEACON_HEADER_LEN 20
#define TILT_IBEACON_DATA_LEN 5
#define TILT_IBEACON_LEN (TILT_IBEACON_HEADER_LEN + TILT_IBEACON_DATA_LEN)
typedef struct  {
    char *colorName;
    uint8_t uuid[TILT_IBEACON_HEADER_LEN];
    tdm_tiltData_t *data;
    int numDataPoints;
    int numDataSeen;
} tilt_t;

#define IBEACON_HEADER 0x4C,0x00,0x02,0x15

static tilt_t tiltDB [] =
{
    {"Red",      {IBEACON_HEADER,0xA4,0x95,0xBB,0x10,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Green" ,   {IBEACON_HEADER,0xA4,0x95,0xBB,0x20,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Black" ,   {IBEACON_HEADER,0xA4,0x95,0xBB,0x30,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Purple",   {IBEACON_HEADER,0xA4,0x95,0xBB,0x40,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Orange",   {IBEACON_HEADER,0xA4,0x95,0xBB,0x50,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Blue"  ,   {IBEACON_HEADER,0xA4,0x95,0xBB,0x60,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"Yellow",   {IBEACON_HEADER,0xA4,0x95,0xBB,0x70,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE},0,0,0},
    {"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))

/*********************************************************************************
* 
* Tilt Data Manager External Interface Queue
*
*********************************************************************************/
typedef enum {
    TDM_CMD_ADD_DATAPOINT,
    TDM_CMD_PROCESS_DATA,
} tdm_cmd_t;

typedef struct {
    tdm_cmd_t cmd;
    tdm_tiltHandle_t tilt;
    void *msg;
} tdm_cmdMsg_t;

static QueueHandle_t tdm_cmdQueue;

static void tdm_submitProcessData();



// There is a bunch of data coming out of the tilts... every second on 3 channels
// So you may endup with a boatload of data
// This function will take an average of all of the data that has been saved

static void tdm_processData()
{
    for(int i=0;i<NUM_TILT;i++)
    {
        if(tiltDB[i].data == 0)
        {
            continue;
        }

        tiltDB[i].data->gravity /= tiltDB[i].numDataPoints;
        tiltDB[i].data->temperature /= tiltDB[i].numDataPoints;
        tiltDB[i].numDataPoints = 1;
        printf("Tilt %s Temperature = %d Gravity =%f\n",tiltDB[i].colorName,tiltDB[i].data->temperature,tiltDB[i].data->gravity);
    }
}


// This function will
// insert new data for that tilt if none has ever been seen
// or it will add the data to the current count
static void tdm_addData(tdm_tiltHandle_t handle, tdm_tiltData_t *data)
{
    if(tiltDB[handle].data == 0)
    {
        tiltDB[handle].data = data; 
        tiltDB[handle].numDataPoints = 1;
        tiltDB[handle].data->next=0;
    }
    else
    {
        tiltDB[handle].data->gravity     += data->gravity;
        tiltDB[handle].data->temperature += data->temperature;
        tiltDB[handle].numDataPoints += 1;

        free(data);
    }

    tiltDB[handle].numDataSeen += 1;
    tiltDB[handle].data->time    = data->time;
    tiltDB[handle].data->rssi    = data->rssi;
    tiltDB[handle].data->txPower = data->txPower;
}


////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Public Functions
// 
////////////////////////////////////////////////////////////////////////////////////////////////////////

void tdm_task(void *arg)
{
    tdm_cmdQueue = xQueueCreate(10,sizeof(tdm_cmdMsg_t));
    tdm_cmdMsg_t msg;

    TimerHandle_t tdm_processDataTimer;

    // Call the process data function once per hour to filter the input data
    tdm_processDataTimer=xTimerCreate("process",1000*30,pdTRUE,0,tdm_submitProcessData);
    xTimerStart(tdm_processDataTimer,0);

    while(1)
    {
        xQueueReceive(tdm_cmdQueue,&msg,portMAX_DELAY);
        switch(msg.cmd)
        {
            case TDM_CMD_ADD_DATAPOINT:
                tdm_addData(msg.tilt,msg.msg);
            break;

            case TDM_CMD_PROCESS_DATA:
                tdm_processData();
            break;
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// These functions submit commands to main command queue: tdm_cmdQueue
// 
////////////////////////////////////////////////////////////////////////////////////////////////////////

void tdm_submitNewData(tdm_tiltHandle_t handle,tdm_tiltData_t *data)
{
    tdm_cmdMsg_t msg;
    msg.msg = data;
    msg.tilt = handle;
    msg.cmd =  TDM_CMD_ADD_DATAPOINT;
    if(xQueueSend(tdm_cmdQueue,&msg,0) != pdTRUE)
    {
        printf("Failed to send to dmQueue\n");
        free(data);
    }
}

static void tdm_submitProcessData()
{
    tdm_cmdMsg_t msg;
    msg.cmd = TDM_CMD_PROCESS_DATA;
    xQueueSend(tdm_cmdQueue,&msg,0);
}


//
// This function is called in the Bluetooth Manager Context
// Specifically it is the advertising observer callback
//
void tdm_processIbeacon(wiced_bt_ble_scan_results_t *p_scan_result,uint8_t *p_adv_data)
{

	uint8_t mfgFieldLen;
	uint8_t *mfgAdvField;
	mfgAdvField = wiced_bt_ble_check_advertising_data(p_adv_data,BTM_BLE_ADVERT_TYPE_MANUFACTURER,&mfgFieldLen);
    
	if(!mfgAdvField)
        return;

    if(mfgFieldLen != TILT_IBEACON_LEN)
        return;

    for(int i=0;i<NUM_TILT;i++)
    {
        if(memcmp(mfgAdvField,tiltDB[i].uuid,TILT_IBEACON_HEADER_LEN) == 0)
        {
            uint32_t timerTime = xTaskGetTickCount() / 1000;
		    int8_t txPower = mfgAdvField[24];
		    float gravity = ((float)((uint16_t)mfgAdvField[22] << 8 | (uint16_t)mfgAdvField[23]))/1000;
		    int temperature = mfgAdvField[20] << 8 | mfgAdvField[21];

            // The tilt repeater will send out 0's if it hasnt heard anything ... and they send out some crazy stuff
            // when they first startup 
            if(gravity>2.0 || gravity<0.95 || temperature > 110 || gravity == 0 || temperature == 0)
                return;

            tdm_tiltData_t *data;
            data = malloc(sizeof(tdm_tiltData_t));
                
            data->gravity     = gravity;
            data->temperature = temperature;
            data->txPower     = txPower;
            data->time        = timerTime;
            data->rssi        = p_scan_result->rssi;

            tdm_submitNewData(i,data);
            
        }
    }
}

 

Tilt Hydrometer (Part 6) Tilt Simulator an Upgrade

Summary

An update to the Tilt Hydrometer Simulator to allow individual advertising packets to be turned on & off using the Multi-Advertising Capability of PSoC & CYW43012

Story

After I finished the last article I was sure that I was done with the Tilt Simulator.  But, when I walked away I started thinking about what I had done, and I decided that I needed to make one more update.  Specifically I wanted to add two more commands, “enable” and “disable”.  I also realized that I had the wrong idea about the implementation of multi-adv.  I thought that you had to have all of the slots from 1 to n active at the same time.  The more I thought about it the more I realized that was wrong.  This is really cool for my situation as there are 8 different color Tilts and the are at least 8 slots.  So, I can assign each Tilt to its own slot in the controller.

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.

 

Change the Database

Remember that my database is just an array of tilt_t structures.  That array goes from 0 to NUM_TILT and that the multi advertising capability goes from 1-a bunch.  So my algorithm for assigning tilts is just to put i+1, in other words the first Tilt is 0 and it will go in slot 1 etc.  Here is the original structure.

typedef struct  {
    char *colorName;    // The color string
    int slot;           // Which Bluetooth ADV Slot this Tilt is using
    bool dirty;         // A flag to tell if there has been a change since the adv was last written
    int rate;           // rate in ms
    int tempUpdate;     // The update rate for temperature
    int gravUpdate;     // The update rate for gravity
    uint8_t advData[TILT_IBEACON_LEN];
} tilt_t;

To make this work, I will

  1. Get rid of slot
  2. Add a boolean field called “enable”
typedef struct  {
    char *colorName;    // The color string
    bool enable;
    bool dirty;         // A flag to tell if there has been a change since the adv was last written
    int rate;           // rate in ms
    int tempUpdate;     // The update rate for temperature
    int gravUpdate;     // The update rate for gravity
    uint8_t advData[TILT_IBEACON_LEN];
} tilt_t;

Add an Initialization Function

Instead of re-initializing each advertising slot each time, I realized that I could initialize them all at once.  So, I create a function to do just that.

void btm_initialize()
{
    static wiced_bt_ble_multi_adv_params_t myParams = {
    .adv_int_min       = BTM_BLE_ADVERT_INTERVAL_MIN,
    .adv_int_max       = 96,
    .adv_type          = MULTI_ADVERT_NONCONNECTABLE_EVENT,
    .channel_map       = BTM_BLE_ADVERT_CHNL_37 | BTM_BLE_ADVERT_CHNL_38 | BTM_BLE_ADVERT_CHNL_39,
    .adv_filter_policy = BTM_BLE_ADVERT_FILTER_ALL_CONNECTION_REQ_ALL_SCAN_REQ,
    .adv_tx_power      = MULTI_ADV_TX_POWER_MAX_INDEX,
    .peer_bd_addr      = {0},
    .peer_addr_type    = BLE_ADDR_PUBLIC,
    .own_bd_addr       = {0},
    .own_addr_type     = BLE_ADDR_PUBLIC,
    };

    for(int i=0;i<NUM_TILT;i++)
    {
        wiced_set_multi_advertisement_data(tiltDB[i].advData,sizeof(tiltDB[i].advData),i+1);
        wiced_set_multi_advertisement_params(i+1,&myParams);
        wiced_start_multi_advertisements( MULTI_ADVERT_STOP, i+1);
    }
}

Then I updated the stack startup event to call the initialize function.

        case BTM_ENABLED_EVT:
            printf("Started BT Stack Succesfully\n");
            btm_initialize();
            btm_cmdQueue = xQueueCreate(10,sizeof(btm_cmdMsg_t));
            wiced_init_timer_ext(&btm_processDataTimer,btm_processCmdQueue,0,WICED_TRUE);
            wiced_start_timer_ext(&btm_processDataTimer,BTM_QUEUE_RATE);
        break;

Update the Function btm_setAdvPacket

When it is time to update the advertising packets, I just loop through the database

  1. If the tilt is dirty
  2. Then update the data
  3. And either turn on/off the advertisements for the tilt based on the state of the enable flag
void btm_setAdvPacket()
{
    for(int i=0;i<NUM_TILT;i++)
    {
        if(tiltDB[i].dirty)
        {   
            tiltDB[i].dirty = false;
            wiced_set_multi_advertisement_data(tiltDB[i].advData,sizeof(tiltDB[i].advData),i+1);
        
            if(tiltDB[i].enable)
                wiced_start_multi_advertisements( MULTI_ADVERT_START, i+1 );
            else
                wiced_start_multi_advertisements( MULTI_ADVERT_STOP, i+1 );
        }
    }
}

Add New Commands

Now I need to add two new commands, BTM_CMD_ENABLE and BTM_CMD_DISABLE

typedef enum {
    BTM_CMD_SET_DATA,
    BTM_CMD_PRINT_TABLE,
    BTM_CMD_SET_UPDATE,
    BTM_CMD_ENABLE,
    BTM_CMD_DISABLE,
} btm_cmd_t;

The code for enable is just to turn the enable flag on then turn on the advertising

            case BTM_CMD_ENABLE:
                tiltDB[msg.num].enable = true;
                wiced_start_multi_advertisements( MULTI_ADVERT_START, msg.num+1 );
            break;

A function for usrcmd is just to queue the message to enable

void btm_updateEnable(int num)
{
    if(btm_cmdQueue == 0 || num<0 || num>=NUM_TILT)
    {
        return;
    }
    btm_cmdMsg_t msg;
    msg.cmd =    BTM_CMD_ENABLE;
    msg.num = num;
    xQueueSend(btm_cmdQueue,&msg,0);
}

The command for disable just disables the flag and the advertising

            case BTM_CMD_DISABLE:
                tiltDB[msg.num].enable = false;
                wiced_start_multi_advertisements( MULTI_ADVERT_STOP, msg.num+1 );
            break;

And the usrcmd function just queues the message.

void btm_updateDisable(int num)
{
    if(btm_cmdQueue == 0 || num<0 || num>=NUM_TILT)
        return;
    btm_cmdMsg_t msg;
    msg.cmd =    BTM_CMD_DISABLE;
    msg.num = num;
    xQueueSend(btm_cmdQueue,&msg,0);
}

Update the Printing

Fix the print out to show the state of enabling instead of the “slot”

void btm_printTable()
{
    printf("\n# Color   E   Rate T  UpT Grav UpG TxP\n");

    for(int i=0;i<NUM_TILT;i++)
    {
        printf("%d %6s  %d %5d %3d %2d %4d %2d %3d\n",i,
            tiltDB[i].colorName,tiltDB[i].enable,
            tiltDB[i].rate*BTM_QUEUE_RATE,
            btm_getTemperature(i),tiltDB[i].tempUpdate,
            btm_getGravity(i),tiltDB[i].gravUpdate,
            tiltDB[i].advData[29]);
    }
}

Program and Test

When I program the board I can

  1. set
  2. print
  3. enable
  4. print
  5. disable
  6. print

And see with the Tilt app on my phone that everything is working

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

Summary

Using the PSoC 6 AnyCloud Bluetooth Stack to send out “multi-adv” Bluetooth advertising packets with a PSoC 6 and CYW43012.  Multi-adv means sending out multiple different simultaneous Bluetooth Advertising packets.  This includes building a project to simulate a Tilt Hydrometer.

Story

As I worked on the Tilt project I came to realize that what I really needed what a Tilt Simulator.  That is, a PSoC programmed with a command line interface that could send out the iBeacon messages that looked like the ones that the Tilt actually sent out.  Why?  Simple, all three of my Tilts are busy doing their day job measuring beer and they don’t have time to fool around being test subjects for my IoT Expert Articles.

The CYW43012 controller that I am using is capable of automatically rotating through a list of at least 8 beacons (maybe more, but I don’t know the exact number. I suppose I should figure it out).  In other words I can simultaneously simulate 8 Tilts at one time with one PSoC.

For this project, I want the project to be able to

  1. Simultaneously broadcast 0-8 of the Tilt iBeacon messages
  2. Have a command line interface to control the Tilt iBeacons
  3. Be able to set a ramp rate for Temperature and Gravity on a per Tilt basis

The commands will be:

  1. print – print out the table of the information about the Tilt iBeacons
  2. set – set the temperature, gravity and txPower e.g. “set 0 77 1020 99” would turn on beacon 1 with the temperature set to 77 and the gravity set to 1020 and the txPower set to 99
  3. update – configure the update rate for temperature and gravity e.g. “update 6 1000 0 -1” would set tilt 6 with an update to happen once per 1000ms with a +0 temperature per update and a -1 gravity points per update

Here is an example of the display.

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.

 

Build the Basic Project

Create a new project using the FreeRTOS NT Shell Template called Tilt Simulator

Enable the bluetooth-freertos and btstack libraries.  Also enable the IoT Expert BT Utilities library.

In the command line copy the template bluetoothManager.* into the project

Start the Bluetooth configurator with “make config_bt” on the command line.  Press the new project file (the blank paper)

Select the P6 connectivity device

Press save and call your file “TiltSimulator” (it actually doesn’t matter what you call it).  This will generate the configuration structures you need.

In command line run “make vscode”.  Then edit the Makefile to include these components.

COMPONENTS=FREERTOS WICED_BLE

You should run a build/program right now as you will have a basic project with a blinking led, the Bluetooth stack running and a command line.

Multiadv

The CYW43012 has the ability to automatically rotate through a list of advertising packets.  Each packet can have different data including a different Bluetooth address (how crazy is that?)  Each packet can also be advertised at a different and configurable rate.  It does this all with no intervention by the host.

This functionality is documented in the Bluetooth Stack section called “MutiAdv” under Device Management.

Build the Database

The database is just an array of structures.  There are only 8 entries in the database, one for each color Tilt.  In addition to a string representing the name, the database will have

  1. Which advertising slot that this tilt is using (0 means that it isn’t active)
  2. If the data is “dirty” meaning it has changed in this table but has not yet been copied to the controller for broadcast
  3. rate = how often to update the data in the that row
  4. tempUPDATE = how much to change the data by each time an update is called
  5. gravUPDATE = how much to change the gravity by each time an update is called
  6. The actual bytes of the advertising packet
/*********************************************************************************
*
* Tilt Database Definition
* 
*********************************************************************************/

#define TILT_IBEACON_HEADER_LEN 25
#define TILT_IBEACON_DATA_LEN 5
#define TILT_IBEACON_LEN (TILT_IBEACON_HEADER_LEN + TILT_IBEACON_DATA_LEN)

typedef struct  {
    char *colorName;    // The color string
    int slot;           // Which Bluetooth ADV Slot this Tilt is using
    bool dirty;         // A flag to tell if there has been a change since the adv was last written
    int rate;           // rate in ms
    int tempUpdate;     // The update rate for temperature
    int gravUpdate;     // The update rate for gravity
    uint8_t advData[TILT_IBEACON_LEN];
} tilt_t;

// 02 01 04 = Flags
// 0x1A 0xFF = Manufacturer specific data
// 0x4c 0x00 = Apple
// 0x02 = iBeacon
// 0x15 = remaining data length
#define IBEACON_HEADER 0x02,0x01,0x04,0x1A,0xFF,0x4C,0x00,0x02,0x15

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

#define IBEACON_TEMP_HI (25)
#define IBEACON_TEMP_LO (26)
#define IBEACON_GRAV_HI (27)
#define IBEACON_GRAV_LO (28)
#define IBEACON_TXPOWER (29)

Now I will make “getters and setters”.  These functions will set the correct bytes in the adverting packets, and can retrieve the current values in those bytes.  Remember in the Apple iBeacon format there are 5 “user” bytes

  1. Major byte 0 and byte 1
  2. Minor byte 0 and bytes 1
  3. txPower

Apple didnt specify what you stored in the major/minor.  In fact they also didnt specify the endianness.  The Tilt guys decided that the would store BIG endian – oh well.

  1. Major = Temperature in degrees F
  2. Minor = Gravity points i.e. 1050 means 1.050

If you look in my code the “set” of temperature and gravity they will “wrap around”.  If you set a value BELOW the minimum it will set the highest value and if you set a value ABOVE the maximum it will set it to the lowest.  This allows my automatic update code to just count one direction.

int btm_getTemperature(int num)
{
    return (uint16_t)tiltDB[num].advData[IBEACON_TEMP_HI] << 8 | tiltDB[num].advData[IBEACON_TEMP_LO];
}

void btm_setTemperature(int num,uint16_t temperature)
{
    if(temperature > 150)
        temperature = 10;
    if(temperature<10)
        temperature = 150;

    int oldtemp = btm_getTemperature(num);

    tiltDB[num].advData[IBEACON_TEMP_HI] = (temperature & 0xFF00) >> 8;    
    tiltDB[num].advData[IBEACON_TEMP_LO] = temperature & 0xFF;
    if(temperature != oldtemp)
        tiltDB[num].dirty = true;    
}

int btm_getGravity(int num)
{
    return (uint16_t)tiltDB[num].advData[IBEACON_GRAV_HI] << 8 | tiltDB[num].advData[IBEACON_GRAV_LO];
}

void btm_setGravity(int num,uint16_t gravity)
{
    // These if's cause the gravity to "wrap around" at 1200 and 900
    if(gravity>1200)
        gravity = 900;
    if(gravity <900)
        gravity = 1200;

    int oldgrav = btm_getGravity(num);
    tiltDB[num].advData[IBEACON_GRAV_HI] = (uint8_t)((gravity & 0xFF00) >> 8);
    tiltDB[num].advData[IBEACON_GRAV_LO] = (uint8_t)(gravity & 0xFF);
    if(oldgrav != gravity)
        tiltDB[num].dirty = true;
}

void btm_setTxPower(int num, int8_t txPower)
{
    tiltDB[num].advData[IBEACON_TXPOWER] = txPower;
    tiltDB[num].dirty = true;
}

Transfer Database to Bluetooth Controller

The next two functions are used to bridge between the database and the controller.  The first function “btm_setAdvPacket” probably should have been called “btm_setAdvPackets” because its function is to copy the database bytes into the controller for actual broadcast.  This function is simply a loop that looks at each of the Tilts in the database.

  1. If they are in a “slot”
  2. AND they are dirty (something has been changed since the last time this was called)
  3. Then copy the data into the controller
  4. Set the parameters
  5. Start it advertising
// btm_setAdvPacket
//
// This function updates what the advertising packets and the state of the BT controller:
// It will loop through the table of iBeacons... then if they are in a slot & they are dirty
// it will move them to the contoller and enable advertisements on that slot

void btm_setAdvPacket()
{
    static wiced_bt_ble_multi_adv_params_t myParams = {
    .adv_int_min       = BTM_BLE_ADVERT_INTERVAL_MIN,
    .adv_int_max       = 96,
    .adv_type          = MULTI_ADVERT_NONCONNECTABLE_EVENT,
    .channel_map       = BTM_BLE_ADVERT_CHNL_37 | BTM_BLE_ADVERT_CHNL_38 | BTM_BLE_ADVERT_CHNL_39,
    .adv_filter_policy = BTM_BLE_ADVERT_FILTER_ALL_CONNECTION_REQ_ALL_SCAN_REQ,
    .adv_tx_power      = MULTI_ADV_TX_POWER_MAX_INDEX,
    .peer_bd_addr      = {0},
    .peer_addr_type    = BLE_ADDR_PUBLIC,
    .own_bd_addr       = {0},
    .own_addr_type     = BLE_ADDR_PUBLIC,
    };

    for(int i=0;i<NUM_TILT;i++)
    {
        if(tiltDB[i].slot && tiltDB[i].dirty)
        {   
            tiltDB[i].dirty = false;
            wiced_set_multi_advertisement_data(tiltDB[i].advData,sizeof(tiltDB[i].advData),tiltDB[i].slot);
            wiced_set_multi_advertisement_params(tiltDB[i].slot,&myParams);
            wiced_start_multi_advertisements( MULTI_ADVERT_START, tiltDB[i].slot );
        }
    }
}

In order to “activate” a row in the database you need to assign it to a slot.  I do this by keeping track of the number of times I have assigned an entry in “btm_active” which is static to this function.  Now, shocker, I check to make sure that the caller didn’t accidentally call this before.  Finally I assign it to a slot and mark it as dirty (so that when the btm_setAdvPacket function is called the data will be put into the controller.

/* btm_activate
*
* This function will put the tilt in the database entry num
* into the next available advertising slot
*
*/
void btm_activate(int num)
{
    // Keep track of the number of currently active iBeacons
    static int  btm_active=0;
    
    CY_ASSERT(num<NUM_TILT);
    if(tiltDB[num].slot == 0) // Make sure that it is not already in a slow
    {
        btm_active += 1;
        tiltDB[num].slot = btm_active;
        tiltDB[num].dirty = true;
    }
}

Command Queue

The command queue is used by the command line interface to submit changes to the Tilt database.  Specifically set, update and print.  The message is just the values for set & update.

/*********************************************************************************
* 
* Tilt Data Manager External Interface Queue
*
*********************************************************************************/
typedef enum {
    BTM_CMD_SET_DATA,
    BTM_CMD_PRINT_TABLE,
    BTM_CMD_SET_UPDATE,
} btm_cmd_t;

typedef struct {
    btm_cmd_t cmd;
    int num;
    int temperature;
    int gravity;
    int txPower;
} btm_cmdMsg_t;

static QueueHandle_t btm_cmdQueue=0;
static wiced_timer_ext_t btm_processDataTimer;

The function to process the command queue has two functions in the systems

  1. Process commands from the command line
  2. Cause the data to be updated (as configured by the command line)

This function is run by a timer that is started when the Bluetooth stack turns on.  I have it set to run every 200ms.  Each time it runs it will read all of the messages in the queue and process them.  Once that is complete it will check to see if the Tilts need to be updated.

In my system there is a “base rate” which is set by the frequency of this function.  In other words the “count” variable counts the number of times this function has been called.  This is about “BTM_QUEUE_RATE”

/* btm_processCmdQueue
*
*  This function is called by a timer every BTM_QUEUE_RATE ms (around 200ms)
*  It processes commands from the GUI
*  It also updates the adverting packets if they are being updated at a rate
*
*/
void btm_processCmdQueue( wiced_timer_callback_arg_t cb_params )
{
    static int count = 0; // This counts the numbers of times the callback has happend

    btm_cmdMsg_t msg;
    while(xQueueReceive(btm_cmdQueue,&msg,0) == pdTRUE)
    {
        switch(msg.cmd)
        {
            case BTM_CMD_SET_DATA:
                btm_setGravity(msg.num,msg.gravity);
                btm_setTemperature(msg.num,msg.temperature);
                btm_setTxPower(msg.num,msg.txPower);
                btm_activate(msg.num);
            break;

            case BTM_CMD_PRINT_TABLE:
                btm_printTable();
            break;

            case BTM_CMD_SET_UPDATE:
                tiltDB[msg.num].tempUpdate = msg.temperature;
                tiltDB[msg.num].gravUpdate = msg.gravity;
                tiltDB[msg.num].rate = msg.txPower / BTM_QUEUE_RATE;
            break;
        }
    }
    count = count + 1;

    // This block of code updates the current values with the update rate
    for(int i=0;i<NUM_TILT;i++)
    {
        // If the slot active
        // and the update rate says that it is time
        if(tiltDB[i].slot && count % tiltDB[i].rate  == 0)
        {
            btm_setTemperature(i,btm_getTemperature(i) + tiltDB[i].tempUpdate);
            btm_setGravity(i,btm_getGravity(i) + tiltDB[i].gravUpdate);
        }
    }
    btm_setAdvPacket();
}

Bluetooth Management

When the Bluetooth Stack is turned on I need to do two things

  1. Initialize the command queue
  2. Initialize a periodic timer that calls the “btm_processCmdQueue”

Notice that every time something changes in the advertising packets the management callback is called with the event type “BTM_MULTI_ADVERT_RESP_EVENT”… to which I do NOTHING.  I suppose that I should check and make sure that whatever I did succeed.

wiced_result_t app_bt_management_callback(wiced_bt_management_evt_t event, wiced_bt_management_evt_data_t *p_event_data)
{
    wiced_result_t result = WICED_BT_SUCCESS;

    switch (event)
    {
        case BTM_ENABLED_EVT:
            printf("Started BT Stack Succesfully\n");
            btm_cmdQueue = xQueueCreate(10,sizeof(btm_cmdMsg_t));
            wiced_init_timer_ext(&btm_processDataTimer,btm_processCmdQueue,0,WICED_TRUE);
            wiced_start_timer_ext(&btm_processDataTimer,BTM_QUEUE_RATE);
        break;

        case BTM_MULTI_ADVERT_RESP_EVENT: // Do nothing...
        break;

        default:
            printf("Unhandled Bluetooth Management Event: %s\n", btutil_getBTEventName( event));
        break;
    }

    return result;
}

Public Functions

In the command line I want the ability to

  1. Print
  2. Configure the data in Tilt advertising packet
  3. Set the update rate

That is what these three functions do.  Notice that I check that the command queue has been initialized.

void btm_printTableCmd()
{
    if(btm_cmdQueue == 0)
        return;
    
    btm_cmdMsg_t msg;
    msg.cmd =    BTM_CMD_PRINT_TABLE;
    xQueueSend(btm_cmdQueue,&msg,0);
}

void btm_setDataCmd(int num,int temperature,int gravity,int txPower)
{
    if(btm_cmdQueue == 0)
        return;

    btm_cmdMsg_t msg;
    msg.cmd =    BTM_CMD_SET_DATA;
    msg.num = num;
    msg.temperature = temperature;
    msg.gravity = gravity;
    msg.txPower = txPower;
    xQueueSend(btm_cmdQueue,&msg,0);
}


void btm_updateDataCmd(int num,int rate ,int temperature,int gravity )
{
    if(btm_cmdQueue == 0)
        return;
    btm_cmdMsg_t msg;
    msg.cmd =    BTM_CMD_SET_UPDATE;
    msg.num = num;
    msg.temperature = temperature;
    msg.gravity = gravity;
    msg.txPower = rate;
    xQueueSend(btm_cmdQueue,&msg,0);
}

Update the Command Line

In usrcmd.c I need the three functions which the command line will call.

  1. usrcmd_print … to print out the table
  2. usrcmd_set to set the values in the Tilt database
  3. usrcmd_update to set the update rate
static int usrcmd_print(int argc, char **argv)
{
    btm_printTableCmd();
    return 0;
}

static int usrcmd_set(int argc, char **argv)
{
    int gravity;
    int temperature;
    int txPower;
    int num;
    if(argc == 5)
    {
        sscanf(argv[1],"%d",&num);
        sscanf(argv[2],"%d",&temperature);
        sscanf(argv[3],"%d",&gravity);
        sscanf(argv[4],"%d",&txPower);

        btm_setDataCmd(num,temperature,gravity,txPower);
    }
    return 0;
}

static int usrcmd_update(int argc, char **argv)
{
    int gravity;
    int temperature;
    int rate;
    int num;
    if(argc == 5)
    {
        sscanf(argv[1],"%d",&num);
        sscanf(argv[2],"%d",&rate);
        sscanf(argv[3],"%d",&temperature);
        sscanf(argv[4],"%d",&gravity);

        btm_updateDataCmd(num,rate,temperature,gravity);
    }
    return 0;
}

And once you have those functions you need to turn them on in the command line

static const cmd_table_t cmdlist[] = {
    { "help", "This is a description text string for help command.", usrcmd_help },
    { "info", "This is a description text string for info command.", usrcmd_info },
    { "clear", "Clear the screen", usrcmd_clear },
    { "pargs","print the list of arguments", usrcmd_pargs},
#ifdef configUSE_TRACE_FACILITY 
#if configUSE_STATS_FORMATTING_FUNCTIONS ==1
    { "tasks","print the list of RTOS Tasks", usrcmd_list},
#endif
#endif
    { "print", "Print table", usrcmd_print },
    { "set", "set num temperature gravity txpower", usrcmd_set },
    { "update", "update num rate temperature gravity", usrcmd_update },
};

In the next article I will be back to the “client” end of my system and will build a database.

Full Functions

Here are the full functions & files.  You can also “git” this at git@github.com:iotexpert/TiltSimulator.git

  1. bluetoothManager.h
  2. bluetoothManager.c
  3. main.c
  4. usrcmd.c

bluetoothManager.h

#pragma once
#include "wiced_bt_stack.h"
#include "wiced_bt_dev.h"

wiced_result_t app_bt_management_callback(wiced_bt_management_evt_t event, wiced_bt_management_evt_data_t *p_event_data);
void btm_printTable();

void btm_printTableCmd();
void btm_setDataCmd(int num,int temperature,int gravity,int txPower);
void btm_updateDataCmd(int num,int rate ,int temperature,int gravity );

bluetoothManager.c

#include <stdio.h>
#include <stdlib.h>
#include "cybsp.h"
#include "FreeRTOS.h"
#include "bluetoothManager.h"
#include "wiced_bt_stack.h"
#include "wiced_bt_dev.h"
#include "wiced_bt_trace.h"
#include "wiced_timer.h"
#include "queue.h"
#include "btutil.h"
// Read the queue
#define BTM_QUEUE_RATE 200
/*********************************************************************************
*
* Tilt Database Definition
* 
*********************************************************************************/
#define TILT_IBEACON_HEADER_LEN 25
#define TILT_IBEACON_DATA_LEN 5
#define TILT_IBEACON_LEN (TILT_IBEACON_HEADER_LEN + TILT_IBEACON_DATA_LEN)
typedef struct  {
char *colorName;    // The color string
int slot;           // Which Bluetooth ADV Slot this Tilt is using
bool dirty;         // A flag to tell if there has been a change since the adv was last written
int rate;           // rate in ms
int tempUpdate;     // The update rate for temperature
int gravUpdate;     // The update rate for gravity
uint8_t advData[TILT_IBEACON_LEN];
} tilt_t;
// 02 01 04 = Flags
// 0x1A 0xFF = Manufacturer specific data
// 0x4c 0x00 = Apple
// 0x02 = iBeacon
// 0x15 = remaining data length
#define IBEACON_HEADER 0x02,0x01,0x04,0x1A,0xFF,0x4C,0x00,0x02,0x15
static tilt_t tiltDB [] =
{
{"Red",    0, true, 0,0,0, {IBEACON_HEADER,0xA4,0x95,0xBB,0x10,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE, 0x00,0x00,0x00,0x00,0x00}},
{"Green" , 0, true, 0,0,0, {IBEACON_HEADER,0xA4,0x95,0xBB,0x20,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE, 0x00,0x00,0x00,0x00,0x00}},
{"Black" , 0, true, 0,0,0, {IBEACON_HEADER,0xA4,0x95,0xBB,0x30,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE, 0x00,0x00,0x00,0x00,0x00}},
{"Purple", 0, true, 0,0,0, {IBEACON_HEADER,0xA4,0x95,0xBB,0x40,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE, 0x00,0x00,0x00,0x00,0x00}},
{"Orange", 0, true, 0,0,0, {IBEACON_HEADER,0xA4,0x95,0xBB,0x50,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE, 0x00,0x00,0x00,0x00,0x00}},
{"Blue"  , 0, true, 0,0,0, {IBEACON_HEADER,0xA4,0x95,0xBB,0x60,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE, 0x00,0x00,0x00,0x00,0x00}},
{"Yellow", 0, true, 0,0,0, {IBEACON_HEADER,0xA4,0x95,0xBB,0x70,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE, 0x00,0x00,0x00,0x00,0x00}},
{"Pink"  , 0, true, 0,0,0, {IBEACON_HEADER,0xA4,0x95,0xBB,0x80,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE, 0x00,0x00,0x00,0x00,0x00}},
};
#define NUM_TILT (sizeof(tiltDB)/sizeof(tilt_t))
#define IBEACON_TEMP_HI (25)
#define IBEACON_TEMP_LO (26)
#define IBEACON_GRAV_HI (27)
#define IBEACON_GRAV_LO (28)
#define IBEACON_TXPOWER (29)
/*********************************************************************************
* 
* Tilt Data Manager External Interface Queue
*
*********************************************************************************/
typedef enum {
BTM_CMD_SET_DATA,
BTM_CMD_PRINT_TABLE,
BTM_CMD_SET_UPDATE,
} btm_cmd_t;
typedef struct {
btm_cmd_t cmd;
int num;
int temperature;
int gravity;
int txPower;
} btm_cmdMsg_t;
static QueueHandle_t btm_cmdQueue=0;
static wiced_timer_ext_t btm_processDataTimer;
/*********************************************************************************
* 
* These functions are used to interact with the Bluetooth Advertising Controller
*
*********************************************************************************/
// btm_setAdvPacket
//
// This function updates what the advertising packets and the state of the BT controller:
// It will loop through the table of iBeacons... then if they are in a slot & they are dirty
// it will move them to the contoller and enable advertisements on that slot
void btm_setAdvPacket()
{
static wiced_bt_ble_multi_adv_params_t myParams = {
.adv_int_min       = BTM_BLE_ADVERT_INTERVAL_MIN,
.adv_int_max       = 96,
.adv_type          = MULTI_ADVERT_NONCONNECTABLE_EVENT,
.channel_map       = BTM_BLE_ADVERT_CHNL_37 | BTM_BLE_ADVERT_CHNL_38 | BTM_BLE_ADVERT_CHNL_39,
.adv_filter_policy = BTM_BLE_ADVERT_FILTER_ALL_CONNECTION_REQ_ALL_SCAN_REQ,
.adv_tx_power      = MULTI_ADV_TX_POWER_MAX_INDEX,
.peer_bd_addr      = {0},
.peer_addr_type    = BLE_ADDR_PUBLIC,
.own_bd_addr       = {0},
.own_addr_type     = BLE_ADDR_PUBLIC,
};
for(int i=0;i<NUM_TILT;i++)
{
if(tiltDB[i].slot && tiltDB[i].dirty)
{   
tiltDB[i].dirty = false;
wiced_set_multi_advertisement_data(tiltDB[i].advData,sizeof(tiltDB[i].advData),tiltDB[i].slot);
wiced_set_multi_advertisement_params(tiltDB[i].slot,&myParams);
wiced_start_multi_advertisements( MULTI_ADVERT_START, tiltDB[i].slot );
}
}
}
/* btm_activate
*
* This function will put the tilt in the database entry num
* into the next available advertising slot
*
*/
void btm_activate(int num)
{
// Keep track of the number of currently active iBeacons
static int  btm_active=0;
CY_ASSERT(num<NUM_TILT);
if(tiltDB[num].slot == 0) // Make sure that it is not already in a slow
{
btm_active += 1;
tiltDB[num].slot = btm_active;
tiltDB[num].dirty = true;
}
}
/*********************************************************************************
* 
* This next set of functions is the API to the database
*
*********************************************************************************/
int btm_getTemperature(int num)
{
return (uint16_t)tiltDB[num].advData[IBEACON_TEMP_HI] << 8 | tiltDB[num].advData[IBEACON_TEMP_LO];
}
void btm_setTemperature(int num,uint16_t temperature)
{
if(temperature > 150)
temperature = 10;
if(temperature<10)
temperature = 150;
int oldtemp = btm_getTemperature(num);
tiltDB[num].advData[IBEACON_TEMP_HI] = (temperature & 0xFF00) >> 8;    
tiltDB[num].advData[IBEACON_TEMP_LO] = temperature & 0xFF;
if(temperature != oldtemp)
tiltDB[num].dirty = true;    
}
int btm_getGravity(int num)
{
return (uint16_t)tiltDB[num].advData[IBEACON_GRAV_HI] << 8 | tiltDB[num].advData[IBEACON_GRAV_LO];
}
void btm_setGravity(int num,uint16_t gravity)
{
// These if's cause the gravity to "wrap around" at 1200 and 900
if(gravity>1200)
gravity = 900;
if(gravity <900)
gravity = 1200;
int oldgrav = btm_getGravity(num);
tiltDB[num].advData[IBEACON_GRAV_HI] = (uint8_t)((gravity & 0xFF00) >> 8);
tiltDB[num].advData[IBEACON_GRAV_LO] = (uint8_t)(gravity & 0xFF);
if(oldgrav != gravity)
tiltDB[num].dirty = true;
}
void btm_setTxPower(int num, int8_t txPower)
{
tiltDB[num].advData[IBEACON_TXPOWER] = txPower;
tiltDB[num].dirty = true;
}
void btm_printTable()
{
printf("\n# Color   S   Rate T  UpT Grav UpG TxP\n");
for(int i=0;i<NUM_TILT;i++)
{
printf("%d %6s  %d %5d %3d %2d %4d %2d %3d\n",i,
tiltDB[i].colorName,tiltDB[i].slot,
tiltDB[i].rate*BTM_QUEUE_RATE,
btm_getTemperature(i),tiltDB[i].tempUpdate,
btm_getGravity(i),tiltDB[i].gravUpdate,
tiltDB[i].advData[29]);
}
}
/* btm_processCmdQueue
*
*  This function is called by a timer every BTM_QUEUE_RATE ms (around 200ms)
*  It processes commands from the GUI
*  It also updates the adverting packets if they are being updated at a rate
*
*/
void btm_processCmdQueue( wiced_timer_callback_arg_t cb_params )
{
static int count = 0; // This counts the numbers of times the callback has happend
btm_cmdMsg_t msg;
while(xQueueReceive(btm_cmdQueue,&msg,0) == pdTRUE)
{
switch(msg.cmd)
{
case BTM_CMD_SET_DATA:
btm_setGravity(msg.num,msg.gravity);
btm_setTemperature(msg.num,msg.temperature);
btm_setTxPower(msg.num,msg.txPower);
btm_activate(msg.num);
break;
case BTM_CMD_PRINT_TABLE:
btm_printTable();
break;
case BTM_CMD_SET_UPDATE:
tiltDB[msg.num].tempUpdate = msg.temperature;
tiltDB[msg.num].gravUpdate = msg.gravity;
tiltDB[msg.num].rate = msg.txPower / BTM_QUEUE_RATE;
break;
}
}
count = count + 1;
// This block of code updates the current values with the update rate
for(int i=0;i<NUM_TILT;i++)
{
// If the slot active
// and the update rate says that it is time
if(tiltDB[i].slot && count % tiltDB[i].rate  == 0)
{
btm_setTemperature(i,btm_getTemperature(i) + tiltDB[i].tempUpdate);
btm_setGravity(i,btm_getGravity(i) + tiltDB[i].gravUpdate);
}
}
btm_setAdvPacket();
}
/**************************************************************************************************
* Function Name: app_bt_management_callback()
***************************************************************************************************
* Summary:
*   This is a Bluetooth stack event handler function to receive management events from
*   the BLE stack and process as per the application.
*
* Parameters:
*   wiced_bt_management_evt_t event             : BLE event code of one byte length
*   wiced_bt_management_evt_data_t *p_event_data: Pointer to BLE management event structures
*
* Return:
*  wiced_result_t: Error code from WICED_RESULT_LIST or BT_RESULT_LIST
*
*************************************************************************************************/
wiced_result_t app_bt_management_callback(wiced_bt_management_evt_t event, wiced_bt_management_evt_data_t *p_event_data)
{
wiced_result_t result = WICED_BT_SUCCESS;
switch (event)
{
case BTM_ENABLED_EVT:
printf("Started BT Stack Succesfully\n");
btm_cmdQueue = xQueueCreate(10,sizeof(btm_cmdMsg_t));
wiced_init_timer_ext(&btm_processDataTimer,btm_processCmdQueue,0,WICED_TRUE);
wiced_start_timer_ext(&btm_processDataTimer,BTM_QUEUE_RATE);
break;
case BTM_MULTI_ADVERT_RESP_EVENT: // Do nothing...
break;
default:
printf("Unhandled Bluetooth Management Event: %s\n", btutil_getBTEventName( event));
break;
}
return result;
}
/*********************************************************************************
* 
* These are publicly callable functions to cause actions by the bluetooth manager
* These are called by the GUI
*
*********************************************************************************/
void btm_printTableCmd()
{
if(btm_cmdQueue == 0)
return;
btm_cmdMsg_t msg;
msg.cmd =    BTM_CMD_PRINT_TABLE;
xQueueSend(btm_cmdQueue,&msg,0);
}
void btm_setDataCmd(int num,int temperature,int gravity,int txPower)
{
if(btm_cmdQueue == 0)
return;
btm_cmdMsg_t msg;
msg.cmd =    BTM_CMD_SET_DATA;
msg.num = num;
msg.temperature = temperature;
msg.gravity = gravity;
msg.txPower = txPower;
xQueueSend(btm_cmdQueue,&msg,0);
}
void btm_updateDataCmd(int num,int rate ,int temperature,int gravity )
{
if(btm_cmdQueue == 0)
return;
btm_cmdMsg_t msg;
msg.cmd =    BTM_CMD_SET_UPDATE;
msg.num = num;
msg.temperature = temperature;
msg.gravity = gravity;
msg.txPower = rate;
xQueueSend(btm_cmdQueue,&msg,0);
}

 

main.c

#include "cyhal.h"
#include "cybsp.h"
#include "cy_retarget_io.h"
#include <stdio.h>
#include "FreeRTOS.h"
#include "task.h"
#include "usrcmd.h"
#include "bluetoothManager.h"
#include "cycfg_bt_settings.h"
#include "bt_platform_cfg_settings.h"
volatile int uxTopUsedPriority ;
TaskHandle_t blinkTaskHandle;
void blink_task(void *arg)
{
cyhal_gpio_init(CYBSP_USER_LED,CYHAL_GPIO_DIR_OUTPUT,CYHAL_GPIO_DRIVE_STRONG,0);
for(;;)
{
cyhal_gpio_toggle(CYBSP_USER_LED);
vTaskDelay(500);
}
}
int main(void)
{
uxTopUsedPriority = configMAX_PRIORITIES - 1 ; // enable OpenOCD Thread Debugging
/* Initialize the device and board peripherals */
cybsp_init() ;
__enable_irq();
cy_retarget_io_init(CYBSP_DEBUG_UART_TX, CYBSP_DEBUG_UART_RX, CY_RETARGET_IO_BAUDRATE);
cybt_platform_config_init(&bt_platform_cfg_settings);
wiced_bt_stack_init (app_bt_management_callback, &wiced_bt_cfg_settings);
// Stack size in WORDs
// Idle task = priority 0
xTaskCreate(blink_task, "blinkTask", configMINIMAL_STACK_SIZE,0 /* args */ ,0 /* priority */, &blinkTaskHandle);
xTaskCreate(usrcmd_task, "usrcmd_task", configMINIMAL_STACK_SIZE*4,0 /* args */ ,0 /* priority */, 0);
vTaskStartScheduler();
}
/* [] END OF FILE */

 

usrcmd.c

/**
* @file usrcmd.c
* @author CuBeatSystems
* @author Shinichiro Nakamura
* @copyright
* ===============================================================
* Natural Tiny Shell (NT-Shell) Version 0.3.1
* ===============================================================
* Copyright (c) 2010-2016 Shinichiro Nakamura
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include "ntopt.h"
#include "ntlibc.h"
#include "ntshell.h"
#include <stdio.h>
#include "ntshell.h"
#include "ntlibc.h"
#include "psoc6_ntshell_port.h"
#include "FreeRTOS.h"
#include "task.h"
#include "bluetoothManager.h"
static ntshell_t ntshell;
typedef int (*USRCMDFUNC)(int argc, char **argv);
static int usrcmd_ntopt_callback(int argc, char **argv, void *extobj);
static int usrcmd_help(int argc, char **argv);
static int usrcmd_info(int argc, char **argv);
static int usrcmd_clear(int argc, char **argv);
static int usrcmd_pargs(int argc, char **argv);
#ifdef configUSE_TRACE_FACILITY
#if configUSE_STATS_FORMATTING_FUNCTIONS ==1
static int usrcmd_list(int argc, char **argv);
#endif
#endif
static int usrcmd_print(int argc, char **argv);
static int usrcmd_set(int argc, char **argv);
static int usrcmd_update(int argc, char **argv);
typedef struct {
char *cmd;
char *desc;
USRCMDFUNC func;
} cmd_table_t;
static const cmd_table_t cmdlist[] = {
{ "help", "This is a description text string for help command.", usrcmd_help },
{ "info", "This is a description text string for info command.", usrcmd_info },
{ "clear", "Clear the screen", usrcmd_clear },
{ "pargs","print the list of arguments", usrcmd_pargs},
#ifdef configUSE_TRACE_FACILITY 
#if configUSE_STATS_FORMATTING_FUNCTIONS ==1
{ "tasks","print the list of RTOS Tasks", usrcmd_list},
#endif
#endif
{ "print", "Print table", usrcmd_print },
{ "set", "set num temperature gravity txpower", usrcmd_set },
{ "update", "update num temperature gravity", usrcmd_update },
};
void usrcmd_task()
{
setvbuf(stdin, NULL, _IONBF, 0);
printf("Started user command task with NT Shell\n");
ntshell_init(
&ntshell,
ntshell_read,
ntshell_write,
ntshell_callback,
(void *)&ntshell);
ntshell_set_prompt(&ntshell, "AnyCloud> ");
vtsend_erase_display(&ntshell.vtsend);
ntshell_execute(&ntshell);
}
int usrcmd_execute(const char *text)
{
return ntopt_parse(text, usrcmd_ntopt_callback, 0);
}
static int usrcmd_ntopt_callback(int argc, char **argv, void *extobj)
{
if (argc == 0) {
return 0;
}
const cmd_table_t *p = &cmdlist[0];
for (unsigned int i = 0; i < sizeof(cmdlist) / sizeof(cmdlist[0]); i++) {
if (ntlibc_strcmp((const char *)argv[0], p->cmd) == 0) {
return p->func(argc, argv);
}
p++;
}
printf("%s","Unknown command found.\n");
return 0;
}
static int usrcmd_help(int argc, char **argv)
{
const cmd_table_t *p = &cmdlist[0];
for (unsigned int i = 0; i < sizeof(cmdlist) / sizeof(cmdlist[0]); i++) {
printf("%s",p->cmd);
printf("%s","\t:");
printf("%s",p->desc);
printf("%s","\n");
p++;
}
return 0;
}
static int usrcmd_info(int argc, char **argv)
{
if (argc != 2) {
printf("%s","info sys\n");
printf("%s","info ver\n");
return 0;
}
if (ntlibc_strcmp(argv[1], "sys") == 0) {
printf("%s","PSoC 6 MBED Monitor\n");
return 0;
}
if (ntlibc_strcmp(argv[1], "ver") == 0) {
printf("%s","Version 0.0.0\n");
return 0;
}
printf("%s","Unknown sub command found\n");
return -1;
}
static int usrcmd_clear(int argc, char **argv)
{
vtsend_erase_display_home(&ntshell.vtsend);
return 0;
}
static int usrcmd_pargs(int argc, char **argv)
{
printf("ARGC = %d\n",argc);
for(int i =0;i<argc;i++)
{
printf("argv[%d] = %s\n",i,argv[i]);
}
return 0;
}
#ifdef configUSE_TRACE_FACILITY
#if configUSE_STATS_FORMATTING_FUNCTIONS ==1
static int usrcmd_list(int argc,char **argv)
{
// 40 bytes/task + some margin
char buff[40*10 + 100];
vTaskList( buff );
printf("Name          State Priority   Stack  Num\n");
printf("------------------------------------------\n");
printf("%s",buff);
printf("‘B’ – Blocked\n‘R’ – Ready\n‘D’ – Deleted (waiting clean up)\n‘S’ – Suspended, or Blocked without a timeout\n");
printf("Stack = bytes free at highwater\n");
return 0;
}
#endif
#endif
static int usrcmd_print(int argc, char **argv)
{
btm_printTableCmd();
return 0;
}
static int usrcmd_set(int argc, char **argv)
{
int gravity;
int temperature;
int txPower;
int num;
if(argc == 5)
{
sscanf(argv[1],"%d",&num);
sscanf(argv[2],"%d",&temperature);
sscanf(argv[3],"%d",&gravity);
sscanf(argv[4],"%d",&txPower);
btm_setDataCmd(num,temperature,gravity,txPower);
}
return 0;
}
static int usrcmd_update(int argc, char **argv)
{
int gravity;
int temperature;
int rate;
int num;
if(argc == 5)
{
sscanf(argv[1],"%d",&num);
sscanf(argv[2],"%d",&rate);
sscanf(argv[3],"%d",&temperature);
sscanf(argv[4],"%d",&gravity);
btm_updateDataCmd(num,rate,temperature,gravity);
}
return 0;
}

 

Tilt Hydrometer (Part 4) Advertising Packet Error?

Summary

This article discusses a detailed Bluetooth Capture using Linux and a Frontline Bluetooth Scanner.  With those tools I discover the cause of my bug.

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.

 

Where is the Tilt?

As I was trying to get the program running, I was faced with a problem that sent me down a serious serious rabbit hole.  The problem was that I could see the Tilts on the iPhone App, but they were not printing out on the screen.  But why?

I ran Light Blue, but there are so many Bluetooth Devices in range that I couldn’t see a way to sort it out.

What in the world is “hcidump”

On Karl’s blog he writes that you should use hcitool and hcidump to view what is going on.  Well, I am mostly an embedded guys, but OK.

I first install Linux on a Raspberry Pi 3B+ which has a Cypress CYW43455 WiFi Bluetooth Combo device onboard.  In this configuration the Bluetooth framework is split into a controller running on the CYW43455 and a Host Stack running on the Raspberry Pi Linux chip BCM2837B0.  The Host and the Controller communicate with each other through a UART.  The protocol that is used on that UART is called “HCI”

Specifically, what Karl told you to do is:

  1. In one window tell the Bluetooth Controller to start looking for advertising packets “lesscan”
  2. In another windows, start snooping on the HCI bus and printing out the packets in hex

In the picture below you can see that I first run “hcitool dev” to get a list of the Bluetooth Devices.  It turns on out the Raspberry Pi configuration that I have there is only one bluetooth device and it is called “hci0”.  Then I use “hcitool -i hci0 lescan” to send a command to the controller to start BLE scanning.  The hcitool command will report just the MAC address and possibly the name (if it happens to be in the advertising packet).  Its too bad that hcitool lescan doesn’t have a switch to display the advertising packets.

In another window I run “sudo hcidump -R > out.txt”.  The hcidump command will “snoop” on the HCI uart and will print out all of the raw packets going between the host and the controller.   After a while, I open on the out.txt and start looking through the raw data to find my Tilt.  I recognize the Tilt by looking for the UUID in the iBeacon which is “A4 95 BB …”

But what is all of the other stuff on that line?  For that answer we will need to dig through the Bluetooth core spec.  If you look in Volume 4 (HCI), Part A (UART Transport Layer), Chapter 1 (General) it starts with a nice picture of the system.

Then in Chapter 2 (Protocol) it says:

So, first byte, the 0x04 means that this is an “HCI Event Packet”, meaning this is something that happened on the controller which is going back to the Host.  In fact if you look at the log above you will see that the lines are preceded by a “>” which means packets coming into the Host.  Now we know that there is an event packet, so we follow further down into the spec in Part “E” which describes the event packet.

The first byte, the “0x3E” is an event code and the 0x2A is the length of the packet (count from the 0x02 through to the end)

Keep digging further down into the spec to find out what the “event code 0x3E” and you will find that it is a “LE Meta event”.

OK so that means that the “02” is the “Subevent_Code”.  OK keep digging and you will find that the 02 Subevven_Code is “HCI_LE_Advertising_Report”.  Then it gives you a list of what data will follow.

So, the 02 is an advertising report.  The 01 says that there is only 1 advertising report.  Q: Why is this a parameter?  A: Because the controller can save a few bytes by clubbing multiple advertising reports into 1 HCI packet.  Which it did not do in this case.

Next is the 03 which is the “Event_Type[i]”.  In this case it says that this thing is non connectable undirected advertising. (more on this later)

OK what about 01 …


The 01 says that it is a random address and then the next 6 bytes is the MAC address.

Now the 0x1E is the length of the data in the advertising packet

The actual advertising pack is next.

Recall the format from section 11.

The 02 01 04 is the first field of the advertising packet and it represents

  • 02 – length of the field
  • 01 – «Flags»
  • 04 – flags

You can find the 01 from the “Assigned numbers and GAP

Then when you look in the Core Specification Supplement Part A section 1.3 you will find that the “4” means “BR/EDR not supported”

The next field is 0x1A in length and a field type of “0xFF” – Ah…. manufacture specific data

The 4C 00 is Apples Company Identifier Code.

Which you can find on the Bluetooth Sig Company Identifiers page.

From the previous article we know the rest is an iBeacon

  • 02 – iBeacon subtype
  • 0x15 – length
  • Then the A4…. is UUID which maps to a “Pink” Tilt

Then we get the Temperature 0x004F (BIG ENDIAN!!!!) also known as 79 Degrees F (see Imperial… my people)

Then the gravity 0x03FD also known as 1.021 – NOTICE THIS IS BIG ENDIAN!!!!

Then transmit power C5 (also known as -59)

Finally the RSSI 0xA5 which is also known as -91 (2’s complement 1 byte)

Debugging the Linux Bluetooth

I had a few problems getting this going which led me to the door of some interesting questions (which I won’t answer here).  But, here are the problems and solutions.

Q1 When I “hcidump -R” I only get this:

A1 You need to be root to see the raw data.  Run “sudo hcidump -R”

Q2: I don’t see my device?

A2: If you start the scan before your run the hcidump you might miss the device.  When you scan try running “hcitool lescan –duplicates” which will turn off duplicate filtering.

Q3: I get “Set scan parameters failed: Input/output error”

A3: I am not totally sure I have this right because I was not able to recreate the problem (which I had in spades when I started).  But, try doing a “sudo hciconfig hci0 down” followed by a “hciconfig hci0 up”

Q4: I read that “hcitool” and “hcidump” are deprecated.

A4: I read the same thing.  If you don’t have them try “sudo apt-get install bluez-tools”.  I would like to know the “real” answer to this question

Q5: How does hcidump get the packets from the HCI?

A5: I have no idea.  But I would like to know that answer to this question

Q6: I tried “btmon”.  Does that work?

A6: Yes

Q7: What is the bluetoothd?

A7: I dont know.

Q8: Do hcidump and hcitools talk to the BluetoothD or do they talk to dbus or do they talk to the kernel through a socket?

A8: I don’t know

Q9: Why can’t I see the Tilt in my advertising scanner

A9: Keep reading 🙂

Frontline Bluetooth Scanner

I can see the Tilt in the Linux and on the iPhone,  but I still can’t see it in my AnyCloud project.  Why?  The next thing that I did was get a Frontline Bluetooth Sniffer.  When I started capturing packets I could see the device.  Here is the “Tilt”

And here is the “Tilt Repeater”

Fix the Advertising Scanner

After more digging I figured it out.  Remember from earlier that the Tilt advertises as “Non connectable”.  Well it turns out that when I built the advertising scanner I used the function “wiced_bt_ble_scan”.  This function was put into WICED to use for the connection procedure.  In other words it FILTERS devices that are non connectable.  In order to see those devices you need to call “wiced_bt_ble_observe”.  Wow that was a long article to explain that one little bug.  Oh well it was a fun learning experience.

Tilt Hydrometer (Part 3) Advertising Scanner

Summary

In this article I will build the basic project and then add the scanner code to look for Tilt Hydrometers that are broadcasting the iBeacon.  It will decode the data and print out Gravity and Temperature.

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.

 

Build the Basic Project

Start by making a new project using the CY8CKIT-062S2-43012.  I chose this kit because that was the one sitting on my desk at the time.  Any of the kits with the Arduino headers where I can plug in the TFT will work.

I typically start from the IoT Expert FreeRTOS NTShell Template.  Ill do that here as well.  Notice that I call the project Tilt2.  You can find this project on GitHub.  git@github.com:iotexpert/Tilt2.git .  I will also be putting in tags for each article so you can look at the code for each article.  To see the tags do “git tags”

After the project is made run “make modlibs” so that you can add some libraries.  Specifically the bluetooth-freertos and btstack libraries.

Now that I have all of the libraries I need, run “make vscode” to build the project files for vscode.

If you remember from the architecture diagram, I will have a task to manage the bluetooth world.  Inside of the btutil library I have a template to start with.  It also includes the stuff you need in main.c to start the stack.

You need to make two changes to the Makefile.  Well you dont have to change the name of the App … but I typically do.  You do need to add the FREERTOS and WICED_BLE components to your project.

APPNAME=Tilt2
....
COMPONENTS=FREERTOS WICED_BLE

Now, edit main.c to include some stuff.

#include "bluetoothManager.h"
#include "cycfg_bt_settings.h"
#include "bt_platform_cfg_settings.h"

And start the bluetooth stack.

    cybt_platform_config_init(&bt_platform_cfg_settings);
wiced_bt_stack_init (app_bt_management_callback, &wiced_bt_cfg_settings);

You need to get the bluetooth configuration files.  To do that run the Bluetooth Configurator by running “make config_bt”.  Click the paper to make a new configuration.

Pick out the PSoC6 MCU with 43xxxx Connectivity.

Go to the GAP Settings tab and add the Observer configuration

Then set the low duty scan parameters to 60ms and turn off the timeout.

Save your configuration.  Notice that I call it tilt2.

Make sure that everything works with a build it and program.  You will have a working shell, blinking LED and the Bluetooth Task started.  Here is what the output looks like.

iBeacon Packet Format

On the Tilt Hydrometer FAQ they give you a link to Karl Urdevics blog where he describes the way that the Tilt works.  Pretty simple, each Tilt Hydrometer is hardcoded to a color.  Each “color” sends out an iBeacon with the Temperature, Gravity and Transmit Power.  iBeacon is just an Apple specified format for a BLE advertising packet.  You can read about the format of the BLE advertising packet here.  But, in summary, a BLE device can advertise up to 31 bytes of data.  That data is divided into fields of the following form:

There are a bunch of specific format types.  However, the one that we are looking for is the iBeacon format.  This format was defined by Apple and is implemented as data in the “«Manufacturer Specific Data»” which has the field code 0xFF.  The Manufacturer Specific Data field type is divided into

  • 16-bit Manufacturers ID
  • Data

For the iBeacon the Manufacturer ID is 0X004C – which is Apple’s ID.  They further subdivided the data.  Here is the complete format of that data.

Bytes Data Name Comment
1 0xFF Manufacturer Data Bluetooth GAP Assigned Number
1 0x1A Length of Field
2 0×04 0x0C Manufacturers' UUID Apples Bluetooth Manufacturer ID
1 02 Apple defined 0x02=Subtype iBeacon
1 0×15 Length Apple defined length
16 ???? UUID The universally unique identifier for the data type of the iBeacon (these are defined by Tilt)
2 ???? Major Value Gravity in 1/1000 of SG
2 ???? Minor Value Temperature in degrees F (yup, Imperial!)
1 ?? Signal Power dBm Transmit Power as measured at 1m

It turns out that Tilt “hardcodes” the UUID during manufacturing for the color of the Tilt (remember each Tilt is one of 8 colors)

Red:    A495BB10C5B14B44B5121370F02D74DE
Green:  A495BB20C5B14B44B5121370F02D74DE
Black:  A495BB30C5B14B44B5121370F02D74DE
Purple: A495BB40C5B14B44B5121370F02D74DE
Orange: A495BB50C5B14B44B5121370F02D74DE
Blue:   A495BB60C5B14B44B5121370F02D74DE
Yellow: A495BB70C5B14B44B5121370F02D74DE
Pink:   A495BB80C5B14B44B5121370F02D74DE

Add Advertising Observer

Now that we have a working project template, and we know what we are looking for in the BLE advertising land, I’ll setup some data structures to map of the colors and UUIDs.

#define TILT_IBEACON_HEADER_LEN 20
#define TILT_IBEACON_DATA_LEN 5
typedef struct  {
char *colorName;
uint8_t uuid[TILT_IBEACON_HEADER_LEN];
} tilt_t;
// Apple Bluetooth Company Code 0x004C
// iBeacon Subtype = 0x02
// Length = 0x15
#define IBEACON_HEADER 0x4C,0x00,0x02,0x15
static tilt_t tiltDB [] =
{
{"Red",    {IBEACON_HEADER,0xA4,0x95,0xBB,0x10,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE}},
{"Green" , {IBEACON_HEADER,0xA4,0x95,0xBB,0x20,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE}},
{"Black" , {IBEACON_HEADER,0xA4,0x95,0xBB,0x30,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE}},
{"Purple", {IBEACON_HEADER,0xA4,0x95,0xBB,0x40,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE}},
{"Orange", {IBEACON_HEADER,0xA4,0x95,0xBB,0x50,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE}},
{"Blue"  , {IBEACON_HEADER,0xA4,0x95,0xBB,0x60,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE}},
{"Yellow", {IBEACON_HEADER,0xA4,0x95,0xBB,0x70,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE}},
{"Pink"  , {IBEACON_HEADER,0xA4,0x95,0xBB,0x80,0xC5,0xB1,0x4B,0x44,0xB5,0x12,0x13,0x70,0xF0,0x2D,0x74,0xDE}},
};
#define NUM_TILT (sizeof(tiltDB)/sizeof(tilt_t))

Then I will create and advertising callback that will

  • Look for the manufacturer specific data field in the advertising packet
  • If the length of that field is 25 then we have found a possible iBeacon
  • Loop through the different possible Tilt’s
  • Compare the data in the advertising packet against the data + UUID in the packet
  • If it matches then decode the Gravity, txPower and Temperature and print it
static void btm_advScanResultCback(wiced_bt_ble_scan_results_t *p_scan_result, uint8_t *p_adv_data )
{
if (p_scan_result == 0)
return;
uint8_t mfgFieldLen;
uint8_t *mfgFieldData;
mfgFieldData = wiced_bt_ble_check_advertising_data(p_adv_data,BTM_BLE_ADVERT_TYPE_MANUFACTURER,&mfgFieldLen);
if(mfgFieldData && mfgFieldLen == TILT_IBEACON_HEADER_LEN + TILT_IBEACON_DATA_LEN)
{
for(int i=0;i<NUM_TILT;i++)
{
if(memcmp(mfgFieldData,tiltDB[i].uuid,TILT_IBEACON_HEADER_LEN)==0)
{
float gravity = ((float)((uint16_t)mfgFieldData[22] << 8 | (uint16_t)mfgFieldData[23]))/1000;
int temperature = mfgFieldData[20] << 8 | mfgFieldData[21];
int8_t txPower = mfgFieldData[24];
printf("Found Color=%s Gravity=%f Temperature = %d txPower=%d\n",tiltDB[i].colorName,gravity,temperature,txPower);
break;
}
}
}
}

In the Bluetooth Management callback I want to turn on scanning (actually observing).

        case BTM_ENABLED_EVT:
if (WICED_BT_SUCCESS == p_event_data->enabled.status)
{
wiced_bt_ble_observe(WICED_TRUE, 0,btm_advScanResultCback);
}
else
{
printf("Error enabling BTM_ENABLED_EVENT\n");
}

To test this thing I brought a black tilt into my office.  As soon as the PSoC saw it, packets starting coming out on the screen about once per second.  The first thing to notice is that they broadcast some crazy data at the start.  That means I should be careful with the error checks (something which if you are a reader you know that I am not always perfect 🙂 .  It is also interesting to see that they broadcast 5 packets at 5dBm, then 5 at -59dBm.

Now that we have data coming out, in the next article Ill address a couple of funky things that I noticed.

Tilt Hydrometer (Part 2) Architecture

Summary

This article is a walk through of the architecture of the PSoC 6 – AnyCloud firmware which I will implement for my Tilt Hydrometer IoT application

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

I started this whole thing by just writing some code without really thinking about the architecture too much.   I had been wanting to try the Bluetooth SDK inside of AnyCloud and I was just focused on how that worked inside of the PSoC 6.  As I wrote the code, I naturally implemented with a common design pattern of

  1. Individual tasks have responsibility for a specific hardware block
  2. Each task has a command queue to which other tasks can send commands
  3. Tasks that need to have data fed to them by other tasks have a data queue

As I worked on the code and things started to get a bit more involved I decided that I had better draw a picture of the system.  This picture served two main purposes.

  1. It kept me on track
  2. I knew that I was going to need it for this series of articles

Here is the architecture of what I have implemented for this series of articles.  As of this writing I have not started started not the wifi or the file system part of the implementation but I think that my design will work.

The system has 7 tasks:

Task Role
Bluetooth Manager Listens for advertising packets in the correct format.  When it finds them it submits the data to the Tilt Data Manager
Tilt Data Manager A database of all of the "Tilt" data
NTShell A serial command line to control the system
Display Manager A task to handle all of the ST7789V display functions
CapSense Manager Responsibility for reading capsense buttons and the slider and sending commands to the display task
File System Manager Responsibility for writing data to the SD Card and possibly the SPI flash
WiFi Manager An MQTT interface to the internet (maybe)

There are two places in the picture above where my lines have to cross, something that I really hate.  So I decided to try a different picture to explain the system.  The picture below describes the tasks, what data they own, what hardware they own, what the inputs are and what the outputs are.

In the next article Ill start the basic project and create an advertising observer.

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

Summary

A discussion of a new series of articles about using the PSoC 6 + 43XXXX Wifi/Bluetooth combo chips to implement a data collection system for the Tilt2 Hydrometer.  Even if you aren’t specifically interested in hydrometers, this is a general purpose discussion of the design of an IoT device.

Story

In the middle of the Covid lockdown my 21-year-old daughter suggested that we start brewing beer.  This was always something that I have been interested in so I said “Sure!”.  What does this have to do with IoT you might ask?  I am an engineer and I love data.  Two of the key pieces of data that you are interested in while fermenting beer are:

  • The gravity of the beer
  • The temperature of the beer

If you don’t know anything about brewing beer, it is simple (people have been doing it a long time… even with no IoT)

  1. Start with grain
  2. Mill the grain
  3. Heat the grain with water to turn it into sugar water (called wort)
  4. Add yeast
  5. Wait while the yeast converts the sugar in the wort into alcohol and carbon dioxide
  6. Bottle the beer (or keg)
  7. Drink

Back to the metrics.  The “specific gravity” or just “gravity” is just the ratio of the density of your solution to plain water.  This is an indication of sugar in the wort solution.  At the start of the fermentation you will have “lots” of sugar, and no alcohol.  By the end you will have “lots” of alcohol and not much sugar.  You can tell how things are going by monitoring the gravity of the beer, which is a proxy metric for how much sugar has been converted to alcohol.

There are two common ways to measure the gravity:

  • A float hydrometer – sugar water is denser then water, so a “float” will float lower in the solution as the sugar gets converted to alcohol.
  • A refractometer – the index of refraction of the solution changes as the sugar concentration changes (this is an amazing old-school technology

As I was learning about this whole process I found the tilt hydrometer.  This device has

As the gravity of the beer changes, the device floats at a different angle (because it floats lower/higher).  They use the accelerometer to measure the apparent angle of gravity to calculate the angle of the device.  This angle is then used to calculate the density of the solution it is floating in.  They then broadcast the calculated gravity and temperature in Apple Bluetooth iBeacon format.

When I saw this, I thought “perfect” I know what to do with that.  I should build a device that can collect all of the data, display it, save it to an SPI flash and put it into the cloud.  It should look something like this: (each Tilt is identified by 1 of 8 colors… pink in this case).

Yes, I know they have an iPhone app, but I want to build a single device that sits in my brewery all of the time.  And yes I know they have a Raspberry Pi app, but that isn’t the point.

My device will have the following characteristics:

A Display with:

  • A Splash Screen
  • A Table of all Tilts, Gravity and Temperature
  • Single: One screen per tilt with the specific data including debugging
  • Single: A graph of the active data for one specific tilt
  • Single: A table of all of the recordings from that specific tilt
  • The WiFi Status
  • The Bluetooth Status

Bluetooth System that can:

  • Record tilt data as broadcast in iBeacon advertising packets
  • Repeat tilt data (maybe)
  • Introducer WiFi (probably)

CapSense button GUI to:

  • Next Screen
  • Auto Mode
  • Reset current
  • Dump recorded data to the SD Card

Command Line

  • A UART based command line to debug & learn

USB

  • Mass Storage to see files
  • USB <-> UART Bridge

Power Supply via USB Port

  • Plug in Type-C using Cypress BCR

WiFi

  • MQTT Publish to AWS
  • NTP – to find the time
  • Local webserver
  • MDNS

RTC

  • Keep Track of current Time

SPI NOR Flash

  • Record the data

SD CARD

  • Dump the fixed SPI Flash  recordings to a removable SD CARD & remove data from the SPI Flash

Here is another picture of what I am thinking (well actually what I implemented for this series of articles)

Un-boxing

To get this show on the road, I ordered three tilts and two repeaters from Baron Brew Equipment.

It included a neat little quick start picture showing how to get going.

Then the box of goodies.

There are 8-possible tilts, Red, Green, Orange, Blue, Black, Yellow, Purpose and Pink (each Tilt his “hardcoded” to identify itself as a specific color)

Tilt Hydrometer

Here is a picture of the “blue” one (notice I put the wrong box in the picture)

The tilt comes in a plastic tube.  Which has a label to remind you to take it out of the tube (my experience is that you should be embarrassed to have to read most warning labels 🙂 )

It is about 100mm long (about 4 inches).  The bluetooth module is at the top, U3 is the temperature sensor and U2 (which is under the black 3-d printed plastic) is the accelerometer.

Repeater

If you put a Bluetooth device floating in a bunch of beer, surrounded by a metal fermentation container, you will not be able to hear the Bluetooth signal.  To solve this problem the Tilt people made a repeater which can rest on the top of the fermenter.  It listens for the weak signal, then rebroadcasts with a higher gain antenna.

Here is a picture of the repeater.  Notice that it uses the BMD-301 which has an external SMA antenna.

It also comes in a nice plastic tube.

The repeater can only re-broadcast one color at a time.  The button to switches between the 8 colors and off.

Each time you press the button the 3-color LED lights up with the color that represents which tilt color that it is repeating. Red->Green->… Pink->Off

It also has a huge rechargeable battery.

The Plan

Here is a list of the articles that I plan to write

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.