Projects Research & Design Hub

Blood Pressure Record Keeping

FIGURE 1 My old Life Source blood pressure monitor. It transmits measurements via RF, but I didn't have its receiver counterpart.
Written by Jeff Bachiochi

Building an ESP32-to-Raspberry Pi Bridge

Many people today will check their BP at home with a digital blood pressure monitor. In this month’s project, I create an MQTT client using an ESP32 to read the monitor’s data and publish that data to a subscribing MQTT server. I use a Raspberry Pi running Node-RED to create and maintain BP data files that can be stored on a thumb drive and printed or displayed for later interpretation by a healthcare provider.

  • How can I build a project to collect my medical data?
  • How can I use both an ESP32 and Raspberry Pi in a project?
  • How can I use a MQTT server and Node-RED in a project?
  • ESP32
  • Raspberry Pi

“You may have white coat syndrome,” said Dr Bey, “Some people experience anxiety when visiting their physician, and this can result in a higher than normal blood pressure. You can help me establish a better baseline by logging your BP over the next few months. Next time you come in, bring your BP monitor, and we’ll compare its readings with the office instrument.” This seemed simple enough. “I could do that,” I responded.

Turns out that the old monitor I had kicking around (Figure 1) actually transmitted its measurements via RF. Problem is, I did not have the receiver counterpart. A quick Google of the BP model showed that it uses a third-party USB dongle. One Philips-head screwdriver plus 2 minutes time exposed the truth. The BP monitor and transmitter were separate PCBs!

I could try to decode the RF transmissions or probe the interface. The transmitter had a 12-position connector (Figure 2); however, there were only four wires in the interface, and the PCB had the label “RX” next to one wire. I dragged out my oscilloscope and hoped for some recognizable signal. There was nothing on this line, but it was in a high state. I wrapped the cuff around my biceps and turned on the monitor.

FIGURE 1 My old Life Source blood pressure monitor. It transmits measurements via RF, but I didn't have its receiver counterpart.
FIGURE 1
My old Life Source blood pressure monitor. It transmits measurements via RF, but I didn’t have its receiver counterpart.
FIGURE 2 
The RF transmitter in this model of BP monitor is separated from the main PCB with a four-wire interface. The RF interface has three of the wires marked with nomenclature that is easily understood.  Note the "G" wire (ground) is red and the "V" wire (3.3V) is brown. This is not what I would have guessed.
FIGURE 2
The RF transmitter in this model of BP monitor is separated from the main PCB with a four-wire interface. The RF interface has three of the wires marked with nomenclature that is easily understood. Note the “G” wire (ground) is red and the “V” wire (3.3V) is brown. This is not what I would have guessed.
FIGURE 3 
I set my oscilloscope to capture the activity of the "RX" signal output from the BP monitor. It seems like a good serial data stream. Measurement of the shortest pulse indicated a baud rate of 10kHz (9600).
FIGURE 3
I set my oscilloscope to capture the activity of the “RX” signal output from the BP monitor. It seems like a good serial data stream. Measurement of the shortest pulse indicated a baud rate of 10kHz (9600).

The pump started to fill the cuff with air. The pressure built until there was no longer any blood flow (I felt no pulsating in my arm), and then the pump turned off. As the pressure dropped, I could feel the blood begin to flow again—the systolic pressure. The pressure continued to bleed off (sorry), until the pulsating could not be detected—the diastolic pressure. Once the cuff had completely deflated, three numbers showed on the display: my systolic pressure, diastolic pressure, and heart rate. Then I noticed a quick flash of data on the ‘scope!

I adjusted the ‘scope to trigger and hold on the next falling edge, and reran the monitor. The trace remained on the ‘scope (Figure 3). It looked like serial data containing a handful of bytes. A measurement of the shortest pulse indicated a 9600 baud rate. I reconnected the TX signal and ground to the RX input of a serial-USB-to-TTL dongle, hoping that a serial terminal set to 9600 baud would display some recognizable data. Sure enough, out popped 10 ASCII characters when I ran another BP test cycle. I ran another five tests and carefully wrote down the data shown on the display and the 10 characters of data (Table 1). It was time for a well deserved lunch break.

TABLE 1 
The table includes six sets of 5-byte data transmitted out of the BP monitor. The first row contains HEX data taken right from the serial terminal screen I had connected to the BP monitor. The second line is the equivalent decimal values. The third line is the actual systolic, diastolic, and Beats Per Minute read from the BP screen for each of the six tests. Perusing the data on a lunch break, I realized that I'd have to translate the HEX data from the terminal program into decimal data. When I listed that, some data seemed to line up—diastolic pressure and pulse (beats per minute). Systolic pressure (mmHg) data was not right but a simple sum corrected that as shown at the bottom of the table. Actually, the systolic data was an offset above the diastolic pressure (mmHg) data. Although I found some correlation between the BP screen's diagnostic bars and bits 6:4 of the last data byte, the fifth value really remains a mystery.
TABLE 1
The table includes six sets of 5-byte data transmitted out of the BP monitor. The first row contains HEX data taken right from the serial terminal screen I had connected to the BP monitor. The second line is the equivalent decimal values. The third line is the actual systolic, diastolic, and Beats Per Minute read from the BP screen for each of the six tests. Perusing the data on a lunch break, I realized that I’d have to translate the HEX data from the terminal program into decimal data. When I listed that, some data seemed to line up—diastolic pressure and pulse (beats per minute). Systolic pressure (mmHg) data was not right but a simple sum corrected that as shown at the bottom of the table. Actually, the systolic data was an offset above the diastolic pressure (mmHg) data. Although I found some correlation between the BP screen’s diagnostic bars and bits 6:4 of the last data byte, the fifth value really remains a mystery.
LUNCH AND PERUSING THE DATA

I prepared the obligatory, do-it-yourself deli sandwich and a cup of hot cocoa. After all, it was January 4, 2023. Although the temperature was over 50 degrees with no snow cover, I still felt chilled by the thought of winter. Staring at the list of displayed data compared to the ASCII data I was not seeing any relationship between the two data groups. The first two characters, “80,” were repeated in each data string. Hmm, could be a sync byte. Typically, sync bytes might be 0x80, 0x55, or 0xAA in hexadecimal, Yep, while most characters were numbers, there were occasional letters, and they were all in the A-F range.

Once I separated the 10 characters into 5 hex bytes, things looked more interesting. Besides the first byte (80), there were 4 additional bytes. When I wrote down the decimal equivalent of these bytes, there were some matches—byte 3, diastolic pressure; and byte 4, beats per minute. For some reason byte 2, systolic pressure was low. Actually, it was low by exactly the value of the diastolic pressure. I’m not sure why byte 2 was not sent as its actual value, but perhaps they were avoiding measurements that could exceed 255 (the maximum size of 1 byte).

Byte 5 was still a mystery. I’m not sure what other data might be pertinent. The other glyphs on the display screen are battery voltage and diagnostics category (level of concern). Or it might be some kind of checksum. The upper bits seemed to follow the measurement category, but the lower bits were not steady enough to reflect battery voltage, and no combination of data bytes seem to indicate a checksum. Since I can get the data required from bytes 2-4, I’ll consider this a success and proceed.

GATHER THE WHEAT

I believe the future of diagnostics by physicians or even AI will depend on the ability to collect medical data outside a doctor’s office. When we log test results such as temperature, weight, blood pressure, heart rate, dissolved oxygen, EKG, and glucose, we have a permanent picture of our health. Better yet, when healthcare providers can review the history, instead of the results of a single measurement or test, they can more easily identify abnormalities.

I’ve been using MQTT and Node-RED for a while now, to view live data on any PC screen. MQTT is a standard messaging protocol for the Internet of Things. It is designed to be an extremely lightweight publish/subscribe messaging transport for connecting remote devices. Node-RED provides a browser-based editor that makes it easy to connect together IO objects from its library. For instance, it can connect a supported MQTT server to data analysis and display objects, and present a graphic display of collected data.

For this project, I will create an MQTT client that will publish data to a subscribing MQTT server. I will use Node-RED to create and maintain files with any data that is sent to it. The received data from a patient will be stored to a patient-specific file on a thumb drive that can be removed if necessary for a permanent record of the data. The data could then be printed or displayed in some form that would make it easy to understand. For now let’s just get the data stored to a removable media device, such as a thumb drive.

OUR BP CLIENT

This project uses an ESP32 board (HTIT-WB32) by Heltec Automation, because it has an integral OLED display. This will allow monitoring of a Wi-Fi connection and subsequent network time protocol (NTP) and MQTT connections. Since all of this is based on a connection to the Internet, we might as well use the Internet to discover the present time and date using NTP. Then there will be a time stamp that can be used to pinpoint when the data was collected.

Using the Arduino IDE we can begin by setting up the necessary things to allow us to use the Heltec display. The display uses a few of the ESP32 IOs, so we must keep away from them. The display is essentially memory mapped; we write into the display’s sandbox memory, and then when ready, a simple command transfers this to the screen memory. Although the drawing commands allow for some complicated graphics, we will use the drawString() commands, since we will just be displaying lines of text.

The default serial USB port will be used for debugging. A second serial port, Serial2, will read the data from the BP monitor (Listing 1).

LISTING 1
This code initializes the on-board OLED display and the serial ports used in this project.

// declare
#include “heltec.h”
const int RXD2 = 17;
const int TXD2 = 2;	// not used

void setup() 
{
  Serial.begin(115200);
  Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2);
  Heltec.begin(true, false, true);
  Heltec.display->screenRotate(ANGLE_0_DEGREE);  
  Serial.println(“Heltec HTIT-WB32 MQTT Life Source BPM”);
  Heltec.display->clear();  
  Heltec.display->drawString(0, 0, “Heltec HTIT-WB32”);
  Heltec.display->drawString(0, 10, “MQTT Life Source BPM”);  
  Heltec.display->display(); 
}
LISTING 2
With the addition of this code, we can read the data from the BP monitor and display it.


//declare
...
int myReading[4] = {0, 0, 0, 0};
String myString[6];
String formattedDate;

void setup()
{
  ...
  Serial.println(“Waiting for BP Data...”);
  Heltec.display->clear(); 
  Heltec.display->drawString(0, 10, “Waiting for BP Data...”);  
  Heltec.display->display(); 
}

void loop()
{
   if(Serial2.available() >= 10)
  {
    getData();
  }
}
//
void getData()
{
  for(int i=0; i<=3; i++)
  {
    myReading[i] = 0;
  }  
  if(Serial2.read() == ‘8’)
  {
    if(Serial2.read() == ‘0’)
    {
      for(int i=0; i<=3; i++)
      {
        myReading[i] = getNibbles();
      }
      Serial.println(“Received all BP data”);
      debugData();
      displayData();
      //sendData(); 
    }
    
}
//
int getNibbles()
{
  int nibble;
  int value = 0;
  for(int i=0; i<=1; i++)
  {
    nibble = Serial2.read() - 0x30;
    if(nibble>9)
    {
      nibble = nibble - 7;
    }
    value = value * 16 + nibble;
  }
  return value;  
}
//
void debugData()
{
  myString[0]=”type = Blood Pressure”;
  Serial.println(myString[0]);
  myString[1]=”epoch = “ + String(formattedDate);
  Serial.println(myString[1]);  
  myString[2]=”systolic = “ + String(myReading[0] + myReading[1]);
  Serial.println(myString[2]);  
  myString[3]=”diatolic = “ + String(myReading[1]);
  Serial.println(myString[3]);
  myString[4]=”bpm = “ + String(myReading[2]);
  Serial.println(myString[4]);
  myString[5]=”? = “ + String(myReading[3]) + “ (0x” + String(myReading[3],HEX) + “)”;
  Serial.println(myString[5]); 
} 
//
void displayData()
{
  Heltec.display->clear();
  Heltec.display->drawString(0, 0, myString[0]);
  Heltec.display->drawString(0, 10, myString[1]);
  Heltec.display->drawString(0, 20, myString[2]);
  Heltec.display->drawString(0, 30, myString[3]);
  Heltec.display->drawString(0, 40, myString[4]);
  Heltec.display->drawString(0, 50, myString[5]);
  Heltec.display->display();
} 
//declare
...
int myReading[4] = {0, 0, 0, 0};
String myString[6];
String formattedDate;

void setup()
{
  ...
  Serial.println(“Waiting for BP Data...”);
  Heltec.display->clear(); 
  Heltec.display->drawString(0, 10, “Waiting for BP Data...”);  
  Heltec.display->display(); 
}

void loop()
{
   if(Serial2.available() >= 10)
  {
    getData();
  }
}
//
void getData()
{
  for(int i=0; i<=3; i++)
  {
    myReading[i] = 0;
  }  
  if(Serial2.read() == ‘8’)
  {
    if(Serial2.read() == ‘0’)
    {
      for(int i=0; i<=3; i++)
      {
        myReading[i] = getNibbles();
      }
      Serial.println(“Received all BP data”);
      debugData();
      displayData();
      //sendData(); 
    }
    
}
//
int getNibbles()
{
  int nibble;
  int value = 0;
  for(int i=0; i<=1; i++)
  {
    nibble = Serial2.read() - 0x30;
    if(nibble>9)
    {
      nibble = nibble - 7;
    }
    value = value * 16 + nibble;
  }
  return value;  
}
//
void debugData()
{
  myString[0]=”type = Blood Pressure”;
  Serial.println(myString[0]);
  myString[1]=”epoch = “ + String(formattedDate);
  Serial.println(myString[1]);  
  myString[2]=”systolic = “ + String(myReading[0] + myReading[1]);
  Serial.println(myString[2]);  
  myString[3]=”diatolic = “ + String(myReading[1]);
  Serial.println(myString[3]);
  myString[4]=”bpm = “ + String(myReading[2]);
  Serial.println(myString[4]);
  myString[5]=”? = “ + String(myReading[3]) + “ (0x” + String(myReading[3],HEX) + “)”;
  Serial.println(myString[5]); 
} 
//
void displayData()
{
  Heltec.display->clear();
  Heltec.display->drawString(0, 0, myString[0]);
  Heltec.display->drawString(0, 10, myString[1]);
  Heltec.display->drawString(0, 20, myString[2]);
  Heltec.display->drawString(0, 30, myString[3]);
  Heltec.display->drawString(0, 40, myString[4]);
  Heltec.display->drawString(0, 50, myString[5]);
  Heltec.display->display();
} 

LISTING 3
This code will allow the BP monitor to wake up the MCU and put it into "deep sleep" when the monitor is powered ON and OFF, respectively. This is handled by the myActiveInput signal from the BP monitor.

// declare
...
const int myActiveInput = 14;
#define BUTTON_PIN_BITMASK 0x1000        / 2^(pin12)
//
void setup()
{
  ...
  esp_sleep_enable_ext1_wakeup(BUTTON_PIN_BITMASK,ESP_EXT1_WAKEUP_ALL_LOW);
}
//
void loop()
{
  ...
  boolean idle = digitalRead(myActiveInput);
  if(idle)
  {
    Serial.println(“Going to deep sleep...”);
    esp_deep_sleep_start();
  }
}
LISTING 4
After declaring the home network name and password, the setupWiFi() routine is called. This routine handles establishing contact and receiving an IP address from the network server. I create a unique ID by adding the prefix “ESP32_” to the last six characters of the MAC address unique to each ESP32 module.

// declare
…
#include <WiFi.h>
const char* ssid = “myNetwork”;
const char* password = “myNetworkPassword”;
String MAC = “000000”;
String ID = “ESP32_” + MAC;
char lastWifiState = 0;
//
void setup()
{
  …
  delay(3000); 
  //--------------------------------------
  setupWIFI(); 
  lastWifiState = 0;
  delay(3000); 
}  	
//
void loop()
{
  ...
  if(WiFi.status() != WL_CONNECTED) 
  {                                   // now not connected
    Serial.println(“Lost WiFi Connection”); 
    setupWIFI();
    lastWifiState = 0;
  } 
  else
  {                                    // now connected}
    if(lastWifiState != WL_CONNECTED)
    {
      Serial.println(“WiFi is Connected”); 
    }
  }
}
//
void setupWIFI()
{
  Heltec.display->clear();
  Heltec.display->drawString(0, 0, “Connecting to”);
  Heltec.display->drawString(0, 10, String(ssid));
  Heltec.display->display();

  WiFi.disconnect(true);
  delay(500);

  WiFi.mode(WIFI_STA);
  WiFi.setAutoConnect(true);
  WiFi.setAutoReconnect(true);   
 
  WiFi.begin(ssid, password);
  byte count = 0;
  while(WiFi.status() != WL_CONNECTED && count < 20)
  {
    count ++;
    delay(500);
    Serial.print(“.”);
  }
  Heltec.display->clear();
  if(WiFi.status() == WL_CONNECTED)
  {  
    String myIP = String(WiFi.localIP()[0]) + “.” + String(WiFi.localIP()[1]) + “.” + String(WiFi.localIP()[2]) + “.” + String(WiFi.localIP()[3]); 
    Serial.println();
    Serial.println(“IP = “ + myIP);
    Heltec.display->drawString(0, 0, “IP = “ + myIP);
    String macAddress = WiFi.macAddress();
    MAC = macAddress.substring(9, 11) + macAddress.substring(12, 14) + macAddress.substring(15, 17);  
    ID = “ESP32_” + MAC + “ “;
    Serial.println(“Name = “ + ID);
    Heltec.display->drawString(0, 10, “Name = “ + ID);
  }
  else
  {
    Heltec.display->drawString(0, 0, “Connect Error”);
  }
  Heltec.display->display();
}
// declare
…
#include <WiFi.h>
const char* ssid = “myNetwork”;
const char* password = “myNetworkPassword”;
String MAC = “000000”;
String ID = “ESP32_” + MAC;
char lastWifiState = 0;
//
void setup()
{
  …
  delay(3000); 
  //--------------------------------------
  setupWIFI(); 
  lastWifiState = 0;
  delay(3000); 
}  	
//
void loop()
{
  ...
  if(WiFi.status() != WL_CONNECTED) 
  {                                   // now not connected
    Serial.println(“Lost WiFi Connection”); 
    setupWIFI();
    lastWifiState = 0;
  } 
  else
  {                                    // now connected}
    if(lastWifiState != WL_CONNECTED)
    {
      Serial.println(“WiFi is Connected”); 
    }
  }
}
//
void setupWIFI()
{
  Heltec.display->clear();
  Heltec.display->drawString(0, 0, “Connecting to”);
  Heltec.display->drawString(0, 10, String(ssid));
  Heltec.display->display();

  WiFi.disconnect(true);
  delay(500);

  WiFi.mode(WIFI_STA);
  WiFi.setAutoConnect(true);
  WiFi.setAutoReconnect(true);   
 
  WiFi.begin(ssid, password);
  byte count = 0;
  while(WiFi.status() != WL_CONNECTED && count < 20)
  {
    count ++;
    delay(500);
    Serial.print(“.”);
  }
  Heltec.display->clear();
  if(WiFi.status() == WL_CONNECTED)
  {  
    String myIP = String(WiFi.localIP()[0]) + “.” + String(WiFi.localIP()[1]) + “.” + String(WiFi.localIP()[2]) + “.” + String(WiFi.localIP()[3]); 
    Serial.println();
    Serial.println(“IP = “ + myIP);
    Heltec.display->drawString(0, 0, “IP = “ + myIP);
    String macAddress = WiFi.macAddress();
    MAC = macAddress.substring(9, 11) + macAddress.substring(12, 14) + macAddress.substring(15, 17);  
    ID = “ESP32_” + MAC + “ “;
    Serial.println(“Name = “ + ID);
    Heltec.display->drawString(0, 10, “Name = “ + ID);
  }
  else
  {
    Heltec.display->drawString(0, 0, “Connect Error”);
  }
  Heltec.display->display();
}

LISTING 5
The added code will be executed only when we have established an IP. We only need to call the getTimeDate() routine once, to document when this BP test is performed.

// declare
…
#include <NTPClient.h>
// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);
// Variables to save date and time
String formattedDate;
//
void setup()
{
 …
 if(WiFi.status() == WL_CONNECTED)
  {
    Serial.println(“Getting NTP”);
    timeClient.begin();
    // Set offset time in seconds to adjust for your timezone, for example:
    // 3600 seconds for each time zone
    // EST -4 hours
    timeClient.setTimeOffset(3600 * -4);
    getTimeDate();
    Serial.println(“Epoch = “ + String(formattedDate));
    Heltec.display->drawString(0, 20, “Epoch = “ + String(formattedDate)); 
    Heltec.display->display(); 
  }
}
//
void getTimeDate()
{
  while (!timeClient.update())
  {
    timeClient.forceUpdate();
  }
  // The formattedDate comes with the following format:
  // 2018-05-28T16:00:13Z
  // We need to extract date and time
  formattedDate = String(timeClient.getEpochTime());
}
LISTING 6
The MQTT can periodically attempt to contact the server to verify its connection. Data can flow in both directions. For this application, we are publishing data but not subscribing to anything.

// declare
...
#include <PubSubClient.h>
const char* mqtt_server = “192.168.0.22”;
WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
const int msgSize = 50;
char msg[msgSize];
char topic[msgSize];
char ClientId[msgSize];
//
void setup()
{
…
if(WiFi.status() == WL_CONNECTED)
  {
    ...
    Serial.println(“Starting MQTT”);
    client.setServer(mqtt_server, 1883);
    client.setCallback(callback);  
    String temp = “MQTT = “ + String(mqtt_server); 
    Heltec.display->drawString(0, 30, temp);
    Heltec.display->display(); 
  }
  delay(3000); 
}
//
void loop()
{
…
if (!client.connected()) 
  {
    Heltec.display->drawString(0, 20, “Reconnecting...”);  
    Heltec.display->display(); 
    reconnect();
    Heltec.display->clear(); 
  }
  client.loop();
}
//
void callback(char* topic, byte* payload, unsigned int length) 
{ 
}
//
void reconnect() 
{ 
  // Loop until we’re reconnected
  while (!client.connected()) 
  {
    Serial.print(“Attempting MQTT connection to: “ + String(mqtt_server) + “...”);
    // Create a client ID
    ID.toCharArray(ClientId,msgSize);
    // Attempt to connect
    if (client.connect(ClientId))
    {
      Serial.println(“MQTT connected”);
    } 
    else 
    {
      Serial.print(“failed, rc=”);
      Serial.println(client.state());
      Serial.println(“ try again in 5 seconds”);
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}
LISTING 7
The sendData() routine handles preparing the required data format for a single published message. The format is topic,message, where the topic will be the application ID (ESP32_BB4DA0) and the message will consist of the six JSON objects (name:value pairs).

void sendData()
{
  String S = “{“ + myString[0] + “,” + myString[1] + “,” + myString[2] + “,” + myString[3] + “,” + myString[4] + “,” + myString[5] + “}”;    //
  S.toCharArray(msg, msgSize); 
  ID.toCharArray(topic, msgSize);  
  Serial.print(“Publish message: “);
  Serial.print(msg);
  Serial.print(“ to topic: “);
  Serial.println(topic);  
  if(WiFi.status() == WL_CONNECTED)
  {  
    pSuccess = client.publish(topic, msg);
  }
  if(!pSuccess)
  {
    Serial.println(“no connection, not published”);  
  }
}

Next, let’s add code to read the BP monitors serial data stream (Listing 2). We know the data comes in 10 bytes at a time. Once we have at least 10 bytes, we’ll save the data into an array, myReading[]. Once all the ASCII characters (representing nibbles) have been received and placed into the array, we can display the data using debugData() and displayData(). The first routine sets up the array myString[] and sends the data messages out the USB port. The second routine takes the array myString[] and sends the data to the OLED.

A typical display will look like this:

type = Blood Pressure
epoch = 1672947560
systolic = 148
diatolic = 91
bpm = 58
? = 118 (0x76)

The four wires that I consider the interface can be labeled 3.3V (brown), ground (red), RX (orange), and myActiveInput (yellow). Refer to the schematic in Figure 4 to see how these are connected to the Heltec microcontroller (MCU). Note that the myActiveInput signal will be used for one function; however, it is actually connected to two pins. Pin 14 is an input that is monitored and puts the MCU into “deep sleep” when it is high. In other words, when the BP monitor is turned OFF, myActiveInput signal goes high. Pin 12 is an input is used to wake up the MCU when the BP monitor is turned ON (myActiveInput signal goes low). Apparently, when configured as a wake-up pin, it cannot be read by a digitalRead(). The code that is added to take care of sleep and wake-up is shown in Listing 3.

FIGURE 4
The ESP32 needs only four wires from the BP monitor (the same four the RF transmitter was using).  These include power, ground, and TX (serial output from the BP monitor).  The 4th wire is power ON, used to wake up the ESP32 from deep sleep."
FIGURE 4
The ESP32 needs only four wires from the BP monitor (the same four the RF transmitter was using). These include power, ground, and TX (serial output from the BP monitor). The 4th wire is power ON, used to wake up the ESP32 from deep sleep.”

Now we can retrieve the BP data and reduce the current load of the MCU when the BP monitor is OFF. As a result, we can add the code necessary to send data packets over the Internet using the MQTT protocol to the MQTT server running on my Raspberry Pi. Since everything depends on a Wi-Fi connection to my network, lets start there.

NETWORK CONNECTION

I’ve been running a Raspberry Pi as a server for many years. It has a backup power supply, and is always on-line on my home network. I will begin a new Node-RED flow that simply listens to the network, waiting for this device to publish (send) an MQTT transmission to it. To do this, the device must have a “device ID” and send transmissions to the MQTT server’s local IP address, 192.168.0.22:1880. So, the first order of business is to add Wi-Fi capability to our growing program. The code given in Listing 4 will allow this MCU to obtain an IP address of its own.

At this point we should be seeing the IP and Name of the device on the display (and in a serial terminal if connected to the MCU’s USB connector).

IP = 192.168.0.32
Name = ESP32_BB4DA0

You may have noticed a declaration earlier for the variable String formattedDate. We have not yet assigned a value to this, so it printed out as <blank>. This will hold the present number of seconds, since the epoch was started at midnight on 1/1/1970, otherwise known as “UNIX time.” There are routines that can calculate the day and time from this number. We can get this number from the NTP Pool Project [1], which is a big virtual cluster of timeservers providing reliable, easy-to-use NTP service for millions of clients. The code given in Listing 5 will be executed only after an IP has been established.

MQTT

Finally, we can add the code to allow our data to be published to the MQTT server (Listing 6). We begin by defining the local network address of the MQTT server. This is a two-way street, meaning this client may subscribe to messages from other clients. These messages will be available through the callback() routine. You might use this if you have enabled the server to publish the time/date. We could periodically receive this info through the callback. Since we’ve already obtained the epoch and only need this info once (each time the application wakes up), we can disregard this routine.

If, for some reason, the MQTT connection is broken, the check in the main loop will “reconnect.” Most routines are not interrupt driven and must be polled periodically, since the MQTT is handled here with client.loop(). Your main loop must not have any significant delays that would prevent these tasks from happening in a timely fashion.

Our data has now been collected and displayed on the OLED. We’ve established our MQTT connection, and now we can add the last routines to publish it. You may have noticed back in Listing 2 that the command sendData() was rem’d out, because this routine hadn’t been provided yet. It’s time to enable this command, and add that routine to handle the actual publishing of six topics of data (Listing 7).

NODE-RED

I am using a Raspberry Pi running Linux-based Debian OS to serve Node-RED and handle MQTT, but you can use Windows PC or MAC. I have a USB drive plugged into the Pi, and store all data to this device. I can transport the data to another device, if needed. If I point to port 1880 of my Pi’s IP address in a Web browser (http://192.168.0.22:1880), the Node-RED editor is launched. The editor allows you to pick and place nodes from a palette library of devices, functions, IO, and so on, and connect them together in ways to create a flow of information.

As shown in Figure 5, I’ve added four nodes to my flow. From left to right, the first node is an “MQTT in” node. It listens to the MQTT traffic. I’ve selected the Blood Pressure device by filtering out all traffic except that which contains topic:ESP32_BB4DA0. Those messages are passed through to the next node. The second node is a “function’ node.” Within this node I can use some javascript to strip off the first object in the message, “type:Blood_Pressure,” and use the value as the fileName for our stored data. We are now left with a message with five objects. This message goes to two more nodes. The upper node is a “debug” node that allows the message to be displayed in the debug area on the right. The lower node is a “write file” node. I’ve chosen a path to point to a “log” folder on the USB device. This node will then store each message to the Blood_Pressure.log file.

FIGURE 5 
This simple logging flow has minimum nodes. The two green "debug" nodes are used to show the message data flowing at two different points. The get fileName node strips off the "type=Blood Pressure" object from the message.
FIGURE 5
This simple logging flow has minimum nodes. The two green “debug” nodes are used to show the message data flowing at two different points. The get fileName node strips off the “type=Blood Pressure” object from the message.

The BP flow of Node-RED can be expanded to display data, but I will save that for another time. There are many ways to display the data once you have a record of it. Since the file data is now stored on a flash drive (Figure 6), I can remove and read the file externally or right on the Pi. A DB application such as Open Office or Libre Office can read this file and use the data to plot a graph of the three data values as in Figure 7. I could write a Liberty Basic application to read the file and plot a graph.

FIGURE 6 
I have a battery backed-up Raspberry Pi right on the wall of my office. The USB (thumb) drive, which holds my medical data, is permanent, yet removable storage.
FIGURE 6
I have a battery backed-up Raspberry Pi right on the wall of my office. The USB (thumb) drive, which holds my medical data, is permanent, yet removable storage.
FIGURE 7 
The .csv file can be easily imported into a spreadsheet and selected columns graphed.
FIGURE 7
The .csv file can be easily imported into a spreadsheet and selected columns graphed.

Figure 8 shows the Heltec Automation ESP32. Its screen is only about 1″x0.5″, but it can display a few lines of text and is sufficient for many projects. I plan to keep adding more devices to this MQTT logging application until I have a more complete record of my daily health. So far, since the summer season, my BP has gone up—I’m guessing because my weight has also increased. This is most likely due to a lack of exercise, which the colder weather has brought on, and an increased intake of sweets, which always seems to peak during the Halloween, Thanksgiving, and Christmas seasons. Thanks to Silver Sneakers I can now run indoors at the local fitness club. However, curbing my appetite, especially in the evenings, might be a bit more difficult. Too many sweets still remain, too little willpower to resist. 

FIGURE 8 
I like Heltec Automation's ESP32 because it has an integrated display. Upon starting the BP monitor, the ESP32 finds my network, gets assigned an IP, makes contact with NTP for the date/time (Epoch format), and then establishes a link with the MQTT server (Raspberry Pi). Now it's ready to get and publish the data it receives from the BP test.
FIGURE 8
I like Heltec Automation’s ESP32 because it has an integrated display. Upon starting the BP monitor, the ESP32 finds my network, gets assigned an IP, makes contact with NTP for the date/time (Epoch format), and then establishes a link with the MQTT server (Raspberry Pi). Now it’s ready to get and publish the data it receives from the BP test.

REFERENCES
[1] NTP Pool Project: https://www.ntppool.org/en/

RESOURCES
Arduino | arduino.cc
Debian | debian.org
Espressif Systems | espressif.com
Heltec Automation | heltec.org

Code and Supporting Files

PUBLISHED IN CIRCUIT CELLAR MAGAZINE • MAY 2023 #394 – Get a PDF of the issue

Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.


Note: We’ve made the Dec 2022 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
Website | + posts

Jeff Bachiochi (pronounced BAH-key-AH-key) has been writing for Circuit Cellar since 1988. His background includes product design and manufacturing. You can reach him at: jeff.bachiochi@imaginethatnow.com or at: www.imaginethatnow.com.

Supporting Companies

Upcoming Events


Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2024 KCK Media Corp.

Blood Pressure Record Keeping

by Jeff Bachiochi time to read: 20 min