计划把这个比较典型的例子写一写,什么时候完工,烂不烂尾再说。

    首先描述一下这是个什么东西:

缸是一个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是否越界。

不要想着开辟一个一定足够的缓冲区,我们只能努力保障数据被正确读取,所以从设计电路开始就需要注意尽可能面各种干扰。