基于MQTT協(xié)議的ESP8266+PulseSensor心率探測(cè)原型系統(tǒng)

簡(jiǎn)單整理了自己在本學(xué)期學(xué)校的“物聯(lián)網(wǎng)技術(shù)與應(yīng)用”課程的課程大作業(yè)中所實(shí)現(xiàn)的一個(gè)小原型羡滑。
該原型實(shí)現(xiàn)了利用esp8266自帶的WIFI模塊,借助Node MCU開(kāi)發(fā)板讀取pulse sensor心率傳感器的數(shù)據(jù)野瘦,通過(guò)MQTT通信協(xié)議萄金,在手機(jī)上查看心率值建瘫,并且顯示在OLED屏上。同時(shí)通過(guò)MQTT協(xié)議荤牍,連接了開(kāi)發(fā)板的傳感器案腺、EMQ服務(wù)器、手機(jī)之間可以相互訂閱参淫、發(fā)布相關(guān)topic的消息救湖。
系統(tǒng)大致框架圖

需要用到:

        esp8266開(kāi)發(fā)板

        杜邦線、USB線

        Pulse Sensor心率傳感器

        0.96寸OLED顯示屏

        Arduino IDE

        EMQ (MQTT消息服務(wù)器)

一涎才、傳感器相關(guān)準(zhǔn)備

用杜邦線將傳感器按照以下的對(duì)應(yīng)關(guān)系連接起來(lái):

                      Pulse Sensor心率傳感器             esp8266


                              S                       A0引腳(ADC)

                              +                          3.3V

                              -                           GND

Pulse Sensor心率傳感器如下圖所示:

Pulse Sensor心率傳感器

網(wǎng)上關(guān)于Pulse Sensor心率傳感器也可以搜到一些鞋既,這里就不再多說(shuō)啦,可以參考這個(gè)博客:史上最全脈搏心率傳感器PulseSensor資料(電路圖+中文說(shuō)明書(shū)+最全源代碼)

Pulse Sensor官方的github庫(kù): PulseSensorPlayground

在Arduino->管理庫(kù)->庫(kù)管理器 搜索PulseSensor Playground下載并安裝,安裝成功后在 Arduino->文件->示例 可以看到如下圖所示的官方示例代碼耍铜。

連接好:

二邑闺、OLED相關(guān)準(zhǔn)備

使用的是0.96寸的OLED屏:

0.96寸的OLED屏

將OLED屏也連接到esp8266開(kāi)發(fā)板上:

三、Arduino下載安裝相關(guān)庫(kù)

在Arduino庫(kù)管理器里搜索并安裝以下的將會(huì)用到的庫(kù)

Arduino庫(kù)管理器
要用到的庫(kù)文件

四棕兼、MQTT相關(guān)知識(shí)

MQTT菜鳥(niǎo)教程:MQTT 入門(mén)介紹

五陡舅、搭建EMQ

EMQ官網(wǎng):https://www.emqx.io/cn/products/broker

官網(wǎng)安裝配置文檔:https://docs.emqx.io/broker/v3/cn/

搭建成功后,在瀏覽器輸入控制臺(tái)地址:http://127.0.0.1:18083

輸入賬號(hào)密碼即可進(jìn)入EMQ控制臺(tái):

EMQ控制臺(tái)

六伴挚、Arduino部分

Arduino->工具里開(kāi)發(fā)板要選NodeMCU1.0靶衍,且端口要選對(duì)

實(shí)現(xiàn)代碼 (計(jì)算心率算法+MQTT+OLED控制):

extern "C" {
#include "user_interface.h"
}
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>  
 // Only needed for Arduino 1.6.5 and earlier
#include "SSD1306Wire.h"
 // legacy include: `#include "SSD1306.h"`

SSD1306Wire  display(0x3c, D4, D5);

//esp8266通過(guò)傳感器采集脈搏模擬信號(hào)灾炭,經(jīng)過(guò)AD轉(zhuǎn)換為數(shù)字電壓 并計(jì)算心率值

//Signal,信號(hào)颅眶,持有原始模擬輸入數(shù)據(jù)引腳0蜈出,每2ms更新一次               int                           
//IBI: 相鄰兩次脈搏的時(shí)間間隔(單位:ms)                            int

//BPM(beats per minute):心率,一分鐘內(nèi)的心跳次數(shù)                    int
//(且BPM = 60 / IBI)是根據(jù)之前 10個(gè)IBI值的 平均值

//Pulse脈沖涛酗,當(dāng)檢測(cè)到心跳時(shí) 為true铡原,其他時(shí)候?yàn)閒alse。它控制LED引腳13商叹。  boolean

//QS 當(dāng)找到Pulse并更新BPM時(shí) 為true燕刻。必須重置。                       boolean

const char* ssid = "";//熱點(diǎn)名
const char* password = "";//熱點(diǎn)密碼
const char* mqtt_server = "";//MQTT服務(wù)器ip

WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
char msg[8];
int value = 0;

#define N 10

volatile int BPM;         // 保持引腳0剖笙,每2ms更新一次
volatile int Signal;      // 保存原始數(shù)據(jù)
volatile int IBI = 600;   // 相鄰兩次心跳之間的時(shí)間間隔是唯一確定的
volatile boolean Pulse = false;     // 脈沖,當(dāng)監(jiān)測(cè)到一個(gè)心跳時(shí)為"True"卵洗,否則為"False"
volatile boolean QS = false;        // 當(dāng)Arduoino監(jiān)測(cè)到心跳時(shí)為true

volatile int Rate[N];         // 設(shè)置數(shù)組保存最后10次IBI數(shù)據(jù)
volatile unsigned long CurrBeatTime = 0;    // 用于確定脈沖時(shí)間
volatile unsigned long LastBeatTime = 0;    // 用于監(jiān)測(cè)IBI
volatile int P = 500;         // 用于尋找脈搏波峰值
volatile int T = 500;         // 用于發(fā)現(xiàn)脈搏波中的波谷
volatile int Threshold = 512; //設(shè)置閥值
volatile int Amplifier = 100;  //放大器

int PulseSensorPin = 17;
int FadePin = 4;
int FadeRate = 0;
String tmp="";

void setupTimer(int m /* msec */) {
  timer0_isr_init();
  timer0_attachInterrupt(timer0_ISR);
  timer0_write(ESP.getCycleCount() + 80000L * m); // 80MHz/1000 == 1msec
}

unsigned long getCurrentTime() {
  return ESP.getCycleCount() / 80000L;
}
void timer0_ISR(void) {
    noInterrupts();
    Signal = system_adc_read();
    CurrBeatTime = getCurrentTime(); // msec
    unsigned long interval = CurrBeatTime - LastBeatTime;

  //  找出脈搏波的波谷
    if ((Signal < Threshold) && (interval >(IBI * 3) / 5)) {    // 通過(guò)等待3/5的IBI,以避免二色噪聲
        if (Signal < T) {                                         // T 是波谷
            T = Signal;                                             // 脈搏波最低點(diǎn)跟蹤
            //Serial.println("T:" + String(T));
        }
    }

  //找出脈搏波的波峰
    if (Signal > Threshold && Signal > P) {     // 閾值條件有助于避免噪音
        P = Signal;                               // P 是波峰
        //Serial.println("P:" + String(P));
    }
 
  //  開(kāi)始尋找心跳
  // 每當(dāng)有脈沖時(shí)枯途,信號(hào)就會(huì)增值忌怎。
    if (interval > 250 /* ms */) {        // 避免高頻率噪音
    //檢查信號(hào)是否超過(guò)閥值
        if ((Signal > Threshold) && !Pulse && (interval > (IBI * 3) / 5)) {
            Pulse = true;                     // 當(dāng)感覺(jué)這有心跳時(shí)設(shè)Pulse flag
            IBI = interval;

            if (Rate[0] < 0) { // first time 第一次
                Rate[0] = 0;
                LastBeatTime = getCurrentTime();
                setupTimer(10);
                noInterrupts();
                return;
            }
            else if (Rate[0] == 0) {  // second time 第二次
                for (int i = 0; i < N; ++i) {   // 以獲得啟動(dòng)時(shí)實(shí)際的BPM
                    Rate[i] = IBI;
                }
            }

      // 保持最后10個(gè)IBI值的累計(jì)總數(shù)
            word running_total = 0;                 // 清零runningTotal
            for (int i = 0; i < N - 1; ++i) {       // 心率數(shù)組中的移位數(shù)據(jù)
                Rate[i] = Rate[i + 1];                // 降低最早的IBI值
                running_total += Rate[i];             // 加上9個(gè)最早的IBI值 一共10個(gè),平均值得到BPM
            }

            Rate[N - 1] = IBI;                      // 將最新的IBI添加到心率數(shù)組中
            running_total += IBI;                   // 將最新的IBI添加到運(yùn)行總數(shù)runningTotal中
            running_total /= N;                     // 取10個(gè)IBI值的平均值=BPM
            BPM = 60000 / running_total;            // 一分鐘內(nèi)的心跳次數(shù)就是BPM
            QS = true;                              // 設(shè)置量化標(biāo)記Quantified Self flag
            LastBeatTime = getCurrentTime();
        }
    }

    // 檢查信號(hào)是否在閥值以下
    if ((Signal < Threshold) && Pulse) {
        Pulse = false;
        Amplifier = P - T;
        Threshold = Amplifier / 2 + T; // 修改閥值
        P = Threshold;
        T = Threshold;
    }

    // 如果沒(méi)有信號(hào)超過(guò)2.5秒 檢查
    if (interval > 2500 /* ms */) {
        Threshold = 512;
        P = 500;
        T = 500;
        LastBeatTime = getCurrentTime();
        for (int i = 0; i < N; ++i) {
            Rate[i] = -1;
        }
    }
    setupTimer(10);
    interrupts();
}


void setup() {
    pinMode(BUILTIN_LED, OUTPUT);
    pinMode(A0, INPUT);
    Serial.begin(115200);
    setup_wifi();
    client.setServer(mqtt_server, 1883);
    client.setCallback(callback);

    pinMode(FadePin, OUTPUT);
    analogWriteRange(255);
    noInterrupts();
    setupTimer(10);
    interrupts();
    LastBeatTime = getCurrentTime(); // msec

    Serial.println();
    display.init();
    display.clear();
  
}

void setup_wifi() {

    delay(10);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }

    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
    Serial.print("Message arrived [");
    Serial.print(topic);
    Serial.print("] ");
    for (int i = 0; i < length; i++) {
        Serial.print((char)payload[i]);
         tmp+=(char)payload[i];
    }
    Serial.println();
    displayData();
    tmp="";
}

void displayData(){
    display.clear();
    display.drawString(0,0,"[receive]:");
    display.drawString(0,20,tmp);
    display.display();
    delay(1000);
}
void reconnect() {
    
    while (!client.connected()) {
        Serial.print("MQTT connection...");
        
        if (client.connect("ESP8266Client")) {
    
            client.subscribe("Topic");    //訂閱主題Topic
        }
        else {
            Serial.print("failed, rc=");
            Serial.print(client.state());
            Serial.println(" try again in 5 seconds");
            
            delay(5000);
        }
    }
}
void loop() {

    if (!client.connected()) {
        reconnect();
    }
    client.loop();

    if (QS) {
        FadeRate = 255;
        Serial.print("BPM: ");
        Serial.println(BPM);

        snprintf(msg, 8, "BPM:%d", BPM);
        client.publish("Topic", msg);//發(fā)布主題Topic
        QS = false;
    }

    FadeRate -= 15;
    FadeRate = constrain(FadeRate, 0, 255);
    analogWrite(FadePin, FadeRate);
    delay(20);
}

程序?qū)懞煤罄乙模瑢㈤_(kāi)發(fā)板上電,并連接到電腦上孽惰,將程序燒制進(jìn)去晚岭,保持手指放在心率傳感器的正面,打開(kāi)串口監(jiān)視器(波特率選擇115200)勋功,當(dāng)成功連接到MQTT服務(wù)器時(shí)坦报,就可以看到串口輸出以下數(shù)據(jù),并可以在OLED上顯示(程序訂閱和發(fā)布的主題都是Topic狂鞋,所以自己能收到自己發(fā)的)

串口監(jiān)視器
OLED顯示

此時(shí)可以打開(kāi)EMQ的控制臺(tái)片择,利用EMQ的WebSocket工具,訂閱主題Topic便可以查看收到的消息數(shù)據(jù)骚揍,也可以發(fā)送Topic主題的消息字管,其可以顯示在OLED屏上。

EMQ的WebSocket工具

七信不、iOS/Android手機(jī)端MQTTClient

相關(guān)MQTTClient的應(yīng)用各大應(yīng)用商城都可以搜到很多嘲叔,可以選擇下載一個(gè)進(jìn)行調(diào)試,查看效果抽活;也可以自己寫(xiě)一個(gè)簡(jiǎn)單的安卓MQTTClient程序硫戈,實(shí)現(xiàn)在安卓手機(jī)端查看心率值,前提是手機(jī)下硕、開(kāi)發(fā)板丁逝、部署了MQTT服務(wù)器的電腦在同一網(wǎng)絡(luò)下汁胆。 以下是自己寫(xiě)的一個(gè)簡(jiǎn)單的安卓MQTTClient客戶端的demo,可以訂閱霜幼、發(fā)布Topic主題的消息嫩码,在安卓手機(jī)上以Toast的形式顯示接收到的心率值。

安卓MQTTClient客戶端app截圖

安卓MQTTClient部分代碼:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末辛掠,一起剝皮案震驚了整個(gè)濱河市谢谦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌萝衩,老刑警劉巖回挽,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異猩谊,居然都是意外死亡千劈,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)牌捷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)墙牌,“玉大人,你說(shuō)我怎么就攤上這事暗甥∠脖酰” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵撤防,是天一觀的道長(zhǎng)虽风。 經(jīng)常有香客問(wèn)我,道長(zhǎng)寄月,這世上最難降的妖魔是什么辜膝? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮漾肮,結(jié)果婚禮上厂抖,老公的妹妹穿的比我還像新娘。我一直安慰自己克懊,他們只是感情好忱辅,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著保檐,像睡著了一般耕蝉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上夜只,一...
    開(kāi)封第一講書(shū)人閱讀 50,050評(píng)論 1 291
  • 那天垒在,我揣著相機(jī)與錄音,去河邊找鬼。 笑死场躯,一個(gè)胖子當(dāng)著我的面吹牛谈为,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播踢关,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼伞鲫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了签舞?” 一聲冷哼從身側(cè)響起秕脓,我...
    開(kāi)封第一講書(shū)人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎儒搭,沒(méi)想到半個(gè)月后吠架,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡搂鲫,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年傍药,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片魂仍。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拐辽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出擦酌,到底是詐尸還是另有隱情俱诸,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布赊舶,位于F島的核電站乙埃,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏锯岖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一甫何、第九天 我趴在偏房一處隱蔽的房頂上張望出吹。 院中可真熱鬧,春花似錦辙喂、人聲如沸捶牢。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)秋麸。三九已至,卻和暖如春炬太,著一層夾襖步出監(jiān)牢的瞬間灸蟆,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工亲族, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留炒考,地道東北人可缚。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像斋枢,于是被迫代替她去往敵國(guó)和親帘靡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容