top of page

Sending Sensor Data to Smartphone over Bluetooth LE (Part 2: Hardware Design and Arduino Sketch)


In the first part of this article ("Sending Data from Multiple Sensors to Smartphone over Bluetooth LE (Part 1: Open-Source Mobile App)"), I explained how you can build a mobile app to receive data on your smartphone. In part 2, I am going to share details of a hardware setup (electronics) and Arduino sketch that read sensor data and send to the smartphone over Bluetooth LE.


Electronic Hardware (Reader)

The electronic hardware used here is almost identical to the one described in article below:


You can reuse the design; however, you need to make the following changes to the circuitry to be able to use it as a Bluetooth LE device:

  1. Instead of the Adafruit M0 Basic/Adalogger, use the Adafruit Feather M0 Bluefruit LE.

  2. You do not need an OLED display, because the Arduino sketch sends sensor data to your smartphone instead of the OLED display.




Arduino Sketch

The sketch that I am going to explain here is a modified version of sketch explained in the article below:


When I was writing that sketch, my intention was to allow for reading up to four sensors simultaneously. In the code below I maintained the same structure, bit I removed some of the lines related to reading additional sensors to simplify the code a little bit. The code is capable of automatically detecting an APAS T1 rockwool moisture sensor connected to an Arduino digital pin.


If you are interested to use the sketch with the HITA E0 rockwool EC/TDS sensor, use the sketch shared in article below and add Bluetooth LE related pieces of code to it:


For your convenience, I have shared a link to the GitHub repository that holds the Arduino sketch.



Main Body

Libraries

You need to include a few main libraries in order for your code to work. These libraries are listed below:

#include <DallasTemperature.h>   // https://github.com/milesburton/Arduino-Temperature-Control-Library
#include <ArduinoUniqueID.h>     // https://github.com/ricaun/ArduinoUniqueID
#include <ADS1115_WE.h>          // https://github.com/wollewald/ADS1115_WE
#include <Adafruit_SleepyDog.h>  // https://github.com/adafruit/Adafruit_SleepyDog
#include <Wire.h>

For Bluetooth LE communications, the following libraries are needed:

// Libs for Bluetooth LE
#include "Adafruit_BLE.h"
#include "Adafruit_BluefruitLE_SPI.h"
#include "Adafruit_BluefruitLE_UART.h"
#include "BluefruitConfig.h"


Arduino Serial Number

Each Arduino board comes with a serial number (device ID), which can be used to identify specific board. This is useful specially if you are using a large number of boards in a network and/or in IoT applications. The subroutine below allows you to obtain the device serial number:


void getSerialNumber() {
  String strSN{ "" };
  for (size_t i = 0; i < UniqueIDsize; i++) {
    if (UniqueID[i] < 100)
      strSN += '0';
    if (UniqueID[i] < 10)
      strSN += '0';
    strSN += UniqueID[i];
    if (i < 15) strSN += " ";
  }

  Serial.print(F("Device ID: "));
  Serial.println(strSN);
  Serial.flush();  // Wait untill all data are sent to computer
}


Temperature

The APAS T1 sensor relies on the DS18B20 chip for temperature measurements. To simplify the code, each temp sensor is assigned to a different Arduino pin. If there are limited number of GPIO pins available, you can connect several temp sensor to one Arduino pin.


As explained above, the main body of the sketch reads only one sensor. In the lines below; however, I kept the structure of the original sketch to allow for reading more sensors (if needed).


// Temperature Sensors
static constexpr int ONE_WIRE_BUS[4]{12, 11, A4, A5};  // Assign Arduino pin to temp sensor
OneWire oneWire[4]{                                  // Setup a oneWire instance to communicate with any OneWire devices
                    // initializers for each instance in the array go here
                    OneWire(ONE_WIRE_BUS[0]),
                    OneWire(ONE_WIRE_BUS[1]),
                    OneWire(ONE_WIRE_BUS[2]),
                    OneWire(ONE_WIRE_BUS[3])
};
DallasTemperature tempSensors[4]{ // Pass our oneWire reference to Dallas Temperature sensor
                                  // initializers for each instance in the array go here
                                  DallasTemperature(&oneWire[0]),
                                  DallasTemperature(&oneWire[1]),
                                  DallasTemperature(&oneWire[2]),
                                  DallasTemperature(&oneWire[3])
};
DeviceAddress deviceAddress[4];  // Arrays to hold device address

The DS18B20 resolution can be set anywhere from 9 to 12. The resolution directly affects the conversion time. In my experience, a resolution of 11 (0.125 °C) is a good place to start. At this resolution, it takes about 375 ms for the sensor to complete the conversion and update its registers with a new temp value.


void measSensorTemp(uint8_t bytSensor) {
  // Number of milliseconds to wait till conversion is complete based on resolution: 94, 188, 375, 750 ms for 9, 10, 11, 12, respectively.
  // Thisequates  to  a  temperature  resolution  of  0.5°C,  0.25°C,  0.125°C,  or  0.0625°C.
  const uint8_t resolution{ 11 };  // Set the resolution to 11 bit
  tempSensors[bytSensor].setResolution(deviceAddress[bytSensor], resolution);
  tempSensors[bytSensor].setWaitForConversion(false);  // Request temperature conversion - non-blocking / async
  tempSensors[bytSensor].requestTemperatures();        // Takes 14 ms
  delay(375);
  // Get Sensor temp
  TpC[bytSensor] = tempSensors[bytSensor].getTempC(deviceAddress[bytSensor]);
}

Before each temperature measurement, we need to check to see whether a sensor is connected or not:


bool IsSensorConnected(uint8_t bytSensor) {
  getAddResult[bytSensor] = tempSensors[bytSensor].getAddress(deviceAddress[bytSensor], 0);  // Takes 14 ms // Check see if a sensor is connected
  return getAddResult[bytSensor];
}


Sensor ID

Every time that a new sensor is connected, the sketch obtains its 64-bit ID and sends it to the user (over Bluetooth LE to smartphone and serial port). I have not done this here, but you can potentially add this unique sensor ID to the beginning of every data string so that you can easily identify specific sensor anywhere on a network of sensors.


void getSensorID(uint8_t bytSensor) {
  if (!getAddResult[bytSensor]) return;

  uint8_t i{ 0 };
  uint8_t ID[8];
  String strID{ "" };

  // Turn Sensor on
  SensorOnFun(true, BOOSTER_PIN[bytSensor]);
  delay(20);  // Wait for the Sensor to wake up

  // Start up the library / locate devices on the bus
  tempSensors[bytSensor].begin();

  // Initiate a search for the OneWire object created and read its value into addr array we declared above
  while (oneWire[bytSensor].search(ID)) {
    // Read each uint8_t in the address array
    for (i = 0; i < 8; i++) {
      // Put each uint8_t in the ID array
      strID += ID[i];
      if (ID[i] < 10) {
        strID += "00";
      }
      if (ID[i] > 10 && ID[i] < 100) {
        strID += '0';
      }
      if (i == 1) {
        strID += ' ';
      }
      if (i == 3) {
        strID += ' ';
      }
      if (i == 5) {
        strID += ' ';
      }
    }
    // A check to make sure that what we read is correct.
    if (OneWire::crc8(ID, 7) != ID[7]) {
      // CRC is not valid!
      return;
    }
  }
  oneWire[bytSensor].reset_search();

  // Turn Sensor off
  SensorOnFun(false, BOOSTER_PIN[bytSensor]);

  // Send sensor ID to GUI
  Serial.print(F("Sensor "));
  Serial.print(bytSensor);
  Serial.print(F(" ID: "));
  Serial.println(strID);
  Serial.flush();  // Wait untill all data are sent to computer before putting Arduino into sleep

  return;
}

Sensor-Specific Calibration Constant

Research-grade APAS-T1 sensors come with a sensor code and sensor-specific calibration coefficient. This data is stored in the memory and is retrieved in a similar fashion (not included in the sketch). This data could be used to identify the type of sensor connected (rockwool moisture or EC sensor) and to improve sensor-to-sensor uniformity by applying the calibration coefficients.



Analog to Digital Convertor

Each ADS1115 chip supports up to two sensors if configured for differential inputs. To have multiple ADC boards connected to the Arduino and therefore support more than two sensors, you need to give each ADC board a separate address. To do so, you can connect the ADDR pin of one to GND (address: 0x48) and the ADDR of another one to VCC (address: 0x49).


// ADS1115 settings; 16-bit version
ADS1115_WE ads1115a(0x48);  // ADC board 1: ADDR pin connected to GND

The APAS T1 sensor analog output (rockwool moisture) ranges from ~0.5 − 1.5 V (this range is approximate). Configuring the ADS1115 for 2x gain allows it to read in the range of +/- 2.048 V (1 bit = 0.0625 mV), which is very close to what we need. We also set the ADC for a conversation rate of 860 samples per second (SPS).


// ADS1115 A to D
  Wire.begin();

  ads1115a.init();
  ads1115a.setVoltageRange_mV(ADS1115_RANGE_2048);  
  ads1115a.setConvRate(ADS1115_860_SPS);            
  

The rest of the ADC routines (below) read individual differential channels and provide a value that should fall between 0 (minimum) and 32,768 (maximum).


unsigned int avgADC(uint8_t bytSensor) {
  float avg{ measureADC(bytSensor) };  
  avg /= 2048.0;

  return { avg * 32768 };
}

float measureADC(uint8_t bytSensor) {
  float rawADCReading{ 0.0 };
  switch (bytSensor) {
    case 0:
      rawADCReading = ads1115a_readChannel(ADS1115_COMP_0_1);  
      break;
    case 1:
      rawADCReading = ads1115a_readChannel(ADS1115_COMP_2_3);  
      break;
    default:
      // if nothing else matches, do the default
      break;
  }
  return rawADCReading;
}

float ads1115a_readChannel(ADS1115_MUX channel) {
  ads1115a.setCompareChannels(channel);
  ads1115a.startSingleMeasurement();
  while (ads1115a.isBusy()) {}
  float volt{ ads1115a.getResult_mV() };  
  return volt;
}


Excitation Voltage

When it comes to connecting a sensor to the Arduino, one might not think much, and quickly connect the voltage wire/pin to the VCC of the circuitry. If you would like to save some power you can take an alternative approach. You can connect the voltage wire of the APAS T1 rockwool moisture sensor to a digital pin of the Arduino, and turn the sensor on only when we want to take measurements. Later, sensor enable pins are initialized as 'output' in the setup subroutine.


In the current sketch, I have taken an even better approach. I am turning the sensor on/off indirectly using a booster module. The Arduino controls the enable pin of the booster module, and in return, the booster module powers the sensor on/off. This way I do not have to worry about the current drawn by the sensor exceeding the maximum allowed current drawn from the Arduino GPIO pin.


// Sensors On/Off
static constexpr int BOOSTER_PIN[4]{ A0, A1, A2, A3 };  

void SensorOnFun(bool blnOn, const int& Pin) {
  delay(1);
  if (blnOn) {
    digitalWrite(Pin, HIGH);
    delay(50);  // Delay to make sure sensor is fully turned on
  } else {
    digitalWrite(Pin, LOW);
  }
}


Take Sensor Measurements

Taking sensor measurements is carried out in a number of steps as the following:

  1. Turn sensor on.

  2. Measure sensor analog output (moisture) and obtain temperature from the DS18B20 [embedded] temp sensor.

  3. Obtain sensor ID (64-bit) and sensor code (1 byte).

  4. Turn sensor off.

  5. Compensate raw soil moisture readings for temperature effect (research-grade sensors only).

  6. Calculate soil water content in %.

  7. Conduct sensor-specific calibration (research-grade sensors only)


void doSensorReadings(uint8_t bytSensor) {
  // Turn sensor on
  SensorOnFun(true, BOOSTER_PIN[bytSensor]);
  // Measure sensor analoge output voltage
  RAW[bytSensor] = readSensor(bytSensor);
  // Obtain sensor ID and sensor code
  if (blnNewSensor[bytSensor]) {
    blnNewSensor[bytSensor] = false;
    getSensorID(bytSensor);  // Get sensor ID
  }
  // Turn sensor off
  SensorOnFun(false, BOOSTER_PIN[bytSensor]);  // Turn sensor off
  if (!getAddResult[bytSensor]) {
    blnNewSensor[bytSensor] = true;
    return;  // Sensor was not connected
  }

  // Calculate water content (%)
  VWC_P[bytSensor] = calculateWaterContent(RAW[bytSensor], bytSensor);
}

As explained, before each measurement, we check to see if a sensor is connected. If a sensor was available at the port, we first measure the temperature and then read the HITA E0 analog output (EC).


unsigned int readSensor(uint8_t bytSensor) {
  if (!IsSensorConnected(bytSensor)) {  // Takes 14 m
    return 0;                           // If sensor is not connected just return.
  }
  measSensorTemp(bytSensor);                  // Measure temperature
  return {avgADC(bytSensor)};  // Takes -- ms
}


Calculate Water Content

Rockwool moisture is calculated by normalizing raw voltage measurements. This will allow us to read on a 0 (dry) to 100 (saturated) scale. The minimum and maximum values are obtained by taking raw sensor measurements in air (or for better accuracy in dry rockwool) and a rockwool cube that has been saturated in water.


static constexpr float MINRAW_WC{ 9600.0 };   
static constexpr float MAXRAW_WC{ 18000.0 };  

float calculateWaterContent(unsigned int rVWC, uint8_t bytSensor) {
  float fltDenom{ MAXRAW_WC - MINRAW_WC };
  // Set the scale to 0-100 using values obtained in the air and water.
  float pVWC = 100.0 * ((rVWC - MINRAW_WC) / fltDenom);

  return pVWC;
}

Note: Both the sensor blade (green section) and head (black) are sensitive to moisture. If you only submerge the blade you will read differently than if you submerge the whole sensor. We have calibrated the APAS T1 sensor for rockwool, in which the sensor head won't be in contact with the substrate/moisture.



BLE Module Configuration

To be able to use the Bluetooth module that comes with the Arduino (Feather) board, you need to configure it as the following:


// Bluetooth Settings
bool blnSendOnceBLE{ false };
bool bleMsgReady{ false };
String strBLENodeName{ "" };
#define FACTORYRESET_ENABLE 0  
#define MINIMUM_FIRMWARE_VERSION "0.6.6"
#define MODE_LED_BEHAVIOUR "DISABLE"  

// Confguration
void SetupBLE() {
  // Initialise the module
  if (!ble.begin(VERBOSE_MODE)) {
    error(F("Couldn't find Bluefruit, make sure it's in CoMmanD mode & check wiring?"));
  }

  if (FACTORYRESET_ENABLE) {
    // Perform a factory reset to make sure everything is in a known state
    if (!ble.factoryReset()) {
      error(F("Couldn't factory reset"));
    }
  }

  // Disable command echo from Bluefruit
  ble.echo(false);

  // Print Bluefruit information
  ble.info();

  // Broadcast device name
  BroadcastDeviceName();

  ble.verbose(false);  // debug info is a little annoying after this point!

  // LED Activity command is only supported from 0.6.6
  if (ble.isVersionAtLeast(MINIMUM_FIRMWARE_VERSION)) {
    // Change Mode LED Activity
    ble.sendCommandCheckOK("AT+HWModeLED=" MODE_LED_BEHAVIOUR);
  }

  // Set module to DATA mode
  ble.setMode(BLUEFRUIT_MODE_DATA);
}

void BroadcastDeviceName() {
  // Setting device name
  String BROADCAST_CMD;
  BROADCAST_CMD = String("AT+GAPDEVNAME=EnviTronics Lab(1)");

  char buf[60];
  BROADCAST_CMD.toCharArray(buf, 60);  //Convert the name change command to a char array
  if (!ble.sendCommandCheckOK(buf)) {
    error(F("BLE Err: Could not set device name!"));
  }
}

// A small helper
void error(const __FlashStringHelper* err) {
  Serial.println(err);
  while (1)
    ;
}

The main configuration steps are as the following:

  1. Initialize the module

  2. Perform a factory reset to make sure everything is in a known state (Adafruit's recommendation)

  3. Broadcast device name


To perform a factory rest of the module, you need to set FACTORYRESET_ENABLE to 1, and once done, set back to 0.


As soon as the module starts, it broadcasts its name. You can change the broadcast name to whatever you like. The default name is EnviTronics Lab(1).



Send Data to Computer and Smartphone

Similar to the rest of the sketch, the serial data subroutine is organized in a way to send data from up to multiple sensors to the computer. The sketch sends sensors measurements to your smartphone at the same time.


void sendAllData(uint8_t bytSensor) {
  // Sensors 0, 1, 2, 3
  if (getAddResult[bytSensor]) {
    serialPrintFunc(10 + bytSensor, VWC_P[bytSensor], TpC[bytSensor], RAW[bytSensor]);  // SI units
    blePrintFunc(10 + bytSensor, VWC_P[bytSensor] + 1000, TpC[bytSensor] + 273, 4.12, 0, RAW[bytSensor]);

  } else {  // Sensor is disconnected, so reset all related variables.
    serialPrintFunc(100 + bytSensor, 0, 0, 0);
    blePrintFunc(100 + bytSensor, 0, 0, 4.12, 0, 0);
  }
}

void serialPrintFunc(int intCommandCode, float fltB, float fltC, float fltD) {
  Serial.print(F(">"));
  Serial.print(intCommandCode);
  serialPrintFuncSum2(',', fltB);
  serialPrintFuncSum2(',', fltC);
  serialPrintFuncSum2(',', fltD);
  Serial.println(F(","));

  Serial.flush();  // Wait untill all data are sent to computer before putting Arduino into sleep
}
void serialPrintFuncSum2(char Letter, float fltValue) {
  Serial.print(Letter);  // Send coefficients to GUI!
  Serial.print(fltValue);
}         

Each sensor data string that is sent to the computer starts with a unique code that can be used to identify specific sensor. Data strings that are sent to the computer are comprised of four different numbers with the following format:


> [sensor code],[Moisture(%)],[temperature(°C)],[raw],


Here's example strings that is generated for the APAS T1 sensor:


>10,68.19,18.50,15328,


Important Note: The sketch takes sensor readings every 5 sec. You can increase the sampling interval to 60 sec or more. It is also strongly recommended that you take averages of several readings (say every 15 min). The averaging will improve the quality of data by removing noise, and give you a smoother moisture curve.


Lines below show how the sketch sends sensor data over Bluetooth LE to the smartphone:

void blePrintFunc(int intCommandCode, float fltB, float fltC, float fltD, float fltE, float fltF) {
  if (!ble.isConnected()) return;

  int DEVICECODE{ 0 };
  int DeviceNumber{ 1 };

  ble.print(F(">"));  // Send coefficients to GUI!
  ble.print(DEVICECODE);
  ble.print(F(","));
  ble.print(DeviceNumber);
  ble.print(F(","));
  ble.print(intCommandCode);
  blePrintFuncSum2(',', fltB);
  blePrintFuncSum2(',', fltC);
  blePrintFuncSum2(',', fltD);
  blePrintFuncSum2(',', fltE);
  blePrintFuncSum2(',', fltF);
  ble.println(F(","));
  ble.println(F(","));

  ble.flush();  // Wait untill all data are sent to computer before putting Arduino into sleep
  delay(250);   // -- ms timeout
}
void blePrintFuncSum2(char Letter, float fltValue) {
  ble.print(Letter);  // Send coefficients to GUI!
  ble.print(fltValue);
}

Sensor data strings that are sent to the smartphone are a bit different than the ones sent to the computer. As explained in Part 1, the strings have the following format:


>[device code], [device number],[sensor channel code],[primary sensor reading],[temperature],[battery voltage],[measurement unit], [secondary sensor reading],


Here's example strings that is generated for the APAS T1 sensor:


>207,0,10,1088.00,328.12,4.12,0.00,8513.00,

I refer you to the  ISHTAR 1P-BLE sensor node user manuals to learn about this communication protocol.



Watchdog Timer

It is always a good idea to use a watchdog timer in your code, to make sure the Arduino automatically restarts in the case of an internal error, and this is what we have here. The watchdog timer overflow time depends on the Arduino board.


// Configure Watchdog Timer
  Watchdog.enable(30000); 

The time is reset in the loop right after taking sensor measurements:


void loop(void) {
  // Sensor 1
  doSensorReadings(0);  // Read the sensor
  sendAllData(0);       // Send all measurements to PC
  /*
  // Sensor 2
  doSensorReadings(1); // Read the sensor
  sendAllData(1); // Send all measurements to PC
  // Sensor 3
  doSensorReadings(2); // Read the sensor
  sendAllData(2); // Send all measurements to PC
  // Sensor 4
  doSensorReadings(3); // Read the sensor
  sendAllData(3); // Send all measurements to PC
  */

  // A little bit of delay between measurements
  delay(5000);

  Watchdog.reset();  // Reset watchdog.
}


Downloads

I have put the electronic design and sketch file that were discussed here on GitHub.   


0 comments
bottom of page