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.
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:
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.
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.
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:
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:
This periodic call_every request is then cancelled using the event handler when the BLE device disconnects.
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.
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.
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.
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 ¶ms) 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 ¶ms) 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 ¶ms) 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 ¶ms) 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 ¶ms) 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.
As MIT App Inventor is a no-code solution, here is a block diagram of the whole application:
And here is a short video showing how the different BLE devices will connect to the hub.
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).
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 |
---|
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.
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.
Top Comments