首页 > 分享 > 【DIY】自动鱼缸控制系统——【一】

【DIY】自动鱼缸控制系统——【一】

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

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

缸是一个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的开发包,将就一下吧,所以如果你修改代码时要非常非常非常小心字节对齐问题。先把完整代码发上,包括完整的注释和一些需要注意的问题,然后一点一点解释:

#include <WiFiUdp.h>

#include <NTPClient.h>

#include <ArduinoJson.h>

#include <EspMQTTClient.h>

struct NetConfig

{

char WifiSSID[16];

char WifiPassword[16];

char MQTTServerIP[16];

char MQTTUserName[16];

char MQTTPassword[96];

char MQTTClientName[16];

uint32_t MQTTServerPort;

}ntConfig;

EspMQTTClient* EMClient = NULL;

#define JsonBuffSize 256

uint8_t PayLoadBuff[JsonBuffSize] = { 0 };

uint32_t PayLoadBuffDataLen = 0;

double SensorValueBuff[8] = { 0 };

WiFiUDP ntpUDP;

NTPClient timeClient(ntpUDP, "cn.pool.ntp.org", 8 * 60 * 60);

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;

int32_t Serial_Buff_Index;

int32_t Serial_Buff_Number;

uint8_t Cmd_ChrChange = 0xAA;

uint8_t Cmd_NumChange = 0xAB;

uint8_t Cmd_ClkButton = 0xAC;

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};

void setup() {

Serial.begin(115200);

while (!Serial)

{

delay(5);

}

ESP.wdtEnable(WDTO_8S);

}

void loop() {

SerialRead();

if (EMClient != NULL) {

EMClient->loop();

}

ESP.wdtFeed();

}

void onConnectionEstablished() {

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));

}

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()) {

String Name ="";

String Value = "";

StaticJsonDocument<JsonBuffSize> JsDoc;

JsonObject JsRoot = JsDoc.to<JsonObject>();

for (int i = 0; i < Serial_Buff_Index; i++) {

Name = "s" + String(i + 100);

Value = String(SensorValueBuff[i]);

JsRoot[Name] = Value;

}

PayLoadBuffDataLen = serializeJson(JsRoot, PayLoadBuff + 3, JsonBuffSize - 3);

PublishJson();

}

}

bool PublishJson() {

PayLoadBuff[0] = uint8_t(0x03);

PayLoadBuff[1] = uint8_t(PayLoadBuffDataLen >> 8);

PayLoadBuff[2] = uint8_t(PayLoadBuffDataLen & 0xff);

PayLoadBuffDataLen += 3;

bool result= EMClient->publish("$dp", PayLoadBuff, PayLoadBuffDataLen);

Serial.println(ESP.getFreeHeap());

return result;

}

void SerialRead() {

if (ReadPacket(&Serial)) {

if (Serial_Buff[0] == Cmd_ChrChange) {

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;

while (hwSerial->available() > 0)

{

Serial_Buff[Serial_FrameBuff_Ptr++] = (uint8_t)hwSerial->read();

if (!PackHead) {

if (Serial_FrameBuff_Ptr >= 1) {

PackHead = Serial_Buff[Serial_FrameBuff_Ptr - 1] == Serial_Command_Start;

if (PackHead) {

Serial_FrameBuff_Ptr = 0;

}

}

}

else {

if (Serial_FrameBuff_Ptr >= 11) {

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) {

memmove(&Serial_Buff_Index, Serial_Buff + 1, sizeof(Serial_Buff_Index));

memmove(&Serial_Buff_Number, Serial_Buff + 5, sizeof(Serial_Buff_Number));

break;

}

delay(5);

}

return PackHead && PackTail;

}

void HMI_ChrChange() {

if (Serial_Buff_Index == 300) {

memset(ntConfig.WifiSSID, 0, sizeof(ntConfig.WifiSSID));

memmove(ntConfig.WifiSSID, Serial_Buff + 9, Serial_Buff_Number);

}

else if (Serial_Buff_Index == 301) {

memset(ntConfig.WifiPassword, 0, sizeof(ntConfig.WifiPassword));

memmove(ntConfig.WifiPassword, Serial_Buff + 9, Serial_Buff_Number);

}

else if (Serial_Buff_Index == 302) {

memset(ntConfig.MQTTServerIP, 0, sizeof(ntConfig.MQTTServerIP));

memmove(ntConfig.MQTTServerIP, Serial_Buff + 9, Serial_Buff_Number);

}

else if (Serial_Buff_Index == 303) {

memset(ntConfig.MQTTUserName, 0, sizeof(ntConfig.MQTTUserName));

memmove(ntConfig.MQTTUserName, Serial_Buff + 9, Serial_Buff_Number);

}

else if (Serial_Buff_Index == 304) {

memset(ntConfig.MQTTPassword, 0, sizeof(ntConfig.MQTTPassword));

memmove(ntConfig.MQTTPassword, Serial_Buff + 9, Serial_Buff_Number);

}

else if (Serial_Buff_Index == 305) {

memset(ntConfig.MQTTClientName, 0, sizeof(ntConfig.MQTTClientName));

memmove(ntConfig.MQTTClientName, Serial_Buff + 9, Serial_Buff_Number);

}

else if (Serial_Buff_Index == 306) {

ntConfig.MQTTServerPort =atoi((char*)Serial_Buff + 9);

}

else if (Serial_Buff_Index == 310) {

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);

}

}

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();

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里面处理数据时主要考虑了字符串的情况,它以''作为结束,但在实际使用时并不是那么回事,很多数据中间是有''存在的,所以你可以像我一样,简单修改一下,指定其发送的字节数而不是遇到''结束(参考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是否越界。

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

相关知识

智能鱼缸控制系统:打造您的专属水族乐园
宠物自动喂食控制系统设计
基于STM32单片机的物联网智能鱼缸控制系统设计 蓝牙WIFI无线控制 定时增氧 浊度 多功能鱼缸 宠物喂食系统 成品套件 DIY设计 实物+源程序+原理图+仿真+其它资料(850
DIY鱼缸改造教程:用低成本实现高端鱼缸效果,你也能做到!
基于STM32的物联网下智能化养鱼鱼缸控制控制系统
基于51单片机的水族箱控制系统的设计与实现
DIY鱼缸指南:打造你的迷你水族世界
【常州大型玻璃鱼缸定做】批发
DIY鱼缸装饰:创意周转箱成就感满满!
一种基于强化学习的宠物自动投食控制系统和方法.pdf

网址: 【DIY】自动鱼缸控制系统——【一】 https://m.mcbbbk.com/newsview590477.html

所属分类:萌宠日常
上一篇: 水质管理之培养强大的硝化系统
下一篇: 鱼缸水浑浊怎么处理