Funpack 3-5:基于 Arduino UNO的传感器数据采集及云端显示
该项目使用了Arduino R4 WIFI、SHT30,实现了温湿度数据上云的设计,它的主要功能为:上电自动连接wifi,并通过MQTT连接阿里云,并定时从SHT30获取温湿度数据,上传阿里云。
标签
Arduino
Funpack活动
WiFi
阿里云
MQTT
SHT30
不爱胡萝卜的仓鼠
更新2025-01-15
258

一.项目描述

本次活动我选择的开发板为Arduino R4 WIFI

选择任务为任务二:搭配传感器,并通过网络连接到智能云端,可以从远程获取传感器的信息


刚好我手上有一块之前自己画的SHT30板子,那我传感器就选择SHT30,采集温湿度数据。它与Arduino之间通过IIC通讯


云端我就使用阿里云,在阿里云上创建一个温湿度记录仪设备,让开发板通过MQTT上传温湿度数据


系统框图如下

开发板实物接线如下图所示


二.软件流程图及各功能对应的主要代码片段及说明

2.1 阿里云配置

我在阿里云IOT平台创建了一个温湿度记录仪的产品,数据格式使用Alink JSON,选择温湿度记录仪有个好处就是符合我要实现的目标,不用自己再去创建topic,阿里云会自动创建好,如下图所示


在这个产品下,再创建一个设备

在这个设备的MQTT连接参数中我们就可以获取对应的信息,这个后续需要填写到mqtt_info.h文件中

2.2 代码讲解

这里只是简单讲解一下代码,想要深入了解的,可以自行阅读代码,会放到附件里,代码中也有很全的注释方便理解

文件结构分为3个文件:SHT30_aliyun.ino、wifi_info.h、mqtt_info.h。

SHT30_aliyun.ino:arduino工程及代码

wifi_info.h:wifi的配置信息,需要用户填写开发板连接的wifi的账号、密码

mqtt_info.h:MQTT配置信息,需要用户填写阿里云对应的broker地址、端口、账号、密码、ID、topic等信息


2.2.1 wifi_info.h

wifi_info.h内容如下

#define SECRET_SSID ""
#define SECRET_PASS ""

需要使用者填写能被开发板扫描并连接,可以上外网的wifi SSID和密码


2.2.2 mqtt_info.h

mqtt_info.h内容如下

#define MQTT_BROKER     ""
#define MQTT_PORT       (0)
#define MQTT_CLIENT_ID  ""


#define MQTT_USER_MANE  ""
#define MQTT_PASSWORD   ""


#define MQTT_REPORT_TEMP_HUM_TOPIC  ""

除了最后一个,其他的都可以在刚才说的MQTT连接参数中获取,直接复制过来就行


最后一项是我们上传温湿度数据使用的TOPIC,规则如下

/sys/[ProductKey]/[DeviceName]/thing/event/property/post

ProductKey和DeviceName可以在如下位置获取


2.2.3 SHT30_aliyun.ino

整个工程的代码都在这一个文件夹中了

程序流程图如下

系统上电后,会开始初始化,初始化会对SHT30初始化、连接wifi、连接MQTT服务器。如果任意步骤异常,系统会reboot。


2.2.3.1 初始化

初始化函数如下

void setup()
{
#if HARD_REBOOT_FUN_ENABLE
  hard_reboot_init();
#endif


  /* 日志串口初始化 */
  log_uart_init();


  /* SHT30初始化 */
  if (RESULT_FAIL == sht30_init())
  {
    reboot();
  }


  /* wifi初始化 */
  if (RESULT_FAIL == wifi_init())
  {
    reboot();
  }


  /* wifi连接AP */
  if (RESULT_FAIL == wifi_connect_AP())
  {
    reboot();
  }
  else
  {
    print_wifi_data();
    print_current_net();
  }


  /* MQTT连接服务器 */
  if (RESULT_FAIL == mqtt_connect_broker())
  {
    reboot();
  }
}


2.2.3.2 while1

当完成初始化后,系统会进入while1循环,里面会定时获取SHT30温湿度数据,并通过MQTT上传阿里云。如果读取失败或上传失败,会对失败计数+1,获取SHT30数据失败或上传数据失败到达最大尝试次数,系统也会重启

while1函数如下

void loop()
{
  mqtt_schedule();


  /* 通过不停的拿当前时间与上一次保存的时间对比,实现一个不是很精准的定时 */
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval)
  {
    previousMillis = currentMillis;


    /* 获取SHT30温湿度数据 */
    if (RESULT_OK == sht30_get_temp_hum(&temperature_value, &humidity_value))
    {
      sht30_get_data_try_cnt = 0;


      Serial.print("Temp *C = ");
      Serial.print(temperature_value);
      Serial.print("\t\t");
      Serial.print("Hum. % = ");
      Serial.println(humidity_value);      


      /* 上报数据给阿里云 */
      if (RESULT_FAIL == mqtt_report_temp_hum_data_to_aliyun(&temperature_value, &humidity_value))
      {
        Serial.print("report temp hum data to aliyun fail, cnt = ");
        Serial.println(mqtt_report_data_try_cnt);
        mqtt_report_data_try_cnt++;
      }
      else
      {
        mqtt_report_data_try_cnt = 0;
      }
    }
    else
    {
      Serial.print("sht30 get temp hum fail, cnt = ");
      Serial.println(sht30_get_data_try_cnt);
      sht30_get_data_try_cnt++;
    }


    if ((SHT30_GET_DATA_TRY_MAX < sht30_get_data_try_cnt) ||
      (MQTT_REPORT_DATA_TRY_MAX < mqtt_report_data_try_cnt))
    {
      /* 重试到达max,直接重启 */
      Serial.println("sht30 or mqtt try max");
      reboot();
    }


    time_count++;
  }
}


2.2.3.3 重启

为了保证系统持续正常运行,我会在异常情况下,并且try max后,让开发板重启,希望重启大法可以解决问题

我一开始尝试了软件重启,但是在这款开发板上无效,会进入dump,然后一直在里面,无法实现reboot

后面我又尝试了硬件重启,GPIO直连RST脚,但是GPIO上电默认是L的,reset pin默认是H的,两个硬件直接连接就导致reset一直拉低,上电后系统根本无法启动

最终我采用外置继电器的方法,通过GPIO控制继电器,继电器NO与rst脚连接,COM与GND连接。通过继电器连接于断开实现硬件复位,这个操作和人按开发板上的RST按钮是类似的


我在代码中依然保留了软件重启的代码,虽然他无效。和硬件重启一起,我通过宏定义控制是否启用(如果都不启用,则会进入死循环,一直持续打印),宏定义如下

#define SOFT_REBOOT_FUN_ENABLE (0)
#define HARD_REBOOT_FUN_ENABLE (1)

重启相关代码如下

#if HARD_REBOOT_FUN_ENABLE
int hard_reboot_pin = 2; /* 硬件重启使用的引脚号 */
#endif

/* R4这个函数无效,会进入dump,然后一直在里面,无法实现reboot */
//TODO 后续看看R4有没有重启函数
//找了貌似没有,软件重启放弃
#if SOFT_REBOOT_FUN_ENABLE
/**
 * @brief     reboot
 * @param[in] none
 * @return    none
 */
void(* resetFunc) (void) = 0;
#endif


/* R4这个函数也无效,GPIO默认是L的,reset pin默认是H的,两个硬件直接连接就导致reset一直拉低,系统根本无法启动 */
/* 硬件重启的思路是对的,但是不能GPIO直连,可以通过外置继电器,模拟按RST按钮,来实现重启,测试过可行 */
#if HARD_REBOOT_FUN_ENABLE
/**
 * @brief     硬件重启功能初始化
 * @param[in] none
 * @return    none
 */
void hard_reboot_init()
{
  /* 这里使用一个GPIO,开控制继电器,继电器NO与reset引脚连接,COM与GND连接,通过继电器变相实现按reset来硬件重启arduino */
  pinMode(hard_reboot_pin, OUTPUT);
  digitalWrite(hard_reboot_pin, HIGH);
  delay(200);
}


/**
 * @brief     硬件重启
 * @param[in] none
 * @return    none
 */
void hard_reboot()
{
  digitalWrite(hard_reboot_pin, LOW);
  delay(500);
  digitalWrite(hard_reboot_pin, HIGH);
  Serial.println("Arduino will never reach there");
}
#endif


/**
 * @brief     重启
 * @param[in] none
 * @return    none
 */
void reboot()
{
  Serial.println("ready to reboot");
  delay(1000);
#if HARD_REBOOT_FUN_ENABLE
  hard_reboot();
#endif
  /* 如果重启功能不生,就会在这里一直死循环 */
  while(1)
  {
    Serial.println("hard_reboot fun unavailable");
    delay(1000);
  }
}


2.2.3.4 wifi连接

wifi初始化代码如下

/**
 * @brief     wifi初始化
 * @param[in] none
 * @return    result_t
 */
result_t wifi_init()
{
  /* 检查wifi模块状态 */
  if (WiFi.status() == WL_NO_MODULE)
  {
    Serial.println("Communication with WiFi module failed!");
    return RESULT_FAIL;
  }


  /* 检查wifi模块固件版本(这步不是必要的) */
  String fv = WiFi.firmwareVersion();
  if (fv < WIFI_FIRMWARE_LATEST_VERSION)
  {
    Serial.println("now wifi firmware = " + fv + ", lastest version is " +
      WIFI_FIRMWARE_LATEST_VERSION + ", suggest update!");
  }


  return RESULT_OK;
}

wifi连接AP代码如下



  /* 检查SSID和PSW是否填写 */
  if (strlen(wifi_ssid) <= 0)
  {
    Serial.println("ERROR: Plase enter WiFi SSID in \"wifi_info.h\"");
    while(1);
  }
  if (strlen(wifi_password) <= 0)
  {
    Serial.println("ERROR: Plase enter WiFi password in \"wifi_info.h\"");
    while(1);
  }


  Serial.print("Attempting to connect to WPA SSID: ");
  Serial.println(wifi_ssid);


  for (uint8_t i = 0; i < 5; i++) //wifi连接超时时间5 * 10s
  {
    wifi_conn_state = WiFi.begin(wifi_ssid, wifi_password);


    if (WL_CONNECTED == wifi_conn_state)
    {
      Serial.println("wifi connect success");
      return RESULT_OK;
    }
    delay(10000);
  }


  Serial.println("wifi connect try max, failed");
  return RESULT_FAIL;
}

2.2.3.5 MQTT连接

MQTT连接服务器代码如下

/**
 * @brief     mqtt连接broker
 * @param[in] none
 * @return    result_t
 */
result_t mqtt_connect_broker()
{
  /* 检查MQTT需要使用的信息是否填写 */
  if (strlen(broker) <= 0)
  {
    Serial.println("ERROR: Plase enter MQTT broker in \"mqtt_info.h\"");
    while(1);
  }
  if (port == 0)
  {
    Serial.println("ERROR: Plase enter MQTT port in \"mqtt_info.h\"");
    while(1);
  }
  if (strlen(clientId) <= 0)
  {
    Serial.println("ERROR: Plase enter MQTT clientId in \"mqtt_info.h\"");
    while(1);
  }
  if (strlen(topic) <= 0)
  {
    Serial.println("ERROR: Plase enter MQTT topic in \"mqtt_info.h\"");
    while(1);
  }
  if (strlen(mqtt_user) <= 0)
  {
    Serial.println("ERROR: Plase enter MQTT mqtt_user in \"mqtt_info.h\"");
    while(1);
  }
  if (strlen(mqtt_pass) <= 0)
  {
    Serial.println("ERROR: Plase enter MQTT mqtt_pass in \"mqtt_info.h\"");
    while(1);
  }


  Serial.print("Attempting to connect to the MQTT broker: ");
  Serial.println(broker);


  mqttClient.setId(clientId);


  /* 设置账号密码 */
  mqttClient.setUsernamePassword(mqtt_user, mqtt_pass);


  for (uint8_t i = 0; i < 5; i++) //mqtt连接超时时间5 * 10s
  {
    if (!mqttClient.connect(broker, port))
    {
      Serial.print("MQTT connection failed! Error code = ");
      Serial.println(mqttClient.connectError());
    }
    else
    {
      Serial.println("MQTT connection success");
      return RESULT_OK;
    }
    delay(10000);
  }


  Serial.println("mqtt connect broker try max, failed");
  return RESULT_FAIL;
}

2.2.3.6 SHT30初始化及读取数据

SHT30初始化代码如下

/**
 * @brief     sht30温湿度传感器初始化
 * @param[in] none
 * @return    result_t
 */
result_t sht30_init()
{
  if (!sht31.begin(sht30_iic_addr))
  {  
    Serial.println("Couldn't find SHT31");
    return RESULT_FAIL;
  }
  return RESULT_OK;
}

SHT30读取温湿度数据代码如下

/**
 * @brief     sht30温湿度传感器读取一次温湿度数据
 * @param[in] p_temperature:温度数据
 * @param[in] p_humidity:湿度数据
 * @return    result_t
 */
result_t sht30_get_temp_hum(float *p_temperature, float *p_humidity)
{
  /* 指针判空 */
  if (NULL == p_temperature)
  {
    Serial.println("p_temperature is NULL");
    return RESULT_FAIL;
  }
  if (NULL == p_humidity)
  {
    Serial.println("p_humidity is NULL");
    return RESULT_FAIL;
  }


  /* 获取温度和湿度值 */
  float temperature_temp = sht31.readTemperature();
  float humidity_temp = sht31.readHumidity();


  /* 检查温度是否为有效值 */
  if (isnan(temperature_temp))
  {
    Serial.println("Failed to read temperature");
    return RESULT_FAIL;
  }


  /* 检查湿度是否为有效值 */
  if (isnan(humidity_temp))
  {
    Serial.println("Failed to read humidity");
    return RESULT_FAIL;
  }


  *p_temperature = temperature_temp;
  *p_humidity = humidity_temp;


  return RESULT_OK;
}

2.2.3.7 MQTT上报阿里云

MQTT上报温湿度数据给阿里云需要按照协议格式组装JSON数据,组装函数如下

/**
 * @brief     mqtt上报温湿度数据至阿里云
 * @param[in] temperature:温度数据
 * @param[in] humidity:湿度数据
 * @param[in] out:组装好的json
 * @return    result_t
 */
result_t build_aliyun_temp_hum_json_data(float temperature, float humidity, String *out)
{
  if (NULL == out)
  {
    Serial.println("out is NULL");
    return RESULT_FAIL;
  }


  /* 初始数据,模板 */
  String input = "{\"id\": \"1235\",\"version\": \"1.0\",\"sys\":{\"ack\":0},\"params\":{\"CurrentHumidity\":{\"value\":00.0},\"CurrentTemperature\":{\"value\":00.0}},\"method\": \"thing.event.property.post\"}";


  /* 加载 */
  JsonDocument doc;
  DeserializationError error = deserializeJson(doc, input);
  if (error)
  {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.c_str());
    return RESULT_FAIL;
  }


  /* 填写温度、湿度数据 */
  doc["params"]["CurrentHumidity"]["value"] = humidity;
  doc["params"]["CurrentTemperature"]["value"] = temperature;


  /* JSON数据输出,输出为一个string */
  serializeJson(doc, *out);


  /* 打印组装好的json数据 */
  Serial.println(*out);


  return RESULT_OK;
}

上报阿里云代码如下

/**
 * @brief     mqtt上报温湿度数据至阿里云
 * @param[in] p_temperature:温度数据
 * @param[in] p_humidity:湿度数据
 * @return    result_t
 */
result_t mqtt_report_temp_hum_data_to_aliyun(float *p_temperature, float *p_humidity)
{
  /* 指针判空 */
  if (NULL == p_temperature)
  {
    Serial.println("p_temperature is NULL");
    return RESULT_FAIL;
  }
  if (NULL == p_humidity)
  {
    Serial.println("p_humidity is NULL");
    return RESULT_FAIL;
  }


  /* 检查WiFi是否连接 */
  if (WiFi.status() == WL_NO_MODULE)
  {
    Serial.println("WiFi is not connect");
    return RESULT_FAIL;
  }


  String output;
  if (RESULT_FAIL == build_aliyun_temp_hum_json_data(*p_temperature, *p_humidity, &output))
  {
    Serial.println("build aliyun temp hum json data fail");
    return RESULT_FAIL;
  }


  mqttClient.beginMessage(topic);
  mqttClient.print(output);
  mqttClient.endMessage();
}

三.功能展示及说明

设备日志及阿里云后台实时数据显示截图

四.对本活动的心得体会

Arduino以前我玩过R3 UNO和MEGA 2560,那时还是我初学单片机的时候,对于初学者,arduino的开发环境对新手非常友好,对应的教程和社区资源也非常多。多年之后玩到了R4和第二代的IDE,还是一如既往的好用。

本次活动对我最大的挑战是通过MQTT上传数据至阿里云,我之前也开发过类似的产品,但是那时站在巨人的肩膀上进行开发,没有自己从0开始配置阿里云,登录等。还是非常感谢主办方举办的本次活动,给我了我机会,也给了我动力,去学习这块的相关知识和动手尝试

附件下载
代码.rar
团队介绍
个人
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号