【DIY】自动鱼缸控制系统 您所在的位置:网站首页 鱼缸里的恒温器是放在哪里 【DIY】自动鱼缸控制系统

【DIY】自动鱼缸控制系统

2024-07-12 07:58| 来源: 网络整理| 查看: 265

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

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

缸是一个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 #include #include #include //网络配置信息——使用结构便于从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 JsDoc; JsonObject JsRoot = JsDoc.to(); 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 JsDoc; JsonObject JsRoot = JsDoc.to(); 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); //后续数据长度 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是否越界。

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

 



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有