一.项目描述
本次活动我选择的开发板为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开始配置阿里云,登录等。还是非常感谢主办方举办的本次活动,给我了我机会,也给了我动力,去学习这块的相关知识和动手尝试