前言
该项目是嵌入式课程学习最后的一个项目设计,做的不是很好(大佬勿喷~),特别是STM32数据处理部分,因为刚学的STM32,并且对C语言的指针等的使用也有些生疏(虽然学过,但大部分都忘了),不敢随便用,所以有些代码设计不好,只能完成一些简单功能。ESP8266使用的是NodeMCU开发板,用ArduinoIDE开发(因为有很多现成的库,资料也多)。APP制作用的是Android Studio开发,从网上参考了很多人的代码,最后修改成自己的。前后花了差不多2周时间(主要是中间还有课要上,一些知识也得现学),不过最后功能基本算实现了,也学到了不少东西。
具体实现的功能包括以下三个方面:
-
TFTLCD可以显示STM32通过DHT11模块和光敏电阻模块采集到的温度(精度±2°C)、湿度(±5%RH)、光照(0~100,最暗为0,最亮为100)信息;以及显示设置的报警温度、最低光照;还有ESP8266WIFI模块通过网络获取到的时间(年-月-日 时:分:秒)、当地城市的天气信息(气温、湿度、天气、风力)。
-
STM32可以在室内温度过高(超过报警温度)时驱动蜂鸣器发出声音,在室内亮度较低(低于设定的最低光照)时驱动LED点亮。
-
在任意可以连接网络的地方,自己设计的手机APP可以显示室内环境信息(温度、湿度、光照强度),并且可以设置报警温度(
0~100°C
)和最低光照(0~100
)。
STM32端程序设计
功能介绍
-
STM32单片机通过DHT11温湿度传感器采集温湿度
-
通过模数转换器ADC采集光敏电阻分压后的电压,然后转成光照强度
-
串口接收ESP8266的信息,提取出时间、天气信息,以及报警温度、最低光照信息(来自APP,用于控制蜂鸣器、led的开闭)
-
当室内温度超过报警温度时,蜂鸣器启动;当室内光照较暗(低于设定的最低光照)时,led点亮
总体框图如下:
具体实现
下面将主要介绍一些功能函数,完整代码请看:
温湿度采集
初始化DHT11模块
//DHT11初始化
//返回0:初始化成功,1:失败
u8 DHT11_Init()
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG,ENABLE);
GPIO_InitStructure.GPIO_Pin=DHT11;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIO_DHT11,&GPIO_InitStructure);
GPIO_SetBits(GPIO_DHT11,DHT11); //拉高
DHT11_Rst();
return DHT11_Check();
}
//复位DHT11
void DHT11_Rst()
{
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()
{
u8 retry=0;
DHT11_IO_IN();//SET INPUT
while (DHT11_DQ_IN&&retry<100)//DHT11会拉低40~50us
{
retry++;
delay_us(1);
};
if(retry>=100)return 1;
else retry=0;
while (!DHT11_DQ_IN&&retry<100)//DHT11拉低后会再次拉高40~50us
{
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)//等待变为低电平 12-14us 开始
{
retry++;
delay_us(1);
}
retry=0;
while(!DHT11_DQ_IN&&retry<100)//等待变高电平 26-28us表示0,116-118us表示1
{
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、humi中
//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;
}
光照采集
使用ADC3模数转换器采集光敏电阻的分压,然后转换为光照强度(转换过程把最亮的当作100,最暗当作0来作为最终结果)
首先初始化配置ADC3
//初始化光敏传感器
void Lsens_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF,ENABLE);//使能PORTF时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC3, ENABLE ); //使能ADC3通道时钟
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC3, ENABLE);//ADC复位
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC3, DISABLE);//复位结束
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;//PF8 anolog输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
GPIO_Init(GPIOF, &GPIO_InitStructure);
ADC_DeInit(ADC3); //复位ADC3,将外设 ADC3的全部寄存器重设为缺省值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式: 独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //模数转换工作在单次转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目
ADC_Init(ADC3, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC3, ENABLE); //使能指定的ADC3
ADC_ResetCalibration(ADC3); //使能复位校准
while(ADC_GetResetCalibrationStatus(ADC3)); //等待复位校准结束
ADC_StartCalibration(ADC3); //开启AD校准
while(ADC_GetCalibrationStatus(ADC3)); //等待校准结束
}
然后采集数据
//获得ADC3某个通道的值
//ch:通道值 0~16
//返回值:转换结果
u16 Get_ADC3(u8 ch)
{
//设置指定ADC的规则组通道,一个序列,采样时间
ADC_RegularChannelConfig(ADC3, ch, 1, ADC_SampleTime_239Cycles5 ); //ADC3,ADC通道,采样时间为239.5周期
ADC_SoftwareStartConvCmd(ADC3, ENABLE); //使能指定的ADC3的软件转换启动功能
while(!ADC_GetFlagStatus(ADC3, ADC_FLAG_EOC ));//等待转换结束
return ADC_GetConversionValue(ADC3); //返回最近一次ADC3规则组的转换结果
}
//读取Light Sens的值
//0~100:0,最暗;100,最亮
u8 Lsens_Get_Val(void)
{
u32 temp_val=0;
u8 t;
for(t=0;t<LSENS_READ_TIMES;t++)
{
temp_val+=Get_ADC3(ADC_Channel_6); //读取ADC值
delay_ms(5);
}
temp_val/=LSENS_READ_TIMES;//得到平均值
if(temp_val>4000)temp_val=4000;
return (u8)(100-(temp_val/40));
}
接收并处理串口USART2数据
初始化配置USART2
void usart2_init( void )
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE );
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //PA2
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;//PA3
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA
//USART2 初始化设置
USART_InitStructure.USART_BaudRate = 115200;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART2, &USART_InitStructure); //初始化串口2
USART_ClearFlag(USART2, USART_FLAG_TC);
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启串口接受和总线空闲中断
//USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);
USART_Cmd(USART2, ENABLE); //使能串口2
//USART2 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
}
接收USART2数据
char USART2_RX_BUF[RX_BUF_MAX_LEN];
u16 USART2_RX_STA = 0;
void USART2_IRQHandler(void)
{
u8 r;
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) //接收中断
{
r = USART_ReceiveData(USART2);//(USART2->DR); //读取接收到的数据
if((USART2_RX_STA&0x8000)==0)//接收未完成
{
if(USART2_RX_STA&0x4000)//接收到了0x0d(即'\r')
{
if(r!=0x0a)USART2_RX_STA=0;//接收错误(即没有接收到'\n'),重新开始
else {
//USART2_RX_STA|=0x8000; //接收完成了 (接收到'\r'后接收到'\n')
USART2_RX_STA = 0; //接收到\n, 重新等待下一次接收
}
}
else //还没收到0X0D(即'\r')
{
if(r==0x0d)USART2_RX_STA|=0x4000; //如果接收到\r, 让USART2_RX_STA的14位置1
else
{
USART2_RX_BUF[USART2_RX_STA&0X3FFF]=r;
USART2_RX_STA++;
if(USART2_RX_STA>(RX_BUF_MAX_LEN-1))USART2_RX_STA=0;//接收数据错误,重新开始接收
}
}
}
}
}
处理USART2接收到的数据
提取时间、天气信息;为了方便,ESP8266发给STM32的数据都是用逗号隔开的,如
时间,天气,气温,湿度,
……
天气数据保存在结构体中
struct WeatherData{
char datetime[20];
char city[10];
char weather[10]; //原本为10
char temp[10];
char humi[10];
char windpower[10]; //风力等级
};
对USART2接收的数据进行处理
struct WeatherData processWdata(char data[]){
//char data[] = "2022-22-22 33:33:33,yin,47,48,<=7";
u8 i=0, j=0, i0=0, k=0;
u8 ind=0, jnd=0;
int slen = strlen(data);
struct WeatherData weather;
//初始化
for(ind=0; ind<8; ind++){
switch(ind){
case 0: {
for(jnd=0; jnd<20; jnd++){
weather.datetime[jnd]='\0';
}
};break;
case 1: {
for(jnd=0; jnd<10; jnd++){
weather.city[jnd]='\0';
}
};break;
case 2: {
for(jnd=0; jnd<10; jnd++){
weather.humi[jnd]='\0';
}
};break;
case 3: {
for(jnd=0; jnd<10; jnd++){
weather.temp[jnd]='\0';
}
};break;
case 4: {
for(jnd=0; jnd<10; jnd++){
weather.weather[jnd]='\0';
}
};break;
case 5: {
for(jnd=0; jnd<10; jnd++){
weather.windpower[jnd]='\0';
}
};break;
case 6: {
for(jnd=0; jnd<10; jnd++){
minLsens_str[jnd]='\0';
}
};break;
case 7: {
for(jnd=0; jnd<10; jnd++){
alarmTemp_str[jnd]='\0';
}
};break;
}
}
strcpy(weather.city, "西安");
for(i=0; i<slen; i++){
if(data[i]==',') {
i0++;
for(j=k; j<i; j++){
if(i0==1) weather.datetime[j-k]=data[j];
else if(i0==2) weather.weather[j-k]=data[j];
else if(i0==3) weather.temp[j-k]=data[j];
else if(i0==4) weather.humi[j-k]=data[j];
else if(i0==5) weather.windpower[j-k]=data[j];
else if(i0==6) alarmTemp_str[j-k]=data[j];
else if(i0==7) minLsens_str[j-k]=data[j];
}
k=i+1;
}
}
return weather;
}
字符串数字转为整型数字
u8 str2int(char s[]){
u8 n=0, i=0, len, sum=0;
len = strlen(s);
for(i=0; i<len; i++){
if(47<s[i]<58){
n = s[i] - 48; //得到一位数字
sum += (n * pow(10, len-1-i));
}
}
return sum;
}
发送数据
通过定时器TIM2定时1s发送一次数据(json字符串格式)给ESP8266模块
初始化配置TIM2
//初始化TIM2
void TIM2_init(u16 arr, u16 psc){
TIM_TimeBaseInitTypeDef TIM_Structure;
NVIC_InitTypeDef NVIC_Structure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
//延时时间(单位:s):arr/(72000000/psc)
TIM_Structure.TIM_Period = arr - 1; //装载值
TIM_Structure.TIM_Prescaler = psc-1; //分频系数
TIM_Structure.TIM_ClockDivision = 0;
TIM_Structure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_Structure);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //开启定时器TIM2中断
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
NVIC_Structure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_Structure.NVIC_IRQChannelPreemptionPriority = 6; //抢占优先级
NVIC_Structure.NVIC_IRQChannelSubPriority = 4; //子优先级
NVIC_Structure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_Structure);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
TIM_Cmd(TIM2, ENABLE);
}
定时时间(项目中使用1s)到达后产生定时器中断,这时候进入中断函数中发送数据
void TIM2_IRQHandler(void){
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET){
ESP8266_Usart("{\"temp\":\"%d\",\"humi\":\"%d\",\"light\":\"%d\",\"ledsta\":\"%d\",\"beepsta\":\"%d\"}\r\n", dhtData.temp, dhtData.humi, lsens, ledSta, beepSta);//输出【光照、温度、湿度、led、beep状态】到esp8266,再发送给手机
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
控制LED与蜂鸣器
void ledBeepOnOrNot(u8 lsens, u8 mlsens, u8 t, u8 alarmT){ //是否启动LED、BEEP
if(lsens < mlsens){
LED1 = 0; //点亮
ledSta = 1;
}
else{
LED1 = 1; //熄灭
ledSta = 0;
}
if(t>alarmT){
BEEP=1; //叫
beepSta = 1;
}
else{
BEEP=0; //不叫
beepSta = 0;
}
}
ESP8266端程序设计
ESP8266WIFI模块通过MQTT协议连接服务器,数据发布到服务器后,Android客户端可以向服务器订阅主题来获取到消息,然后进行展示。ESP8266客户端通过订阅也能获取到Android客户端发布的消息。
功能介绍
- 连接WiFi
- 启动后led点亮,连接上MQTT服务端后闪烁,未连接常亮
- 通过网络获取如下信息
- 时间:使用【苏宁的API】
http://quan.suning.com/getSysTime.do
- 天气:使用【高德的天气API】
http://restapi.amap.com/v3/weather/weatherInfo?key=xxxxx&city=610100)
(参数key要自己获取,city是城市编码)
- 时间:使用【苏宁的API】
- 与STM32通过串口进行数据通信
- 建立与MQTT服务器的连接
- 向服务器指定的主题发布消息和接收消息
- json数据处理
具体实现
ESP8266使用NodeMCU模块,ArduinoIDE开发
一些全局变量
//wifi配置
const char* ssid = "xxxxxx";
const char* password = "xxxxxxxx";
//mqtt服务器连接配置
const char *mqttBroker = "xxxxxx.iotcloud.tencentdevices.com";
const char *mqttUsername = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const char *mqttPassword = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
char *mqttClientId = "xxxxxxxxxxxesp8266";
const char *pubTopic = "xxxxxx/esp8266/data";
const char *subTopic = "xxxxxx/esp8266/data";
const int mqttPort = 1883;
//时间api
const String timeUrl = "http://quan.suning.com/getSysTime.do";
//高德地图api(自己获取key, city是城市编码)
const String gaoDeUrl = "http://restapi.amap.com/v3/weather/weatherInfo?key=xxxxx&city=610100";
连接WiFi
void connectWifi(){
WiFi.mode(WIFI_STA); //WIFI_STA、WIFI_AP、WIFI_AP_STA
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
发送HTTP请求
String callHttp(String httpUrl) {
HTTPClient hClient;
String message;
hClient.begin(httpUrl);
int httpCode = hClient.GET();
if (httpCode > 0) {
if (httpCode == HTTP_CODE_OK) {
String payload = hClient.getString();
message = payload;
}
} else {
message = "[1-HTTP]failed, error:" + String(hClient.errorToString(httpCode).c_str());
}
hClient.end();
return message;
}
连接MQTT服务器
void connectMqttServer(){
while (!mqttClient.connected()) {
if (mqttClient.connect(mqttClientId, mqttUsername, mqttPassword)) {
subscribeTopic(subTopic); //订阅主题
} else {
delay(2000);
// Serial.println("连接失败");
}
}
}
发布信息到指定主题
void publishTopic(const char* topic, char* jsonMsg){
mqttClient.publish(topic, jsonMsg, false);
}
订阅主题
// 订阅指定主题
void subscribeTopic(const char* topic){
if(mqttClient.subscribe(topic)){
} else {
}
}
回调函数(当收到服务器的消息后会调用)
void receiveCallback(char* topic, byte* payload, unsigned int length) {
serverMsg = ""; //当下次调用时清空数据
for (int i = 0; i < length; i++) {
// Serial.print((char)payload[i]);
serverMsg += (char)payload[i];
}
// Serial.println("\n");
// Serial.println(serverMsg);
}
完整代码
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <ESP8266HTTPClient.h>
#include <WiFiManager.h>
//wifi配置
const char* ssid = "xxxxxx";
const char* password = "xxxxxxxx";
//mqtt服务器连接配置
const char *mqttBroker = "xxxxxx.iotcloud.tencentdevices.com";
const char *mqttUsername = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const char *mqttPassword = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
char *mqttClientId = "xxxxxxxxxxxesp8266";
const char *pubTopic = "xxxxxx/esp8266/data";
const char *subTopic = "xxxxxx/esp8266/data";
const int mqttPort = 1883;
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
struct Weather{
String wea="";
String temp="";
String windpower=""; //风力,单位:级
String humi="";
};
struct ServerMsg{
String alarmTemp="";
String minLight="";
};
//时间api
const String timeUrl = "http://quan.suning.com/getSysTime.do";
//高德地图api(自己获取key, city是城市编码)
const String gaoDeUrl = "http://restapi.amap.com/v3/weather/weatherInfo?key=xxxxx&city=610100";
String timeMsg="";
String weatherMsg="";
String timeAfterParse="";
struct Weather weatherAfterParse;
String data2stm=""; //打包后发送给stm32的信息
String data2server=""; //发布到mqtt服务器的信息
String callHttp(String httpUrl);
String parseJsonTime(String tjson);
struct Weather parseJsonWeather(String wjson);
void ledOnOff();
String tqzh2py(String zhtq); //中文天气转拼音
String windpowerFormat(String wp);
void connectWifi();
void setLedOnOff();
void publishTopic(const char* topic, char* jsonMsg);
void subscribeTopic(const char* topic);
struct ServerMsg parseServerMsg(String jsonMsg);
void getSerialDataAndProcess();
void setup() {
pinMode(LED_BUILTIN, OUTPUT); // 设置板上LED引脚为输出模式
digitalWrite(LED_BUILTIN, LOW); // 启动后打开板上LED
Serial.begin(115200); // 启动串口通讯
connectWifi();
mqttInit();
connectMqttServer();
timeMsg = callHttp(timeUrl);
weatherMsg = callHttp(gaoDeUrl);
}
struct ServerMsg serverMsgAfterParse;
String serverMsg="";
String stm32Msg="";
int n = 0;
void loop() {
delay(500);
n++;
//获取网络数据(时间、天气)
if(n % 2 == 0){
ledOnOff();
timeMsg = callHttp(timeUrl);
if(n >= 20){ //10s获取一次天气
weatherMsg = callHttp(gaoDeUrl);
n=0;
}
}
//获取stm32数据,并发送到mqtt服务器
getSerialDataAndProcess();
//对网络获取到的数据处理
if(timeMsg!=""){
timeAfterParse = parseJsonTime(timeMsg);
}
if(weatherMsg!=""){
weatherAfterParse = parseJsonWeather(weatherMsg);
}
if(serverMsg!=""){
serverMsgAfterParse = parseServerMsg(serverMsg);
}
//打包发送到stm32
if(timeMsg!="" && weatherMsg!=""){
data2stm = (timeAfterParse + "," + tqzh2py(weatherAfterParse.wea) + "," + weatherAfterParse.temp + "," +weatherAfterParse.humi +"," + windpowerFormat(weatherAfterParse.windpower) + "," + serverMsgAfterParse.alarmTemp + "," + serverMsgAfterParse.minLight + "," + "\r\n"); //日期,天气,温度,湿度,风力等级,报警温度,最低光照
Serial.print(data2stm);
}
if (mqttClient.connected()) { // 如果开发板成功连接服务器
mqttClient.loop(); // 处理信息以及心跳
} else { // 如果开发板未能成功连接服务器
connectMqttServer(); // 则尝试连接服务器
}
}
void mqttInit(){
mqttClient.setServer(mqttBroker, mqttPort);
// 设置MQTT订阅回调函数
mqttClient.setCallback(receiveCallback);
}
// 连接MQTT服务器并订阅信息
void connectMqttServer(){
while (!mqttClient.connected()) {
if (mqttClient.connect(mqttClientId, mqttUsername, mqttPassword)) {
subscribeTopic(subTopic); //订阅主题
} else {
delay(2000);
// Serial.println("连接失败");
}
}
}
// 订阅指定主题
void subscribeTopic(const char* topic){
if(mqttClient.subscribe(topic)){
} else {
}
}
// 收到信息后的回调函数
void receiveCallback(char* topic, byte* payload, unsigned int length) {
serverMsg = ""; //当下次调用时清空数据
for (int i = 0; i < length; i++) {
// Serial.print((char)payload[i]);
serverMsg += (char)payload[i];
}
// Serial.println("\n");
// Serial.println(serverMsg);
}
struct ServerMsg parseServerMsg(String jsonMsg){
struct ServerMsg smsg;
const size_t capacity = 96;
DynamicJsonDocument sdoc(capacity);
// 反序列化数据
deserializeJson(sdoc, jsonMsg);
smsg.alarmTemp = sdoc["atemp"].as<String>();
smsg.minLight = sdoc["mlight"].as<String>();
return smsg;
}
void publishTopic(const char* topic, char* jsonMsg){
mqttClient.publish(topic, jsonMsg, false);
}
void ledOnOff(){
static bool LEDState=0;
if(mqttClient.connected()){
LEDState = !LEDState; //连上,led闪烁
}
else{
LEDState = 0; //没连上,led常亮
}
digitalWrite(LED_BUILTIN, LEDState);
}
String callHttp(String httpUrl) {
HTTPClient hClient;
String message;
hClient.begin(httpUrl);
int httpCode = hClient.GET();
if (httpCode > 0) {
if (httpCode == HTTP_CODE_OK) {
String payload = hClient.getString();
message = payload;
}
} else {
message = "[1-HTTP]failed, error:" + String(hClient.errorToString(httpCode).c_str());
}
hClient.end();
return message;
}
void getTimeMsg(){
timeMsg = callHttp(timeUrl);
}
void getWeatherMsg(){
weatherMsg = callHttp(gaoDeUrl);
}
String parseJsonTime(String tjson){
const size_t capacity = 96;
DynamicJsonDocument tdoc(capacity);
// 反序列化数据
deserializeJson(tdoc, tjson);
// 获取解析后的数据信息
String datetime = tdoc["sysTime2"].as<String>();
return datetime;
}
//{"status":"1","count":"1","info":"OK","infocode":"10000","lives":[{"province":"陕西","city":"西安市","adcode":"610100","weather":"晴","temperature":"13","winddirection":"北","windpower":"≤3","humidity":"88","reporttime":"2021-10-29 21:03:10"}]}
struct Weather parseJsonWeather(String wjson){
struct Weather weather;
const size_t capacity = 512;
DynamicJsonDocument wdoc(capacity);
// 反序列化数据
deserializeJson(wdoc, wjson);
JsonObject lives_0 = wdoc["lives"][0];
weather.wea = lives_0["weather"].as<String>(); // "晴"
weather.temp = lives_0["temperature"].as<String>(); // "13"
weather.humi = lives_0["humidity"].as<String>(); // "88"
weather.windpower = lives_0["windpower"].as<String>(); // "≤3"
return weather;
}
String tqzh2py(String zhtq){
String zh_cn[68] = {"晴","少云","晴间多云","多云","阴","有风","平静","微风","和风","清风","强风/劲风","疾风","大风","烈风","风暴","狂爆风","飓风","热带风暴","霾","中度霾","重度霾","严重霾","阵雨","雷阵雨","雷阵雨并伴有冰雹","小雨","中雨","大雨","暴雨","大暴雨","特大暴雨","强阵雨","强雷阵雨","极端降雨","毛毛雨/细雨","雨","小雨-中雨","中雨-大雨","大雨-暴雨","暴雨-大暴雨","大暴雨-特大暴雨","雨雪天气","雨夹雪","阵雨夹雪","冻雨","雪","阵雪","小雪","中雪","大雪","暴雪","小雪-中雪","中雪-大雪","大雪-暴雪","浮尘","扬沙","沙尘暴","强沙尘暴","龙卷风","雾","浓雾","强浓雾","轻雾","大雾","特强浓雾","热","冷","未知",};
String py[68] = {"qing","shaoyun","qingjianduoyun","duoyun","yin","youfeng","pingjing","weifeng","hefeng","qingfeng","qiangfeng/jinfeng","jifeng","dafeng","liefeng","fengbao","kuangbaofeng","jufeng","redaifengbao","mai","zhongdumai","zhongdumai","yanzhongmai","zhenyu","leizhenyu","leizhenyubingbanyoubingbao","xiaoyu","zhongyu","dayu","baoyu","dabaoyu","tedabaoyu","qiangzhenyu","qiangleizhenyu","jiduanjiangyu","maomaoyu/xiyu","yu","xiaoyu-zhongyu","zhongyu-dayu","dayu-baoyu","baoyu-dabaoyu","dabaoyu-tedabaoyu","yuxuetianqi","yujiaxue","zhenyujiaxue","dongyu","xue","zhenxue","xiaoxue","zhongxue","daxue","baoxue","xiaoxue-zhongxue","zhongxue-daxue","daxue-baoxue","fuchen","yangsha","shachenbao","qiangshachenbao","longjuanfeng","wu","nongwu","qiangnongwu","qingwu","dawu","teqiangnongwu","re","leng","weizhi"};
for(int i=0; i<68; i++){
// if(strstr())
if(zh_cn[i] == zhtq)
return py[i];
}
return zhtq; //没有就返回中文
}
String windpowerFormat(String wp){
if(wp=="≤3"){
return "<=3";
}
return wp;
}
//得到数据后处理方式,把它当作一个串口(伪)中断函数
void getSerialDataAndProcess(){
if (Serial.available()) { //串口读取到的转发到wifi,因为串口是一位一位的发送所以在这里缓存完再发送
size_t countBytes = Serial.available();
char sbuf[countBytes];
Serial.readBytes(sbuf, countBytes);
// Serial.write(sbuf, countBytes);
publishTopic(pubTopic, sbuf);
Serial.flush();
}
}
void connectWifi(){
// WiFiManager wifiManager;
//
// 自动连接WiFi。以下语句的参数是连接ESP8266时的WiFi名称
// wifiManager.autoConnect("IamESP8266");
WiFi.mode(WIFI_STA); //WIFI_STA、WIFI_AP、WIFI_AP_STA
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
Android端程序设计
功能介绍
主要是通过MQTT服务器,获取ESP8266发布的信息,然后进行处理显示出来;同时APP也可以发布消息,用于修改报警温度、最低光照值。
具体实现
使用Android Studio进行开发(具体安装下载、环境配置请百度)
工程代码已开源,前往GitHub请点击前去Github,前往Gitee请点击前去Gitee
添加MQTT依赖
- 在自己的[project]下的
build.gradle
添加如下代码
repositories {
google()
mavenCentral()
//添加下面这句代码
maven {
url "https://repo.eclipse.org/content/repositories/paho-releases/"
}
}
- 在[project]/app下的
build.gradle
添加如下代码
dependencies {
compile 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.1'
}
然后选择顶部弹出的 Sync Now
信息
权限配置
修改AndroidManifest.xml
,添加权限
<!--允许程序打开网络套接字-->
<uses-permission android:name="android.permission.INTERNET" />
<!--允许程序获取网络状态-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
如下:
显示界面
修改文件activity_main.xml
,实现如下显示效果
具体代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" >
<TextView android:layout_marginTop="20dp" android:layout_marginLeft="20dp" android:layout_marginBottom="10dp" android:id="@+id/house_env" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="室内环境" android:textStyle="bold" android:textSize="20dp" />
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal">
<TextView android:id="@+id/show_temp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginLeft="20dp" android:layout_marginBottom="10dp" android:text="温度(°C)" android:textSize="20dp" />
<TextView android:id="@+id/temp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginLeft="20dp" android:layout_marginBottom="10dp" android:text="0" android:textSize="20dp" />
<TextView android:id="@+id/show_humi" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginLeft="20dp" android:layout_marginBottom="10dp" android:text="湿度(%RH)" android:textSize="20dp" />
<TextView android:id="@+id/humi" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginLeft="20dp" android:layout_marginBottom="10dp" android:text="0" android:textSize="20dp" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" >
<TextView android:id="@+id/show_light" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:text="光照强度" android:textSize="20dp" />
<TextView android:id="@+id/light" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:text="0" android:textSize="20dp" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal">
<TextView android:id="@+id/ledsta1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:text="LED状态" android:textSize="20dp" />
<TextView android:id="@+id/ledsta2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:text="关" android:textSize="20dp" />
<TextView android:id="@+id/beepsta1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:text="蜂鸣器状态" android:textSize="20dp" />
<TextView android:id="@+id/beepsta2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:text="关" android:textSize="20dp" />
</LinearLayout>
<TextView android:id="@+id/sets" android:layout_column="1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:text="设置" android:textStyle="bold" android:textSize="20dp" />
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal">
<TextView android:id="@+id/set_temp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:text="报警温度(当前温度超过该值蜂鸣器将报警)" android:textSize="15dp" />
<TextView android:id="@+id/seekbarval1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginLeft="10dp" android:text="40"/>
</LinearLayout>
<SeekBar android:id="@+id/seekBar1" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:layout_gravity="center" android:max="100" android:progress="40"/>
<!-- android:progressDrawable="@drawable/seekbar_progress" />-->
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal">
<TextView android:id="@+id/set_tlight" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:text="最低光照(当前光照小于该值将点亮led灯)" android:textSize="15dp" />
<TextView android:id="@+id/seekbarval2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginLeft="20dp" android:text="12"/>
</LinearLayout>
<SeekBar android:id="@+id/seekBar2" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:layout_gravity="center" android:max="100" android:progress="12"/>
<!-- android:progressDrawable="@drawable/seekbar_progress2" />-->
<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content">
</LinearLayout>
<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" >
<Button android:layout_marginLeft="100dp" android:layout_marginTop="20dp" android:id="@+id/connectbtn" android:layout_height="wrap_content" android:layout_width="wrap_content" android:textColor="@color/white" android:text="连接"/>
<Button android:layout_marginTop="20dp" android:layout_marginRight="20dp" android:layout_marginLeft="20dp" android:id="@+id/exitbtn" android:layout_height="wrap_content" android:layout_width="wrap_content" android:textColor="@color/white" android:text="退出"/>
</LinearLayout>
<TextView android:layout_margin="5dp" android:id="@+id/msgTxt" android:layout_height="wrap_content" android:layout_width="wrap_content" android:scrollbars="vertical" android:text=""/>
</LinearLayout>
界面控制
接下来修改MainActivity.java
,添加控制操作,让数据可以通过TextView显示,按下按钮可以作出反应,上面只是完成一个静态界面的创建
package com.ajream.mqttdemo4;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity {
//定义成员
private Button connectbtn;
private Button exitbtn;
private TextView temptv;
private TextView humitv;
private TextView lighttv;
private TextView ledtv;
private TextView beeptv;
private TextView bar1tv;
private TextView bar2tv;
private TextView showmsgtv;
private SeekBar tempbar;
private SeekBar lightbar;
//MQTT客户端配置所需信息
private String host = "tcp://xxxxxxx.iotcloud.tencentdevices.com:1883"; //mqtt服务器(腾讯云)地址、端口
private String userName = "xxxxxxxxxx"; //用户名(在腾讯云查看自己的设备信息)
private String passWord = "xxxxxxxxxx"; //密码(在腾讯云查看自己的设备信息)
private String mqtt_id = "xxxxxxxxxxxxxxxxxxx";
private String mqtt_sub_topic = "xxxxxxxxxx/AndroidClient/data";
private String mqtt_pub_topic = "xxxxxxxxxx/AndroidClient/data";
private ScheduledExecutorService scheduler;
private MqttClient mqttClient;
private MqttConnectOptions options;
private Handler handler;
private String msgToPublish = ""; //要发布的消息
private String alarmTempMsg = "";
private String minLightMsg = "";
JSONObject msgGet;
@SuppressLint("HandlerLeak")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//关联控件
connectbtn = findViewById(R.id.connectbtn);
exitbtn = findViewById(R.id.exitbtn);
temptv = findViewById(R.id.temp);
humitv = findViewById(R.id.humi);
lighttv = findViewById(R.id.light);
ledtv = findViewById(R.id.ledsta2);
beeptv = findViewById(R.id.beepsta2);
bar1tv = findViewById(R.id.seekbarval1);
bar2tv = findViewById(R.id.seekbarval2);
showmsgtv = findViewById(R.id.msgTxt);
tempbar = findViewById(R.id.seekBar1);
lightbar = findViewById(R.id.seekBar2);
alarmTempMsg = String.valueOf(tempbar.getProgress());
minLightMsg = String.valueOf(lightbar.getProgress());
/*点击按钮连接*/
connectbtn.setOnClickListener(view -> {
Mqtt_init();
startReconnect();
});
exitbtn.setOnClickListener(view -> {
android.os.Process.killProcess(android.os.Process.myPid());
});
tempbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
Toast.makeText(MainActivity.this, "设置报警温度为: " + progress, Toast.LENGTH_LONG).show();
bar1tv.setText(check(progress));
alarmTempMsg = check(progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// msgToPublish = alarmTempMsg + "," + minLightMsg;
msgToPublish = "{\"atemp\":\"" + alarmTempMsg + "\",\"mlight\":\"" + minLightMsg+"\"}";
publishMsg(mqtt_pub_topic, msgToPublish);
}
});
lightbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
Toast.makeText(MainActivity.this, "设置最低光照为: " + i, Toast.LENGTH_LONG).show();
bar2tv.setText(check(i));
minLightMsg = check(i);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
msgToPublish = "{\"atemp\":\"" + alarmTempMsg + "\",\"mlight\":\"" + minLightMsg+"\"}";
publishMsg(mqtt_pub_topic, msgToPublish);
}
});
handler = new Handler() {
@SuppressLint("SetTextI18n")
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 1: //开机校验更新回传
break;
case 2: // 反馈回传
break;
case 3: //MQTT 收到消息回传 UTF8Buffer msg=newUTF8Buffer(object.toString());
// Toast.makeText(MainActivity.this,msg.obj.toString(),Toast.LENGTH_SHORT).show();
showmsgtv.setText(msg.obj.toString());
JSONObject msgGet = null;
try {
msgGet = new JSONObject(msg.obj.toString());
temptv.setText(msgGet.get("temp").toString());
humitv.setText(msgGet.get("humi").toString());
lighttv.setText(msgGet.get("light").toString());
if(Integer.parseInt(msgGet.get("ledsta").toString())==0) ledtv.setText("关");
else ledtv.setText("开");
if(msgGet.get("beepsta").toString().charAt(0)=='0') beeptv.setText("关");
else beeptv.setText("开");
} catch (JSONException e) {
e.printStackTrace();
}
break;
case 30: //连接失败
Toast.makeText(MainActivity.this,"连接失败" ,Toast.LENGTH_SHORT).show();
break;
case 31: //连接成功
Toast.makeText(MainActivity.this,"连接成功" ,Toast.LENGTH_SHORT).show();
try {
mqttClient.subscribe(mqtt_sub_topic,1);
}
catch (MqttException e) {
e.printStackTrace();
}
break;
default:
break;
}
}
};
}
private String check(int progress) {
int curValue = 100 * progress/Math.abs(100);
return String.valueOf(curValue);
}
private void publishMsg(String topic, String message2) {
if (mqttClient == null || !mqttClient.isConnected()) {
return;
}
MqttMessage message = new MqttMessage();
message.setPayload(message2.getBytes());
try {
mqttClient.publish(topic, message);
} catch (MqttException e) {
e.printStackTrace();
}
}
private void Mqtt_init() {
try {
//host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存
mqttClient = new MqttClient(host, mqtt_id, new MemoryPersistence());
//MQTT的连接设置
options = new MqttConnectOptions();
//设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(false);
options.setUserName(userName); //设置连接的用户名
options.setPassword(passWord.toCharArray()); //设置连接的密码
// 设置超时时间 单位为秒
options.setConnectionTimeout(10);
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
//设置回调函数
mqttClient.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
//连接丢失后,一般在这里面进行重连
// System.out.println("connectionLost----------");
// startReconnect();
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
//publish后会执行到这里
System.out.println("deliveryComplete---------"
+ token.isComplete());
}
@Override
public void messageArrived(String topicName, MqttMessage message)
throws Exception {
//subscribe后得到的消息会执行到这里面
System.out.println("getMsg: ");
Message msg = new Message();
msg.what = 3; //收到消息标志位
msg.obj = message.toString();
handler.sendMessage(msg); // hander 回传
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private void Mqtt_connect() {
new Thread(new Runnable() {
@Override
public void run() {
try {
if(!(mqttClient.isConnected())) {
mqttClient.connect(options);
Message msg = new Message();
msg.what = 31;
handler.sendMessage(msg);
}
} catch (Exception e) {
e.printStackTrace();
Message msg = new Message();
msg.what = 30;
handler.sendMessage(msg);
}
}
}).start();
}
private void startReconnect() {
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (!mqttClient.isConnected()) {
Mqtt_connect();
}
}
}, 0 * 1000, 10 * 1000, TimeUnit.MILLISECONDS);
}
}
注意:MQTT服务端配置这里没有介绍,可以使用一些公用的MQTT服务器(可以去百度搜索),也可以自己搭建一个,我这里使用的是自己通过腾讯云配置的MQTT服务,具体搭建可以到我的博客查看或者看官方文档
效果预览
TFTLCD屏幕显示
Android APP界面
今天的文章智能家居项目设计分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/22327.html