element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • About Us
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products
    Products
    • Arduino
    • Avnet Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
Code Exchange
  • Technologies
  • More
Code Exchange
Blog Creating two BLE "Feather Dusters", i.e. Particulate Matter (PM) sensor nodes using Mbed OS 6.15, and MIT App Inventor for the hub with added data-logging to the cloud.
  • Blog
  • Forum
  • Documents
  • Events
  • Polls
  • Files
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Join Code Exchange to participate - click to join for free!
  • Share
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: BigG
  • Date Created: 28 Feb 2022 4:37 PM Date Created
  • Views 4185 views
  • Likes 9 likes
  • Comments 11 comments
  • ble
  • SN-GCJA5
  • mit app inventor
  • mbedos
  • nrf52840
  • pm sensor
  • B5W-LD0101
Related
Recommended

Creating two BLE "Feather Dusters", i.e. Particulate Matter (PM) sensor nodes using Mbed OS 6.15, and MIT App Inventor for the hub with added data-logging to the cloud.

BigG
BigG
28 Feb 2022

Introduction

This blog follows on from my previous blog, which explained how to get started with the ARM Mbed OS 6.15 BLE API and how to develop a simple Button-LED BLE app.

I’m now taking it a step further and will be explaining how to develop a small BLE network using two BLE dust sensor nodes built on two Xenon nRF52840 Feather boards. I will also be expanding on the two different BLE API design approaches (flat-file vs hierarchical structures) and I will also be using two different dust sensors which different interface protocols - one dust sensor uses I2C and the other uses pulse counts.

image

Finally I will demonstrate how this can all be connected together using an Android app on a tablet, which also acts as a bridge to Google spreadsheets for data collection.

A BLE Feather Dust Sensor Node using I2C Interface

I’ll start with the I2C Interface and a single-file BLE library option using ble_app2.h (a copy of this header file can be found on my GitHub repository: https://github.com/Gerriko/mbedOS6_BLE_DustSensorNetwork).

The dust sensor I’m using for this BLE node is the SN-GCJA5 Laser Type Particulate Matter (PM) Sensor from Panasonic Industries. This sensor can communicate either via UART or via I2C. For this project I am using I2C.

If you feel MbedOS is a little daunting, then I also have two getting started guides for those who would like to learn how to obtain data via UART or I2C using a 3V3 Arduino board:

/products/roadtest/b/blog/posts/getting-started-with-the-panasonic-sn-gcja5-laser-type-particulate-matter-pm-sensor-using-uart-ttl

/products/roadtest/b/blog/posts/using-i2c-communication-with-the-panasonic-sn-gcja5-laser-type-particulate-matter-pm-sensor

image

For this exercise, I am using an nRF52840 Bluetooth 5.x SoC. It just so happens that I chose an old Particle.io Xenon board for this demo as it is conveniently in the Adafruit feather form factor, which fitted nicely inside my test enclosure.

Note that this code project works equally as well on a Panasonic PAN1780 Evaluation kit or the Nordic Semiconductor nRF52840-DK board.

So to start my MbedOS project I first created a library for the SN-GCJA5 PM sensor.

This library contains both a manual blocking write, wait +500us & read method, and a non-blocking write+read transfer-callback method. I did not encounter any problems using either method. 

/* mbed Microcontroller Library for 
 * Panasonic SN-GCJA5 Particular Matter Sensor
 * Copyright (c) 2022 C Gerrish (Gerrikoio)
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef I2C_SN_GCJA5_H
#define I2C_SN_GCJA5_H

#include "mbed.h"

/** The base class for the Panasonic SN-GCJA5 PM sensor driver using I2C. */

#define SNGCJA5_ADDRESS                 (0x33u)
#define SNGCJA5_ALL                     (0x00u)
#define SNGCJA5_PM1                     (0x00u)
#define SNGCJA5_PM25                    (0x04u)
#define SNGCJA5_PM10                    (0x08u)
#define SNGCJA5_CNTA                    (0x0Cu)
#define SNGCJA5_CNTB                    (0x14u)
#define SNGCJA5_REG1                    (0x0Cu)              ///< 0.3um Particle Count
#define SNGCJA5_REG2                    (0x0Eu)              ///< 0.5um Particle Count
#define SNGCJA5_REG3                    (0x10u)              ///< 1.0um Particle Count
#define SNGCJA5_REG4                    (0x14u)              ///< 2.5um Particle Count
#define SNGCJA5_REG5                    (0x16u)              ///< 5.0um Particle Count
#define SNGCJA5_REG6                    (0x18u)              ///< 7.5um Particle Count

#define SNGCJA5_STATUS                  (0x26u)
#define TIME2FIRSTREAD                  8s
#define UNSTABLECOUNTER                 (20u)

/**! Structure holding Panasonic's PM Sensor payload data **/
// Includes mass-density (μg/m 3 ) values and counts based on size of particle
typedef struct SNGCJA5_datastruct {
  uint32_t 
      pm10_mdv,             ///< Standard PM1.0
      pm25_mdv,             ///< Standard PM2.5
      pm100_mdv;            ///< Standard PM10.0
  uint16_t 
      reg1_pc,              ///< 0.3um Particle Count
      reg2_pc,              ///< 0.5um Particle Count
      reg3_pc,              ///< 1.0um Particle Count
      reg4_pc,              ///< 2.5um Particle Count
      reg5_pc,              ///< 5.0um Particle Count
      reg6_pc;              ///< 7.5um Particle Count
} PM_MDVPC_Data;



class Panasonic_SNGCJA5
{
public:
	Panasonic_SNGCJA5(I2C &i2c, uint8_t i2cAddress):
    _i2c(i2c), _i2cAddress(i2cAddress << 1) {};

    ~Panasonic_SNGCJA5();

    /* Manual write-read method, followig the SN-GCJA5 Communication spec
     * Writes to specified register, waits 600us then reads data buffer 
     * Returns the read Acknowledgement response */
    
	int getData(char* reg, uint8_t *buff, uint8_t ds) {
        int readAck = 2;
        readAck = _i2c.write(_i2cAddress, reg, 1, true);
        if (readAck != 2) {                 // 2 = timeout
            wait_us(600);
            readAck = _i2c.read(_i2cAddress, (char*)buff, ds, false);

        }
        return readAck;
    }

    /* Non-blocking transfer method, which writes to specified registe and then reads data buffer */

    int getData_NonBlocking (char* reg, uint8_t *buff, uint8_t ds, const event_callback_t &cb) {
        int readAck = 2;            // Returns zero if the transfer has started, or -1 if I2C peripheral is busy
        // With this sensor only 1 byte is ever transferred before data is returned
        readAck = _i2c.transfer(_i2cAddress, reg, 1, (char*)buff, ds, cb);
        return readAck;
    }

    /* Some utility helpers */

    uint32_t convert4byte(uint8_t buff[4]) {
        uint32_t Val = (buff[0] | buff[1] <<8 | buff[2] <<16 | buff[3] <<24);
        return Val;
    }

    uint16_t convert2byte(uint8_t buff[2]) {
        uint32_t Val = (buff[0] | buff[1] <<8);
        return Val;
    }

    uint16_t calcAveCount(uint8_t buff[6]) {
        uint8_t cntArray[2] = {'\0'};
        uint16_t cntTotal = 0;
        for (uint8_t i = 0; i < 6; i+=2) {
            memcpy(cntArray, buff+i, 2);
            cntTotal += convert2byte(cntArray);
        }
        return cntTotal/3;
    }

    PM_MDVPC_Data convert2struct(uint8_t PMbuffer[26]) {
        PM_MDVPC_Data pmdata;
        pmdata.pm10_mdv = (PMbuffer[0] | PMbuffer[1]<< 8 | PMbuffer[2]<< 16 | PMbuffer[3]<< 24);
        pmdata.pm25_mdv = (PMbuffer[4] | PMbuffer[5]<< 8 | PMbuffer[6]<< 16 | PMbuffer[7]<< 24);
        pmdata.pm100_mdv = (PMbuffer[8] | PMbuffer[9]<< 8 | PMbuffer[10]<< 16 | PMbuffer[11]<< 24);
        pmdata.reg1_pc = (PMbuffer[12] | PMbuffer[13]<< 8);
        pmdata.reg2_pc = (PMbuffer[14] | PMbuffer[15]<< 8);
        pmdata.reg3_pc = (PMbuffer[16] | PMbuffer[17]<< 8);
        //gap
        pmdata.reg4_pc = (PMbuffer[20] | PMbuffer[21]<< 8);
        pmdata.reg5_pc = (PMbuffer[22] | PMbuffer[23]<< 8);
        pmdata.reg6_pc = (PMbuffer[24] | PMbuffer[25]<< 8);

        return pmdata;
    }
	
    
protected:
	// the memory buffer for the sensor
    I2C &_i2c;
    uint8_t _i2cAddress;
};

#endif // I2C_SN_GCJA5_H

Here’s a short demo using the non-blocking transfer-callback method.

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

For the BLE interface, I used my ble_app2 library (see my GitHub repository: https://github.com/Gerriko/mbedOS6_BLE_DustSensorNetwork).

As I was planning to have a BLE Dust Sensor Node network I decided to create a single Dust Sensor Gatt Service UUID with just 2 characteristics, which could then be used by all the nodes. The first characteristic would contain an array of dust count values and the second characteristic would hold the sample interval value. For the purposes of commonality between the dust sensor nodes, only two Particulate Matter counts will be recorded. This is due to the limitations of the other node used in this project (see next section), which can only distinguish between two categories, namely dust < 2.5um and dust >= 2.5um.

There’s also an option to allow for the inclusion of Particulate Mass Density values and the dust sensor status value in the code. Note that this option is only relevant to the SN-GCJA5 PM sensor.

// The Gatt Service UUID which is also used to advertise the service
const char *GATTSERVICE_UUID = "20220214-1313-1313-1313-f8f381aa84ed";
// PM Count and PM Interval Characteristics
const char *PMCOUNTCHAR_UUID = "20220214-1515-1515-1515-f8f381aa84ed";
const char *PMINTERVALCHAR_UUID = "20220214-1616-1616-1616-f8f381aa84ed";
//const char *PMSenseApp::PMDENSITYCHAR_UUID = "20220214-1414-1414-1414-f8f381aa84ed";
//const char *PMSenseApp::PMSTATUSCHAR_UUID = "20220214-1717-1717-1717-f8f381aa84ed";

As I’m using two custom 128-bit BLE characteristics, I decided to add in two standard attributes, which will tell the user what these characteristics are (using 0x2901: standard attribute UUID containing user description) and the format of the data (using 0x2904: standard attribute containing presentation format).

An example of how this is done is shown here:

uint8_t PMCOUNTCHAR_DESCR[28] = "+0.5um and +2.5um PM Counts";
GattAttribute *pmcount_descriptor_attribute = new GattAttribute(
PMSENSE_ATTRI_2901, // attribute type
PMCOUNTCHAR_DESCR, // descriptor
28, // length of the buffer containing the value
32, // max length
true // variable length
);

// Interval Presentation Format: 0x1B: opaque structure; 0x00: no exponent; 0x27B5: unit = concentration(count per m3); 0x01: Bluetooth SIG namespace; 0x0000: No description
uint8_t PMCOUNT_PRESENTFORMAT_STR[7] = {0x1B, 0x00, 0xB5, 0x27, 0x01, 0x00, 0x00};
GattAttribute *pmcount_presentformat_attribute = new GattAttribute(
PMSENSE_ATTRI_2904, // attribute type
PMCOUNT_PRESENTFORMAT_STR, // descriptor
7, // length of the buffer containing the value
7, // max length
true // variable length
);
GattAttribute *pmcount_descriptors[] = {pmcount_descriptor_attribute, pmcount_presentformat_attribute};


Then as I will be using two different sensor devices, I decided to also include another standard Gatt service 0x180A, which allows you to provide the user with device information. Here I used another header file called “DeviceInformation.h” which is available on GitHub: https://github.com/ARMmbed/mbed-os-experimental-ble-services

The challenge I had with using ble_app.h, was to determine where to instantiate this DeviceInformation library class in my app code as this library required a reference to the BLE instance and this was created inside the library’s BLE_APP class rather than inside the app code itself. Thankfully I had a callback function (bleApp_InitCompletehandler), which was linked to app.start() that provided a reference to this BLE instance. As such, all I had to do was move the BLE services setup for the app to this function and it worked great. Here's the code for the bleApp_InitCompletehandler function:

void bleApp_InitCompletehandler(BLE &ble, events::EventQueue &_event)
{

    /* Declare our device name - note that the ble_app library does not 
       automatically shorten full names if too long.
    */
    const char *DEVICE_NAME =         "PMsense";

    // The Gatt Service UUID which is also used to advertise the service
    const char *GATTSERVICE_UUID =    "20220214-1313-1313-1313-f8f381aa84ed";
    // PM Count and PM Interval Characteristics
    const char *PMCOUNTCHAR_UUID =     "20220214-1515-1515-1515-f8f381aa84ed";
    const char *PMINTERVALCHAR_UUID =  "20220214-1616-1616-1616-f8f381aa84ed";
    //const char *PMSenseApp::PMDENSITYCHAR_UUID =        "20220214-1414-1414-1414-f8f381aa84ed";
    //const char *PMSenseApp::PMSTATUSCHAR_UUID =        "20220214-1717-1717-1717-f8f381aa84ed";

    UUID PMSENSE_ATTRI_2901 = 0x2901;                       // attribute UUID containing user description
    UUID PMSENSE_ATTRI_2904 = 0x2904;                       // attribute UUID containing presentation format

    /* setup the default phy used in connection to 2M to reduce power consumption */
    if (ble.gap().isFeatureSupported(ble::controller_supported_features_t::LE_2M_PHY)) {
        ble::phy_set_t phys(/* 1M */ false, /* 2M */ true, /* coded */ false);
        ble_error_t error = ble.gap().setPreferredPhys(/* tx */&phys, /* rx */&phys);
        
        /* PHY 2M communication will only take place if both peers support it */
        if (error) {
            print_error(error, "GAP::setPreferedPhys failed\r\n");
        }
        else {
            printf("using 2M PHY\r\n");
            fflush(stdout);           // Just for serial output
        }
    } else {
        /* otherwise it will use 1M by default */
        printf("2M not supported. Sticking with 1M PHY\r\n");
    }

    // Add in new service
    printf("Adding Device Information Service\r\n");
    fflush(stdout);           // Just for serial output
    // Start by adding our Device Information data
    DeviceInformationService::add_service(
        ble,
        "Panasonic", 
        "SN-GCJA5",
        "0000",
        "18P:2021-08-23",
        "nRF52840 v0.01",
        nullptr,
        nullptr,
        nullptr,
        nullptr
    );

    // Add in new service
    printf("Adding PM Sense Service\r\n");
    fflush(stdout);           // Just for serial output

    uint8_t PMCOUNTCHAR_DESCR[28] = "+0.5um and +2.5um PM Counts";
    GattAttribute *pmcount_descriptor_attribute  = new GattAttribute( 
                                PMSENSE_ATTRI_2901, // attribute type
                                PMCOUNTCHAR_DESCR,           // descriptor 
                                28,           // length of the buffer containing the value
                                32,         // max length
                                true // variable length
                            );

    // Interval Presentation Format: 0x1B: opaque structure; 0x00: no exponent; 0x27B5: unit = concentration(count per m3); 0x01: Bluetooth SIG namespace; 0x0000: No description
    uint8_t PMCOUNT_PRESENTFORMAT_STR[7] = {0x1B, 0x00, 0xB5, 0x27, 0x01, 0x00, 0x00};
    GattAttribute *pmcount_presentformat_attribute = new GattAttribute( 
                                PMSENSE_ATTRI_2904, // attribute type
                                PMCOUNT_PRESENTFORMAT_STR,           // descriptor 
                                7,           // length of the buffer containing the value
                                7,         // max length
                                true // variable length
                            );
    GattAttribute *pmcount_descriptors[] = {pmcount_descriptor_attribute, pmcount_presentformat_attribute};


    uint8_t PMINTERVALCHAR_DESCR[28] = "Update Interval (>= 10 sec)";
    GattAttribute *pminterval_descriptor_attribute = new GattAttribute( 
                                PMSENSE_ATTRI_2901, // attribute type
                                PMINTERVALCHAR_DESCR,           // descriptor 
                                28,           // length of the buffer containing the value
                                32,         // max length
                                true // variable length
                            );

    // Interval Presentation Format: 0x04: unsigned 8 bit integer; 0x00: no exponent; 0x2703: unit = time(second); 0x01: Bluetooth SIG namespace; 0x0000: No description
    uint8_t PMINTERVAL_PRESENTFORMAT_STR[7] = {0x04, 0x00, 0x03, 0x27, 0x01, 0x00, 0x00};
    GattAttribute *pminterval_presentformat_attribute = new GattAttribute( 
                                PMSENSE_ATTRI_2904, // attribute type
                                PMINTERVAL_PRESENTFORMAT_STR,           // descriptor 
                                7,           // length of the buffer containing the value
                                7,         // max length
                                true // variable length
                            );
    GattAttribute *pminterval_descriptors[] = {pminterval_descriptor_attribute, pminterval_presentformat_attribute};

    // Create our Gatt Service Profile
    // For PM Count Characteristic, we add in an additional notification property and our descriptors
    ReadOnlyArrayGattCharacteristic<uint16_t,ARRSIZE> pmcount_characteristic(UUID(PMCOUNTCHAR_UUID), pmcountchar_values, 
                                        GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY, pmcount_descriptors, 2);
    
    ReadWriteGattCharacteristic<uint8_t> pminterval_characteristic(UUID(PMINTERVALCHAR_UUID), &interval_value, 
                                        GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NONE, pminterval_descriptors, 2);

    GattCharacteristic *charTable[] = { &pmcount_characteristic, & pminterval_characteristic };
    GattService BLS_GattService(UUID(GATTSERVICE_UUID), charTable, sizeof(charTable) / sizeof(charTable[0]));
    
    // We now add in our button & led service
    app.add_new_gatt_service(BLS_GattService);

    pmcount_handle = pmcount_characteristic.getValueHandle();
    pminterval_handle = pminterval_characteristic.getValueHandle();
    printf("PM Count Charactertistic handle: %u\r\n", pmcount_handle);
    printf("PM Interval Charactertistic handle: %u\r\n", pminterval_handle);
    fflush(stdout);           // Just for serial output

    // Set up advertising information
    app.set_GattUUID_128(GATTSERVICE_UUID);

    app.set_advertising_name(DEVICE_NAME);

}

The rest of the app was then pretty much as before. All I needed was to periodically request dust measurements from the sensor. This was done using a call_every function when the Connection Event callback function was triggered, as follows:

void bleApp_Connectionhandler(BLE &ble, events::EventQueue &event, const ble::ConnectionCompleteEvent &params)
{
LED_Blink.detach();
ble_led = 1;

connectionhandle = params.getConnectionHandle();

printf("Now connected to: ");
print_address(params.getPeerAddress());
printf("Connection handle %u.\r\n", connectionhandle);

// Initialise a event call every 1 second to retrieve data from the panasonic PM sensor (spec says data updated every 1 second)
PMSenseEventNo = event.call_every(1000ms, &PMSense_tickerhandler);
}

This periodic call_every request is then cancelled using the event handler when the BLE device disconnects.

void bleApp_Disconnectionhandler(BLE &ble, events::EventQueue &event, const ble::DisconnectionCompleteEvent &params)
{
printf("Disconnection event. Handle %u\r\n", params.getConnectionHandle());
// Detach ticker
event.cancel(PMSenseEventNo);
LED_Blink.attach(LED_Blinkhandler, 1s);
}

And that’s basically it.

Here is a video showing the serial monitor output and the mobile phone screen capture of the nRF Connect app, which is used to check that it all works as desired.

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

So far so good. I was now ready to build the next sensor node.

A BLE Feather Dust Sensor Node using pulse counts

The second dust sensor node uses a B5W-LD0101-1/2 Air Quality Sensor from Omron.

According to the Omron product page(https://components.omron.com/us-en/products/sensors/B5W-LD0101-1-2) this sensor will detect particles down to 0.5 μm in diameter while using an LED light source. It is worth noting for future reference that this sensor will be discontinued with last orders on 31 March 2022.

image

The 5W-LD0101 provides two pulse outputs. One is for 0.5 μm or larger and the other is for 2.5 μm or larger. According to the data sheet, the number of pulses counted within the measurement time can be taken to be the sensor count. The minimum pulse width is 0.5 msec and it recommends using a sampling frequency of 4 kHz or more. For my application, I decided to use external interrupt triggers rather than polling at 4kHz.

Here is the pulse counting code:

/* Code for Mbed OS 6
 * Copyright (c) 2022 C Gerrish (Gerrikoio)
 * SPDX-License-Identifier: Apache-2.0
 */

#include "mbed.h"


// Xenon Pin Map - Digital Pins differ
// Xenon Pin Map - the Analog pins match up
#define XEN_D2      p33
#define XEN_D3      p34
#define XEN_D4      p40
#define XEN_D5      p42
#define XEN_D6      p43
#define XEN_D7      p44
#define XEN_D8      p35


InterruptIn pin_Vout1(XEN_D2);
InterruptIn pin_Vout2(XEN_D3);
// We create our own user LED
DigitalOut user_led(XEN_D7, 0);
DigitalOut pin_Vth(XEN_D4, 0);		// Not used in final application

volatile uint16_t counts_vout1 = 0;
volatile uint16_t counts_vout2 = 0;

uint16_t total_vout1 = 0;
uint16_t total_vout2 = 0;


Ticker B5W_sample;

EventQueue queue(32 * EVENTS_EVENT_SIZE);
Thread t;

void print_handler(uint16_t PN1_count, uint16_t PN2_count);

Event<void(uint16_t, uint16_t)> Print_event(&queue, print_handler);

void PN1_PulseCount_handler()
{
    counts_vout1++;
}

void PN2_PulseCount_handler()
{
    counts_vout2++;
    
}

void DustSample_Calculater()
{
    B5W_sample.detach();
    pin_Vout1.disable_irq();
    pin_Vout2.disable_irq();

    total_vout1 = counts_vout1;
    total_vout2 = counts_vout2;

    counts_vout1 = 0;
    counts_vout2 = 0;

    user_led = !user_led;

    queue.call(Print_event, total_vout1,total_vout2);

}

void print_handler(uint16_t PN1_count, uint16_t PN2_count)
{
    printf("PN1: %d | PN2: %d\r\n", total_vout1, total_vout2);
    printf("PN1-PN2: %d\r\n", (total_vout1-total_vout2));

    B5W_sample.attach(&DustSample_Calculater, 2s);
    pin_Vout1.rise(&PN1_PulseCount_handler);
    pin_Vout2.rise(&PN2_PulseCount_handler);

    return;
}

void sampling_start()
{
    printf("done\r\n");
    
    user_led.write(0);
    pin_Vth.write(1);					// This was an option to turn off Vth if needed (not used)

    B5W_sample.attach(&DustSample_Calculater, 2s);
    pin_Vout1.rise(&PN1_PulseCount_handler);
    pin_Vout2.rise(&PN2_PulseCount_handler);

}

int main()
{
   // Start the event queue
    t.start(callback(&queue, &EventQueue::dispatch_forever));

    printf("\r\nB5W-LD0101 pulse-interrupt UART output demo!\r\n");
    printf("Sensor sampled at 2 second interval.\r\n");
    printf("Waiting 8 seconds to warm up...");
    fflush(stdout);

    queue.call_in(8s, &sampling_start);

}

Also worth noting that unlike the SN-GCJA5, this sensor communicates at 5V rather than 3V3 logic. Hence a logic shifter is used to interface with the nRF52840 MCU.

Here is a short test video of the UART output from periodic pulse count measurements using interrupt triggers on v1 and v2.

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

For the BLE app development part I decided to use two different library files, ble_process.h and gatt_server_process.h, instead of ble_app.h. These files are available on ARM Mbed’s GitHub repository: https://github.com/ARMmbed/mbed-os-ble-utils

/* Code for Mbed OS 6.x
 * Copyright (c) 2022 C Gerrish (Gerrikoio)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "mbed.h"
#include "platform/Callback.h"
#include <cstdio>
#include <events/mbed_events.h>

#include "ble/BLE.h"
#include "gatt_server_process.h"
#include "DeviceInformationService.h"
#include "mbed-trace/mbed_trace.h"

#define TIME2FIRSTREAD  10s

// Xenon Pin Map - Digital Pins differ
// Xenon Pin Map - the Analog pins match up
#define XEN_D2      p33
#define XEN_D3      p34
#define XEN_D4      p40
#define XEN_D5      p42
#define XEN_D6      p43
#define XEN_D7      p44
#define XEN_D8      p35


InterruptIn pin_Vout1(XEN_D2);
InterruptIn pin_Vout2(XEN_D3);

// We create our own user LED
DigitalOut user_led(XEN_D7, 0);

// Global declarations

volatile uint16_t counts_vout1 = 0;
volatile uint16_t counts_vout2 = 0;

using mbed::callback;
using namespace std::literals::chrono_literals;

static EventQueue event_queue(/* event count */ 16 * EVENTS_EVENT_SIZE);


void PN1_PulseCount_handler()
{
    if (counts_vout1 < 0xFFFF) counts_vout1++;
}

void PN2_PulseCount_handler()
{
    if (counts_vout2 < 0xFFFF) counts_vout2++;
    
}

class PMSenseApp : ble::GattServer::EventHandler {

public:

    PMSenseApp() 
    {    
    }

    ~PMSenseApp()
    {
    }

    void start(BLE &ble, events::EventQueue &event_queue)
    {
        _server = &ble.gattServer();
        _event_queue = &event_queue;

        // Start by adding our Device Information data
        DeviceInformationService::add_service(
            ble,
            "Omron",
            "B5W-LD0101",
            "0142",
            "0798C1",
            "nRF52840 v0.01",
            nullptr,
            nullptr,
            nullptr,
            nullptr
        );

        UUID PMServiceuuid = GattServerProcess::GATTSERVICE_UUID;

        UUID PMSENSE_ATTRI_2901 = 0x2901;                       // attribute UUID containing user description
        UUID PMSENSE_ATTRI_2904 = 0x2904;                       // attribute UUID containing presentation format

        //UUID PMDensityuuid = PMDENSITYCHAR_UUID;
        UUID PMCountuuid = PMCOUNTCHAR_UUID;
        UUID PMIntervaluuid = PMINTERVALCHAR_UUID;

        uint8_t PMCOUNTCHAR_DESCR[28] = "+0.5um and +2.5um PM Counts";
        GattAttribute *_pmcount_descriptor_attribute  = new GattAttribute( 
                                    PMSENSE_ATTRI_2901, // attribute type
                                    PMCOUNTCHAR_DESCR,           // descriptor 
                                    28,           // length of the buffer containing the value
                                    32,         // max length
                                    true // variable length
                                );

        // Interval Presentation Format: 0x1B: opaque structure; 0x00: no exponent; 0x27B5: unit = concentration(count per m3); 0x01: Bluetooth SIG namespace; 0x0000: No description
        uint8_t PMCOUNT_PRESENTFORMAT_STR[7] = {0x1B, 0x00, 0xB5, 0x27, 0x01, 0x00, 0x00};
        GattAttribute *_pmcount_presentformat_attribute = new GattAttribute( 
                                    PMSENSE_ATTRI_2904, // attribute type
                                    PMCOUNT_PRESENTFORMAT_STR,           // descriptor 
                                    7,           // length of the buffer containing the value
                                    7,         // max length
                                    true // variable length
                                );
        GattAttribute *_pmcount_descriptors[] = {_pmcount_descriptor_attribute, _pmcount_presentformat_attribute};

        _pmcount_characteristic = new ReadNotifyCharacteristic<uint16_t,_ARRSIZE> (PMCountuuid, _pmcountchar_values, _pmcount_descriptors, 2);
        if (!_pmcount_characteristic) {
            printf("Allocation of ReadNotifyCharacteristic for pmcount failed\r\n");
        }


        uint8_t PMINTERVALCHAR_DESCR[28] = "Update Interval (>= 10 sec)";
        GattAttribute *_pminterval_descriptor_attribute = new GattAttribute( 
                                    PMSENSE_ATTRI_2901, // attribute type
                                    PMINTERVALCHAR_DESCR,           // descriptor 
                                    28,           // length of the buffer containing the value
                                    32,         // max length
                                    true // variable length
                                );

        // Interval Presentation Format: 0x04: unsigned 8 bit integer; 0x00: no exponent; 0x2703: unit = time(second); 0x01: Bluetooth SIG namespace; 0x0000: No description
        uint8_t PMINTERVAL_PRESENTFORMAT_STR[7] = {0x04, 0x00, 0x03, 0x27, 0x01, 0x00, 0x00};
        GattAttribute *_pminterval_presentformat_attribute = new GattAttribute( 
                                    PMSENSE_ATTRI_2904, // attribute type
                                    PMINTERVAL_PRESENTFORMAT_STR,           // descriptor 
                                    7,           // length of the buffer containing the value
                                    7,         // max length
                                    true // variable length
                                );
        GattAttribute *_pminterval_descriptors[] = {_pminterval_descriptor_attribute, _pminterval_presentformat_attribute};

        _pminterval_characteristic = new ReadWriteGattCharacteristic<uint8_t> (PMIntervaluuid, &_interval_value, 0, _pminterval_descriptors, 2);
        if (!_pminterval_characteristic) {
            printf("Allocation of ReadWriteGattCharacteristic for pminterval failed\r\n");
        }

        GattCharacteristic* charTable[] = { _pmcount_characteristic, _pminterval_characteristic };

        GattService PMgattprofile(PMServiceuuid, charTable, 2);

        ble.gattServer().addService(PMgattprofile);

        _pmcount_handle = _pmcount_characteristic->getValueHandle();
        _pminterval_handle = _pminterval_characteristic->getValueHandle();
        printf("PM Count Charactertistic handle: %u\r\n", _pmcount_handle);
        printf("PM Interval Charactertistic handle: %u\r\n", _pminterval_handle);

        // add the service

        ble.gattServer().setEventHandler(this);

    }

    void onConnections_handler(BLE &ble, events::EventQueue &event_queue, const ble::ConnectionCompleteEvent &event)
    {

        printf("Attaching interrupts for PM measurement.\r\n");
 
        UpdateEventQueueID = _event_queue->call_every(_interval_value*1000ms, callback(this, &PMSenseApp::increment_PMdata));

        pin_Vout1.rise(&PN1_PulseCount_handler);
        pin_Vout2.rise(&PN2_PulseCount_handler);

    }

    void onDisconnect_handler(BLE &ble, events::EventQueue &event_queue, const ble::DisconnectionCompleteEvent &event)
    {

        _event_queue->cancel(UpdateEventQueueID);

        pin_Vout1.disable_irq();
        pin_Vout2.disable_irq();

        user_led = 1;               // This is off

        printf("Interrupts for PM measurement detached.\r\n");
    }

private:


    /**
     * Update the sample data within BLE stack.
     */
    void increment_PMdata(void)
    {
        
        pin_Vout1.disable_irq();
        pin_Vout2.disable_irq();

        uint16_t smplvals[_ARRSIZE] = {0};

        smplvals[0] = counts_vout1 - counts_vout2;
        smplvals[1] = counts_vout2;

        counts_vout1 = 0;
        counts_vout2 = 0;

        user_led = !user_led;

        ble_error_t err = _pmcount_characteristic->set(*_server, smplvals, _ARRSIZE);
        if (err) {
            printf("write of the PMcount value returned error %u\r\n", err);
            return;
        }

        pin_Vout1.rise(&PN1_PulseCount_handler);
        pin_Vout2.rise(&PN2_PulseCount_handler);

    }
    
    /**
     * Handler called when a notification or an indication has been sent.
     */
    void onDataSent(const GattDataSentCallbackParams &params) override
    {
        printf("PM count update\r\n");
        fflush(stdout);
    }

    /**
     * This callback allows the IntervalService to receive updates to the ledState Characteristic.
     *
     * @param[in] params Information about the characterisitc being updated.
     */
    void onDataWritten(const GattWriteCallbackParams &params) override
    {
        if (_pminterval_handle == params.handle) {
            _interval_value = params.data[0];
            printf("Update Interval changed to %u seconds\r\n", _interval_value);
        }
    }

   /**
     * Handler called after an attribute has been read.
     */
    void onDataRead(const GattReadCallbackParams &params) override
    {
        printf("Read Event for handle %u.\r\n", params.handle);
        if (params.handle == _pminterval_handle) printf("Sample Interval characteristic data read: %u\r\n", params.data[0]);
        else if (params.handle == _pmcount_handle) printf("PM Count characteristic data read: %u %u\r\n", params.data[0], params.data[2]);
    }

    /**
     * Handler called after a client has subscribed to notification or indication.
     *
     * @param handle Handle of the characteristic value affected by the change.
     */
    void onUpdatesEnabled(const GattUpdatesEnabledCallbackParams &params) override
    {
        printf("update enabled on handle %d\r\n", params.attHandle);
    }

    /**
     * Handler called after a client has cancelled his subscription from
     * notification or indication.
     *
     * @param handle Handle of the characteristic value affected by the change.
     */
    void onUpdatesDisabled(const GattUpdatesDisabledCallbackParams &params) override
    {
        printf("update disabled on handle %d\r\n", params.attHandle);
    }

private:
    /**
     * Read, Notify Characteristic declaration helper.
     *
     * @tparam T type of data held by the characteristic.
     */
    template<typename T, unsigned NUM_ELEMENTS>
    class ReadNotifyCharacteristic : public GattCharacteristic {
    public:
        /**
         * Construct a characteristic that can be read or emit notification.
         *
         * @param[in] uuid The UUID of the characteristic.
         * @param[in] initial_value Initial value contained by the characteristic.
         * @param[in] valuePtr Pointer to an array of length NUM_ELEMENTS containing
         * the characteristic's initial value. The pointer is reinterpreted as a
         * pointer to an uint8_t buffer.
        */
        ReadNotifyCharacteristic<T, NUM_ELEMENTS>(
            const UUID &uuid,
            T valuePtr[NUM_ELEMENTS],
            GattAttribute *descriptors[] = nullptr,
            unsigned numDescriptors = 0
        ) : GattCharacteristic (
            uuid,
            reinterpret_cast<uint8_t *>(valuePtr),
            sizeof(T) * NUM_ELEMENTS,
            sizeof(T) * NUM_ELEMENTS,
            BLE_GATT_CHAR_PROPERTIES_READ | BLE_GATT_CHAR_PROPERTIES_NOTIFY,
            descriptors,
            numDescriptors,
            false   /* variable len */ 
            )
        {
        }

        /**
         * Get the value of this characteristic.
         *
         * @param[in] server GattServer instance that contain the characteristic
         * value.
         * @param[in] dst Variable that will receive the characteristic value.
         *
         * @return BLE_ERROR_NONE in case of success or an appropriate error code.
         */
        ble_error_t get(GattServer &server, T& dst) const
        {
            uint16_t value_length = NUM_ELEMENTS;
            return server.read(getValueHandle(), &dst, &value_length);
        }

        /**
         * Assign a new value to this characteristic.
         *
         * @param[in] server GattServer instance that will receive the new value.
         * @param[in] value The new value to set.
         * @param[in] local_only Flag that determine if the change should be kept
         * locally or forwarded to subscribed clients.
         */
        ble_error_t set(GattServer &server, uint16_t *value, uint16_t size, bool local_only = false) const
        {
            // For this template, we're using MSB-LSB
            uint8_t u8vals[size*2];
            for (uint8_t i = 0; i < size; i++) {
                u8vals[i*2] = value[i] >> 8;
                u8vals[(i*2)+1] = value[i];
            }

            return server.write(getValueHandle(), u8vals, size*2, local_only);
        }

    private:
        uint8_t _value;
    };
    
    private:
        //static const char *PMDENSITYCHAR_UUID;
        static const char *PMCOUNTCHAR_UUID;
        static const char *PMINTERVALCHAR_UUID;

        static const uint8_t _ARRSIZE = 2;

        GattServer *_server = nullptr;
        events::EventQueue *_event_queue = nullptr;
        int UpdateEventQueueID = 0;

        ReadNotifyCharacteristic<uint16_t, _ARRSIZE> *_pmcount_characteristic = nullptr;
        ReadWriteGattCharacteristic<uint8_t> *_pminterval_characteristic = nullptr;

        uint16_t _pmcountchar_values[_ARRSIZE] = {0x00};
        uint8_t _interval_value = 0x0A;           //10sec

        GattAttribute::Handle_t _pmcount_handle = 0;
        GattAttribute::Handle_t _pminterval_handle = 0;
        uint8_t connectionhandle = 0;

};

// We declare these after the class definition
const char *GattServerProcess::DEVICE_NAME =        "PMsense";
const char *GattServerProcess::GATTSERVICE_UUID =   "20220214-1313-1313-1313-f8f381aa84ed";

const char *PMSenseApp::PMCOUNTCHAR_UUID =          "20220214-1515-1515-1515-f8f381aa84ed";
const char *PMSenseApp::PMINTERVALCHAR_UUID =       "20220214-1616-1616-1616-f8f381aa84ed";


int main()
{

    printf("\r\nOmron B5W-LD0101 Particulate Matter Sensing BLE Application\r\n");
    printf("Monitoring 0.5um to 2.5um and +2.5um counts\r\n");

    //mbed_trace_init();

    BLE &ble = BLE::Instance();

    user_led = 0;               // This is off

    // sleep for 10 seconds to allow for sensor to warmup
    printf("Waiting for PM Sensor to warm up (takes 8 seconds)...");
    fflush(stdout);           // Just for serial output
    ThisThread::sleep_for(TIME2FIRSTREAD);
    printf("done!\r\n");
    fflush(stdout);           // Just for serial output
    user_led = 1;               // This is on

    PMSenseApp demo;

    /* this process will handle basic setup and advertising for us */
    GattServerProcess ble_process(event_queue, ble);

    /* once it's done it will let us continue with our demo*/
    ble_process.on_init(callback(&demo, &PMSenseApp::start));

    /* this is triggered when central client connects with our demo*/
    ble_process.on_connect(callback(&demo, &PMSenseApp::onConnections_handler));

    /* this is triggered when central client disconnects from our demo*/
    ble_process.on_disconnect(callback(&demo, &PMSenseApp::onDisconnect_handler));

    ble_process.start();

    return 0;
}

As before, we have an initialisation process to create our services and the code has callback functions for all the relevant BLE event triggers.

The BLE service for this sensor is exactly the same as the other sensor node, hence there is no need to add in another a video showing the serial monitor output and the mobile phone screen capture of the nRF Connect app.

A Smart Feather Duster Hub

For my central hub, I decided to use MIT APP Inventor, as I could quickly and easily copy and paste sections from my other app projects.

The hub design was a bit of a stretch for me to get the logic working as it used 3 BLE instances. Two of the BLE instances were allocated to the sensor nodes and the 3rd was to allow the hub to use active battery monitoring and control for the tablet itself. This allowed the tablet to be powered through USB via a separate BLE charger controller.

image

As MIT App Inventor is a no-code solution, here is a block diagram of the whole application:

image

And here is a short video showing how the different BLE devices will connect to the hub.

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

The hub essentially acts as a bridge to Google docs where data is stored in a spreadsheet. The data validation and splitting the data into two sheets is all controlled through a Google Apps Script.

function doPost(e) { 
  var ss = SpreadsheetApp.openByUrl("https://docs.google.com/spreadsheets/d/####### ADD YOUR REFERENCE HERE ###################");
  var sheet1 = ss.getSheetByName("Sheet1_name");
  var sheet2 = ss.getSheetByName("Sheet2_name");
  
  addBLEData(e,sheet1,sheet2);
}

function addBLEData(e,sheet1,sheet2) {
  var TimeStamp = new Date();

  var DataType = e.parameter.DataType;

  if (DataType == 1) {
    var Cntr = e.parameter.Cntr;
    var Type = e.parameter.Type;
    var Status = e.parameter.Status;
    var Voltage = e.parameter.Voltage;
    var Level = e.parameter.Level;
    var Temperature = e.parameter.Temperature;
    var UUID = e.parameter.UUID;

    sheet1.appendRow([TimeStamp,Cntr,Type,Status,Voltage,Level,Temperature,UUID]);

  }
  else if (DataType == 2) {
    var Cntr = e.parameter.Cntr;
    var Sensor = e.parameter.Sensor;
    var PC05 = e.parameter.PC05;
    var PC25 = e.parameter.PC25;

    sheet2.appendRow([TimeStamp,Cntr,Sensor,PC05,PC25]);
  }
}

The data will then automatically update. Here's the dust sensor data appearing in the spreadsheet (battery voltage data is sent to another sheet).

image

But the whole point of pushing data to the cloud is to allow for data analysis. So let's use the devices and see what happens.

Testing the BLE Feather Dusters

For my test I decided to place my sensors close to the carpet to see what dust levels there are. The height of the sensor from the floor was approximate 30cm or 12" away. I was always curious to know how good or bad a vacuum cleaner was. Unfortunately the really bad one (was won in a raffle) was binned last year so all I had was a trusty Dyson, which tends to do a very good job when the filters are clean etc.

{gallery:autoplay=false}Particulate Matter Sensor Apparatus

image

image

image

So here are the results from a couple of hours of automated data logging.

First I will start with the non-related battery monitoring. Basically, the smart dust hub app monitors charge percentage and allows charging when battery is below 50% and stops charging when battery charge reaches 55% (these values can be altered by the user). Battery charging will also stop if the battery temperature exceeds 28 degrees. The app then sends the changeover charge/discharge & discharge/charge events to the Google cloud.

This chart plots the battery voltage and temperature over time.

image

Now onto the dust data. Unfortunately, I did not have a means to calibrate the two sensors so I have no idea which absolute value is correct. What the data does show is agreement on the degree of change, especially with smaller particles so I am quite pleased with the test results.

image

image

  • Sign in to reply

Top Comments

  • shabaz
    shabaz over 3 years ago in reply to shabaz +1
    Well, the first project I tried works! I'm sure there may be issues with some of the projects I've migrated over, but it's a good start.
  • BigG
    BigG over 3 years ago in reply to cstanton

    Thanks for positve comment cstanton

    Who needs to wait until hayfever season. You should see my workspace sometimes GrimacingSmiley Thankfully I'm now getting frequent reminders to clean up.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • BigG
    BigG over 3 years ago in reply to shabaz

    Oh wow. Nice one. Yes it is a new feature but I haven't tried it yet. I still rather like Mbed Studio TBH. It's a little quirky but it works perfectly for my needs.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • shabaz
    shabaz over 3 years ago in reply to shabaz

    Well, the first project I tried works! I'm sure there may be issues with some of the projects I've migrated over, but it's a good start.

    image

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • shabaz
    shabaz over 3 years ago

    Hi Colin,

    I guess you've already experienced this.. it was new to me today! I quite liked the original basic online compiler they had. 

    Currently importing all my projects into the new environment.. I had 80 projects online (I have a few with local compiler, but I like the online option usually).

    image

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • ralphjy
    ralphjy over 3 years ago in reply to BigG

    Would be nice to put rooms where I don't have a HEPA filter or maybe just to check to see how it compares with the sensors on the filters that I have.  We've had a lot of problems with wildfire smoke the past few years.

    Too bad it doesn't have any connectivity (WiFi or BLE) but maybe that's an easy hack.  I saw that reviews complain about not including the USB-C cable or adapter but I guess most of us have those...

    Thanks for pointing this out.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
>
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2025 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

ICP 备案号 10220084.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube