Contents:

 

 

1. Introduction

 

This blog is about me exploring how to develop my own BLE Air Quality Sensor application using the Mikroe SGP30 Air Quality 4 click board and a Silicon Labs BGM220 Explorer Kit (BGM220-EK4314A).

 

The aim this project was to find the simplest and/or quickest method to get my application working. Knowing that I have a habit of over complicating things I knew this was going to be a trial and error exercise as I have limited experience using Simplicity Studio 5, the Gecko Bluetooth SDK and the BGM220 Explorer Kit. I have also not used this click before.


So where does one start?

 

Well, I suppose I could start by reviewing the documentation about the click.

 

 

2. Mikroe’s SGP30 Air Quality 4 click

 

Mikroe.com

 

Opening the mikroe.com website on my browser, I typed “SGP30” in the search box and the Air quality 4 click appears as a search result.

 

Search resultsProduct Landing Page

 

On this click's product page, I’m informed that this click can output a TVOC value readings as well as the CO2 equivalent concentration readings in an indoor environment. The click can also output raw values of H2 and Ethanol in the air, which can be used to calculate gas concentrations, relative to a reference concentration. I am also informed that the device allows air humidity corrections over the results for both the compensated and raw values. However, a separate absolute humidity sensor will needed to obtain the humidity reading as this sensor is not provided on the click.

 

The documentation goes on to explain that this click takes care of delivering the required sensor operating supply voltage of 1.8V (max supply voltage for the SGP30 is 1.98V) using an onboard 500mA LDO voltage regulator (SPX3819M5) and it uses a PCA9306 dual bidirectional I2C bus and SMBus voltage level shifter for data comms.

 

Last but not least, we are told that the sensor uses the I2C interface.

 

Further down on the webpage the Specs and the Sensirion SGP30 datasheet are also included.

 

And software libraries?

 

Yes, a link is provided to their LibStock page at the bottom of the webpage. Here I was able to download a zip file containing a c library.

 

 

And here’s my first lesson learnt…

 

Selecting the big “Download All” option did not help as when opening up the downloaded zip file I am presented with a bunch of “MPKG” files. That's a file format I am not familiar with.

 

 

So what went wrong?

 

Well with a bit of pondering, and closer scrutiny, it appears that the older version (1.0.0.0) is presented first and not the latest version (2.0.0.0). The “Last Updated” field confirms that version 2 is the newer version (last updated on 2021-04-27).

 

So I downloaded this zip file, and success! I had some c code.

 

 

Now what?

 

Well, I figured that I would now need to find out how to get that code (assuming it works as is) into Simplicity Studio. But then past experience kicked in and my first thought was “let’s not reinvent the wheel”.

 

 

Turning to Element14

 

Searching for SGP30 on Element14 returned 4 great roadtest reviews for the Sensirion Environmental Sensor Shield (ESS), which includes the SGP30 sensor. These are well worth reading.

 

There’s also a great blog about using the Air Quality 4 click with an Azure Sphere development board. Another great read.

 

So progress, but this information was not really focused on code for Simplicity Studio.

 

 

Searching for other SGP30 drivers online

 

Being heavily influenced by the Arduino ecosystem, I’m thinking that there’s bound to be something published online, so I undertook a keyword search using Google.

 

 

And, as shown above, when searching for “sensirion sgp30 code examples”, the top 3 search results on Google was a link to the Sensirion GitHub repository a Seed Studio Arduino example (also on GitHub) and a Hookup Guide for the Sparkfun SGP30 breakout. When I dropped “code” from the search term I was also presented with a link to the (NRND) SGP30 page on the Sensirion website.

 

However, there was still nothing specific to the BGM220 or any other Silicon Labs development boards.

 

Broadening my search terms, I found a useful link when searching for “silicon labs air quality sensor example” on Google. I discovered that this was the same link that pops up when searching for “Air Quality” on silabs.com.

 

 

Still nothing specific to the BGM220, but the Silabs driver for the (CCS811) Cambridge CMOS Air Quality Sensor plus documentation on docs.silabs.com portal on how to use this driver was very useful.

 

I then also stumbled on a blog about this very sensor on element14: https://www.element14.com/community/people/koudelad/blog/2017/09/14/thunderboard-sense-board-experience

 

 

Silicon Labs GitHub page for I2C-based Bluetooth Applications

 

To close out my search efforts I decided to search on the Silicon Labs Github page. Here I found three bluetooth sensor examples which used the I2C bus.

 

 

So, this was not quite the copy and paste scenario I was hoping for to get the SGP30 working on the BGM220 board, but I now had all the building blocks I need.

 

It was time to attempt building my app.

 

But first, I needed to develop and test my Air Quality sensor code.

 

 

3. Using Simplicity Studio 5

 

Starting with an empty UART example

 

To keep things simple, I decided to avoid moving straight to a Bluetooth use case and looked at the other software examples on Simplicity Studio 5 for my Explorer Kit. Unlike most other MCU vendors, there were no baseline I2C examples available on Simplicity Studio 5 to work from.

 

Having experimented with a few of the "platform" examples to understand the basics, I decided to use the "Platform - I/O Stream USART Bare-metal" example as this allowed me to send text to a serial terminal. Whilst the 2 I2C examples found on GitHub were useful they were not suitable to learn from, in my opinion, as each example applied the logic slightly differently and as I was road testing I wanted to rely on the documentation as best I could to see if I could build from the ground

 

I found it difficult to work from the detail code libraries without understanding the big picture. Part of the problem stems from the fact that there are all these low level gecko SDK "em_" based code libraries, some of which are becoming deprecated, and then there are these higher level "sl_" based code wrappers, which simplify things to a degree. You are never sure which to include as headers and are never sure which functions are then more applicable. Here's one random example, where at least you're told not to use the "em_" library.

 

 

Then when you lack understanding of the architecture logic behind this philosophy and you find that there's no way-finding guidelines to help you navigate through the vast low level documentation vault and there is a general lack of code snippet type tutorials to get you started, you quickly find yourself in no mans land. The thing that rather annoyed me was that none of the function documentation, found in the Software Components tool or even on docs.silabs.com told you what the library name was to include.

 

Here is another random example, for Energy Management (EMU) where it does not tell you what library file or files to use. Instead, it just dives straight into all the data structures and functions. As someone learning I found this very frustrating especially as I found this problem to be throughout the SDK:

 

 

So it was quite arduous to build even the simple stuff like an I2C library, but I did it.

 

 

 

It's worth walking through the 2-step process I applied.

 

Step 1: Choose and configure your components from the Software Components tool. This can be somewhat confusing to start with as there is no obvious order prescribed and the documentation does not highlight the dependencies and what these dependencies do. So you tend to just add what you can until it works. What I rather liked about the BGM220 Explorer Kit's SDK is that there's two pre-made I2C instance names provided, one for the Mikroe click and one for the quiic connector. These instances then pre-populate the SDA and SCL pins associated with those configurations.

 

 

Step 2: Create your SGP30 drivers with the required I2C read / write and setup functions. As highlighted in the previous sections, I had a few code libraries to choose from. I had seriously considered the official Sensirion library option but struggled to get their library structure to fit with the Simplicity Studio example structure. So, as I had previously used the Sparkfun library with Arduino I decided to use this option as it looked the simplest to implement, even if the code could be written more efficiently. It was very easy to understand, which helps to get things going but now that I have the application working I will take another look at the Mikroe library.

 

With my code, I decided to bundle in all the I2C functionality into the driver files so that I only had a header and source file for the SGP30.

 

Then I create an application wrapper for the click board. This follows the same structure as all the other Simplicity Studio examples where an init() function is used to initialise the driver and then another repeat function to retrieve and/or write data. In my code example, a timer is also used to repeat the data request at the beginning as the sensor requires at least 15 seconds to warm up before meaningful data is returned.

 

The initial headache was working out which Gecko 3.1/3.2 header files to include in the code. Then it was trying to get the I2C transfer function to work correctly.

 

To initialise the SGP30 we need to send the initialise instruction (0x2003).

 

//Initialises sensor for air quality readings
//measureAirQuality should be called in 1 second intervals after this function
SGP30ERR initAirQuality(void)
{
  I2C_TransferSeq_TypeDef    seq;
  I2C_TransferReturn_TypeDef ret;
  uint8_t i2c_read_data[1];

  // Initialising I2C transferSGP30_I2C_ADDRESS
  seq.addr          = (uint16_t)(SGP30Address);
  seq.flags         = I2C_FLAG_WRITE;
  seq.buf[0].data   = (uint8_t*)init_air_quality;
  seq.buf[0].len    = 2;
  /* Select length of data to be read */
  seq.buf[1].data = i2c_read_data;
  seq.buf[1].len  = 0;

  // Do a polled transfer
  ret = I2C_TransferInit(sl_i2cspm_mikroe, &seq);
  while (ret == i2cTransferInProgress)
  {
    ret = I2C_Transfer(sl_i2cspm_mikroe);
  }

  if(ret != i2cTransferDone) return SGP30_ERR_I2C_TIMEOUT;
  else return SGP30_SUCCESS;

}

 

 

Initially I left seq.buf[1] empty, but this did not work. So I followed the examples found in the Silabs GitHub repository and added in a dummy uint8_t i2c_read_data[1] array for seq.buf[1].data and then set seq.buf[1].len = 0 and it works.

 

Then I applied the polling transfer method as shown in the Silabs API docs,rather than what is used in the GitHub examples. So here you use I2C_TransferInit(sl_i2cspm_mikroe, &seq) to initialise the transfer and then you can check transfer progress by polling I2C_Transfer(sl_i2cspm_mikroe) until it completes.

 

Then to read data it uses the same transfer function but with a I2C_FLAG_READ parameter instead of the I2C_FLAG_WRITE. It is well worth a review of the I2C code library as there are 2 other flags available too, namely: I2C_FLAG_WRITE_READ and I2C_FLAG_WRITE_WRITE.

 

//Returns the current calculated baseline from
//the sensor's dynamic baseline calculations
//Save baseline periodically to non volatile memory
//(like EEPROM) to restore after new power up or
//after soft reset using setBaseline();
//Returns SGP30_SUCCESS if successful or other error code if unsuccessful
SGP30ERR getBaseline(uint16_t* baselineCO2, uint16_t* baselineTVOC)
{

  I2C_TransferSeq_TypeDef    seq;
  I2C_TransferReturn_TypeDef ret;
  uint8_t i2c_read_data[1];

  // Initialising I2C transferSGP30_I2C_ADDRESS
  seq.addr          = (uint16_t)(SGP30Address);
  seq.flags         = I2C_FLAG_WRITE;
  seq.buf[0].data   = (uint8_t*)get_baseline;
  seq.buf[0].len    = 2;
  /* Select length of data to be read */
  seq.buf[1].data = i2c_read_data;
  seq.buf[1].len  = 0;

  // Do a polled transfer
  ret = I2C_TransferInit(sl_i2cspm_mikroe, &seq);
  while (ret == i2cTransferInProgress)
  {
    ret = I2C_Transfer(sl_i2cspm_mikroe);
  }

  if(ret != i2cTransferDone) return SGP30_ERR_I2C_TIMEOUT;

  //Hang out while measurement is taken. datasheet says 10ms
  sl_sleeptimer_delay_millisecond(10);

  uint8_t rx_buff[6];

  seq.flags         = I2C_FLAG_READ;
  seq.buf[0].data   = rx_buff;
  seq.buf[0].len    = 6;

  // Do a polled transfer
  ret = I2C_TransferInit(sl_i2cspm_mikroe, &seq);
  while (ret == i2cTransferInProgress)
  {
    ret = I2C_Transfer(sl_i2cspm_mikroe);
  }

  if(ret != i2cTransferDone) return SGP30_ERR_I2C_TIMEOUT;

  uint16_t _baselineCO2 = rx_buff[0] << 8 | rx_buff[1]; //store MSB then LSB in  _baselineCO2

  uint8_t checkSum = rx_buff[2];                        //verify checksum
  if (checkSum != _CRC8(_baselineCO2)) return SGP30_ERR_BAD_CRC;      //checksum failed

  uint16_t _baselineTVOC = rx_buff[3] | rx_buff[4];                  //store MSB & LSB in _baselineTVOC

  checkSum = rx_buff[5];                                //verify checksum
  if (checkSum != _CRC8(_baselineTVOC)) return SGP30_ERR_BAD_CRC;         //checksum failed

  *baselineCO2 = _baselineCO2;   //publish valid data
  *baselineTVOC = _baselineTVOC; //publish valid data

  return SGP30_SUCCESS;
}

 

 

My source code can be found in my repository: https://github.com/Gerriko/SimplicityStudio5_SGP30driver

 

 

Moving to a Bluetooth example

 

Based on experience with other Bluetooth SoC's I initially thought that getting the Bluetooth part working would be the harder part of project to complete. But it wasn't... to a degree.

 

With Simplicity Studio 5, Silicon Labs have really simplified the process of configuring GATT services and installing these services onto the stack that you can be viewed on a phone, for example. It is almost a drag and drop / low code setup.

 

What lets the system down is some of the quirks and software bugs, which no doubt will be ironed out as the system matures. So for now, if you stick with the basics it is quick and easy, but if you try dig deeper and customise parameters it does have bugs and niggles... I'll get back to this issue in my road test review.

 

Let me now demonstrate some of the features.

 

Here I demonstrate how to add in a new GATT service (in this case I'll add in a dummy Battery Service) into an existing example. It just so happened that Simplicity Studio released their Bluetooth SDK version 3.2 so I created a video demonstrating how this is done with the new Bluetooth SoC-blinky example.

 

 

 

I decided to stick with the methodology of using an XML file to create my GATT services. Note that this is not the recommended method to get started, especially if you are unfamiliar with this setup and also this method is quite buggy.

 

In most circumstances you would start with the Bluetooth GATT Configurator tool. This is a very user friendly set up. However, I found this tool a little slow for my liking as it does not have any keyboard shortcuts included. For example, if you want to delete an item you cannot use your keyboard delete key. Instead you have to click on the red X and then confirm for it to proceed. This gets tiresome quite quickly.

 

 

So, I started designing my GATT Database by looking at how the Thunderboard Sense 2 example created its Air Quality GATT service for the CCS811 air quality sensor. Here the Thunderboard Sense 2 example defined a custom Gatt Service named "IAQ" and then defined two Characteristics named "ECO2" and "TVOC".

 

<gatt>
  <!--IAQ-->
  <service advertise="false" name="IAQ" requirement="mandatory" type="primary" uuid="efd658ae-c400-ef33-76e7-91b00019103b">
    <informativeText/>
    
    <!--ECO2-->
    <characteristic id="iaq_eco2" name="ECO2" uuid="efd658ae-c401-ef33-76e7-91b00019103b">
      <informativeText/>
      <value length="2" type="user" variable_length="false"/>
      <properties read="true" read_requirement="optional"/>
    </characteristic>
    
    <!--TVOC-->
    <characteristic id="iaq_tvoc" name="TVOC" uuid="efd658ae-c402-ef33-76e7-91b00019103b">
      <informativeText/>
      <value length="2" type="user" variable_length="false"/>
      <properties read="true" read_requirement="optional"/>
    </characteristic>
  </service>
</gatt>

 

 

But this did not quite make sense to me, as I tend to think of Air Quality as an Environmental Service. So I changed my XML file have my two custom characteristics for CO2 and TVOC to be listed under Environmental Sensing (0x181A) and also have advertising set to true. I then gave it a custom name (gatt_service_airqual.xml):

 

<gatt>
  <!--IAQ-->
  <service advertise="true" id="environment_sensing" name="Environment Sensing" requirement="mandatory" type="primary" uuid="181a" instance_id="es_s1">
    <informativeText/>    
    
    <!--ECO2-->
    <characteristic id="iaq_eco2" name="ECO2" uuid="efd658ae-c401-ef33-76e7-91b00019103b">
      <informativeText/>
      <value length="2" type="user" variable_length="false"/>
      <properties read="true" read_requirement="optional"/>
      <!--Characteristic User Description-->
    </characteristic>
    
    <!--TVOC-->
    <characteristic id="iaq_tvoc" name="TVOC" uuid="efd658ae-c402-ef33-76e7-91b00019103b">
      <informativeText/>
      <value length="2" type="user" variable_length="false"/>
      <properties read="true" read_requirement="optional"/>
    </characteristic>
  </service>
</gatt>


 

The nice thing about Simplicity Studio is that this is imported in automatically if it is in the correct folder and all the field values are in the correct format - at the moment any error found prevents all the XML data to be loaded (and no error is given). Now I had the four characteristics listed within my Environmental Sensing Service. As you can see these are all listed as Contributed items on the Bluetooth GATT Configurator tool - note that you cannot edit any of the fields as they are all greyed out:

 

 

I was then scratching my head as to which example was the easiest to modify.

 

I decided to use the Bluetooth - SoC Thermometer (Mock) example as this included mock values for temperature and humidity. All I had to do was remove the Health Thermometer GATT service and replace it with the Environmental Sensing - Relative Humidity and Temperature GATT service. I also had to then make sure I had "advertising set to true" inside the XML file, otherwise it bombed.

 

 

It was almost good to go. But now I needed to amend the Bluetooth event table inside the code. It took a good while to figure out what was going on. What complicates matters is that there is a good mix of automated code injected into the application.

 

So it turns out that all you need to modify is the app.c file as this handles all the events.

 

/**************************************************************************//**
 * Bluetooth stack event handler.
 * This overrides the dummy weak implementation.
 *
 * @param[in] evt Event coming from the Bluetooth stack.
 *****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t *evt)
{
  sl_status_t sc;
  bd_addr address;
  uint8_t address_type;
  uint8_t system_id[8];

  // Handle stack events
  switch (SL_BT_MSG_ID(evt->header)) {
    // -------------------------------
    // This event indicates the device has started and the radio is ready.
    // Do not call any stack command before receiving this boot event!
    case sl_bt_evt_system_boot_id:
      // Print boot message.
      app_log_info("Bluetooth stack booted: v%d.%d.%d-b%d\r\n",
                   evt->data.evt_system_boot.major,
                   evt->data.evt_system_boot.minor,
                   evt->data.evt_system_boot.patch,
                   evt->data.evt_system_boot.build);

      // Extract unique ID from BT Address.
      sc = sl_bt_system_get_identity_address(&address, &address_type);
      app_assert_status(sc);

      // Pad and reverse unique ID to get System ID.
      system_id[0] = address.addr[5];
      system_id[1] = address.addr[4];
      system_id[2] = address.addr[3];
      system_id[3] = 0xFF;
      system_id[4] = 0xFE;
      system_id[5] = address.addr[2];
      system_id[6] = address.addr[1];
      system_id[7] = address.addr[0];

      sc = sl_bt_gatt_server_write_attribute_value(gattdb_system_id,
                                                   0,
                                                   sizeof(system_id),
                                                   system_id);
      app_assert_status(sc);

      app_log_info("Bluetooth %s address: %02X:%02X:%02X:%02X:%02X:%02X\r\n",
                   address_type ? "static random" : "public device",
                   address.addr[5],
                   address.addr[4],
                   address.addr[3],
                   address.addr[2],
                   address.addr[1],
                   address.addr[0]);

      // Create an advertising set.
      sc = sl_bt_advertiser_create_set(&advertising_set_handle);
      app_assert_status(sc);

      // Set advertising interval to 100ms.
      sc = sl_bt_advertiser_set_timing(
        advertising_set_handle, // advertising set handle
        160, // min. adv. interval (milliseconds * 1.6)
        160, // max. adv. interval (milliseconds * 1.6)
        0,   // adv. duration
        0);  // max. num. adv. events
      app_assert_status(sc);
      // Start general advertising and enable connections.
      sc = sl_bt_advertiser_start(
        advertising_set_handle,
        advertiser_general_discoverable,
        advertiser_connectable_scannable);
      app_assert_status(sc);
      app_log_info("Started advertising\r\n");
      break;

    // -------------------------------
    // This event indicates that a new connection was opened.
    case sl_bt_evt_connection_opened_id:
      app_log_info("Connection opened\r\n");

#ifdef SL_CATALOG_BLUETOOTH_FEATURE_POWER_CONTROL_PRESENT
      // Set remote connection power reporting - needed for Power Control
      sc = sl_bt_connection_set_remote_power_reporting(
        evt->data.evt_connection_opened.connection,
        connection_power_reporting_enable);
      app_assert_status(sc);
#endif // SL_CATALOG_BLUETOOTH_FEATURE_POWER_CONTROL_PRESENT

      break;

      // -------------------------------
      // This event is trigger upon a read request.
    case sl_bt_evt_gatt_server_user_read_request_id:
      // Handle stack events
      if (sl_bt_evt_gatt_server_user_read_request_id == SL_BT_MSG_ID(evt->header)) {
        //COS
        if (gattdb_iaq_eco2 == evt->data.evt_gatt_server_user_read_request.characteristic) {
            gas_eco2_read_cb(&evt->data.evt_gatt_server_user_read_request);
        //TVOC
        } else if (gattdb_iaq_tvoc == evt->data.evt_gatt_server_user_read_request.characteristic) {
            gas_tvoc_read_cb(&evt->data.evt_gatt_server_user_read_request);
        }
      }
      break;

    // -------------------------------
    // This event indicates that a connection was closed.
    case sl_bt_evt_connection_closed_id:
      app_log_info("Connection closed\r\n");
      // Restart advertising after client has disconnected.
      sc = sl_bt_advertiser_start(
        advertising_set_handle,
        advertiser_general_discoverable,
        advertiser_connectable_scannable);
      app_assert_status(sc);
      app_log_info("Started advertising\r\n");
      break;

    ///////////////////////////////////////////////////////////////////////////
    // Add additional event handlers here as your application requires!      //
    ///////////////////////////////////////////////////////////////////////////

    // -------------------------------
    // Default event handler.
    default:
      break;
  }
}

 

 

The secret ingredient here is adding in sl_bt_evt_gatt_server_user_read_request_id. This triggers each time a user makes a read request from a connected device, like a phone. From this event you can then split out the Characteristic ID's and call other functions like the two callback functions:

 

gas_eco2_read_cb

 

static void gas_eco2_read_cb(sl_bt_evt_gatt_server_user_read_request_t *data)
{
  sl_status_t sc;

  // update measurement data
  if (!gas_updated) gas_update();

  sc = sl_bt_gatt_server_send_user_read_response(
    data->connection,
    data->characteristic,
    0,
    sizeof(gas_eco2),
    (uint8_t*)&gas_eco2,
    NULL);
  app_assert_status(sc);
}

 

and gas_tvoc_read_cb.

 

static void gas_tvoc_read_cb(sl_bt_evt_gatt_server_user_read_request_t *data)
{
  sl_status_t sc;

  // update measurement data
  if (!gas_updated) gas_update();

  sc = sl_bt_gatt_server_send_user_read_response(
    data->connection,
    data->characteristic,
    0,
    sizeof(gas_tvoc),
    (uint8_t*)&gas_tvoc,
    NULL);
  app_assert_status(sc);
}

 

Both these call backs use the gas_update() function, which then pulls data from my new I2C Air Quality 4 click library. A simple timer is used to prevent concurrent measurement requests - the library calculates both CO2 and TVOC but the Bluetooth GATT event request each sensor reading individually.

 

static void gas_update(void)
{
  uint16_t eco2;
  uint16_t tvoc;
  sl_status_t sc;
  // keep previous data if measurement fails
  sc = get_airqualitydata(&eco2, &tvoc);
  if (sc == 0) {
    gas_eco2 = eco2;
    gas_tvoc = tvoc;
    app_log_info("CO2: %u | TVOC %u\r\n", gas_eco2, gas_tvoc);

    gas_updated = true;
    // set up a timer to change gas_updated = false
    sc = sl_sleeptimer_start_timer_ms(
                          &GasUpdate_timer,
                          GASUPDATE_TIMERVAL,
                          on_timer_callback,
                          (void *)NULL,
                          1,
                          SL_SLEEPTIMER_NO_HIGH_PRECISION_HF_CLOCKS_REQUIRED_FLAG);

    if(sc != SL_STATUS_OK) app_log_warning("Timer failed to start.\r\n");
  }
  else app_log_info("No reading\r\n");
}

static void on_timer_callback(sl_sleeptimer_timer_handle_t *handle,
                       void *data)
{
  (void)&handle;
  (void)&data;
  gas_updated = false;
}


 

 

4. Air Quality 4 click BLE Demo

 

And here is the demo showing how the air quality data can be read from the Silabs EFR Connect app.

 

 

 

An export of the Simplicity Studio project (soc_iaq_sgp30_click.sls) is now available in my GitHub repository.

 

 

 

5. Revisiting my Air Quality Sensor App (to simplify and add enhancements)

 

Now that the dust has settled and I understand much more about how to use Simplicity Studio 5 and all the necessary SDK’s, I decided to revisit my endeavours and add in some improvements... I also fixed one error in the SGP30 library, which was found in the set humidity function.

 

 

SGP30 code changes

 

According to the Sensirion SGP30 datasheet, the sensor shows best performance when used with a 1Hz sampling rate as the on-chip baseline compensation algorithm has been optimized for this sampling rate.

 

As such, I decided to hardcode this sampling rate so that the SGP30 is measuring CO2 and TVOC values every second (1Hz) and then I added in a periodic measurement interval which would give the average values of the sampled measurements over that user-defined period as well as spot values at the end of the measurement period. So for example, if the periodic measurement interval is 15 seconds (which I’ve set as the default interval) then after every 15 seconds we get the average of 15 samples taken during the measurement period plus the most recent spot sampled values for CO2 and TVOC.

 

So my enhanced application code now provides 4 values obtained from the SGP30: spot CO2, spot TVOC, ave CO2 and ave TVOC (I provide more detail on how this is structured within a new BLE characteristic further down in this blog).

 

The SGP30 sensor also allows for baseline and humidity compensation. The  SGP30  provides  the  possibility  to read  and  write  the  baseline values  of  the  baseline  compensation  algorithm (a get and set baseline function is provided in the sgp30 library), but for our purposes we’ll keep the baseline values as is. And the SGP30 allows for on-chip humidity compensation of the sensor raw signals and derived eCO2 and TVOC measurements. To use the on-chip humidity compensation an absolute humidity value from an external humidity sensor is required.

 

So for my application I decided to add one in.

 

 

DHT22 Temperature and Humidity Sensor

 

 

I decided to add in a DHT22 temperature and humidity sensor to measure humidity values, as I had one available and I wanted to test out microsecond timings using a 1-wire custom GPIO interface with the BGM220P module.

 

 

To configure my SS5 project to handle the DHT22 sensor I first needed to add in a new component, which was the “Microsecond Delay” component.

 

 

What is interesting about this component is that the delay function is designed to not use any hardware peripherals and instead uses a while loop, based on the frequency of the MCU core clock. The documentation informs us that it is only suitable for very small delays.

 

What I also liked about this function is that within the function’s code it gave me a method to measure the number of ticks and convert this to a pulse duration using the MCU clock frequency.

 

I then needed to define my signal port and pin number, and configure my input and output modes. Here I used  #define:

 

#define GPIO_DHT22PIN                  gpioPortC, 1
#define GPIO_DHT22PIN_INPUTMODE        gpioModeInputPullFilter
#define GPIO_DHT22PIN_OUTPUTMODE   gpioModePushPull

 

I then created an initialisation function for the sensor, where I used the SystemCoreClockGet() to determine my “maxcycles” parameter.

 

void dht22_init(void)
{
  CMU_ClockEnable(cmuClock_GPIO, true);
  GPIO_PinModeSet(GPIO_DHT22PIN,
                  GPIO_DHT22PIN_INPUTMODE,
                  GPIO_DHT22PIN_HIGH);

  // First we set out max cycles to represent a 1ms timeout
  maxcycles = SystemCoreClockGet() / 1000U;

}

 

 

I then created a “dht22_read()” function, which is the core function for the sensor. Within this function, the timings are critical and here I used the Atomic functions to set this as critical:

 

CORE_DECLARE_IRQ_STATE;

CORE_ENTER_ATOMIC();

 

Then after the critical timings section of the code I closed the section off with a:

 

CORE_EXIT_ATOMIC();

 

The rest of the library code is fairly self explanatory as it follows similar logic to the Adafruit DHT Sensor library (it’s basically a port of the code).

 

/*!
 * @file dht22lib.c
 *
 *  Created on: 12 Jul 2021
 *  Author: CGerrish
 *
 *  This is a port of the Adafruit Arduino library and is specifically for the DHT22 sensor (DHT11 requires mods to timings)
 *
 *
 *  Adafruit invests time and resources providing this open source code,
 *  please support Adafruit andopen-source hardware by purchasing products
 *  from Adafruit!
 *
 *  Arduino DHT sensor library is written by Adafruit Industries.
 *
 *  MIT license, all text above must be included in any redistribution
 */

#include "dht22lib.h"
#include "em_gpio.h"
#include "em_cmu.h"
//#include "sl_sleeptimer.h"
#include "sl_udelay.h"

#define GPIO_DHT22PIN               gpioPortC, 1

#define GPIO_DHT22PIN_INPUTMODE         gpioModeInputPullFilter
#define GPIO_DHT22PIN_OUTPUTMODE        gpioModePushPull

#define GPIO_DHT22PIN_LOW            0
#define GPIO_DHT22PIN_HIGH           1

#define TIMEOUT UINT32_MAX

const float NAN = -99.0;

sl_status_t status;

static uint32_t maxcycles;

bool lastresult;

uint8_t data[5];

static uint32_t expectPulse(bool level)
{
  uint32_t count = 0;

  // TODO: Look into using DMA functionality here
  while ( GPIO_PinInGet(GPIO_DHT22PIN) == level) {
    if (count++ >= maxcycles) {
      return TIMEOUT; // Exceeded timeout, fail.
    }
  }
  return count;
}


static bool dht22_read()
{
  memset(data, '\0', 5);
  // Send start signal.  See DHT datasheet for full signal diagram:
  //   http://www.adafruit.com/datasheets/Digital%20humidity%20and%20temperature%20sensor%20AM2302.pdf

  // Go into high impedance state to let pull-up raise data line level and start the reading process.
  GPIO_PinModeSet(GPIO_DHT22PIN,
                  GPIO_DHT22PIN_INPUTMODE,
                  GPIO_DHT22PIN_HIGH);

  sl_udelay_wait(1100);

  // First set data line low for a period according to sensor type
  GPIO_PinModeSet(GPIO_DHT22PIN,
                  GPIO_DHT22PIN_OUTPUTMODE,
                  GPIO_DHT22PIN_LOW);


  GPIO_PinOutClear(GPIO_DHT22PIN);

  sl_udelay_wait(1100);         // data sheet says "at least 1ms"

  GPIO_PinOutSet(GPIO_DHT22PIN);

  // Go into high impedance state to let pull-up raise data line level and start the reading process.
  GPIO_PinModeSet(GPIO_DHT22PIN,
                  GPIO_DHT22PIN_INPUTMODE,
                  GPIO_DHT22PIN_HIGH);

  sl_udelay_wait(50);

  uint32_t cycles[80];

  // We now enter a critical timing phase -------------------------------------
  // ==========================================================================
  CORE_DECLARE_IRQ_STATE;
  CORE_ENTER_ATOMIC();

  // First expect a low signal for ~80 microseconds followed by a high signal
  // for ~80 microseconds again.
  if (expectPulse(0) == TIMEOUT) {
    lastresult = false;
    return lastresult;
  }
  if (expectPulse(1) == TIMEOUT) {
    lastresult = false;
    return lastresult;
  }
  // Now read the 40 bits sent by the sensor.  Each bit is sent as a 50
  // microsecond low pulse followed by a variable length high pulse.  If the
  // high pulse is ~28 microseconds then it's a 0 and if it's ~70 microseconds
  // then it's a 1.  We measure the cycle count of the initial 50us low pulse
  // and use that to compare to the cycle count of the high pulse to determine
  // if the bit is a 0 (high state cycle count < low state cycle count), or a
  // 1 (high state cycle count > low state cycle count). Note that for speed
  // all the pulses are read into a array and then examined in a later step.
  for (int i = 0; i < 80; i += 2) {
    cycles[i] = expectPulse(0);
    cycles[i + 1] = expectPulse(1);
  }

  // We now leave a critical timing phase -------------------------------------
  // ==========================================================================
  CORE_EXIT_ATOMIC();

  // Inspect pulses and determine which ones are 0 (high state cycle count < low
  // state cycle count), or 1 (high state cycle count > low state cycle count).
  for (int i = 0; i < 40; ++i) {
    uint32_t lowCycles = cycles[2 * i];
    uint32_t highCycles = cycles[2 * i + 1];
    if ((lowCycles == TIMEOUT) || (highCycles == TIMEOUT)) {
      lastresult = false;
      return lastresult;
    }
    data[i / 8] <<= 1;
    // Now compare the low and high cycle times to see if the bit is a 0 or 1.
    if (highCycles > lowCycles) {
      // High cycles are greater than 50us low cycle count, must be a 1.
      data[i / 8] |= 1;
    }
    // Else high cycles are less than (or equal to, a weird case) the 50us low
    // cycle count so this must be a zero.  Nothing needs to be changed in the
    // stored data.
  }

  // Check we read 40 bits and that the checksum matches.
  if (data[4] == ((data[0] + data[1] + data[2] + data[3]) & 0xFF)) {
    lastresult = true;
    return lastresult;
  } else {
    lastresult = false;
    return lastresult;
  }

}

static int16_t CalcTemperature()
{
  int16_t fTemp = NAN;
  fTemp = ((int16_t)(data[2] & 0x7F)) << 8 | data[3];
  if (data[2] & 0x80) {
      fTemp *= -1;
  }

  return fTemp;
}

static int16_t CalcHumidity()
{
  int16_t fHumidity = NAN;
  fHumidity = ((int16_t)data[0]) << 8 | data[1];

  return fHumidity;
}

sl_status_t dht22_getRHTdata(int16_t* TEMP, int16_t* RH)
{

  if (dht22_read()) {
      // Use the data received
      *TEMP = CalcTemperature();
      *RH = CalcHumidity();
      return SL_STATUS_OK;
  }
  return SL_STATUS_FAIL;

}


void dht22_init(void)
{
  CMU_ClockEnable(cmuClock_GPIO, true);
  GPIO_PinModeSet(GPIO_DHT22PIN,
                  GPIO_DHT22PIN_INPUTMODE,
                  GPIO_DHT22PIN_HIGH);

  // First we set out max cycles to represent a 1ms timeout
  maxcycles = SystemCoreClockGet() / 1000U;

}

 

 

 

Modifying the Environmental Sensing GATT Service

 

When I created the original code, I could quickly see how messy the code had become simply because the Environmental GATT service had defined separate characteristics for CO2, TVOC, mock Temperature and mock Relative Humidity value.

 

This made no sense from a coding and performance point of view, in light of how much data you could transmit across using the Bluetooth Low Energy Protocol - the Maximum Transmission Unit (MTU) is the maximum length of an ATT packet and the minimum length of the MTU ATT packet is 23 bytes.

 

As the SGP30 returns both the equivalent CO2 and TVOC values upon a single code request, and similarly the DHT22 does the same by returning both a Temperature and Relative Humidity value when requested, it made more sense to create just two characteristics which would transmit a composite byte array for each sensor.

 

Thus for the revisited app, I decided to create one characteristic for the SGP30 sensor and one characteristic for the DHT22 sensor and use byte arrays to transmit the data. Thus the characteristic for the SGP30 sensor transmits 4 x uint16 data types and the characteristic for the DHT22 sensor transmits 2 x int16 data types.

 

Then, as I wanted to create an app which could be used for data analysis I set the property for each characteristic to notify. An optional read property was added to the SGP30 characteristic to provide spot values at given moment rather than the notify option which was set at a periodic interval of 15 seconds.

 

Two additional read / write characteristics were added to allow the user to change the periodic update intervals for both the SGP30 and DHT22 sensors.

 

{gallery:autoplay=false} BLE application structure

 

Then for the core application logic (app.c) I flattened out my original example so that most logic is handled within app.c with direct function calls to sgp30 and dht22 libraries:

 

/***************************************************************************//**
 * @file app.c
 * @brief Core application logic for the soc Air Quality BLE example (revisited)
 *        developed by CGerrish July 2021 as part of element14 road test.
 *******************************************************************************
 * # License
 * <b>Copyright 2020 Silicon Laboratories Inc. www.silabs.com</b>
 *******************************************************************************
 *
 * SPDX-License-Identifier: Zlib
 *
 * The licensor of this software is Silicon Laboratories Inc.
 *
 * This software is provided 'as-is', without any express or implied
 * warranty. In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 *
 ******************************************************************************/

#include <stdio.h>
#include <math.h>
#include "em_common.h"
#include "app_assert.h"
#include "app_log.h"
#include "sl_status.h"
#include "sl_bluetooth.h"
#include "gatt_db.h"
#include "sl_simple_led.h"
#include "sl_simple_led_instances.h"
#include "sl_simple_button_instances.h"
#include "sl_i2cspm_instances.h"
#include "sl_sleeptimer.h"

#include "sgp30/sgp30.h"
#include "dht22/dht22lib.h"
#include "app.h"


// BLE Connection handle.
static uint8_t app_connection = 0;

// The advertising set handle allocated from Bluetooth stack.
static uint8_t advertising_set_handle = 0xff;

/*******************************************************************************
 *******************************   DEFINES   ***********************************
 ******************************************************************************/
#define AGREGATE_VALS

#ifndef SENSORMEASURE_TICK
#define SENSORMEASURE_TICK         (1000u)
#endif

/*******************************************************************************
 ***************************  LOCAL VARIABLES   ********************************
 ******************************************************************************/

const uint8_t SAMPLINGWARMUP_COUNTER = 20;
const uint8_t DHT22SAMPLING_INTERVAL = 5;       // We measure temperature and RH every 5 seconds (5*SENSORMEASURE_TICK)

sl_sleeptimer_timer_handle_t SensorMeasure_timer;
sl_status_t sc;

static volatile bool Ready2Measure = false;
static volatile bool Ready2Advertise = false;
static volatile bool app_btn0_pressed = false;


static bool SGP30warmup = false;
static bool SGP30resetHumidity = false;
static uint32_t sampling_counter = 1;

uint16_t _CO2 = 0;
uint16_t _TVOC = 0;
int16_t _TEMP = 0;
int16_t _RH = 0;

#ifdef AGREGATE_VALS
  const uint16_t DATA_COUNT = 15;

  uint32_t T_CO2 = 0;
  uint32_t T_TVOC = 0;
  uint32_t aggr_counter = 1;
#endif

/*******************************************************************************
 *********************   LOCAL FUNCTION PROTOTYPES   ***************************
 ******************************************************************************/

static void sgp30_readrequest_cb(sl_bt_evt_gatt_server_user_read_request_t *data);
static void sl_bt_es_dht22_notification_changed_cb(uint8_t connection, sl_bt_gatt_client_config_flag_t client_config);
static void sl_bt_es_sgp30_notification_changed_cb(uint8_t connection, sl_bt_gatt_client_config_flag_t client_config);
static void sl_bt_connection_closed_cb(uint16_t reason, uint8_t connection);


static void on_SamplingTimer_callback(sl_sleeptimer_timer_handle_t *handle,
                       void *data);


static double RHtoAbsolute (float relHumidity, float tempC) {
  double eSat = 6.11 * pow(10.0, (7.5 * tempC / (237.7 + tempC)));
  double vaporPressure = (relHumidity * eSat) / 100; //millibars
  double absHumidity = 1000 * vaporPressure * 100 / ((tempC + 273) * 461.5); //Ideal gas law with unit conversions
  return absHumidity;
}

static uint16_t doubleToFixedPoint( double number) {
  int power = 1 << 8;
  double number2 = number * power;
  uint16_t value = floor(number2 + 0.5);
  return value;
}

/**************************************************************************//**
 * Convert temperature and humidity values to characteristic buffer.
 * @param[in] spot value Temperature value in multiplied by 10.
 * @param[in] spot value Humidity value in multiplied by 10.
 * @param[out] buffer Buffer to hold GATT characteristics value.
 *****************************************************************************/
static void dht22_measurement_vals_to_buf(int16_t temp_value,
                                          int16_t rh_value,
                                          uint8_t *buffer)
{
  buffer[0] = temp_value & 0xff;
  buffer[1] = (temp_value >> 8) & 0xff;
  buffer[2] = rh_value & 0xff;
  buffer[3] = (rh_value >> 8) & 0xff;
}

/**************************************************************************//**
 * Convert co2 and tvoc values to characteristic buffer.
 * @param[in] value spot CO2 value.
 * @param[in] value spot TVOC value.
 * @param[in] value Ave CO2 value.
 * @param[in] value Ave TVOC value.
 * @param[out] buffer Buffer to hold GATT characteristics value.
 *****************************************************************************/
static void sgp30_measurement_vals_to_buf(uint16_t co2_value,
                                          uint16_t tvoc_value,
                                          uint16_t aveco2_value,
                                          uint16_t avetvoc_value,
                                          uint8_t *buffer)
{
  buffer[0] = co2_value;
  buffer[1] = (co2_value >> 8);
  buffer[2] = tvoc_value;
  buffer[3] = (tvoc_value >> 8);
  buffer[4] = aveco2_value;
  buffer[5] = (aveco2_value >> 8);
  buffer[6] = avetvoc_value;
  buffer[7] = (avetvoc_value >> 8);
}

/**************************************************************************//**
 * Send DHT22 Temperature & Humidity Measurement characteristic notification to the client.
 *****************************************************************************/
sl_status_t sl_bt_es_dht22_measurement_notify(uint8_t connection)
{
  uint8_t buf[4] = { 0 };
  // Convert the most recent vals to the buffer
  dht22_measurement_vals_to_buf(_TEMP, _RH, buf);
  sc = sl_bt_gatt_server_send_notification(
    connection,
    gattdb_es_dht22,
    sizeof(buf),
    buf);
  return sc;
}

/**************************************************************************//**
 * Send SGP30 CO2 and TVOC Measurement characteristic notification to the client.
 *****************************************************************************/
sl_status_t sl_bt_es_sgp30_measurement_notify(uint8_t connection, uint16_t aveco2_value, uint16_t avetvoc_value)
{

  uint8_t buf[8] = { 0 };

  sgp30_measurement_vals_to_buf(_CO2, _TVOC, aveco2_value, avetvoc_value, buf);

  sc = sl_bt_gatt_server_send_notification(
    connection,
    gattdb_es_sgp30,
    sizeof(buf),
    buf);
  return sc;
}


/**************************************************************************//**
 * Application Init.
 *****************************************************************************/
SL_WEAK void app_init(void)
{
  /////////////////////////////////////////////////////////////////////////////
  // Put your additional application init code here!                         //
  // This is called once during start-up.                                    //
  /////////////////////////////////////////////////////////////////////////////

  uint64_t SGP30serialID;
  uint16_t SGP30featureSetVer;

  // disable button events until ready to do so.
  sl_button_disable(SL_SIMPLE_BUTTON_INSTANCE(0));

  generalCallReset();       // Reset all I2C peripherals
  sl_sleeptimer_delay_millisecond(200); // wait a short bit

  if (SGP30_SUCCESS == getSerialID(&SGP30serialID)) {
    app_log_info("SGP30 Serial ID: 0x%u\n", SGP30serialID);
    sl_led_turn_on(&sl_led_led0);

    if (SGP30_SUCCESS == getFeatureSetVersion(&SGP30featureSetVer)) {
      app_log_info("SGP30 Feature Set Version: 0x%u\n", SGP30featureSetVer);

      // Initialise the DHT22 temperature and humidity sensor
      dht22_init();

      // Initialise the SGP30 eCO2 TVOC sensor
      if (SGP30_SUCCESS == initAirQuality()) {
        // Set up a 1 second sample timer
        app_log_info("Begin 20 sec of sampling to warm up...\n");
        SGP30warmup = true;
        sc = sl_sleeptimer_start_periodic_timer_ms(
                              &SensorMeasure_timer,
                              SENSORMEASURE_TICK,
                              on_SamplingTimer_callback,
                              (void *)NULL,
                              1,
                              SL_SLEEPTIMER_NO_HIGH_PRECISION_HF_CLOCKS_REQUIRED_FLAG);

        if(sc != SL_STATUS_OK) {
            app_log_warning("SGP30 Sampling Timer failed to start.\n");
        }
      }
    }
  }
}

/**************************************************************************//**
 * Application Process Action.
 *****************************************************************************/
SL_WEAK void app_process_action(void)
{
  /////////////////////////////////////////////////////////////////////////////
  // Put your additional application code here!                              //
  // This is called infinitely.                                              //
  // Do not call blocking functions from here!                               //
  /////////////////////////////////////////////////////////////////////////////

  if (Ready2Measure) {
    Ready2Measure = false;

    measureAirQuality(&_CO2, &_TVOC);

    if (sampling_counter % DHT22SAMPLING_INTERVAL == 0) {
        if (SL_STATUS_OK == dht22_getRHTdata(&_TEMP, &_RH)) {
            app_log_info("DHT22: Temp = %d C & RH = %d %%\n", (_TEMP+5)/10, (_RH+5)/10);
            // Send data to gatt database if we have a connection
            if (app_connection > 0) sl_bt_es_dht22_measurement_notify(app_connection);
        }
    }

    if (SGP30warmup) {
        if (sampling_counter <= SAMPLINGWARMUP_COUNTER) {
            app_log_info("* %u\n", sampling_counter);
        }
        else {
            app_log_info("... warmup complete. Air Quality data updated every 15 seconds\n");
            app_log_info("... Use BTN0 to enable/disable Humidity Compensation.\n");
            sl_led_turn_off(&sl_led_led0);
            // Button events can be received from now on.
            sl_button_enable(SL_SIMPLE_BUTTON_INSTANCE(0));

            app_log_info("Checking if ok to start BLE advertising...\n");
            while (!Ready2Advertise) {;;}

            // Start general advertising and enable connections.
            sc = sl_bt_advertiser_start(
              advertising_set_handle,
              sl_bt_advertiser_general_discoverable,
              sl_bt_advertiser_connectable_scannable);
            app_assert_status(sc);
            app_log_info("...BLE Adveritising started.\n");
            sampling_counter = 1;
            SGP30warmup = false;

        }
    }
    if (!SGP30warmup) {
        // Normal sampling can start
#ifndef AGREGATE_VALS

        app_log_info("AQ4 sample [%u]: CO2 = %u ppm & TVOC = %u ppb\n", sampling_counter, _CO2, _TVOC);
        if (app_connection > 0) sl_bt_es_sgp30_measurement_notify(app_connection, 0, 0);

#else
        if (aggr_counter < DATA_COUNT) {
          T_CO2 += _CO2;
          T_TVOC += _TVOC;
        }
        else {
          //Convert relative humidity to absolute humidity
          double absHumidity = RHtoAbsolute((float)(_RH+5.0)/10.0, (float)(_TEMP+5.0)/10.0);

          //Convert the double type humidity to a fixed point 8.8bit number
          uint16_t sensHumidity = doubleToFixedPoint(absHumidity);

          app_log_info("Absolute Humidity 0x%x (%u) vs default 0xF80 (3968).\n", sensHumidity, sensHumidity);

          T_CO2 += _CO2;
          T_CO2 = T_CO2/DATA_COUNT;
          T_TVOC += _TVOC;
          T_TVOC = T_TVOC/DATA_COUNT;
          aggr_counter = 0;
          app_log_info("AQ4 spot: CO2 = %u ppm & TVOC = %u ppb | AQ4 ave: CO2 = %u ppm & TVOC = %u ppb\n", _CO2, _TVOC, T_CO2, T_TVOC);
          if (app_connection > 0) sl_bt_es_sgp30_measurement_notify(app_connection, T_CO2, T_TVOC);
          // reset those average values
          T_CO2 = 0;
          T_TVOC = 0;

          if (app_btn0_pressed) {
              if (SGP30_SUCCESS == setHumidity(sensHumidity)) app_log_info("updated SGP30 with latest humidity value\n");
              SGP30resetHumidity = false;
          }
          else {
              // We reset using 0x0000
              if (!SGP30resetHumidity) {
                  SGP30resetHumidity = true;
                  if (SGP30_SUCCESS == setHumidity(0x0000)) app_log_info("reset SGP30 back to default humidity values\n");
              }
          }

        }
        aggr_counter++;
#endif
    }

    sampling_counter ++;
  }
}

/**************************************************************************//**
 * Bluetooth stack event handler.
 * This overrides the dummy weak implementation.
 *
 * @param[in] evt Event coming from the Bluetooth stack.
 *****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t *evt)
{
  sl_status_t sc;
  bd_addr address;
  uint8_t address_type;
  uint8_t system_id[8];

  switch (SL_BT_MSG_ID(evt->header)) {
    // -------------------------------
    // This event indicates the device has started and the radio is ready.
    // Do not call any stack command before receiving this boot event!
    case sl_bt_evt_system_boot_id:

      // Print boot message.
      app_log_info("Bluetooth stack booted: v%d.%d.%d-b%d\n",
                   evt->data.evt_system_boot.major,
                   evt->data.evt_system_boot.minor,
                   evt->data.evt_system_boot.patch,
                   evt->data.evt_system_boot.build);

      // Extract unique ID from BT Address.
      sc = sl_bt_system_get_identity_address(&address, &address_type);
      app_assert_status(sc);

      // Pad and reverse unique ID to get System ID.
      system_id[0] = address.addr[5];
      system_id[1] = address.addr[4];
      system_id[2] = address.addr[3];
      system_id[3] = 0xFF;
      system_id[4] = 0xFE;
      system_id[5] = address.addr[2];
      system_id[6] = address.addr[1];
      system_id[7] = address.addr[0];

      sc = sl_bt_gatt_server_write_attribute_value(gattdb_system_id,
                                                   0,
                                                   sizeof(system_id),
                                                   system_id);
      app_assert_status(sc);

      app_log_info("Bluetooth %s address: %02X:%02X:%02X:%02X:%02X:%02X\n",
                   address_type ? "static random" : "public device",
                   address.addr[5],
                   address.addr[4],
                   address.addr[3],
                   address.addr[2],
                   address.addr[1],
                   address.addr[0]);

      // Create an advertising set.
      sc = sl_bt_advertiser_create_set(&advertising_set_handle);
      app_assert_status(sc);

      // Set advertising interval to 100ms.
      sc = sl_bt_advertiser_set_timing(
        advertising_set_handle,
        160, // min. adv. interval (milliseconds * 1.6)
        160, // max. adv. interval (milliseconds * 1.6)
        0,   // adv. duration
        0);  // max. num. adv. events
      app_assert_status(sc);

      // We use the custom flag to indicate we are ready to advertise. This is checked in the app_process_action function.
      Ready2Advertise = true;

      break;

    // -------------------------------
    // This event indicates that a new connection was opened.
    case sl_bt_evt_connection_opened_id:
      app_log_info("BLE Connection opened\n");
      break;

    // -------------------------------
    // This event indicates that a connection was closed.
    case sl_bt_evt_connection_closed_id:

      sl_bt_connection_closed_cb(evt->data.evt_connection_closed.reason,
                                       evt->data.evt_connection_closed.connection);

      app_log_info("BLE Connection closed. Starting BLE Advertiser.\n");
      // Restart advertising after client has disconnected.
      sc = sl_bt_advertiser_start(
        advertising_set_handle,
        sl_bt_advertiser_general_discoverable,
        sl_bt_advertiser_connectable_scannable);
      app_assert_status(sc);
      break;

    ///////////////////////////////////////////////////////////////////////////
    // Add additional event handlers here as your application requires!      //
    ///////////////////////////////////////////////////////////////////////////

    // -------------------------------
    // This event indicates that the value of an attribute in the local GATT
    // database was changed by a remote GATT client.
    case sl_bt_evt_gatt_server_attribute_value_id:
      // The value of the gattdb_es_dht22_meas_int characteristic was changed.
      if (gattdb_es_dht22_meas_int == evt->data.evt_gatt_server_characteristic_status.characteristic) {
        uint16_t data_recv;
        size_t data_recv_len;

        // Read characteristic value.
        sc = sl_bt_gatt_server_read_attribute_value(gattdb_es_dht22_meas_int,
                                                    0,
                                                    2,
                                                    &data_recv_len,
                                                    &data_recv);
        (void)data_recv_len;
        app_log_status_error(sc);

        if (sc != SL_STATUS_OK) {
            app_log_warning("Failed to get new DHT22 Measurement Interval\n");
        }
        else {
          // If within the define 5 sec min/ 15 min max parameters we change the timer interval.
          if (data_recv >= 5 && data_recv <= 300) {
              app_log_info("New DHT22 Measurement Interval: %u sec\n", data_recv);
              //SL_BT_ES_DHT11_MEASUREMENT_INTERVAL_SEC = data_recv; -- not implemented

          }
          else {
              app_log_warning("New DHT22 Measurement Interval: %u sec outside valid (5s to 5min) range\n", data_recv);
          }
        }
      }
      // The value of the gattdb_es_sgp30_meas_int characteristic was changed.
      else if (gattdb_es_sgp30_meas_int == evt->data.evt_gatt_server_characteristic_status.characteristic) {
        uint16_t data_recv;
        size_t data_recv_len;

        // Read characteristic value.
        sc = sl_bt_gatt_server_read_attribute_value(gattdb_es_sgp30_meas_int,
                                                    0,
                                                    2,
                                                    &data_recv_len,
                                                    &data_recv);
        (void)data_recv_len;
        app_log_status_error(sc);

        if (sc != SL_STATUS_OK) {
            app_log_warning("Failed to get new SGP30 Measurement Interval\n");
        }
        else {
          // If within the define 5 sec min/ 15 min max parameters we change the timer interval.
          if (data_recv >= 5 && data_recv <= 300) {
              app_log_info("New SGP30 Measurement Interval: %u sec\n", data_recv);
              //SL_BT_ES_SGP30_MEASUREMENT_INTERVAL_SEC = data_recv; -- not implemented

          }
          else {
              app_log_warning("New SGP30 Measurement Interval: %u sec outside valid (5s to 5min) range\n", data_recv);
          }
        }
      }
      break;

      // This event indicates a read request.
      case sl_bt_evt_gatt_server_user_read_request_id:
        if (gattdb_es_sgp30 == evt->data.evt_gatt_server_characteristic_status.characteristic) {
          sgp30_readrequest_cb(&evt->data.evt_gatt_server_user_read_request);
        }
        break;

        // This event indicates a notification request.
      case sl_bt_evt_gatt_server_characteristic_status_id:
        if (gattdb_es_dht22 == evt->data.evt_gatt_server_characteristic_status.characteristic) {
          // client characteristic configuration changed by remote GATT client
          if (sl_bt_gatt_server_client_config == (sl_bt_gatt_server_characteristic_status_flag_t)evt->data.evt_gatt_server_characteristic_status.status_flags) {
              sl_bt_es_dht22_notification_changed_cb(
              evt->data.evt_gatt_server_characteristic_status.connection,
              (sl_bt_gatt_client_config_flag_t)evt->data.evt_gatt_server_characteristic_status.client_config_flags);
          } else {
            app_assert(false,
                       "[E: 0x%04x] Unexpected status flag in evt_gatt_server_characteristic_status\n",
                       (int)evt->data.evt_gatt_server_characteristic_status.status_flags);
          }
        }
        else if (gattdb_es_sgp30 == evt->data.evt_gatt_server_characteristic_status.characteristic) {
          // client characteristic configuration changed by remote GATT client
          if (sl_bt_gatt_server_client_config == (sl_bt_gatt_server_characteristic_status_flag_t)evt->data.evt_gatt_server_characteristic_status.status_flags) {
              sl_bt_es_sgp30_notification_changed_cb(
              evt->data.evt_gatt_server_characteristic_status.connection,
              (sl_bt_gatt_client_config_flag_t)evt->data.evt_gatt_server_characteristic_status.client_config_flags);
          } else {
            app_assert(false,
                       "[E: 0x%04x] Unexpected status flag in evt_gatt_server_characteristic_status\n",
                       (int)evt->data.evt_gatt_server_characteristic_status.status_flags);
          }
        }
        break;

    // -------------------------------
    // Default event handler.
    default:
      break;
  }
}

/***************************************************************************//**
 * Sampling Timer callback routine for SGP30 sensor.
 ******************************************************************************/
static void on_SamplingTimer_callback(sl_sleeptimer_timer_handle_t *handle,
                       void *data)
{
  (void)&handle;
  (void)&data;

  Ready2Measure = true;

}

/**************************************************************************//**
 * Callback function of connection close event.
 *
 * @param[in] reason Unused parameter required by the health_thermometer component
 * @param[in] connection Unused parameter required by the health_thermometer component
 *****************************************************************************/
static void sl_bt_connection_closed_cb(uint16_t reason, uint8_t connection)
{
  (void)reason;
  (void)connection;

  // Stop any timers.
}

/**************************************************************************//**
 * Callback function for SGP30 spot values read event.
 *
  * @param[in] data component used for response
 *****************************************************************************/
static void sgp30_readrequest_cb(sl_bt_evt_gatt_server_user_read_request_t *data)
{

  uint8_t sgp30buf[8] = { 0 };    // two bytes for each CO2 and TVOC spot value (average values left at zero)

  sgp30_measurement_vals_to_buf(_CO2, _TVOC, 0, 0, sgp30buf);

  sc = sl_bt_gatt_server_send_user_read_response(
    data->connection,
    data->characteristic,
    0,
    sizeof(sgp30buf),
    (uint8_t*)&sgp30buf,
    NULL);

  app_assert_status(sc);
}

/**************************************************************************//**
 * DHT22 Temperature and Humidity Measurement characteristic's CCCD has changed.
 *****************************************************************************/
/**************************************************************************//**
 * Environmental Sensing - Temperature and Humidity Measurement from DHT22 sensor
 * Notification changed callback
 *
 * Called when notification of dht22 measurement is enabled/disabled by
 * the client.
 *****************************************************************************/
static void sl_bt_es_dht22_notification_changed_cb(uint8_t connection, sl_bt_gatt_client_config_flag_t client_config)
{
  (void)connection;
  (void)client_config;

  app_connection = connection;
  // Notification enabled.
  if (sl_bt_gatt_notification == client_config) {
    // Start timer used for periodic indications.
    app_log_info("DHT22 Notifications enabled\n");

  }
  // Notifications disabled.
  else {
    app_log_info("DHT22 Notifications disabled\n");
  }
}

/**************************************************************************//**
 * SGP30 CO2 and TVOC Measurement characteristic's CCCD has changed.
 *****************************************************************************/
/**************************************************************************//**
 * Environmental Sensing - CO2 and TVOC Measurement from SGP30 sensor
 * Notification changed callback
 *
 * Called when notification of sgp30 measurement is enabled/disabled by
 * the client.
 *****************************************************************************/

static void sl_bt_es_sgp30_notification_changed_cb(uint8_t connection, sl_bt_gatt_client_config_flag_t client_config)
{
  (void)connection;
  (void)client_config;

  app_connection = connection;
  // Indication or notification enabled.
  if (sl_bt_gatt_notification == client_config) {
    app_log_info("SGP30 Notifications enabled\n");
  }
  // Notifications disabled.
  else {
    app_log_info("SGP30 Notifications disabled\n");
  }
}


/**************************************************************************//**
 * Simple Button
 * Button state changed callback - we modified purpose to now compensate CO2/TVOC
 * with humidity values
 * @param[in] handle Button event handle
 *****************************************************************************/
void sl_button_on_change(const sl_button_t *handle)
{
  // Button pressed - we create a toggle state for humidity compensation.
  if (sl_button_get_state(handle) == SL_SIMPLE_BUTTON_PRESSED) {
    if (&sl_button_btn0 == handle) {
        if (app_btn0_pressed) {
            sl_led_turn_off(&sl_led_led0);
            app_btn0_pressed = false;
            app_log_info("SGP30 Humidity Compensation Deactivated.\n");
        }
        else {
            sl_led_turn_on(&sl_led_led0);
            app_btn0_pressed = true;
            app_log_info("SGP30 Humidity Compensation Activated.\n");
        }
    }
  }
}

 

 

 

Custom Android OS Phone (BLE+MQTT) and Windows 10 Desktop (MQTT) Apps

 

To develop my custom Android BLE phone app, I could’ve gone with Google's UI toolkit flutter.dev as it allows you to deploy both IOS and Android OS apps from just one code base, but instead I stuck with MIT App Inventor, the no-code solution to developing apps (this too now allows you to deploy apps for both IOS and Android).

 

My decision was narrowly based on the fact that I could reuse previous project “code” or blocks, such as the app I developed for the BGX13P road test (see comments below road test review).

 

So there was not much to add to this app, other than adding in the MQTT extension. Here I used this open source extension from Ullis Roboter: https://ullisroboterseite.de/android-AI2-PahoMQTT-en.html

 

 

For my Windows 10 (or LinuxOS) desktop app, I used Processing 3.0 (I have to say, Processing is proving to be very versatile these days).

 

Here I used two Processing libraries:

 

 

The implementation of MQTT was made even simpler as this MQTT processing library provided a ready to use example for the shift.io MQTT broker I was using.

 

// This example sketch connects to the public shiftr.io instance and sends a message on every keystroke.
// After starting the sketch you can find the client here: https://www.shiftr.io/try.
//
// Note: If you're running the sketch via the Android Mode you need to set the INTERNET permission
// in Android > Sketch Permissions.
//
// by Joël Gähwiler
// https://github.com/256dpi/processing-mqtt

// This code was modified by CGerrish (BigG) to suit the BGM220 Explorer Kit MQTT application
// 22 July 2021

import mqtt.*;
import meter.*;

PFont f1, f2;
  
MQTTClient client;

Meter mTMP, mRH, mCO2, mTVOC;

final int temperatureSensor = 1;
final int minInTMP = 0;
final int maxInTMP = 80;
final int humiditySensorC = 2;
final int minInRH = 0;
final int maxInRH = 100;
final int CO2Sensor = 3;
final int minInCO2 = 0;
final int maxInCO2 = 4000;
final int TVOCSensor = 4;
final int minInTVOC = 0;
final int maxInTVOC = 3000;
final int sensorCount = 4;         // Number of sensors being polled
String[] sensorList = {"1", "2", "3", "4"};
int i = 0;  // sensor list index

void setup() {
  size(920, 640);
  background(33, 173, 255);
  
  // Centigrade Temperature Meter
  mTMP = new Meter(this, 15, 5);
  String[] scaleLabelsC = {"0.0", "10.0", "20.0", "30.0", "40.0", "50.0", " 60.0", " 70.0", " 80.0"};
  mTMP.setScaleLabels(scaleLabelsC);
  mTMP.setUp(minInTMP, maxInTMP, 0.0f, 80.0f, 180.0f, 360.0f);
  mTMP.setTitle("Centigrade" + "\u00B0");  // Added the degree symbol
  mTMP.setInformationAreaText("Temperature = %.1f\u00B0");
  mTMP.setDisplayDigitalMeterValue(true);
  mTMP.updateMeter(24);

  // Relative Humidity Meter
  mRH = new Meter(this, 15, mTMP.getMeterHeight() + 15);
  String[] scaleLabelsRH = {"0.0", "10.0", "20.0", "30.0", "40.0", "50.0", "60.0", "70.0", " 80.0", " 90.0", "  100.0"};
  mRH.setScaleLabels(scaleLabelsRH);
  mRH.setUp(minInRH, maxInRH, 0.0f, 100.0f, 180.0f, 360.0f);
  mRH.setTitle("Percentage (%)");  // Added the degree symbol
  mRH.setInformationAreaText("Humidity = %.1f%%");
  mRH.setDisplayDigitalMeterValue(true);
  mRH.updateMeter(50);
  
  // CO2 Meter
  mCO2 = new Meter(this, mTMP.getMeterWidth() + 15, 5);
  String[] scaleLabelsCO2 = {"0", "400", "800", "1200", "1600", "2000", "2400", " 2800", " 3200", "  3600", "   4000"};
  mCO2.setScaleLabels(scaleLabelsCO2);
  mCO2.setUp(minInCO2, maxInCO2, 0.0f, 4000.0f, 180.0f, 360.0f);
  mCO2.setTitle("Parts per million (ppm)");  // Added the degree symbol
  mCO2.setInformationAreaText("eq CO2 = %.1f ppm");
  mCO2.setDisplayDigitalMeterValue(true);
  mCO2.updateMeter(400);

  // TVOC Meter
  mTVOC = new Meter(this, mTMP.getMeterWidth() + 15, mTMP.getMeterHeight() + 15);
  String[] scaleLabelsTVOC = {"0", "300", "600", "900", "1200", "1500", " 1800", " 2100", " 2400", "  2700", "   3000"};
  mTVOC.setScaleLabels(scaleLabelsTVOC);
  mTVOC.setUp(minInTVOC, maxInTVOC, 0.0f, 3000.0f, 180.0f, 360.0f);
  mTVOC.setTitle("Parts per billion (ppb)");  // Added the degree symbol
  mTVOC.setInformationAreaText("TVOC = %.1f ppb");
  mTVOC.setDisplayDigitalMeterValue(true);
  mTVOC.updateMeter(0);
  
  f1 = createFont("SourceCodePro-Regular.ttf", 32);
  f2 = createFont("SourceCodePro-Regular.ttf", 16);
  textFont(f1);
  fill(10);
  textAlign(CENTER);
  text("BGM220P Air Quality BLE-MQTT Bridge Demo", width/2, height-45);


  client = new MQTTClient(this);
  try {
  client.connect("mqtt://INSTANCENAME+CREDENTIALS.cloud.shiftr.io", "processing_app");      // here I used a private instance but you can also the public option as per library example
  }
  catch (RuntimeException e){
    e.printStackTrace();
    println("Failed to connect to MQTT broker.");
    textFont(f2);
    fill(0);
    textAlign(CENTER);
    text("Failed to connect to MQTT broker.", width/2, height-20);
  }
}

void draw() {

}

void keyPressed() {
  client.publish("e14demo/commands", "Interval: 20sec");
}

void clientConnected() {
  println("DeviceXYZ client connected");
  textFont(f2);
  fill(0);
  textAlign(CENTER);
  text("Connected to MQTT broker", width/2, height-20);

  client.subscribe("e14demo/ambient/*");
  client.subscribe("e14demo//air_quality/*");
}

void messageReceived(String topic, byte[] payload) {
  String PLstr = "";
  String[] m1 = match(topic, "ambient/temperature");
  String[] m2 = match(topic, "ambient/rel_humidity");
  String[] m3 = match(topic, "air_quality/co2");
  String[] m4 = match(topic, "air_quality/tvoc");
  String[] ValPL = split(new String(payload), ':');
  
  if (m1 != null) {  // If not null, then a match was found
    // temperature value
    PLstr = trim(ValPL[1]);
    println("temperature: " + PLstr);
    int Cint = int(PLstr.substring(0, PLstr.length()-2));
    if (Cint < 25) mTMP.setInformationAreaFontColor(#006600);
    else if  (Cint < 35) mTMP.setInformationAreaFontColor(#ace600);
    else if  (Cint < 435) mTMP.setInformationAreaFontColor(#ff6600);
    else mTMP.setInformationAreaFontColor(#ff0000);
    mTMP.updateMeter(constrain(Cint, 0, 80));
  }
  if (m2 != null) {  // If not null, then a match was found
    // relative humidity
    PLstr = trim(ValPL[1]);
    println("relative humidity: " +  PLstr);
    int RHint = int(PLstr.substring(0, PLstr.length()-2));
    if (RHint < 70) mRH.setInformationAreaFontColor(#006600);
    else if  (RHint < 80) mRH.setInformationAreaFontColor(#ace600);
    else if  (RHint < 90) mRH.setInformationAreaFontColor(#ff6600);
    else mRH.setInformationAreaFontColor(#ff0000);
    mRH.updateMeter(constrain(RHint, 0, 100));
    
  }
  if (m3 != null) {  // If not null, then a match was found
    // co2 value
    PLstr = trim(ValPL[1]);
    println("co2: " +  PLstr);
    int CO2int = int(PLstr.substring(0, PLstr.length()-4));
    if (CO2int < 600) mCO2.setInformationAreaFontColor(#006600);
    else if  (CO2int < 1200) mCO2.setInformationAreaFontColor(#ace600);
    else if  (CO2int < 2400) mCO2.setInformationAreaFontColor(#ff6600);
    else mCO2.setInformationAreaFontColor(#ff0000);
    mCO2.updateMeter(constrain(CO2int, 0, 4000));
    
  }
  if (m4 != null) {  // If not null, then a match was found
    // tvoc value
    PLstr = trim(ValPL[1]);
    println("tvoc: " +  PLstr);
    int TVOCint = int(PLstr.substring(0, PLstr.length()-4));
    if (TVOCint < 240) mTVOC.setInformationAreaFontColor(#006600);
    else if  (TVOCint < 720) mTVOC.setInformationAreaFontColor(#ace600);
    else if  (TVOCint < 2200) mTVOC.setInformationAreaFontColor(#ff6600);
    else mTVOC.setInformationAreaFontColor(#ff0000);
    mTVOC.updateMeter(constrain(TVOCint, 0, 3000));
    
  }
  if (m1 == null && m2 == null && m3 == null && m4 == null) {
  
    println("new message: " + topic + " - " + new String(payload));
  }
  
}

void connectionLost() {
  println("connection lost");
  textFont(f2);
  fill(0);
  textAlign(CENTER);
  text("MQTT connection lost", width/2, height-20);
}

 

 

Demo of revisited Air Quality Application