计划把这个比较典型的例子写一写,什么时候完工,烂不烂尾再说。
首先描述一下这是个什么东西:
缸是一个30*30*40左右的超白,做的是上滤,控制和硬件差不多,侧滤底滤稍加修改管路就可以了。
一、功能:
1、自动温度控制
2、自动水位控制(就近没有上下水管,所以只写到了屏显,真是一大遗憾)
3、水质检测
4、自动喂食
二、硬件实现:
0、控制板:Arduino Mage 2560板子,所以后续的全部编程内容都是C++的。玩这个东西的人很多,门槛也低的令人发指,呵呵。
1、温度控制:传感器——DS18B20;加热:PTC(铝壳封装);降温:制冷片12706(大约是30L水一片够用)、铝排、散热片。
2、水位控制:传感器——HCSR04;上水:一个自吸泵,如果有自来水条件就一个电磁阀;排水:上水泵上一个支管(潜水泵自吸泵开口位置不一样而已)
3、水质检测:传感器——浊度计模块一套(一般般),DTS(电导率传感器一套)。
4、自动喂食:微型减速电机一只,3D打印螺杆、亚克力管等。
5、物联网:ESP8266模块一只。使用的是中移物联。
驱动大功率硬件使用的都是MOS管模块,最好买带光电隔离的,我就懒,所以不得已焊接了好几个续流二极管。HMI USART屏一块,风扇、杜邦线、面包板、电容电阻啥啥的若干。
三、软件
1、Arduino端:整合各种传感器、执行器、屏显,实现全自动控制。
2、ESP8266端:用的是ESPMQTTCLIENT.h,稍微增加了一个NTP(时间同步)。
3、触屏端:就写了一点通讯,设计了界面,整个二维码没了。
4、3D模型:用的SW,根据自己的需要稍微弄几下打印出来就行了。
5、中移物联:这些物联网平台用起来差不多的,在ESP8266这边比较喜欢用MQTT,手机端或者电脑比较习惯用HTTP。
关于和这些物联网平台通讯不在本系列之内,去看参考文档就可以了,没有必要扒一遍。
那么,首先从ESP8266开始,我用的Arduino For VS 开发,这并不是一个很好的选择,但是我又懒得安ESP的开发包,将就一下吧,所以如果你修改代码时要非常非常非常小心字节对齐问题。先把完整代码发上,包括完整的注释和一些需要注意的问题,然后一点一点解释:
/*
Name: ESPMQTTProject.ino
Created: 2020/4/10 20:17:21
Author: zcsor
接收Arduino的命令并执行与MQTT服务器的交互。所以,ESP这边代码比较少,写在一个文件里也比较容易读。
修改了EspMQTTClient库的内容,以满足中移物联MQTT的要求(后续数据长度的表示中可能出现\0,导致发送信息失败)。
注意:这个工程虽然在vs中编写,但写入ESP8266时使用Arduino,否则ESP8266会发热严重并不断重启。
*/
// the setup function runs once when you press reset or power the board
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <ArduinoJson.h>
#include <EspMQTTClient.h>
//网络配置信息——使用结构便于从EEPROM读写
struct NetConfig
{
char WifiSSID[16]; //Wifi ssid
char WifiPassword[16]; //Wifi password
char MQTTServerIP[16]; //Server Address
char MQTTUserName[16]; //产品ID
char MQTTPassword[96]; //API key
char MQTTClientName[16]; //设备ID
uint32_t MQTTServerPort; //服务器端口号——和Arduino不同,Arduino中定义的是char[]。
}ntConfig;
//MQTT客户端,用指针声明可以在Setup函数中按参数初始哈
EspMQTTClient* EMClient = NULL;
//Json
#define JsonBuffSize 256 //缓存大小
uint8_t PayLoadBuff[JsonBuffSize] = { 0 }; //用于生成Json字符串和发布
uint32_t PayLoadBuffDataLen = 0; //缓存的指针
//传感器数据
double SensorValueBuff[8] = { 0 }; //转换值
//时间校准
WiFiUDP ntpUDP; //UDP通讯
NTPClient timeClient(ntpUDP, "cn.pool.ntp.org", 8 * 60 * 60); //东八区
//串口通讯(此处对应接收缓存,默认大小为128。)
bool PackHead = false; //是否找到帧头
bool PackTail = false; //是否找到帧尾
uint32_t Serial_FrameBuff_Ptr = 0; //缓存指针
#define SerialBuffSize 128 //缓存大小
uint8_t Serial_Buff[SerialBuffSize] = { 0 }; //缓存
size_t Serial_Buff_Ptr = 0; //缓存的指针
//缓存中第1-4字节5-9字节的数值
int32_t Serial_Buff_Index; //保存的是控件名中数字部分(ID)。
int32_t Serial_Buff_Number; //在传递数字时,保存数字的值,在传递字符串时保存字符串长度。
//接收的串口命令类型
uint8_t Cmd_ChrChange = 0xAA; //接收到的HMI指令类型——界面字符串更改
uint8_t Cmd_NumChange = 0xAB; //接收到的HMI指令类型——界面数字更改
uint8_t Cmd_ClkButton = 0xAC; //接收到的HMI指令类型——界面按钮状态切换
uint8_t Cmd_SenChange = 0xAD; //传感器变化
//发送的串口数据类型
uint8_t Cmd_SvrCommand = 0xCA; //服务器下发字符串
uint8_t Cmd_NTPChange = 0xCB; //时间校准(接收、发送)
uint8_t Cmd_SvrState = 0xCC; //连接状态变化
uint8_t Cmd_MqttUpDate = 0xCD; //上传数据
uint8_t Serial_Command_Start = 0xee; //指令开始符号
uint8_t Serial_Command_End[3] = { 0xff,0xff,0xff}; //指令的结束符
//指令说明:EE + CMD识别(1字节) + Serial_Buff_Index(4字节) + Serial_Buff_Number(4字节) + 字符串(如果有) + FF FF FF
void setup() {
//打开串口
Serial.begin(115200);
while (!Serial)
{
delay(5);
}
//设置默认MQTT服务器
//EMClient = new EspMQTTClient(
// ntConfig.WifiSSID,
// ntConfig.WifiPassword,
// ntConfig.MQTTServerIP,
// ntConfig.MQTTUserName,
// ntConfig.MQTTPassword,
// ntConfig.MQTTClientName,
// ntConfig.MQTTServerPort);
//打开看门狗
ESP.wdtEnable(WDTO_8S);
}
// the loop function runs over and over again until power down or reset
void loop() {
//读取命令
SerialRead();
//MQTT客户端
if (EMClient != NULL) {
EMClient->loop();
}
//喂狗
ESP.wdtFeed();
}
//成功登录MQTT服务器
void onConnectionEstablished() {
//注册一个回调,将开头为$creq/的消息用MQTTCommandMessageReceivedCallback函数处理
EMClient->subscribe("$creq/#", MQTTCommandMessageReceivedCallback);
//校准
NTPTime();
//更新服务器状态
UpServerState();
}
//收到服务器消息时的回调函数
void MQTTCommandMessageReceivedCallback(const String& topic, const String& payload) {
int id = payload.indexOf(',');
Serial_Buff_Index = payload.substring(0, id).toInt();
Serial_Buff_Number = payload.substring(id + 1).toInt();
Serial.write((uint8_t*)&Serial_Command_Start, 1);
Serial.write((uint8_t*)&Cmd_SvrCommand, 1);
Serial.write((uint8_t*)&Serial_Buff_Index, sizeof(Serial_Buff_Index));
Serial.write((uint8_t*)&Serial_Buff_Number, sizeof(Serial_Buff_Number));
Serial.write((uint8_t*)Serial_Command_End, sizeof(Serial_Command_End));
}
//推送一个数据流,值为32位整数型
void PublishJson(String ObjName, uint32_t ObjVal) {
StaticJsonDocument<JsonBuffSize> JsDoc;
JsonObject JsRoot = JsDoc.to<JsonObject>();
JsRoot[ObjName] = (String)ObjVal;
PayLoadBuffDataLen = serializeJson(JsRoot, PayLoadBuff + 3, JsonBuffSize - 3); //从第三位开始写入
PublishJson();
}
void PublishSensors() {
if (EMClient->isMqttConnected()) {
// char name[8] = { 0 };
String Name ="";
String Value = "";
StaticJsonDocument<JsonBuffSize> JsDoc;
JsonObject JsRoot = JsDoc.to<JsonObject>();
for (int i = 0; i < Serial_Buff_Index; i++) {
// name[0] = 's';
// itoa(i + 100, name + 1, 10);
//JsRoot[name] = (String)SensorValueBuff[i];
Name = "s" + String(i + 100);
Value = String(SensorValueBuff[i]);
JsRoot[Name] = Value;
}
PayLoadBuffDataLen = serializeJson(JsRoot, PayLoadBuff + 3, JsonBuffSize - 3); //从第三位开始写入
PublishJson();
}
}
bool PublishJson() {
//这个地方有一个大坑:OneNet规定发送的第2第3字节是表示后续长度的。
//所以,如果后续字符串长度不满足256个,高位就会为0,如果按String操作,就截断了,导致发送失败!
//于是只好改了Library文件,调用publish带有发送长度的重载。
PayLoadBuff[0] = uint8_t(0x03); //OneNet指定的数据类型Json为3
PayLoadBuff[1] = uint8_t(PayLoadBuffDataLen >> 8); //后续数据长度
PayLoadBuff[2] = uint8_t(PayLoadBuffDataLen & 0xff); //
PayLoadBuffDataLen += 3;
//推送
bool result= EMClient->publish("$dp", PayLoadBuff, PayLoadBuffDataLen);
//调试时的推送消息
//Serial.print("pub:");
//Serial.print((char*)(PayLoadBuff + 3));
//Serial.println(result);
Serial.println(ESP.getFreeHeap()); //剩余内存
return result;
}
void SerialRead() {
if (ReadPacket(&Serial)) {
//命令解析
if (Serial_Buff[0] == Cmd_ChrChange) { //HIM控制字符串更改(WIFI SSID、WIFI PASSWORD、产品ID)
HMI_ChrChange();
}
else if (Serial_Buff[0] == Cmd_NumChange) { //设置各项数据
if (EMClient != NULL)HMI_NumChange();
}
else if (Serial_Buff[0] == Cmd_ClkButton) { //点击控制按钮
if (EMClient != NULL)HMI_ClkButton();
}
else if (Serial_Buff[0] == Cmd_SenChange) { //传感器变化
if (EMClient != NULL)SensorChange();
}
else if (Serial_Buff[0] == Cmd_NTPChange) { //时间校准
if (EMClient != NULL)NTPTime();
}
else if (Serial_Buff[0] == Cmd_SvrState) { //服务器连接状态
if (EMClient != NULL)UpServerState();
}
else if (Serial_Buff[0] == Cmd_MqttUpDate) { //上传传感器
if (EMClient != NULL)PublishSensors();
}
}
}
bool ReadPacket(HardwareSerial* hwSerial)
{
PackHead = false;
PackTail = false;
Serial_FrameBuff_Ptr = 0;
//memset(Serial_Buff, 0, sizeof(Serial_Buff)); //数据清零(不清零也不影响正确工作)
//循环读取串口数据,得到一个完整帧(舍弃了包头,但是包尾还在)
while (hwSerial->available() > 0)
{
Serial_Buff[Serial_FrameBuff_Ptr++] = (uint8_t)hwSerial->read();
if (!PackHead) { //没找到包头时
//Serial.print((char)Serial_FrameBuff[Serial_FrameBuff_Ptr-1]); //输出其它信息
if (Serial_FrameBuff_Ptr >= 1) { //确定缓存中后3字节是不是包头
PackHead = Serial_Buff[Serial_FrameBuff_Ptr - 1] == Serial_Command_Start;
if (PackHead) {
Serial_FrameBuff_Ptr = 0; //找到包头时从缓存头部写入数据
}
}
}
else { //找到包头时
if (Serial_FrameBuff_Ptr >= 11) { //确定缓存中后3字节是不是包尾
PackTail = Serial_Buff[Serial_FrameBuff_Ptr - 1] == Serial_Command_End[2] &&
Serial_Buff[Serial_FrameBuff_Ptr - 2] == Serial_Command_End[1] &&
Serial_Buff[Serial_FrameBuff_Ptr - 3] == Serial_Command_End[0];
}
}
if (PackHead && PackTail) { //找到完整包的时候,把两个32位数字提取出来
memmove(&Serial_Buff_Index, Serial_Buff + 1, sizeof(Serial_Buff_Index));
memmove(&Serial_Buff_Number, Serial_Buff + 5, sizeof(Serial_Buff_Number));
//memset(Serial_Buff + 9 + Serial_Buff_Number, 0, 3); //剔除包尾
break;
}
delay(5); //确保一帧数据完全达到串口
}
return PackHead && PackTail;
}
void HMI_ChrChange() {
//WIFI是可以通过液晶屏设置的,其它是通过Arduino设置的。
//用memmove赋值防止ESP8266四字节对齐错误
if (Serial_Buff_Index == 300) { //Wifi SSID
memset(ntConfig.WifiSSID, 0, sizeof(ntConfig.WifiSSID));
memmove(ntConfig.WifiSSID, Serial_Buff + 9, Serial_Buff_Number);
//Serial.println(ntConfig.WifiSSID);
}
else if (Serial_Buff_Index == 301) { //Wifi Password
memset(ntConfig.WifiPassword, 0, sizeof(ntConfig.WifiPassword));
memmove(ntConfig.WifiPassword, Serial_Buff + 9, Serial_Buff_Number);
//Serial.println(ntConfig.WifiPassword);
}
else if (Serial_Buff_Index == 302) { //服务器地址
memset(ntConfig.MQTTServerIP, 0, sizeof(ntConfig.MQTTServerIP));
memmove(ntConfig.MQTTServerIP, Serial_Buff + 9, Serial_Buff_Number);
//Serial.println(ntConfig.MQTTServerIP);
}
else if (Serial_Buff_Index == 303) { //MQTT用户名(产品ID)
memset(ntConfig.MQTTUserName, 0, sizeof(ntConfig.MQTTUserName));
memmove(ntConfig.MQTTUserName, Serial_Buff + 9, Serial_Buff_Number);
//Serial.println(ntConfig.MQTTUserName);
}
else if (Serial_Buff_Index == 304) { //MQTT密码(产品API KEY)
memset(ntConfig.MQTTPassword, 0, sizeof(ntConfig.MQTTPassword));
memmove(ntConfig.MQTTPassword, Serial_Buff + 9, Serial_Buff_Number);
//Serial.println(ntConfig.MQTTPassword);
}
else if (Serial_Buff_Index == 305) { //MQTT客户端名(设备ID)
memset(ntConfig.MQTTClientName, 0, sizeof(ntConfig.MQTTClientName));
memmove(ntConfig.MQTTClientName, Serial_Buff + 9, Serial_Buff_Number);
//Serial.println(ntConfig.MQTTClientName);
}
else if (Serial_Buff_Index == 306) { //MQTT服务器端口
ntConfig.MQTTServerPort =atoi((char*)Serial_Buff + 9);
//Serial.println(ntConfig.MQTTServerPort);
}
else if (Serial_Buff_Index == 310) { //重新连接MQTT服务器
if (EMClient != NULL) {
delete EMClient;
EMClient = NULL;
}
WiFi.disconnect(); //断开当前网络。
EMClient = new EspMQTTClient(
ntConfig.WifiSSID,
ntConfig.WifiPassword,
ntConfig.MQTTServerIP,
ntConfig.MQTTUserName,
ntConfig.MQTTPassword,
ntConfig.MQTTClientName,
ntConfig.MQTTServerPort);
//Serial.println("MQTT Client Reset");
//EMClient->enableDebuggingMessages(); //这个是MQTT库带的调试显示功能。
}
}
void HMI_NumChange() {
String str = "n" + String(Serial_Buff_Index);
if (EMClient->isMqttConnected()) {
PublishJson(str, Serial_Buff_Number);
}
}
void HMI_ClkButton() {
String str = "c" + String(Serial_Buff_Index);
if (EMClient->isMqttConnected()) {
PublishJson(str, Serial_Buff_Number);
}
}
void SensorChange() {
SensorValueBuff[Serial_Buff_Index - 100] = 1.0 * Serial_Buff_Number / 10.0;
}
void NTPTime() {
//时间校准
if (EMClient->isWifiConnected()) {
timeClient.begin();
//给Arduino更新时间
if (timeClient.update()) {
Serial.write((uint8_t*)&Serial_Command_Start, 1);
Serial.write((uint8_t*)&Cmd_NTPChange, 1);
Serial_Buff_Index = timeClient.getHours(); //时
Serial_Buff_Index |= timeClient.getMinutes()<<8; //分
Serial_Buff_Index |= timeClient.getSeconds()<<16; //秒
Serial.write((uint8_t*)&Serial_Buff_Index, sizeof(Serial_Buff_Index));
Serial.write((uint8_t*)&Serial_Buff_Index, sizeof(Serial_Buff_Index));
Serial.write((uint8_t*)Serial_Command_End, sizeof(Serial_Command_End));
}
timeClient.end();
}
}
void UpServerState() {
Serial_Buff_Index = EMClient->isWifiConnected();
Serial_Buff_Number = EMClient->isMqttConnected();
Serial.write((uint8_t*)&Serial_Command_Start, 1);
Serial.write((uint8_t*)&Cmd_SvrState, 1);
Serial.write((uint8_t*)&Serial_Buff_Index, sizeof(Serial_Buff_Index));
Serial.write((uint8_t*)&Serial_Buff_Number, sizeof(Serial_Buff_Number));
Serial.write((uint8_t*)Serial_Command_End, sizeof(Serial_Command_End));
}
首先,整体看代码,有一个严重缺失的功能:把参数设置保存在EEPROM中,实际上这个问题我努力尝试解决过,不知道是我在TB上买的模块被阉了还是怎么的,无法成功存储到ESP8266的EEPROM中,所以都保存在Arduino Mage 2560的EEPROM中,然后通过通讯得到。接下来分别说一下这个代码的各个部分:
//网络配置信息——使用结构便于从EEPROM读写
struct NetConfig
{
char WifiSSID[16]; //Wifi ssid
char WifiPassword[16]; //Wifi password
char MQTTServerIP[16]; //Server Address
char MQTTUserName[16]; //产品ID
char MQTTPassword[96]; //API key
char MQTTClientName[16]; //设备ID
uint32_t MQTTServerPort; //服务器端口号——和Arduino不同,Arduino中定义的是char[]。
}ntConfig;
//MQTT客户端,用指针声明可以在Setup函数中按参数初始哈
EspMQTTClient* EMClient = NULL;
//Json
#define JsonBuffSize 256 //缓存大小
uint8_t PayLoadBuff[JsonBuffSize] = { 0 }; //用于生成Json字符串和发布
uint32_t PayLoadBuffDataLen = 0; //缓存的指针
这些都是为了进行MQTT设置和通讯而编写的,写的时候注意字节对齐问题和后续使用的时候怎么方便。MQTT通讯用的Json,所以使用了一个通用的Json类。就像最开始的说明里所说的那样,ESPMQTTCLIENT.H里面处理数据时主要考虑了字符串的情况,它以'\0'作为结束,但在实际使用时并不是那么回事,很多数据中间是有'\0'存在的,所以你可以像我一样,简单修改一下,指定其发送的字节数而不是遇到'\0'结束(参考c++ string.length,size(),sizeof(string))。时间校准部分非常简单,略过时间校准部分,如果你希望理解它们可以参考网络上其他文章或者NTP的范例。通讯部分的定义需要稍微解释一下:
//串口通讯(此处对应接收缓存,默认大小为128。)
bool PackHead = false; //是否找到帧头
bool PackTail = false; //是否找到帧尾
uint32_t Serial_FrameBuff_Ptr = 0; //缓存指针
#define SerialBuffSize 128 //缓存大小
uint8_t Serial_Buff[SerialBuffSize] = { 0 }; //缓存
size_t Serial_Buff_Ptr = 0; //缓存的指针
//缓存中第1-4字节5-9字节的数值
int32_t Serial_Buff_Index; //保存的是控件名中数字部分(ID)。
int32_t Serial_Buff_Number; //在传递数字时,保存数字的值,在传递字符串时保存字符串长度。
//接收的串口命令类型
uint8_t Cmd_ChrChange = 0xAA; //接收到的HMI指令类型——界面字符串更改
uint8_t Cmd_NumChange = 0xAB; //接收到的HMI指令类型——界面数字更改
uint8_t Cmd_ClkButton = 0xAC; //接收到的HMI指令类型——界面按钮状态切换
uint8_t Cmd_SenChange = 0xAD; //传感器变化
//发送的串口数据类型
uint8_t Cmd_SvrCommand = 0xCA; //服务器下发字符串
uint8_t Cmd_NTPChange = 0xCB; //时间校准(接收、发送)
uint8_t Cmd_SvrState = 0xCC; //连接状态变化
uint8_t Cmd_MqttUpDate = 0xCD; //上传数据
uint8_t Serial_Command_Start = 0xee; //指令开始符号
uint8_t Serial_Command_End[3] = { 0xff,0xff,0xff}; //指令的结束符
//指令说明:EE + CMD识别(1字节) + Serial_Buff_Index(4字节) + Serial_Buff_Number(4字节) + 字符串(如果有) + FF FF FF
通讯部分使用的是包头包尾的形式,其定义就在这行上面,这里没有使用进一步的校验。如果你有兴趣可以定义一个其他命令,并且当发送数据后一定时间内没有得到该命令,则认为通讯校验失败,重新发送当前缓存的内容。而校验过程可以自己定义一个简单的CRC16。接下来是代码部分:
void setup() {
//打开串口
Serial.begin(115200);
while (!Serial)
{
delay(5);
}
//设置默认MQTT服务器
//EMClient = new EspMQTTClient(
// ntConfig.WifiSSID,
// ntConfig.WifiPassword,
// ntConfig.MQTTServerIP,
// ntConfig.MQTTUserName,
// ntConfig.MQTTPassword,
// ntConfig.MQTTClientName,
// ntConfig.MQTTServerPort);
//打开看门狗
ESP.wdtEnable(WDTO_8S);
}
其中注释掉的部分是我用来测试用的,对于一个单片机程序来说,设置看门狗是一个很好的做法,至少它可以保证单片机程序跑飞了的时候恢复到初始状态。而loop函数同样非常简单,只是读取串口、调用MQTT库的loop、喂狗。接来下的逻辑过程中可以看到HMI_ChrChange函数中会检查MQTT连接情况并重连,而该函数被处理串口数据的SerialRead函数调用,在Arduino的主循环中,会查询MQTT连接状态,当丢失若干次之后,将会发送命令进行重连,从而保证了MQTT在线。下面的程序中需要注意的就是数组的操作、使用Serial.write时明确指定使用哪一个重载即(uint8_t*)这部分。看一下PublishSensors函数:
void PublishSensors() {
if (EMClient->isMqttConnected()) {
// char name[8] = { 0 };
String Name ="";
String Value = "";
StaticJsonDocument<JsonBuffSize> JsDoc;
JsonObject JsRoot = JsDoc.to<JsonObject>();
for (int i = 0; i < Serial_Buff_Index; i++) {
// name[0] = 's';
// itoa(i + 100, name + 1, 10);
//JsRoot[name] = (String)SensorValueBuff[i];
Name = "s" + String(i + 100);
Value = String(SensorValueBuff[i]);
JsRoot[Name] = Value;
}
PayLoadBuffDataLen = serializeJson(JsRoot, PayLoadBuff + 3, JsonBuffSize - 3); //从第三位开始写入
PublishJson();
}
}
这个函数中,按传感器个数发送数据,在中移物联上MQTT设置时,我的传感器命名规则是"s"开头,从100开始,所以Json中Name成员设置为"s"+String(i+100);这里怎么写取决于你的在物联网平台上是如何制定的规则。而物联网平台也有各自不同的规矩:
bool PublishJson() {
//这个地方有一个大坑:OneNet规定发送的第2第3字节是表示后续长度的。
//所以,如果后续字符串长度不满足256个,高位就会为0,如果按String操作,就截断了,导致发送失败!
//于是只好改了Library文件,调用publish带有发送长度的重载。
PayLoadBuff[0] = uint8_t(0x03); //OneNet指定的数据类型Json为3
PayLoadBuff[1] = uint8_t(PayLoadBuffDataLen >> 8); //后续数据长度
PayLoadBuff[2] = uint8_t(PayLoadBuffDataLen & 0xff); //
PayLoadBuffDataLen += 3;
//推送
bool result= EMClient->publish("$dp", PayLoadBuff, PayLoadBuffDataLen);
//调试时的推送消息
//Serial.print("pub:");
//Serial.print((char*)(PayLoadBuff + 3));
//Serial.println(result);
Serial.println(ESP.getFreeHeap()); //剩余内存
return result;
}
例如上面PayLoadBuff的前几个字节就是按照中移物联的规矩发送的。这里你应该使用物联网平台提供的MQTT测试程序来逐个对照你的代码是不是按照他们的规矩在发送数据。控制按钮的处理和这个没有什么差异。解析串口命令的函数没有什么特别的地方,但是我想提示一下,不要把常量写在这里。接下的代码中有价值进行说明的只有串口读取函数:
bool ReadPacket(HardwareSerial* hwSerial)
{
PackHead = false;
PackTail = false;
Serial_FrameBuff_Ptr = 0;
//memset(Serial_Buff, 0, sizeof(Serial_Buff)); //数据清零(不清零也不影响正确工作)
//循环读取串口数据,得到一个完整帧(舍弃了包头,但是包尾还在)
while (hwSerial->available() > 0)
{
Serial_Buff[Serial_FrameBuff_Ptr++] = (uint8_t)hwSerial->read();
if (!PackHead) { //没找到包头时
//Serial.print((char)Serial_FrameBuff[Serial_FrameBuff_Ptr-1]); //输出其它信息
if (Serial_FrameBuff_Ptr >= 1) { //确定缓存中后3字节是不是包头
PackHead = Serial_Buff[Serial_FrameBuff_Ptr - 1] == Serial_Command_Start;
if (PackHead) {
Serial_FrameBuff_Ptr = 0; //找到包头时从缓存头部写入数据
}
}
}
else { //找到包头时
if (Serial_FrameBuff_Ptr >= 11) { //确定缓存中后3字节是不是包尾
PackTail = Serial_Buff[Serial_FrameBuff_Ptr - 1] == Serial_Command_End[2] &&
Serial_Buff[Serial_FrameBuff_Ptr - 2] == Serial_Command_End[1] &&
Serial_Buff[Serial_FrameBuff_Ptr - 3] == Serial_Command_End[0];
}
}
if (PackHead && PackTail) { //找到完整包的时候,把两个32位数字提取出来
memmove(&Serial_Buff_Index, Serial_Buff + 1, sizeof(Serial_Buff_Index));
memmove(&Serial_Buff_Number, Serial_Buff + 5, sizeof(Serial_Buff_Number));
//memset(Serial_Buff + 9 + Serial_Buff_Number, 0, 3); //剔除包尾
break;
}
delay(5); //确保一帧数据完全达到串口
}
return PackHead && PackTail;
}
我见过一些处理串口的方式,自己也写过一些读写串口不够健壮、不易扩展的代码。串口间的通讯尤其是当你使用软串口(真的需要那么多还是用mega吧)波特率较高时,出错机会非常多,不能像在本地处理一样保障数据的可靠性。所以,无论电路设计还是代码,都需要注意这方面的问题,虽然不用写一个N层的通讯结构,但当你通讯经常失败时,最好还是在这个例子的基础上增加CRC校验。这个例子中我没有进行CRC校验,但已经可以非常容易扩展:发送时在FF FF FF前添加2字节的CRC16校验码,得到包之后就可以进行校验了。成功则回复对方,不成功时的处理可以采用各种不同的方式:主动请求、被动等待等等。现在,说明一下上面这段函数:
1、开辟一个缓冲区从数据流中查找包头。这里需要注意的是,包头不一定就是真的。
2、在找到包头的情况下,查找是不是找到包尾。这里需要注意的是包尾也不一定就是真的,但你定义的包头包尾越长,找到真的的可能性就越大,但通讯效率也会降低。并且上述代码中,如果在真的包头前面有一个假的,那么会得到一个前面有冗余数据的包,这个包将会不合法。
3、如果需要,在if (PackHead && PackTail) 之后,首先CRC校验,然后返回(PackHead && PackTail && CRCCheck) 。
4、修改上述代码在从串口读取之前,检测Serial_FrameBuff_Ptr是否越界。
不要想着开辟一个一定足够的缓冲区,我们只能努力保障数据被正确读取,所以从设计电路开始就需要注意尽可能面各种干扰。