目录
1、项目简介
2、整体项目架构设计
2、硬件架构设计
(1)硬件型号
(2)传感器驱动程序设计
3、基于MQTT协议的数据传输
4、微信小程序上位机设计
1.主界面
2. 健康监测系统?
3. 环境监测系统?????????
4. 显示实时动态曲线?????????
1、项目简介
????????该项目是利用STM32开发板进行开发的基于MQTT协议的物联网健康监测系统,并开发了微信小程序作为该项目的软件上位机。该产品可以用来实时监测人体的心率、血氧等生理参数,也可以监测家庭环境中的温湿度、烟雾浓度等环境参数,从而为您营造出一个健康的居住环境。此项目是本人在寒假利用业余时间开发的,从理论知识的学习到做出最终成品耗时将近两个月,可用于嵌入式软件方向的毕业设计。
????????该项目源码已全部开源,在本文中仅介绍核心代码原理,源码已发布在我的GitHub主页,如对源码有疑惑,或者对该项目有改进性意见,可以评论或私信本人,欢迎大家一起交流学习!
????????我的github主页:https://github.com/SichengLong26 ????????源代码地址:https://github.com/SichengLong26/health-iot
2、整体项目架构设计
????????该项目主要分为硬件架构设计和软件架构设计,硬件架构设计包括电路设计、PCB焊接、驱动程序编写、数据传输(数据上云)的程序编写;软件架构设计则包括前端UI界面的设计和后端的数据处理,此次软件上位机是基于微信小程序来开发的。
2、硬件架构设计
(1)硬件型号
主控芯片:STM32F103RCT6 传感器:MAX30102心率传感器、DHT11温湿度传感器、MQ2烟雾传感器 通信模块:ESP8266 WIFI模块
(2)传感器驱动程序设计
MAX30102心率血氧传感器(通过IIC驱动) ????????MAX3010VCC引脚连接STM32F103mini单片机的5伏引脚,GND连接5伏对应的GND,SCL连PC12,SDA连PC11,INT连PA5。MAX30102的其他引脚没有用到。 ????????本代码能够正常接收MAX30102心率血氧传感器返回的red与ir的数值,能够比较正常计算出心率血氧数值。当心率或血氧值的计算结果有误时对应的变量值为-999。
main.c
#include "delay.h"
#include "sys.h"
#include "usart.h"
#include "myiic.h"
#include "max30102.h"
#include "algorithm.h"
#define MAX_BRIGHTNESS 255
#define START 100
#define DATA_LENGTH 500
uint32_t aun_ir_buffer[DATA_LENGTH]; //IR LED sensor data
int32_t n_ir_buffer_length; //data length
uint32_t aun_red_buffer[DATA_LENGTH]; //Red LED sensor data
int32_t n_sp02; //SPO2 value
int8_t ch_spo2_valid; //indicator to show if the SP02 calculation is valid
int32_t n_heart_rate; //heart rate value
int8_t ch_hr_valid; //indicator to show if the heart rate calculation is valid
uint8_t uch_dummy;
int main(void)
{
uint32_t un_min, un_max, un_prev_data; //variables to calculate the on-board LED brightness that reflects the heartbeats
int i;
int32_t n_brightness;
float f_temp;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
delay_init(); //延时函数初始化
uart_init(9600); //串口初始化为115200
IIC_Init();
maxim_max30102_reset(); //resets the MAX30102
// initialize serial communication at 115200 bits per second:
//read and clear status register
maxim_max30102_read_reg(0,&uch_dummy);
maxim_max30102_init(); //initializes the MAX30102
n_brightness=0;
un_min=0x3FFFF;
un_max=0;
n_ir_buffer_length=DATA_LENGTH; //buffer length of 100 stores 5 seconds of samples running at 100sps
//read the first 500 samples, and determine the signal range
for(i=0;i<n_ir_buffer_length;i++)
{
while(PAin(5)==1); //wait until the interrupt pin asserts
maxim_max30102_read_fifo((aun_red_buffer+i), (aun_ir_buffer+i)); //read from MAX30102 FIFO
if(un_min>aun_red_buffer[i])
un_min=aun_red_buffer[i]; //update signal min
if(un_max<aun_red_buffer[i])
un_max=aun_red_buffer[i]; //update signal max
printf("心率:%i次/min,", aun_red_buffer[i]/1000);
printf("血氧=%i % \r\n", aun_ir_buffer[i]/1000+16);
}
un_prev_data=aun_red_buffer[i];
//calculate heart rate and SpO2 after first 500 samples (first 5 seconds of samples)
maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
while(1)
{
i=0;
un_min=0x3FFFF;
un_max=0;
//dumping the first 100 sets of samples in the memory and shift the last 400 sets of samples to the top
for(i=START;i<DATA_LENGTH;i++)
{
aun_red_buffer[i-START]=aun_red_buffer[i];
aun_ir_buffer[i-START]=aun_ir_buffer[i];
//update the signal min and max
if(un_min>aun_red_buffer[i])
un_min=aun_red_buffer[i];
if(un_max<aun_red_buffer[i])
un_max=aun_red_buffer[i];
}
//take 100 sets of samples before calculating the heart rate.
for(i=400;i<DATA_LENGTH;i++)
{
un_prev_data=aun_red_buffer[i-1];
while(PAin(5)==1);
maxim_max30102_read_fifo((aun_red_buffer+i), (aun_ir_buffer+i));
if(aun_red_buffer[i]>un_prev_data)//just to determine the brightness of LED according to the deviation of adjacent two AD data
{
f_temp=aun_red_buffer[i]-un_prev_data;
f_temp/=(un_max-un_min);
f_temp*=MAX_BRIGHTNESS;
n_brightness-=(int)f_temp;
if(n_brightness<0)
n_brightness=0;
}
else
{
f_temp=un_prev_data-aun_red_buffer[i];
f_temp/=(un_max-un_min);
f_temp*=MAX_BRIGHTNESS;
n_brightness+=(int)f_temp;
if(n_brightness>MAX_BRIGHTNESS)
n_brightness=MAX_BRIGHTNESS;
}
//re_oxen=(float)aun_red_buffer[i]/(float)aun_ir_buffer[i];
//oxen=45.06*re_oxen*re_oxen+30.354*re_oxen+94.845;
//send samples and calculation result to terminal program through UART
// printf("red=%i,", aun_red_buffer[i]);
// printf(" ir=%i,", aun_ir_buffer[i]);
}
maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
printf(" HR=%i,", n_heart_rate);
printf(" HRvalid=%i,", ch_hr_valid);
printf(" SpO2=%i,", n_sp02);
printf(" SPO2Valid=%i\r\n", ch_spo2_valid);
}
}
max30102.c(驱动程序)
#include "max30102.h"
#include "myiic.h"
#define max30102_WR_address 0xAE
bool maxim_max30102_write_reg(uint8_t uch_addr, uint8_t uch_data)
/**
* \brief Write a value to a MAX30102 register
* \par Details
* This function writes a value to a MAX30102 register
*
* \param[in] uch_addr - register address
* \param[in] uch_data - register data
*
* \retval true on success
*/
{
/* 第1步:发起I2C总线启动信号 */
IIC_Start();
/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */
/* 第3步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第4步:发送字节地址 */
IIC_Send_Byte(uch_addr);
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第5步:开始写入数据 */
IIC_Send_Byte(uch_data);
/* 第6步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 发送I2C总线停止信号 */
IIC_Stop();
return true; /* 执行成功 */
cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
/* 发送I2C总线停止信号 */
IIC_Stop();
return false;
}
bool maxim_max30102_read_reg(uint8_t uch_addr, uint8_t *puch_data)
/**
* \brief Read a MAX30102 register
* \par Details
* This function reads a MAX30102 register
*
* \param[in] uch_addr - register address
* \param[out] puch_data - pointer that stores the register data
*
* \retval true on success
*/
{
/* 第1步:发起I2C总线启动信号 */
IIC_Start();
/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */
/* 第3步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第4步:发送字节地址, */
IIC_Send_Byte((uint8_t)uch_addr);
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第6步:重新启动I2C总线。下面开始读取数据 */
IIC_Start();
/* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
IIC_Send_Byte(max30102_WR_address | I2C_RD); /* 此处是读指令 */
/* 第8步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第9步:读取数据 */
{
*puch_data = IIC_Read_Byte(); /* 读1个字节 */
IIC_NAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
}
/* 发送I2C总线停止信号 */
IIC_Stop();
return true; /* 执行成功 返回data值 */
cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
/* 发送I2C总线停止信号 */
IIC_Stop();
return false;
}
bool maxim_max30102_init(void)
/**
* \brief Initialize the MAX30102
* \par Details
* This function initializes the MAX30102
*
* \param None
*
* \retval true on success
*/
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC,ENABLE);//使能PORTA,PORTC时钟
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);//关闭jtag,使能SWD,可以用SWD模式调试
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;//PA5
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //PA5设置成浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA5
if(!maxim_max30102_write_reg(REG_INTR_ENABLE_1, 0xc0)) // INTR setting
return false;
if(!maxim_max30102_write_reg(REG_INTR_ENABLE_2, 0x00))
return false;
if(!maxim_max30102_write_reg(REG_FIFO_WR_PTR, 0x00)) //FIFO_WR_PTR[4:0]
return false;
if(!maxim_max30102_write_reg(REG_OVF_COUNTER, 0x00)) //OVF_COUNTER[4:0]
return false;
if(!maxim_max30102_write_reg(REG_FIFO_RD_PTR, 0x00)) //FIFO_RD_PTR[4:0]
return false;
if(!maxim_max30102_write_reg(REG_FIFO_CONFIG, 0x6f)) //sample avg = 8, fifo rollover=false, fifo almost full = 17
return false;
if(!maxim_max30102_write_reg(REG_MODE_CONFIG, 0x03)) //0x02 for Red only, 0x03 for SpO2 mode 0x07 multimode LED
return false;
if(!maxim_max30102_write_reg(REG_SPO2_CONFIG, 0x2F)) // SPO2_ADC range = 4096nA, SPO2 sample rate (400 Hz), LED pulseWidth (411uS)
return false;
if(!maxim_max30102_write_reg(REG_LED1_PA, 0x17)) //Choose value for ~ 4.5mA for LED1
return false;
if(!maxim_max30102_write_reg(REG_LED2_PA, 0x17)) // Choose value for ~ 4.5mA for LED2
return false;
if(!maxim_max30102_write_reg(REG_PILOT_PA, 0x7f)) // Choose value for ~ 25mA for Pilot LED
return false;
return true;
}
bool maxim_max30102_read_fifo(uint32_t *pun_red_led, uint32_t *pun_ir_led)
/**
* \brief Read a set of samples from the MAX30102 FIFO register
* \par Details
* This function reads a set of samples from the MAX30102 FIFO register
*
* \param[out] *pun_red_led - pointer that stores the red LED reading data
* \param[out] *pun_ir_led - pointer that stores the IR LED reading data
*
* \retval true on success
*/
{
uint32_t un_temp;
uint8_t uch_temp;
*pun_ir_led = 0;
*pun_red_led = 0;
maxim_max30102_read_reg(REG_INTR_STATUS_1, &uch_temp);
maxim_max30102_read_reg(REG_INTR_STATUS_2, &uch_temp);
/* 第1步:发起I2C总线启动信号 */
IIC_Start();
/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */
/* 第3步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第4步:发送字节地址, */
IIC_Send_Byte((uint8_t)REG_FIFO_DATA);
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第6步:重新启动I2C总线。下面开始读取数据 */
IIC_Start();
/* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
IIC_Send_Byte(max30102_WR_address | I2C_RD); /* 此处是读指令 */
/* 第8步:发送ACK */
if (IIC_Wait_Ack() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
un_temp = IIC_Read_Byte();
IIC_Ack();
un_temp <<= 16;
*pun_red_led += un_temp;
un_temp = IIC_Read_Byte();
IIC_Ack();
un_temp <<= 8;
*pun_red_led += un_temp;
un_temp = IIC_Read_Byte();
IIC_Ack();
*pun_red_led += un_temp;
un_temp = IIC_Read_Byte();
IIC_Ack();
un_temp <<= 16;
*pun_ir_led += un_temp;
un_temp = IIC_Read_Byte();
IIC_Ack();
un_temp <<= 8;
*pun_ir_led += un_temp;
un_temp = IIC_Read_Byte();
IIC_Ack();
*pun_ir_led += un_temp;
*pun_red_led &= 0x03FFFF; //Mask MSB [23:18]
*pun_ir_led &= 0x03FFFF; //Mask MSB [23:18]
/* 发送I2C总线停止信号 */
IIC_Stop();
return true;
cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
/* 发送I2C总线停止信号 */
IIC_Stop();
return false;
}
bool maxim_max30102_reset()
/**
* \brief Reset the MAX30102
* \par Details
* This function resets the MAX30102
*
* \param None
*
* \retval true on success
*/
{
if(!maxim_max30102_write_reg(REG_MODE_CONFIG, 0x40))
return false;
else
return true;
}
?alogrihm.c心率血氧算法:
/** \file algorithm.cpp ******************************************************
*
* Project: MAXREFDES117#
* Filename: algorithm.cpp
* Description: This module calculates the heart rate/SpO2 level
*
*
* --------------------------------------------------------------------
*
* This code follows the following naming conventions:
*
* char ch_pmod_value
* char (array) s_pmod_s_string[16]
* float f_pmod_value
* int32_t n_pmod_value
* int32_t (array) an_pmod_value[16]
* int16_t w_pmod_value
* int16_t (array) aw_pmod_value[16]
* uint16_t uw_pmod_value
* uint16_t (array) auw_pmod_value[16]
* uint8_t uch_pmod_value
* uint8_t (array) auch_pmod_buffer[16]
* uint32_t un_pmod_value
* int32_t * pn_pmod_value
*
* ------------------------------------------------------------------------- */
/*******************************************************************************
* Copyright (C) 2016 Maxim Integrated Products, Inc., All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL MAXIM INTEGRATED BE LIABLE FOR ANY CLAIM, DAMAGES
* OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* Except as contained in this notice, the name of Maxim Integrated
* Products, Inc. shall not be used except as stated in the Maxim Integrated
* Products, Inc. Branding Policy.
*
* The mere transfer of this software does not imply any licenses
* of trade secrets, proprietary technology, copyrights, patents,
* trademarks, maskwork rights, or any other form of intellectual
* property whatsoever. Maxim Integrated Products, Inc. retains all
* ownership rights.
*******************************************************************************
*/
#include "algorithm.h"
//uch_spo2_table is approximated as -45.060*ratioAverage* ratioAverage + 30.354 *ratioAverage + 94.845 ;
const uint8_t uch_spo2_table[184] = { 95, 95, 95, 96, 96, 96, 97, 97, 97, 97, 97, 98, 98, 98, 98, 98, 99, 99, 99, 99,
99, 99, 99, 99, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
100, 100, 100, 100, 99, 99, 99, 99, 99, 99, 99, 99, 98, 98, 98, 98, 98, 98, 97, 97,
97, 97, 96, 96, 96, 96, 95, 95, 95, 94, 94, 94, 93, 93, 93, 92, 92, 92, 91, 91,
90, 90, 89, 89, 89, 88, 88, 87, 87, 86, 86, 85, 85, 84, 84, 83, 82, 82, 81, 81,
80, 80, 79, 78, 78, 77, 76, 76, 75, 74, 74, 73, 72, 72, 71, 70, 69, 69, 68, 67,
66, 66, 65, 64, 63, 62, 62, 61, 60, 59, 58, 57, 56, 56, 55, 54, 53, 52, 51, 50,
49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 31, 30, 29,
28, 27, 26, 25, 23, 22, 21, 20, 19, 17, 16, 15, 14, 12, 11, 10, 9, 7, 6, 5,
3, 2, 1
} ;
void maxim_heart_rate_and_oxygen_saturation(uint32_t *pun_ir_buffer, int32_t n_ir_buffer_length, uint32_t *pun_red_buffer, int32_t *pn_spo2, int8_t *pch_spo2_valid,
int32_t *pn_heart_rate, int8_t *pch_hr_valid)
/**
* \brief Calculate the heart rate and SpO2 level
* \par Details
* By detecting peaks of PPG cycle and corresponding AC/DC of red/infra-red signal, the an_ratio for the SPO2 is computed.
* Since this algorithm is aiming for Arm M0/M3. formaula for SPO2 did not achieve the accuracy due to register overflow.
* Thus, accurate SPO2 is precalculated and save longo uch_spo2_table[] per each an_ratio.
*
* \param[in] *pun_ir_buffer - IR sensor data buffer
* \param[in] n_ir_buffer_length - IR sensor data buffer length
* \param[in] *pun_red_buffer - Red sensor data buffer
* \param[out] *pn_spo2 - Calculated SpO2 value
* \param[out] *pch_spo2_valid - 1 if the calculated SpO2 value is valid
* \param[out] *pn_heart_rate - Calculated heart rate value
* \param[out] *pch_hr_valid - 1 if the calculated heart rate value is valid
*
* \retval None
*/
{
uint32_t un_ir_mean;
int32_t k, n_i_ratio_count;
int32_t i, n_exact_ir_valley_locs_count, n_middle_idx;
int32_t n_th1, n_npks;
int32_t an_ir_valley_locs[15] ;
int32_t n_peak_interval_sum;
int32_t n_y_ac, n_x_ac;
int32_t n_spo2_calc;
int32_t n_y_dc_max, n_x_dc_max;
int32_t n_y_dc_max_idx, n_x_dc_max_idx;
int32_t an_ratio[5], n_ratio_average;
int32_t n_nume, n_denom ;
// calculates DC mean and subtract DC from ir
un_ir_mean = 0;
for (k = 0 ; k < n_ir_buffer_length ; k++ ) un_ir_mean += pun_ir_buffer[k] ;
un_ir_mean = un_ir_mean / n_ir_buffer_length ;
// remove DC and invert signal so that we can use peak detector as valley detector
for (k = 0 ; k < n_ir_buffer_length ; k++ )
an_x[k] = -1 * (pun_ir_buffer[k] - un_ir_mean) ;
// 4 pt Moving Average
for(k = 0; k < BUFFER_SIZE - MA4_SIZE; k++)
{
an_x[k] = ( an_x[k] + an_x[k + 1] + an_x[k + 2] + an_x[k + 3]) / (int)4;
}
// calculate threshold
n_th1 = 0;
for ( k = 0 ; k < BUFFER_SIZE ; k++)
{
n_th1 += an_x[k];
}
n_th1 = n_th1 / ( BUFFER_SIZE);
if( n_th1 < 30) n_th1 = 30; // min allowed
if( n_th1 > 60) n_th1 = 60; // max allowed
for ( k = 0 ; k < 15; k++) an_ir_valley_locs[k] = 0;
// since we flipped signal, we use peak detector as vSalley detector
maxim_find_peaks( an_ir_valley_locs, &n_npks, an_x, BUFFER_SIZE, n_th1, 4, 15 );//peak_height, peak_distance, max_num_peaks
n_peak_interval_sum = 0;
if (n_npks >= 2)
{
for (k = 1; k < n_npks; k++) n_peak_interval_sum += (an_ir_valley_locs[k] - an_ir_valley_locs[k - 1] ) ;
n_peak_interval_sum = n_peak_interval_sum / (n_npks - 1);
*pn_heart_rate = (int32_t)( (FS * 60) / n_peak_interval_sum );
*pch_hr_valid = 1;
}
else
{
*pn_heart_rate = -999; // unable to calculate because # of peaks are too small
*pch_hr_valid = 0;
}
// load raw value again for SPO2 calculation : RED(=y) and IR(=X)
for (k = 0 ; k < n_ir_buffer_length ; k++ )
{
an_x[k] = pun_ir_buffer[k] ;
an_y[k] = pun_red_buffer[k] ;
}
// find precise min near an_ir_valley_locs
n_exact_ir_valley_locs_count = n_npks;
//using exact_ir_valley_locs , find ir-red DC andir-red AC for SPO2 calibration an_ratio
//finding AC/DC maximum of raw
n_ratio_average = 0;
n_i_ratio_count = 0;
for(k = 0; k < 5; k++) an_ratio[k] = 0;
for (k = 0; k < n_exact_ir_valley_locs_count; k++)
{
if (an_ir_valley_locs[k] > BUFFER_SIZE )
{
*pn_spo2 = -999 ; // do not use SPO2 since valley loc is out of range
*pch_spo2_valid = 0;
return;
}
}
// find max between two valley locations
// and use an_ratio betwen AC compoent of Ir & Red and DC compoent of Ir & Red for SPO2
for (k = 0; k < n_exact_ir_valley_locs_count - 1; k++)
{
n_y_dc_max = -16777216 ;
n_x_dc_max = -16777216;
if (an_ir_valley_locs[k + 1] - an_ir_valley_locs[k] > 3)
{
for (i = an_ir_valley_locs[k]; i < an_ir_valley_locs[k + 1]; i++)
{
if (an_x[i] > n_x_dc_max)
{
n_x_dc_max = an_x[i];
n_x_dc_max_idx = i;
}
if (an_y[i] > n_y_dc_max)
{
n_y_dc_max = an_y[i];
n_y_dc_max_idx = i;
}
}
n_y_ac = (an_y[an_ir_valley_locs[k + 1]] - an_y[an_ir_valley_locs[k] ] ) * (n_y_dc_max_idx - an_ir_valley_locs[k]); //red
n_y_ac = an_y[an_ir_valley_locs[k]] + n_y_ac / (an_ir_valley_locs[k + 1] - an_ir_valley_locs[k]) ;
n_y_ac = an_y[n_y_dc_max_idx] - n_y_ac; // subracting linear DC compoenents from raw
n_x_ac = (an_x[an_ir_valley_locs[k + 1]] - an_x[an_ir_valley_locs[k] ] ) * (n_x_dc_max_idx - an_ir_valley_locs[k]); // ir
n_x_ac = an_x[an_ir_valley_locs[k]] + n_x_ac / (an_ir_valley_locs[k + 1] - an_ir_valley_locs[k]);
n_x_ac = an_x[n_y_dc_max_idx] - n_x_ac; // subracting linear DC compoenents from raw
n_nume = ( n_y_ac * n_x_dc_max) >> 7 ; //prepare X100 to preserve floating value
n_denom = ( n_x_ac * n_y_dc_max) >> 7;
if (n_denom > 0 && n_i_ratio_count < 5 && n_nume != 0)
{
an_ratio[n_i_ratio_count] = (n_nume * 100) / n_denom ; //formular is ( n_y_ac *n_x_dc_max) / ( n_x_ac *n_y_dc_max) ;
n_i_ratio_count++;
}
}
}
// choose median value since PPG signal may varies from beat to beat
maxim_sort_ascend(an_ratio, n_i_ratio_count);
n_middle_idx = n_i_ratio_count / 2;
if (n_middle_idx > 1)
n_ratio_average = ( an_ratio[n_middle_idx - 1] + an_ratio[n_middle_idx]) / 2; // use median
else
n_ratio_average = an_ratio[n_middle_idx ];
if( n_ratio_average > 2 && n_ratio_average < 184)
{
n_spo2_calc = uch_spo2_table[n_ratio_average] ;
*pn_spo2 = n_spo2_calc ;
*pch_spo2_valid = 1;// float_SPO2 = -45.060*n_ratio_average* n_ratio_average/10000 + 30.354 *n_ratio_average/100 + 94.845 ; // for comparison with table
}
else
{
*pn_spo2 = -999 ; // do not use SPO2 since signal an_ratio is out of range
*pch_spo2_valid = 0;
}
}
void maxim_find_peaks( int32_t *pn_locs, int32_t *n_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height, int32_t n_min_distance, int32_t n_max_num )
/**
* \brief Find peaks
* \par Details
* Find at most MAX_NUM peaks above MIN_HEIGHT separated by at least MIN_DISTANCE
*
* \retval None
*/
{
maxim_peaks_above_min_height( pn_locs, n_npks, pn_x, n_size, n_min_height );
maxim_remove_close_peaks( pn_locs, n_npks, pn_x, n_min_distance );
*n_npks = min( *n_npks, n_max_num );
}
void maxim_peaks_above_min_height( int32_t *pn_locs, int32_t *n_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height )
/**
* \brief Find peaks above n_min_height
* \par Details
* Find all peaks above MIN_HEIGHT
*
* \retval None
*/
{
int32_t i = 1, riseFound = 0, holdOff1 = 0, holdOff2 = 0, holdOffThresh = 4;
*n_npks = 0;
while (i < n_size - 1)
{
if (holdOff2 == 0)
{
if (pn_x[i] > n_min_height && pn_x[i] > pn_x[i - 1]) // find left edge of potential peaks
{
riseFound = 1;
}
if (riseFound == 1)
{
if ((pn_x[i] < n_min_height) && (holdOff1 < holdOffThresh)) // if false edge
{
riseFound = 0;
holdOff1 = 0;
}
else
{
if (holdOff1 == holdOffThresh)
{
if ((pn_x[i] < n_min_height) && (pn_x[i - 1] >= n_min_height))
{
if ((*n_npks) < 15 )
{
pn_locs[(*n_npks)++] = i; // peak is right edge
}
holdOff1 = 0;
riseFound = 0;
holdOff2 = 8;
}
}
else
{
holdOff1 = holdOff1 + 1;
}
}
}
}
else
{
holdOff2 = holdOff2 - 1;
}
i++;
}
}
void maxim_remove_close_peaks(int32_t *pn_locs, int32_t *pn_npks, int32_t *pn_x, int32_t n_min_distance)
/**
* \brief Remove peaks
* \par Details
* Remove peaks separated by less than MIN_DISTANCE
*
* \retval None
*/
{
int32_t i, j, n_old_npks, n_dist;
/* Order peaks from large to small */
maxim_sort_indices_descend( pn_x, pn_locs, *pn_npks );
for ( i = -1; i < *pn_npks; i++ )
{
n_old_npks = *pn_npks;
*pn_npks = i + 1;
for ( j = i + 1; j < n_old_npks; j++ )
{
n_dist = pn_locs[j] - ( i == -1 ? -1 : pn_locs[i] ); // lag-zero peak of autocorr is at index -1
if ( n_dist > n_min_distance || n_dist < -n_min_distance )
pn_locs[(*pn_npks)++] = pn_locs[j];
}
}
// Resort indices int32_to ascending order
maxim_sort_ascend( pn_locs, *pn_npks );
}
void maxim_sort_ascend(int32_t *pn_x, int32_t n_size)
/**
* \brief Sort array
* \par Details
* Sort array in ascending order (insertion sort algorithm)
*
* \retval None
*/
{
int32_t i, j, n_temp;
for (i = 1; i < n_size; i++)
{
n_temp = pn_x[i];
for (j = i; j > 0 && n_temp < pn_x[j - 1]; j--)
pn_x[j] = pn_x[j - 1];
pn_x[j] = n_temp;
}
}
void maxim_sort_indices_descend( int32_t *pn_x, int32_t *pn_indx, int32_t n_size)
/**
* \brief Sort indices
* \par Details
* Sort indices according to descending order (insertion sort algorithm)
*
* \retval None
*/
{
int32_t i, j, n_temp;
for (i = 1; i < n_size; i++)
{
n_temp = pn_indx[i];
for (j = i; j > 0 && pn_x[n_temp] > pn_x[pn_indx[j - 1]]; j--)
pn_indx[j] = pn_indx[j - 1];
pn_indx[j] = n_temp;
}
}
DHT11温湿度传感器驱动程序:?
dht11.c
#include "dht11.h"
#include "delay.h"
//由于DHT11为单总线通信,即发送、接收都为同一根数据线,STM32的GPIO无法像51的IO同时配置为输入输出模式,
//因此需要将与DHT11数据线相连的GPIO写两套初始化函数,向DHT11发送数据时先调用DHT11_IO_OUT()函数,再
//发送数据,接收DHT11的数据时先调用DHT11_IO_IN()函数,再接收数据
void DHT11_IO_OUT(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// RCC_APB2PeriphClockCmd(DHT11_GPIO_CLK|RCC_APB2Periph_AFIO, ENABLE); //使能PG端口时钟
//GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); //由于PA15为JTAG调试接口,需要先禁用JTAG功能才能作为普通的GPIO口
//使用,若使用的是普通的GPIO,可将 RCC_APB2Periph_AFIO 与 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE) 去掉 //禁用JTAG
GPIO_InitStructure.GPIO_Pin = DHT11_GPIO_PIN; //PG11端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStructure); //初始化IO口
}
void DHT11_IO_IN(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(DHT11_GPIO_CLK|RCC_APB2Periph_AFIO, ENABLE); //使能PG端口时钟
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); //禁用JTAG
GPIO_InitStructure.GPIO_Pin = DHT11_GPIO_PIN; //PG11端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //推挽输出
GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStructure); //初始化IO口
}
//复位DHT11
void DHT11_Rst(void)
{
DHT11_IO_OUT(); //SET OUTPUT
DHT11_DQ_OUT=0; //拉低DQ
delay_ms(20); //拉低至少18ms
DHT11_DQ_OUT=1; //DQ=1
delay_us(30); //主机拉高20~40us
}
//等待DHT11的回应
//返回1:未检测到DHT11的存在
//返回0:存在
u8 DHT11_Check(void)
{
u8 retry=0;
DHT11_IO_IN();//SET INPUT
while (DHT11_DQ_IN&&retry<100)//DHT11会拉低40~80us
{
retry++;
delay_us(1);
};
if(retry>=100)return 1;
else retry=0;
while (!DHT11_DQ_IN&&retry<100)//DHT11拉低后会再次拉高40~80us
{
retry++;
delay_us(1);
};
if(retry>=100)return 1;
return 0;
}
//从DHT11读取一个位
//返回值:1/0
u8 DHT11_Read_Bit(void)
{
u8 retry=0;
while(DHT11_DQ_IN&&retry<100)//等待变为低电平
{
retry++;
delay_us(1);
}
retry=0;
while(!DHT11_DQ_IN&&retry<100)//等待变高电平
{
retry++;
delay_us(1);
}
delay_us(40);//等待40us
if(DHT11_DQ_IN)return 1;
else return 0;
}
//从DHT11读取一个字节
//返回值:读到的数据
u8 DHT11_Read_Byte(void)
{
u8 i,dat;
dat=0;
for (i=0;i<8;i++)
{
dat<<=1;
dat|=DHT11_Read_Bit();
}
return dat;
}
//从DHT11读取一次数据
//temp:温度值(范围:0~50°)
//humi:湿度值(范围:20%~90%)
//返回值:0,正常;1,读取失败
u8 DHT11_Read_Data(u8 *temp,u8 *humi)
{
u8 buf[5];
u8 i;
DHT11_Rst();
if(DHT11_Check()==0)
{
for(i=0;i<5;i++)//读取40位数据
{
buf[i]=DHT11_Read_Byte();
}
if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4])
{
*humi=buf[0];
*temp=buf[2];
}
}else return 1;
return 0;
}
//初始化DHT11的IO口 DQ 同时检测DHT11的存在
//返回1:不存在
//返回0:存在
u8 DHT11_Init(void)
{
DHT11_Rst(); //复位DHT11
return DHT11_Check();//等待DHT11的回应
}
MQ2烟雾传感器驱动程序:?
bsp_adc.c
#include "bsp_adc.h"
#define ADC1_DR_Address ((u32)0x40012400+0x4c) //定义ADC的内存地址
#include <math.h>
static int floag1=0;
#define CAL_PPM 20 // 校准环境中PPM值
#define RL 5 // RL阻值
static float R0=1; // 元件在洁净空气中的阻值
float ppm;
__IO uint16_t ADC_ConvertedValue;
static void ADC1_GPIO_Config(void) //ADC端口配置
{
GPIO_InitTypeDef GPIO_InitStructure;//GPIO初始化结构体;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//使能DMA的时钟;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOC, ENABLE);//打开ADC1和GPIOC的时钟;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//配置PC0引脚;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//设置工作模式为模拟输入;
GPIO_Init(GPIOC, &GPIO_InitStructure);//初始化GPIOC;
}
static void ADC1_Mode_Config(void) //配置ADC1的模式
{
/********以下是有关DMA的相关配置*************/
DMA_InitTypeDef DMA_InitStructure;//DMA初始化结构体定义DMA初始化变量
ADC_InitTypeDef ADC_InitStructure;//ADC初始化结构体定义ADC初始化变量
DMA_DeInit(DMA1_Channel1);//设置DMA1通道1
DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address;//设定ADC的地址;
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&ADC_ConvertedValue;//内存地址,采集的数据存在这里;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//设为源,表示数据是从这里出发的;
DMA_InitStructure.DMA_BufferSize = 1;//因为一次只发送一个数据所以设为1;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable;//因为只涉及一路数据的采集发送因此内存地址不变
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//因为只涉及一路数据的采集发送因此外设地址不变
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设至少要半字即16位才可以满足要求
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//内存至少要半字即16位才可以满足要求
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//DMA模式为循环传输,因为要采集多次;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;//设置为高、中、低优先级都可以因为只有一路在采集
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//关闭内存到内存的传输,因为我们需要的是外设传到内存的传输
DMA_Init(DMA1_Channel1,&DMA_InitStructure);//DMA1通道1最后初始化
DMA_Cmd(DMA1_Channel1, ENABLE);//使能DMA1的通道1;
/********以下是有关ADC的相关配置*************/
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//设置为独立ADC模式,因为其采集只有一个通道;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;//禁止扫描模式,扫描模式适用于多通道采集
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//开启连续转换,以不停地进行ADC转换
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//不使用外部触发转换,而使用内部软件触发
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//采集数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1;//ADC number of channel,即要转换的通道数目;
ADC_Init(ADC1, &ADC_InitStructure);//调用ADC初始化库函数
RCC_ADCCLKConfig(RCC_PCLK2_Div8);//配置ADC时钟为8分频,9MHZ
ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_55Cycles5);//配置ADC1的通道为55.5个采样周期,
ADC_DMACmd(ADC1, ENABLE);//使能ADC1的DMA传输
ADC_Cmd(ADC1, ENABLE);//使能ADC1;
while(ADC_GetResetCalibrationStatus(ADC1));//等待校准寄存器复位完成;
ADC_StartCalibration(ADC1);//调用校准函数开始ADC校准;
while(ADC_GetResetCalibrationStatus(ADC1));//等待校准寄存器复位完成;
ADC_SoftwareStartConvCmd(ADC1, ENABLE);//前面不采用外部触发,而是采用内部软件触发,此处使能软件触发
}
void ADC1_Init(void)
{
ADC1_GPIO_Config();
ADC1_Mode_Config();
}
// 传感器校准函数
void MQ2_PPM_Calibration(float RS)
{
R0 = RS / pow(CAL_PPM / 613.9f, 1 / -2.074f);
}
// MQ2传感器数据处理
float MQ2_GetPPM(void)
{
float Vrl = (float) ADC_ConvertedValue/4096*3.3;
float RS = (3.3f - Vrl) / Vrl * RL;
if(Vrl>1&&floag1==0) // 获取系R0
{
MQ2_PPM_Calibration(RS);
floag1=1;
}
ppm = 613.9f * pow(RS/R0, -2.074f);
return ppm;
}
3、基于MQTT协议的数据传输
????????STM32开发板通过ESP8266模块连接上局域网(也就是我们家庭中的路由器、WIFI),同时ESP8266通过互联网连接到远程MQTT服务器(MQTT Server),这样便达到了将传感器所采集到的数据储存在云端,可供多个客户端进行访问。 ?????????在数据传输中,用到了一种很核心的物联网数据传输协议:MQTT协议。MQTT是一个客户端服务端架构的发布/订阅模式的消息传输协议。它的设计思想是轻巧、开放、简单、规范,易于实现。这些特点使得它对很多场景来说都是很好的选择,特别是对于受限的环境如机器与机器的通信(M2M)以及物联网环境(IoT)。它的基本原理是:一个客户端(ESP8266)向服务器发布(Publish)一个带有传感采集到的数据的主题,则另外一个客户端(软件上位机)则通过向服务器订阅(Subscribe)服务器上ESP8266客户端发布的主题,即可以接收到各传感器采集到的数据,从而达到实时显示、实时检测的目的。 ????????同时数据在传输的过程中都是以JSON格式进行传输的。
?核心代码介绍:
????????由于该部分核心代码过多,就不在本文展示,请到我的github源码仓库进行访问,在这里简要对一些程序文件进行说明:
esp8266.c?: ESP8266模块的驱动程序 onenet.c : 数据上传云域网程序 MqttKit.c : 客户端对服务器进行一系列数据传输程序(例如主题的发布、订阅)
4、微信小程序上位机设计
????????小程序是一种全新的连接用户与服务的方式,它可以在微信内被便捷地获取和传播,同时具有出色的使用体验。同时因为它不像安卓APP一样需要下载,可以免安装使用,有一定的便利性,所以本项目采用微信小程序来开发上位机。下面该软件上位机的各个UI界面及其所对应的功能
1.主界面
????????这是该软件的首页,通过和风天气平台的支持,可以获取到当前你所在地的天气状况,同时正中间的三个控件可以进入三个不同的系统。
代码:
index.wxml:
<!--pages/home/home.wxml-->
<view class="wrapper">
<view class="header-wrapper" bindtap="toDetail">
<view class="header-title">
<text>空气质量-{{weather_quality}}</text>
<text>{{city}}-{{area}}</text>
</view>
<view class="header-text">
<text>{{weather_temp}}℃</text>
<text>{{weather_text}}</text>
</view>
<view class="weather-advice">
<text>{{advice}}</text>
</view>
</view>
<view class="botton-wrapper">
<button id="btn1" bindtap="toEnv">环境监测系统</button>
<button id="btn2" bindtap="toHealth">健康监测系统</button>
<button id="btn2" bindtap="toCanvas">查看实时动态曲线</button>
</view>
</view>
?index.wxss
/* pages/home/home.wxss */
.wrapper{
padding: 30rpx 20rpx;
/* background-color: beige; */
}
.header-wrapper{
background-color: #3d7ef6;
border-radius: 40rpx;
padding: 30rpx 50rpx;
box-shadow: #d6d6d6 1px 1px 1px;
color: floralwhite;
}
.header-title{
display: flex;
font-size: 30rpx;
justify-content: space-between;
}
.header-text{
display: flex;
font-size: 48rpx;
font-weight: 400;
padding: 7rpx 0rpx;
justify-content: space-between;
}
.weather-advice{
font-size: 26rpx;
margin-top: 50rpx;
}
.botton-wrapper{
padding: 240rpx 30rpx;
}
#btn1{
height: 80rpx;
width: 60% ;
color: white;
font-size: 30rpx ;
background-color: #3d7ef6 ;
margin-top: 5rpx;
justify-content: center;
box-shadow: #d6d6d6 2px 2px 2px;
border-radius: 40rpx;
}
#btn2{
height: 80rpx;
width: 60% ;
color: white;
font-size: 30rpx ;
background-color: #3d7ef6 ;
margin-top: 50rpx;
justify-content: center;
box-shadow: #d6d6d6 2px 2px 2px;
border-radius: 40rpx;
}
?index.js
Page({
/**
* 页面的初始数据
*/
data: {
weather_quality:"请求中",
weather_text:"请求中",
weather_temp:"请求中",
city:"请求中",
area:"请求中",
advice:"请求中"
},
toEnv(){
wx.navigateTo({
url: '../env/env'
})
},
toHealth(){
wx.navigateTo({
url: '../healthy/healthy'
})
},
toCanvas(){
wx.navigateTo({
url: '../curves/curves'
})
},
toDetail(){
wx.navigateTo({
url: '../weather/weather'
})
},
toBluetooth(){
wx.navigateTo({
url: '../bluetooth/bluetooth'
})
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
},
onShow: function () {
var that=this
wx.getLocation({
success(res){
const {latitude} = res
const {longitude} = res
const key = '1c6a3dc86f2544a3b18828ca409858c9'
// 请求温度与天气状况
wx.request({
url: `https://devapi.qweather.com/v7/weather/now?location=${longitude},${latitude}&key=${key}`,
success(res){
// console.log(res)
that.setData({
weather_temp:res.data.now.temp,
weather_text:res.data.now.text
})
}
})
// 地理位置
wx.request({
url: `https://geoapi.qweather.com/v2/city/lookup?location=${longitude},${latitude}&key=${key}`,
success(res){
// console.log(res)
that.setData({
city:res.data.location[0].adm2,
area:res.data.location[0].name
})
}
})
//指数
wx.request({
url: `https://devapi.qweather.com/v7/indices/1d?location=${longitude},${latitude}&key=${key}&type=${0}`,
success(res){
// console.log(res)
that.setData({
advice:res.data.daily[2].text
})
}
})
//空气质量
wx.request({
url: `https://devapi.qweather.com/v7/air/now?location=${longitude},${latitude}&key=${key}`,
success(res){
console.log(res)
that.setData({
weather_quality:res.data.now.category
})
}
})
}
})
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {
}
})
2. 健康监测系统?
用于显示MAX30102心率血氧传感器所采集到的人体的心率、血氧值,可以对其进行实时监测并显示
healthy.wxml :
<view class="body-wrapper">
<view class="sensor-others">
<image class="sensor-logo-water" src="../../icon/heart.png"/>
<view class="sensor-text">
<view class="sensor-title">实时心率</view>
<view class="sensor-value">{{Heart}}</view>
</view>
</view>
</view>
<view class="body-wrapper">
<view class="sensor-others">
<image class="sensor-logo-oxygen" src="../../icon/oxygen.png"/>
<view class="sensor-text">
<view class="sensor-title">血氧浓度</view>
<view class="sensor-value">{{Spo2}}%</view>
</view>
</view>
</view>
healty.wxss:?
/* pages/healthy/healthy.wxss *//* pages/env/env.wxss */
.body-wrapper{
padding: 30rpx 20rpx;
}
.sensor{
width: 100%;
height: 190rpx;
border-radius: 40rpx;
box-shadow: #d6d6d6 1px 1px 5px;
margin-top: 50rpx;
display: flex;
justify-content: space-between;
}
.sensor-logo{
padding: 20rpx 30rpx;
height: 130rpx;
width: 190rpx;
margin-top: 10rpx;
}
.sensor-logo-water{
padding: 20rpx 50rpx;
height: 130rpx;
width: 180rpx;
margin-top: 10rpx;
}
.sensor-logo-oxygen{
padding: 20rpx 90rpx;
height: 118rpx;
width: 120rpx;
margin-top: 10rpx;
}
.sensor-text{
padding: 0rpx 120rpx;
margin-top: 26rpx;
color: #2e2e2e;
}
.sensor-title{
font-size: 35rpx;
}
.sensor-value{
font-size: 66rpx;
}
.sensor-others{
width: 100%;
height: 190rpx;
border-radius: 40rpx;
box-shadow: #d6d6d6 1px 1px 5px;
margin-top: 0rpx;
display: flex;
justify-content: space-between;
}
healty.js?
var mqtt=require('../../utils/mqtt.min.js')
let client=null
Page({
data: {
Heart:0,
Spo2:0
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
this.connectmqtt()
},
connectmqtt:function(){
var that=this
const options={
connectTimeout:4000,
clientId:"d1f22er224",
port:8084,
username:'f585c3d5f499ffea9b710f13709a855d',
password:'123456'
}
client=mqtt.connect('wxs://t.yoyolife.fun/mqtt',options)
client.on('connect',(e)=>{
console.log("mqtt服务器连接成功")
client.subscribe('/iot/943/pub',
{qos:0},function(err){
if(!err){
console.log("订阅成功!")
}
})
})
client.on('message',function(topic,message){
let dataFrameDev=[]
dataFrameDev=JSON.parse(message)
console.log(dataFrameDev)
})
}
})
3. 环境监测系统?????????
????????用于显示DHT11温湿度传感器、MQ2传感器所采集到的环境参数数据,可以对其进行实时监测并显示。
env.wxml:
<!--pages/env/env.wxml-->
<view class="body-wrapper">
<view class="sensor">
<image class="sensor-logo" src="../../icon/temperature.png"/>
<view class="sensor-text">
<view class="sensor-title">实时温度</view>
<view class="sensor-value">{{Temp}}℃</view>
</view>
</view>
</view>
<view class="body-wrapper">
<view class="sensor-others">
<image class="sensor-logo-water" src="../../icon/water.png"/>
<view class="sensor-text">
<view class="sensor-title">实时湿度</view>
<view class="sensor-value">{{Hum}}%</view>
</view>
</view>
</view>
<view class="body-wrapper">
<view class="sensor-others">
<image class="sensor-logo-smoke" src="../../icon/smoke.png"/>
<view class="sensor-text">
<view class="sensor-title">烟雾浓度</view>
<view class="sensor-value">{{Smoke}}</view>
</view>
</view>
</view>
<view class="body-wrapper">
<view class="sensor-others">
<image class="sensor-logo" src="../../icon/lx.png"/>
<view class="sensor-text">
<view class="sensor-title">光照度</view>
<view class="sensor-value">29lx</view>
</view>
</view>
</view>
<view class="control-wrapper">
<view class="body-wrapper">
<view class="control">
<image class="control-logo" src="../../icon/beep.png"/>
<view class="control-text">
<view class="control-title">报警器</view>
<view class="control-value">
<switch bindchange="handleLED" checked="{{true}}"></switch>
</view>
</view>
</view>
</view>
<view class="body-wrapper">
<view class="control">
<image class="control-logo" src="../../icon/led.png"/>
<view class="control-text">
<view class="control-title">房间灯</view>
<view class="control-value">
<switch bindchange="handleLED" checked="{{true}}"></switch>
</view>
</view>
</view>
<view>{{event}}</view>
</view>
</view>
?env.wxss
/* pages/env/env.wxss */
.body-wrapper{
padding: 30rpx 20rpx;
}
.sensor{
width: 100%;
height: 190rpx;
border-radius: 40rpx;
box-shadow: #d6d6d6 1px 1px 5px;
margin-top: 50rpx;
display: flex;
justify-content: space-between;
}
.sensor-logo{
padding: 20rpx 30rpx;
height: 130rpx;
width: 190rpx;
margin-top: 10rpx;
}
.sensor-logo-water{
padding: 20rpx 50rpx;
height: 130rpx;
width: 180rpx;
margin-top: 10rpx;
}
.sensor-logo-smoke{
padding: 20rpx 50rpx;
height: 130rpx;
width: 140rpx;
margin-top: 10rpx;
}
.sensor-text{
padding: 0rpx 120rpx;
margin-top: 26rpx;
color: #2e2e2e;
}
.sensor-title{
font-size: 35rpx;
}
.sensor-value{
font-size: 66rpx;
}
.sensor-others{
width: 100%;
height: 190rpx;
border-radius: 40rpx;
box-shadow: #d6d6d6 1px 1px 5px;
margin-top: 0rpx;
display: flex;
justify-content: space-between;
}
.control{
width: 100%;
height: 190rpx;
border-radius: 40rpx;
box-shadow: #d6d6d6 1px 1px 5px;
margin-top: 0rpx;
display: flex;
justify-content: space-between;
}
.control-logo{
padding: 40rpx 30rpx;
height: 100rpx;
width: 100rpx;
margin-top: 10rpx;
}
.control-text{
padding: 0rpx 30rpx;
margin-top: 26rpx;
color: #2e2e2e;
}
.control-title{
font-size: 33rpx;
}
.control-value{
font-size: 60rpx;
}
.control-wrapper{
display: flex;
padding: 0rpx 19.5rpx;
}
?env.js
var mqtt=require('../../utils/mqtt.min.js')
var client=null
Page({
/**
* 页面的初始数据
*/
data: {
Temp:0,
Hum:0,
Smoke:0,
Led:false,
Beep:false,
event:""
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(){
this.connectmqtt()
},
connectmqtt:function(){
var that=this
const options={
connectTimeout:4000,
clientId:"df2er24",
port:8084,
username:'f585c3d5f499ffea9b710f13709a855d',
password:'123456'
}
client=mqtt.connect('wxs://t.yoyolife.fun/mqtt',options)
client.on('connect',(e)=>{
console.log("服务器连接成功")
client.subscribe('/iot/943/pub',{qos:0},function(err){
if(!err){
console.log("订阅成功")
}
})
})
// 信息监听事件
client.on('message', function(topic,message){
// console.log(topic)
let dataFrameDev ={}
dataFrameDev = JSON.parse(message)
console.log(dataFrameDev)
that.setData({
Temp:dataFrameDev.Temp,
Hum:dataFrameDev.Hum,
Smoke:dataFrameDev.Smoke
})
console.log(that.data.Temp)
})
client.on('reconnect', (error)=>{
console.log('正在重新连接'+error)
})
client.on('error', (error)=>{
console.log('连接失败')
})
},
handleLED(e){
var that=this
// console.log(e)
let {value}=e.detail
// console.log(value)
that.setData({
Led:value,
})
if(value===true){
that.setData({
event:"您已开灯!",
})
client.publish('/iot/943/sub','{"target":"LED","value":1}',function(err){
if(!err){
console.log("成功发布开灯命令")
}
})
}else{
that.setData({
event:""
})
client.publish('/iot/943/sub','{"target":"LED","value":0}',function(err){
if(!err){
console.log("成功发布关灯命令")
}
})
}
}
})
4. 显示实时动态曲线?????????
????????为了能显示出一段时间内各个参数的动态变化过程,这里开发了一个实时动态曲线功能,每当接收到服务端传来的消息,则刷新一次曲线从而达到实时更新的目的。
curves.wxml
<Tabs list="{{list}}" binditemChange="handleItemChange">
<block wx:if="{{list[0].isActive}}" class="tabs">
<view class="body">
<view class="body-content">
<view class="body-title">温湿度、烟雾浓度实时曲线图</view>
<view class="meandata-body">
<view class="meandata">
<view>
<view>平均浓度</view>
<view>{{smoke_average}}bpm</view>
</view>
<view>
<view>平均湿度</view>
<view>{{hum_average}}%</view>
</view>
<view>
<view>平均温度</view>
<view>{{temp_average}}℃</view>
</view>
</view>
</view>
<canvas canvas-id="lineCanvas" disable-scroll="true" class="canvas" bindtouchstart="touchHandler"></canvas>
<view class="timestyle">{{time}}</view>
</view>
</view>
</block>
<block wx:elif="{{list[1].isActive}}">
<view class="detail-title">
<view class="title-time">时间</view>
<view class="title-temp">温度</view>
<view class="title-hum">湿度</view>
<view class="title-smoke">烟雾浓度</view>
</view>
<view class="data">
<view class="data-time">
<view wx:for="{{time_array}}" wx:key="*this">{{item}}</view>
</view>
<view class="data-temp">
<view wx:for="{{Temp_array}}" wx:key="*this" class="data-temp">{{item}}℃</view>
</view>
<view class="data-hum">
<view wx:for="{{Hum_array}}" wx:key="*this" class="data-hum">{{item}}%</view>
</view>
<view class="data-smoke">
<view wx:for="{{Smoke_array}}" wx:key="*this" class="data-smoke">{{item}}bpm</view>
</view>
</view>
</block>
</Tabs>
curves.wxss?
page {
background-color: rgba(239, 239, 240);
}
.body{
padding: 0rpx 20rpx;
margin-top: 100rpx;
}
.body-content{
background-color:#ffffff;
border-radius: 40rpx;
}
.body-title{
display: flex;
justify-content: center;
font-size: 30rpx;
padding: 50rpx 0rpx 20rpx;
}
.meandata-body{
padding: 20rpx 39rpx;
}
.meandata{
display: flex;
justify-content: space-around;
border-bottom: 1rpx solid rgba(216, 216, 216, 1);
border-top: 1rpx solid rgba(216, 216, 216, 1);
padding: 24rpx;
}
.canvas {
width: 100%;
height: 550rpx;
}
.timestyle{
padding: 50rpx 165rpx;
}
.detail-title{
display: flex;
}
.data-temp{
padding: 19rpx 30rpx;
}
.data-hum{
padding: 19rpx 30rpx;
}
.data-smoke{
padding: 19rpx 30rpx;
}
.data{
display: flex;
}
.title-time{
padding: 0rpx 0rpx 0rpx 60rpx;
}
.title-temp{
padding: 0rpx 0rpx 0rpx 111rpx;
}
.title-hum{
padding: 0rpx 0rpx 0rpx 117rpx;
}
.title-smoke{
padding: 0rpx 0rpx 0rpx 140rpx;
}
curves.js
// pages/index/lookrecord/lookrecord.js
var wxCharts = require('../../utils/wxcharts.js'); //引入wxChart文件
var mqtt=require('../../utils/mqtt.min.js') // 引入mqtt文件
var util = require("../../utils/util.js");
var client=null
var app = getApp();
var lineChart = null;
Page({
/**
* 页面的初始数据
*/
data: {
list:[
{
id:0,
name:"趋势图",
isActive:true
},{
id:1,
name:"数据记录",
isActive:false
},
],
time:"",
xtime:"",
Temp:0,
temp_average:0,
temp_sum:0,
Hum:0,
hum_average:0,
hum_sum:0,
Smoke:0,
smoke_average:0,
smoke_sum:0,
Temp_array:[],
Hum_array:[],
Smoke_array:[],
time_array:[],
waterwaterdata:[50, 100, 80, 115, 120, 90, 125],
smokesmokedata:[60, 70, 90, 105, 120, 130, 95],
tempdata: [60,90, 60, 110,120,105,70], //数据点
categories: ['2018-6-13', '2018-6-14', '2018-6-15', '2018-6-16', '2018-6-17', '2018-6-18', '2018-6-19'], //模拟的x轴横坐标参数
},
touchHandler: function (e) {
lineChart.showToolTip(e, {
// background: '#7cb5ec',
format: function (item, category) {
return category + ' ' + item.name + ':' + item.data
}
});
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(){
this.curve()
this.connectmqtt()
// this.gettime()
},
onShow:function(){
this.notification() // 调用方法
},
connectmqtt:function(){
var that=this
const options={
connectTimeout:4000,
clientId:"df2er24",
port:8084,
username:'f585c3d5f499ffea9b710f13709a855d',
password:'123456'
}
client=mqtt.connect('wxs://t.yoyolife.fun/mqtt',options)
client.on('connect',(e)=>{
console.log("服务器连接成功")
client.subscribe('/iot/943/pub',{qos:0},function(err){
if(!err){
console.log("订阅成功")
}
})
})
// 信息监听事件
client.on('message', function(topic,message){
// console.log(topic)
let dataFrameDev ={}
dataFrameDev = JSON.parse(message)
console.log(dataFrameDev)
that.setData({
Temp:dataFrameDev.Temp,
Hum:dataFrameDev.Hum,
Smoke:dataFrameDev.Smoke,
})
// 设置温度、湿度、烟雾浓度的数组
that.setData({
Temp_array:that.data.Temp_array.concat(that.data.Temp),
Hum_array:that.data.Hum_array.concat(that.data.Hum),
Smoke_array:that.data.Smoke_array.concat(that.data.Smoke)
})
console.log(that.data.Temp)
console.log(that.data.Temp_array)
console.log(that.data.Hum_array)
console.log(that.data.Smoke_array)
// 获取到sensor data 后开始获取本地时间
var xtime=that.data.xtime
that.setData({
xtime:util.formatTime(new Date())
})
console.log(that.data.xtime)
that.setData({
time_array:that.data.time_array.concat(that.data.xtime)
})
// 求烟雾浓度平均值
var smoke_average=that.data.smoke_average
var Smoke_array=that.data.Smoke_array
var smoke_sum=that.data.smoke_sum
that.setData({
smoke_sum:smoke_sum+that.data.Smoke,
smoke_average:parseInt(that.data.smoke_sum/(Smoke_array.length))
})
console.log(that.data.time_array)
console.log("平均浓度"+that.data.smoke_average)
// 求温度平均值
var temp_average=that.datatempe_average
var Temp_array=that.data.Temp_array
var temp_sum=that.data.temp_sum
that.setData({
temp_sum:temp_sum+that.data.Temp,
temp_average:parseInt(that.data.temp_sum/(Temp_array.length))
})
console.log(that.data.time_array)
console.log("平均温度"+that.data.temp_average)
// 求平均湿度
var hum_average=that.data.hum_average
var Hum_array=that.data.Hum_array
var hum_sum=that.data.hum_sum
that.setData({
hum_sum:hum_sum+that.data.Hum,
hum_average:parseInt(that.data.hum_sum/(Hum_array.length))
})
console.log(that.data.time_array)
console.log("平均湿度"+that.data.hum_average)
})
client.on('reconnect', (error)=>{
console.log('正在重新连接'+error)
})
client.on('error', (error)=>{
console.log('连接失败')
})
},
curve (e) {
var that=this
var windowWidth = '', windowHeight=''; //定义宽高
that.data.setInter = setInterval(function(){
var waterwaterdata=that.data.Hum_array
var smokesmokedata=that.data.Smoke_array
var tempdata=that.data.Temp_array
var categories=that.data.time_array
try {
var res = wx.getSystemInfoSync(); //试图获取屏幕宽高数据
windowWidth = res.windowWidth / 750 * 690; //以设计图750为主进行比例算换
windowHeight = res.windowWidth / 750 * 550 //以设计图750为主进行比例算换
} catch (e) {
console.error('getSystemInfoSync failed!'); //如果获取失败
}
lineChart = new wxCharts({ //定义一个wxCharts图表实例
canvasId: 'lineCanvas', //输入wxml中canvas的id
type: 'line', //图标展示的类型有:'line','pie','column','area','ring','radar'
categories: categories,
animation: true, //是否开启动画
series: [{ //具体坐标数据
name: '温度', //名字
data: tempdata, //数据点
format: function (val, name) { //点击显示的数据注释
return val + '℃';
}
}, {
name: '烟雾浓度',
data: smokesmokedata,
format: function (val, name) {
return val + 'bpm';
}
}, {
name: '湿度',
data: waterwaterdata,
format: function (val, name) {
return val + '%';
}
}
],
xAxis: { //是否隐藏x轴分割线
disableGrid: true,
},
yAxis: { //y轴数据
title: '数值', //标题
format: function (val) { //返回数值
return val.toFixed(2);
},
min: 30, //最小值
max:180, //最大值
gridColor: '#D8D8D8',
},
width: windowWidth, //图表展示内容宽度
height: windowHeight, //图表展示内容高度
dataLabel: false, //是否在图表上直接显示数据
dataPointShape: true, //是否在图标上显示数据点标志
extra: {
lineStyle: 'curve' //曲线
},
});
},9000)
},
notification: function () {
var _this = this;
var time = _this.data.time;
_this.data.setInter = setInterval(function () {
_this.setData({
time: util.formatTime(new Date())
});
//console.log("时间为"+_this.data.time);
}, 1000);
},
// gettime(){
// var that=this
// var xtime=that.data.xtime
// that.setData({
// xtime:util.formatTime(new Date())
// })
// }
handleItemChange(e){
// console.log(e)
const {index}=e.detail;
let {list}=this.data;
list.forEach((v,i)=>i===index?v.isActive=true:v.isActive=false);
this.setData({
list
})
}
})
|