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

关于串口屏的使用我不打算介绍,只需要参考你的屏的开发文档就可以了。这属于一个“工作性学习”,和使用Arduino或者ESP是有区别的,完全可以用完忘了,下次用再看。并且我的时间很不充裕,把其他部分都精简掉,只讲一下Arduino部分,因为Arduino是这个综合性较强的闭环控制系统的控制器,所以它读取传感器信息、控制控制器动作,代码相对比较多,看起来更像一个工程了:

一、整体的代码:

/*
 Name:		ArduinoProject.ino
 Created:	2020/4/8 14:01:14
 Author:	zcsor
 中控,负责:控制HMI、ESP、执行器,读取传感器,进行运算。 为了更好的适应实际情况,修改了库部分库的内容。
 由于代码比较多,所以定义了一些模块和类以实现功能:
一、信息交换子系统:
    1、和HMI串口通讯,实现可视化的本地信息交互。
    2、和ESP传偶通讯,通过APP实现可视化的远程信息交互。
    3、接收Serial的set命令,从而完成对各项数值的设置。
    使用轮询——事件模式进行设计
 二、控制系统
    1、定时器:控制系统实现了精确的1秒定时器,用于将信息交换子系统和控制系统操作进行隔离:
               提高信息交换子系统的实时性,减少控制系统本身对资源的浪费。
    2、传感器:DS18B20传感器测定水温
               HCSR04传感器测定水位
               TS300B传感器测定浊度
               *DIY TDS传感器测定电导率
    3、控制器:处理信息交换子系统的信息交互,并对操作作出响应
               读取传感器的值,根据传感器的值控制执行器的动作
               给执行器发送指令,更改执行器的状态达到控制目的
    4、执行器:根据控制器的命令,进行状态转移从而达到控制目的
    传感器使用轮询模式进行设计
    控制器和执行器使用有限状态机进行设计
*/

/*
热敏电阻
温度 电压读数
70 --200
23 --530
*/

#include <Fsm.h>
#include "ActuatorAirPump.h"
#include "ActuatorFeed.h"
#include "ActuatorTemp.h"
#include "ActuatorWaterCycle.h"
#include "ActuatorLED.h"
#include "ActuatorWaterCycle.h"
#include "SensorTurb.h"
#include "SensorDist.h"
#include "SensorTemp.h"
#include "SensorColor.h"
#include "SensorTDS.h"
#include "ParameterConfig.h"    //负责HMI、ESP的各项参数的读写。数据保存在EEPROM中。
#include "ESPMessage.h"         //负责与ESP通讯。将数据帧分类派发到回调,向ESP发送指令。
#include "HMIMessage.h"         //负责与HMI通讯。将数据帧分类派发到回调,向HMI发送指令。
#include "SerialFrame.h"        //负责与指定串口通讯。从串口数据中整理出数据帧。
#include <MsTimer2.h>           //负责主循环中按秒计时的计时器。除了信息交换子系统的轮询外,其它的轮询都是每秒一次。
//#include <avr/interrupt.h>

//====================信息交换子系统====================
HardwareSerial* Serial_HMI = &Serial1;
HardwareSerial* Serial_ESP = &Serial2;
uint8_t ESPLost = 0;
uint8_t ESPLostMax = 3;
uint8_t ESPCH_PDPin = 22;       //硬件重启ESP8266(未接)
//====================本地控制子系统====================
//------------------------计时器------------------------
volatile uint8_t Time_Hour = 0;              //当前时
volatile uint8_t Time_Minute = 0;            //当前分
volatile uint8_t Time_Second = 0;            //当前1秒
volatile bool Time_IsChecked = false;        //时间是否校准
uint8_t Time_LastSecond = 0;        //上次的1秒
uint8_t Time_Step_Count = 0;        //经历的1秒的次数
uint8_t Time_Run_Cycle = 0;         //周期切换

//------------------------传感器------------------------
//1.0、水位传感器
uint8_t Sensor_Pin_Dist_TRIG = 40;
uint8_t Sensor_Pin_Dist_ECHO = 41;
//2.0、浊度传感器(以电压表示)
uint8_t Sensor_Pin_Turb = A13;
//3.0、电导率传感器
uint8_t Sensor_Pin_Conductivity_Read = A14;
//4.0、温度传感器
uint8_t Sensor_Pin_Temp = A15;
//5.0、制冷器热端温度传感器
uint8_t Sensor_Pin_CollTemp = A12;
//------------------------执行器------------------------
//在Setup中设置了底层Timer4分频,从使得6、7、8引脚的PWM频率达到31372.55 Hz
//1.1、循环水泵(电压输入端加续流二极管)
uint8_t Actuator_WaterPump_Pin = 8;
//1.2、补水水泵(电压输入端加续流二极管)
uint8_t Actuator_WaterIn_Pin = 5;
//1.3、排水三通电磁阀(电压输入端加续流二极管)
uint8_t Actuator_WaterOut_Pin = 4;
//4.1、降温风扇(电压输入端加续流二极管)。
//uint8_t Actuator_FanLeft_Pin = 7;   
//uint8_t Actuator_FanRight_Pin = 7; 
//4.2、PTC加热。PID控制(最大功率60w)
uint8_t Actuator_Heat_Pin = 2;
//4.3、半导体制冷片、风扇、水冷泵
uint8_t Actuator_ChillPlate_Pin = 11;       //制冷片
//5.1、光源LED。PID控制(最低亮度占空比32,频率≈490/255*32≈61Hz。最高亮度96)
uint8_t Actuator_LED_Pin = 3;
//6.1、自动喂食
uint8_t Actuator_Feed_Pin = 37;                      
//7.1、气泵(时间控制,电压输入端加续流二极管)
uint8_t Actuator_AirPump_Pin = 6;
//------------------------控制器//------------------------
//读写EEPROM保存的配置信息
ParameterConfigClass ParamConfig = ParameterConfigClass();

//初始化函数
void setup() {
    //设置Timer4的分频,将6、7、8引脚的PWM频率修改为31372.55Hz,使气泵、降温风扇、水泵从而实现更好的近似DC,降低它们的噪音。
    TCCR4B = TCCR4B & B11111000 | B00000001;
    //设置Timer3的分频,将2、3、5上的PWM频率修改为3921.16Hz,从而使LED的电流更平稳
    TCCR3B = TCCR3B & B11111000 | B00000010;
    //打开串口
    Serial.begin(115200);
    while (!Serial)
    {
        delay(5);
    }
    //读配置
    ParamConfig.init();
    //先设置HMI,初始化时会初始化按钮状态
    HMIMessage::Init(HMI_NumberChange, HMI_StringChange, HMI_ClickButton);
    HMIMessage::Begin(Serial_HMI, 115200);
    HMIMessage::SetDefUI(
        ParamConfig.HMI_ParamConfig,
        sizeof(ParamConfig.HMI_ParamConfig) / sizeof(ParamConfig.HMI_ParamConfig[0]),
        ParamConfig.ESP_Config.WifiSSID,
        ParamConfig.ESP_Config.MQTTPassword);
    //设置ESP
    ESPMessage::Init(ESPCH_PDPin, ESP_ServerCommand, ESP_NTPCheck, ESP_NetState);
    ESPMessage::Begin(Serial_ESP, 115200);
    ESPMessage::SetDefConfig(ParamConfig.ESP_Config);
    ESPMessage::SendReconnect();
    //初始化传感器
    SensorTemp.init(Sensor_Pin_Temp);
    SensorDist.init(Sensor_Pin_Dist_TRIG, Sensor_Pin_Dist_ECHO, 20, 1000);
    SensorTurb.init(Sensor_Pin_Turb);
    SensorTDS.init(Sensor_Pin_Conductivity_Read);
    //初始化传感器值,避免执行器错误动作
    SensorTemp.Read();
    SensorDist.Read(SensorTemp.Value);
    SensorTurb.Read(SensorTemp.Value);
    SensorTDS.Read(SensorTemp.Value);
    //初始化执行器
    ActuatorWaterCycle::init(Actuator_WaterPump_Pin, Actuator_WaterIn_Pin, Actuator_WaterOut_Pin, &SensorDist, &ParamConfig);
    ActuatorLED::init(Actuator_LED_Pin, &ParamConfig);
    ActuatorTemp::init(Actuator_Heat_Pin, Actuator_ChillPlate_Pin, Sensor_Pin_CollTemp, &ParamConfig, &SensorTemp);
    ActuatorFeed::init(Actuator_Feed_Pin, &ParamConfig);
    ActuatorAirPump::init(Actuator_AirPump_Pin, &ParamConfig);
    //开启计时器,975,快1.5625
    MsTimer2::set(1000, MathBaseTime);
    MsTimer2::start();
}

//主循环
void loop() {
    //处理Serial命令
    SerialCommand();
    //处理HMI的串口信息
    HMIMessage::Loop();
    //处理ESP8266的串口信息
    ESPMessage::Loop();
    //执行器
    ActuatorWaterCycle::Run();
    ActuatorLED::Run(Time_Second);
    ActuatorFeed::Run(Time_Second);
    //每过至少1秒
    if (Time_Second != Time_LastSecond) {
        Time_LastSecond = Time_Second;
        //若未校准时间,则校准时间
        if (!Time_IsChecked) ESPMessage::SendNTPCheck();
        if (Time_Second == 0) {             //每分钟运行一次
            //更新分钟
            ParamConfig.SetHMIConfigByIndex(HMI_ID_SettingCurTimeMinute, Time_Minute);
            HMIMessage::SetFormatNumber(HMI_ID_SettingType, HMI_ID_SettingCurTimeMinute, Time_Minute);
            ESPMessage::SendHMIConfig(HMI_ID_SettingCurTimeMinute, Time_Minute);
        }
        if (Time_Minute == 0) {             //每小时运行一次
            //更新小时
            ParamConfig.SetHMIConfigByIndex(HMI_ID_SettingCurTimeHour, Time_Hour);
            HMIMessage::SetFormatNumber(HMI_ID_SettingType, HMI_ID_SettingCurTimeHour, Time_Hour);
            ESPMessage::SendHMIConfig(HMI_ID_SettingCurTimeHour, Time_Hour);
        }
        if (Time_Step_Count == 0) {
            //温度转换,DS18B20转换和读取都需要较长时间,最好分开运行。
            SensorTemp.Conversion();
            //更新温度
            SensorTemp.Read();
            //发送给HMI显示
            HMIMessage::SetVal(HMI_ID_SensorType, HMI_ID_SensorTemp, SensorTemp.Value * 10);
            //更新距离
            SensorDist.Read(SensorTemp.Value);
            //发送给HMI显示
            HMIMessage::SetVal(HMI_ID_SensorType, HMI_ID_SensorDist, SensorDist.Value);       //这里不用*10,单位转化为cm
        }
        else if (Time_Step_Count == 2) {
            //更新浊度
            SensorTurb.Read(SensorTemp.Value);
            //发送给HMI显示
            HMIMessage::SetVal(HMI_ID_SensorType, HMI_ID_SensorTurb, SensorTurb.Value * 10);
            //当浊度高于设定值时进行提醒
            HMIMessage::SetPco(HMI_ID_SensorType, HMI_ID_SensorTurb, SensorTurb.Value > ParamConfig.GetHMIConfigByIndex(HMI_ID_SettingTurbUpper) ? 0 : 2);
            //更新电导率
            SensorTDS.Read(SensorTemp.Value);
            //发送给HMI显示
            HMIMessage::SetVal(HMI_ID_SensorType, HMI_ID_SensorTDS, SensorTDS.Value * 10);
            //当电导率高于设定值时进行提醒
            HMIMessage::SetPco(HMI_ID_SensorType, HMI_ID_SensorTDS, SensorTDS.Value > ParamConfig.GetHMIConfigByIndex(HMI_ID_SettingTDSUpper) ? 0 : 2);
        }
        else if (Time_Step_Count == 4) {        //这两个是全自动控制,不用特别快的响应速度
            ActuatorTemp::Run();
            ActuatorAirPump::Run();
        }
        if (Time_Step_Count == 1 && Time_Run_Cycle == 1) {
            ESPMessage::SendSensorValue(HMI_ID_SensorTemp, SensorTemp.Value * 10);
            ESPMessage::SendSensorValue(HMI_ID_SensorDist, SensorDist.Value);
        }
        else if (Time_Step_Count == 3 && Time_Run_Cycle == 1) {
            ESPMessage::SendSensorValue(HMI_ID_SensorTurb, SensorTurb.Value * 10);
            ESPMessage::SendSensorValue(HMI_ID_SensorTDS, SensorTDS.Value * 10);
        }
        else if (Time_Step_Count == 4 && Time_Run_Cycle == 1) {
            //查询服务器状态,若未连接服务器,则会连接服务器
            ESPMessage::SendGetNetworkState();
            //上传服务器数据
            ESPMessage::SendPublishSensors();
            //检查ESP工作状态
            ESPLost++;          //增加无通讯状态记录
            if (ESPLost >= ESPLostMax) {
                //硬件重启ESP8266

                //等待

                //重新初始化ESP8266
                ESPMessage::Init(ESPCH_PDPin, ESP_ServerCommand, ESP_NTPCheck, ESP_NetState);
                ESPMessage::Begin(Serial_ESP, 115200);
                ESPMessage::SetDefConfig(ParamConfig.ESP_Config);
                ESPMessage::SendReconnect();
                //归零无通讯状态记录
                ESPLost = 0;
            }
        }
        if (Time_Step_Count == 5) {
            Time_Step_Count = 0;
            Time_Run_Cycle = 1 - Time_Run_Cycle;
        }
        else {
            Time_Step_Count ++;
        }
    }
}

//计时器函数。全部控制都是以1秒为时间单位进行的。
void MathBaseTime() {
    //每次少1.5625个计数
    volatile static int cnt = 0;
    volatile static int ofm = 0;
    cnt += 1;
    if (cnt >=125) {              //每个计数1.024毫秒,125个计数为128毫秒
        cnt -= 16;               
        ofm += 128;                 //累计快的毫秒数
    }
    if (ofm > 1000) {           //将快的超出1秒的部分找回
        ofm -= 1000;
    }
    else {
        Time_Second++; 
        if (Time_Second >= 60) {
            Time_Second = 0;
            Time_Minute++;
        }
        if (Time_Minute >= 60) {
            Time_Minute = 0;
            Time_Hour++;
        }
        if (Time_Hour >= 24) {
            Time_Hour = 0;
            //每天校准一次
            Time_IsChecked = false;
        }
    }
}

//设置网络参数
void SetNtConfig(uint32_t id, String val) {
    if (id == 300) {
        ParamConfig.SetWifiSSID(val);
        HMIMessage::SetText("n", 300, val);
    }
    else if (id == 301) {
        ParamConfig.SetWifiPassword(val);
        HMIMessage::SetText("n", 301, val);
    }
    else if (id == 302) {
        ParamConfig.SetMQTTServerIP(val);
    }
    else if (id == 303) {
        ParamConfig.SetMQTTUserName(val);
    }
    else if (id == 304) {
        ParamConfig.SetMQTTPassword(val);
    }
    else if (id == 305) {
        ParamConfig.SetMQTTClientName(val);
    }
    else if (id == 306) {
        ParamConfig.SetMQTTServerPort(val);
    }
    ESPMessage::SendNetworkConfig(id, val);
}

//串口命令——字符串变化(WIFI SSID,WIFI PASSWORD)
void HMI_StringChange(uint32_t id, String str, uint32_t strlen) {
    //发送消息给HMI,这样用户可以看到设置值是否被正确执行。(HMI不接收包头,发送包尾时使用write保证传输个数和类型正确)
    HMIMessage::SetText("n",id, str);
    //发消息给ESP8266(ESP识别包头,除字符串以外都用write传输个数和类型正确)
    ESPMessage::SendNetworkConfig(id, str);
    //重新连接
    ESPMessage::SendReconnect();
    //保存数据设置到EEPROM
    if (id == ESP_ID_SettingWifiSSID) {
        ParamConfig.SetWifiSSID(str);
    }
    if (id == ESP_ID_SettingWifiPassword) {
        ParamConfig.SetWifiPassword(str);
    }
    ParamConfig.WriteESPConfigToEEPROM();
}

//串口命令——数字变化(时间、上下限等)
void HMI_NumberChange(uint32_t id,  uint32_t num) {
    //发送数据到HMI
    HMIMessage::SetFormatNumber("n", id, num);
     //发送数据到ESP8266(ESP识别包头,除字符串以外都用write传输个数和类型正确)
    ESPMessage::SendHMIConfig(id, num);
    //保存数据设置到EEPROM
    ParamConfig.SetHMIConfigByIndex(id, num);
    ParamConfig.WriteHMIConfigToEEPROM();
}

void HMI_ClickButton(uint32_t id, uint32_t num) {
    //保存按钮值(有限状态机用)
    HMIMessage::SaveControlButtonValue(id, num);
    //发送数据到ESP8266——除了下载按钮(直接发送下载地址)
    if (id == 400) {
        //发送数据到HMI
        HMIMessage::SetText("qr", 0, ParamConfig.GetHMIDownloadUrl());
    }
    else if (id == 401) {
        HMIMessage::SetText("qr", 0, "qywl" + (String)ParamConfig.ESP_Config.MQTTUserName + "|" + (String)ParamConfig.ESP_Config.MQTTPassword + "|" + (String)ParamConfig.ESP_Config.MQTTClientName);
    }
}

//接收到服务器控制命令回调
void ESP_ServerCommand(uint32_t id, uint32_t num) {
    HMIMessage::SaveControlButtonValue(id, num);
}

//同步时间回调
void ESP_NTPCheck(uint32_t id, uint32_t num) {
    //记录时间、更新上次执行时的秒数
    Time_Hour = id & 0xff;
    Time_Minute = id >> 8 & 0xff;
    Time_Second = (id >> 16 & 0xff);
    //更新分钟
    HMIMessage::SetFormatNumber(HMI_ID_SettingType, HMI_ID_SettingCurTimeMinute, Time_Minute);
    //更新小时
    HMIMessage::SetFormatNumber(HMI_ID_SettingType, HMI_ID_SettingCurTimeHour, Time_Hour);
    //更新记录
    ParamConfig.SetHMIConfigByIndex(HMI_ID_SettingCurTimeMinute, Time_Minute);
    ParamConfig.SetHMIConfigByIndex(HMI_ID_SettingCurTimeHour, Time_Hour);
}

//更新服务器连接状态,若未连接则重新发送数据并连接
void ESP_NetState(uint32_t id, uint32_t num) {
    static bool oldWifiIsConnected = false;
    static bool oldMqttIsConnected = false;
    //归零无通讯状态记录
    ESPLost = 0;
    //Wifi连接状态
    HMIMessage::SetPco("n", 302, id);
    //MQTT服务器状态
    HMIMessage::SetPco("n", 303, num);
    if (oldWifiIsConnected==false && id == 1) {         //连接上Wifi,同步时间
        ESPMessage::SendNTPCheck();    
    }
    if (oldMqttIsConnected == false && num == 1) {      //连接上MQTT,发送按钮值、设置值
        for (int i = 402; i <= 409; i++) {
            delay(50);
            ESPMessage::SendClickButton(i, HMIMessage::LoadControlButtonValue(i));
        }
        for (int i = 202; i <= 212; i++) {
            delay(50);
            ESPMessage::SendHMIConfig(i, ParamConfig.GetHMIConfigByIndex(i));
        }
        for (int i = 220; i <= 225; i++) {
            delay(50);
            ESPMessage::SendHMIConfig(i, ParamConfig.GetHMIConfigByIndex(i));
        }
    }
    oldWifiIsConnected = id == 1;
    oldMqttIsConnected = num == 1;
    //如果没有连接服务器,则重新发送连接参数并连接
    if (!oldMqttIsConnected) {
        ESPMessage::SetDefConfig(ParamConfig.ESP_Config);
        ESPMessage::SendReconnect();
    }
}

//串口命令,主要用于手动配置各个端口和HMI、ESP的参数
void SerialCommand() {
    String SerialCommandStr = "set304=……………………………………=\r\n";
    int SerialCommandID = 0;
    int SerialCommandVal = 0;
    int SerialCommandSplit = 0;
    if (Serial.available()>3 && Serial.find('set')) {
        SerialCommandStr = Serial.readStringUntil('\r');
        if (SerialCommandStr !="") {
            SerialCommandSplit = SerialCommandStr.indexOf('=');
            SerialCommandID = atoi(SerialCommandStr.substring(0, SerialCommandSplit).c_str());
            SerialCommandStr = SerialCommandStr.substring(SerialCommandSplit + 1);
            if (SerialCommandID < 100) {      //设置端口
                pinMode(SerialCommandID, OUTPUT);
                SerialCommandVal = atoi(SerialCommandStr.c_str());
                Serial.println((String)"Set pin " + SerialCommandID + "=" + SerialCommandVal);
                if (SerialCommandVal <= 1) {
                    digitalWrite(SerialCommandID, SerialCommandVal);
                }
                else {
                    analogWrite(SerialCommandID, SerialCommandVal);
                }
            }
            else if (SerialCommandID == 100) {   //设置下载地址
                Serial.println("Set DwonloadUrl " + SerialCommandStr);
                ParamConfig.WriteHMIDownloadUrlToEEPROM(SerialCommandStr);
                Serial.println("Set DwonloadUrl " + ParamConfig.GetHMIDownloadUrl());
            }
            else if (SerialCommandID >= 300) {              //设置ESP
                Serial.println((String)"Set esp " + SerialCommandID + "=" + SerialCommandStr);
                //300=234                                    WifiSSID
                //301=234                               WifiPassword
                //302=234                             MQTTServerIP
                //303=234                                    MQTTUserName
                //304=234              MQTTPassword
                //305=234                                 MQTTClientName
                //306=234                                      MQTTServerPort
                //310=234                                         重连MQTT,保存设置
                SetNtConfig(SerialCommandID, SerialCommandStr);
                if (SerialCommandID == 310) {
                    ParamConfig.WriteESPConfigToEEPROM();
                    ESPMessage::SendReconnect();
                }
            }
        }
    }
}

首先,setup函数中使用一些小技巧来更改了一些计时器的分频,这样做的好处显而易见——除了LED其他的我并没有使用滤波电路,并且它们经受住了时间的考验。而使用PWM控制LED亮度时的滤波电路也仅仅是一个简单的RC滤波,虽然它很热,但依旧经受住了时间的考验。接下来就是各种初始化,在loop函数中,按时间间隔处理各种事物,而不是一直在处理,这是一个非常好玩的事情——使用delay不利于更新时间,因为很多自动控制是被设计为到达指定时间开始和结束的,并且我不希望它在一分钟之内反复横跳:

void HMI_ClickButton(uint32_t id, uint32_t num) {
    //保存按钮值(有限状态机用)
    HMIMessage::SaveControlButtonValue(id, num);
    //发送数据到ESP8266——除了下载按钮(直接发送下载地址)
    if (id == 400) {
        //发送数据到HMI
        HMIMessage::SetText("qr", 0, ParamConfig.GetHMIDownloadUrl());
    }
    else if (id == 401) {
        HMIMessage::SetText("qr", 0, "qywl" + (String)ParamConfig.ESP_Config.MQTTUserName + "|" + (String)ParamConfig.ESP_Config.MQTTPassword + "|" + (String)ParamConfig.ESP_Config.MQTTClientName);
    }
}

这个函数动态的设置了二维码的内容,所以,这个二维码就像上面显示的一样,当点击下载时将会显示下载APP的URL,而点击登录时会显示当前设备的信息,使用APP扫码即可登录。关于最后一个函数:


//串口命令,主要用于手动配置各个端口和HMI、ESP的参数
void SerialCommand() {
    String SerialCommandStr = "set304=………………………………………………g=\r\n";
    int SerialCommandID = 0;
    int SerialCommandVal = 0;
    int SerialCommandSplit = 0;
    if (Serial.available()>3 && Serial.find('set')) {
        SerialCommandStr = Serial.readStringUntil('\r');
        if (SerialCommandStr !="") {
            SerialCommandSplit = SerialCommandStr.indexOf('=');
            SerialCommandID = atoi(SerialCommandStr.substring(0, SerialCommandSplit).c_str());
            SerialCommandStr = SerialCommandStr.substring(SerialCommandSplit + 1);
            if (SerialCommandID < 100) {      //设置端口
                pinMode(SerialCommandID, OUTPUT);
                SerialCommandVal = atoi(SerialCommandStr.c_str());
                Serial.println((String)"Set pin " + SerialCommandID + "=" + SerialCommandVal);
                if (SerialCommandVal <= 1) {
                    digitalWrite(SerialCommandID, SerialCommandVal);
                }
                else {
                    analogWrite(SerialCommandID, SerialCommandVal);
                }
            }
            else if (SerialCommandID == 100) {   //设置下载地址
                Serial.println("Set DwonloadUrl " + SerialCommandStr);
                ParamConfig.WriteHMIDownloadUrlToEEPROM(SerialCommandStr);
                Serial.println("Set DwonloadUrl " + ParamConfig.GetHMIDownloadUrl());
            }
            else if (SerialCommandID >= 300) {              //设置ESP
                Serial.println((String)"Set esp " + SerialCommandID + "=" + SerialCommandStr);
                //300=123                                    WifiSSID
                //301=123                               WifiPassword
                //302=123                             MQTTServerIP
                //303=123                                   MQTTUserName
                //304=123              MQTTPassword
                //305=123                                 MQTTClientName
                //306=123                                      MQTTServerPort
                //310=1                                         重连MQTT,保存设置
                SetNtConfig(SerialCommandID, SerialCommandStr);
                if (SerialCommandID == 310) {
                    ParamConfig.WriteESPConfigToEEPROM();
                    ESPMessage::SendReconnect();
                }
            }
        }
    }
}

这个函数中有一段不太好理解的东西:>=300时的处理,这是在串口调试器中手工调试与ESP通讯时使用的测试代码。

二、几个核心类的实现:

1、串口通讯

#include "SerialFrame.h"

bool SerialFrame::Read(HardwareSerial* hwSerial,bool debugMessage)
{
    PackHead = false;                               //是否找到帧头   
    PackTail = false;                               //是否找到帧尾
    Serial_FrameBuff_Ptr = 0;                       //缓存指针
    //memset(Serial_FrameBuff, 0, sizeof(Serial_FrameBuff));  //数据清零(不清零也不影响正确工作)
    //循环读取串口数据,得到一个完整帧(舍弃了包头,但是包尾还在)
    while (hwSerial->available() > 0)
    {
        Serial_FrameBuff[Serial_FrameBuff_Ptr++] = (uint8_t)hwSerial->read();
        if (!PackHead) {                                    //没找到包头时
            if (debugMessage) {
                Serial.print((char)Serial_FrameBuff[Serial_FrameBuff_Ptr - 1]);
            }
            //Serial.print((char)Serial_FrameBuff[Serial_FrameBuff_Ptr-1]);     //输出其它信息
            if (Serial_FrameBuff_Ptr >= 1) {                //确定缓存中后3字节是不是包头
                PackHead = Serial_FrameBuff[Serial_FrameBuff_Ptr - 1] == Serial_Frame_Start;
                if (PackHead) {
                    Serial_FrameBuff_Ptr = 0;               //找到包头时从缓存头部写入数据
                }
            }
        }
        else {                                              //找到包头时
            if (Serial_FrameBuff_Ptr >= 11) {               //确定缓存中后3字节是不是包尾
                PackTail = Serial_FrameBuff[Serial_FrameBuff_Ptr - 1] == Serial_Frame_End[2] &&
                    Serial_FrameBuff[Serial_FrameBuff_Ptr - 2] == Serial_Frame_End[1] &&
                    Serial_FrameBuff[Serial_FrameBuff_Ptr - 3] == Serial_Frame_End[0];
            }
        }
        if (PackHead && PackTail) {                         //找到完整包的时候,把两个32位数字提取出来
            memmove(&Serial_FrameBuff_Index, Serial_FrameBuff + 1, sizeof(Serial_FrameBuff_Index));
            memmove(&Serial_FrameBuff_Number, Serial_FrameBuff + 5, sizeof(Serial_FrameBuff_Number));
            //memset(Serial_FrameBuff + 9 + Serial_FrameBuff_Number, 0, 3);       //剔除包尾
            break;
        }
        delay(5);                                           //确保一帧数据完全达到串口
    }
    return PackHead && PackTail;
}

void SerialFrame::Write(HardwareSerial* hwSerial, uint8_t cmd,uint32_t id,uint32_t val)
{
    hwSerial->write((uint8_t*)&Serial_Frame_Start, 1);
    hwSerial->write((uint8_t*)&cmd, 1);
    hwSerial->write((uint8_t*)&id, sizeof(id));
    hwSerial->write((uint8_t*)&val, sizeof(val));
    hwSerial->write((uint8_t*)Serial_Frame_End, sizeof(Serial_Frame_End));
    delay(10);
}

void SerialFrame::Write(HardwareSerial* hwSerial, uint8_t cmd, uint32_t id, String str,uint32_t strlen)
{
    hwSerial->write((uint8_t*)&Serial_Frame_Start, 1);
    hwSerial->write((uint8_t*)&cmd, 1);
    hwSerial->write((uint8_t*)&id, sizeof(id));
    hwSerial->write((uint8_t*)&strlen, sizeof(strlen));
    hwSerial->print(str);
    hwSerial->write((uint8_t*)Serial_Frame_End, sizeof(Serial_Frame_End));
    delay(10);
}

void SerialFrame::Write(HardwareSerial* hwSerial, String str)
{
    hwSerial->print(str);
    hwSerial->write((uint8_t*)Serial_Frame_End, sizeof(Serial_Frame_End));
    delay(10);
}

uint8_t SerialFrame::GetFlag()
{
    return Serial_FrameBuff[0];
}

uint32_t SerialFrame::GetIndex()
{
    return Serial_FrameBuff_Index;
}

uint32_t SerialFrame::GetNumber()
{
    return Serial_FrameBuff_Number;
}

String SerialFrame::GetString()
{
    String val = (char*)(Serial_FrameBuff + 9);
    val = val.substring(0, Serial_FrameBuff_Number);
    return val;
}

void SerialFrame::PrintData()
{
    Serial.println("Frame Data:");
    for (int i = 0; i < Serial_FrameBuff_Ptr; i++) {
        Serial.print((char)Serial_FrameBuff[i]);
    }
    Serial.println();
}

SerialFrame Frame;

这个类使用的代码和之前ESP的类似,所以你也需要进行类似的修改。在整个通讯过程中,每个包只有3、4个数据需要发送或接收,所以这里很容易编写这些读写函数需要注意的还是指针类型的转换。

2、TDS传感器

// 
// 
// 

#include "SensorTDS.h"


void SensorTDSClass::init( uint8_t analogPin)
{
	Sensor_Turb_Read_Pin = analogPin;
	pinMode(Sensor_Turb_Read_Pin, INPUT);
}

void SensorTDSClass::ReadTDS(float curTemp)
{
	//读取模块输出的模拟电压转换成参考电压5v,并从0~1024映射到0~5V:
	//V=analogRead() * Sensor_Turb_Drive_Voltate/5.0f;
	//V=V/1024*5.0f
	float V = analogRead(Sensor_Turb_Read_Pin) * Sensor_Turb_Ref_Voltate / 1024.0f;
	//计算TDS
	float TDS = (133.42f * V * V * V - 255.86f * V * V + 857.39 * V) / 2;
	//计算温度矫正
	float TK = curTemp > 50.0f ? 1.0f : 1.0f + 0.02f * (curTemp - 25.0f);
	//应用矫正系数k和温度矫正
	Value = TDS * RatioK / TK;
}

float SensorTDSClass::MathK(float sampleTemp)
{
	//计算参考TK
	float refTK = 1.0f + 0.02f * (refTemp - 25.0f);
	//把参考TDS换算成25℃下的值
	//TDSn/TKn=TDS25/TK25,TK25=1
	float refTDS = refTDSppm / refTK;
	//计算样本TK
	float samTK = 1.0f + 0.02f * (sampleTemp - 25.0f);
	//获取样本TDS(未校正)
	RatioK = 1.0f;
	ReadTDS(sampleTemp);
	//计算校正系数k
	//Value=TDS*RatioK
	RatioK = refTDS / Value;
	return RatioK;
}

//纯净水=0;自来水=6~7;自然界淡水18;达到19说明盐分高于海水。
void SensorTDSClass::Read(float curTemp)
{
	//读取模块输出的模拟电压转换成参考电压5v,并从0~1024映射到0~5V:
	//V=analogRead() * Sensor_Turb_Drive_Voltate/5.0f;
	//V=V/1024*5.0f
	float V = analogRead(Sensor_Turb_Read_Pin) * Sensor_Turb_Ref_Voltate / 1024.0f;
	//计算TDS
	float TDS = (133.42f * V * V * V - 255.86f * V * V + 857.39 * V) / 2;
	//计算温度矫正
	float TK = curTemp > 50.0f ? 1.0f : 1.0f + 0.02f * (curTemp - 25.0f);
	//应用矫正系数k和温度矫正
	Value = TDS * RatioK / TK;
	Value /= 12.0f;
	//规范化:
	if (Value > 100.0f) {
		Value = 100.0f;
	}
	if (Value < 0.0f) {
		Value = 0.0f;
	}
}


SensorTDSClass SensorTDS;

这个类负责驱动TDS传感器,这个模块在我使用的时候发现很多问题,包括驱动的范例、供电使用脉冲或一直供电时读数相差甚远等等,所以没办法只好找来一些数据表然后参照原始的驱动代码进行了修改。

3、超声波传感器

// 
// 
// 

#include "SensorDist.h"

void SensorDistClass::init(int pinTRIG, int pinECHO, int minDict, int maxDict)
{
	Sensor_Dict = new HCSR04(pinTRIG, pinECHO, minDict, maxDict);
}

void SensorDistClass::Read(float temp)
{
	Value = Sensor_Dict->distanceInMillimeters() + Dist_Offset;
	delay(5);
	Value += Sensor_Dict->distanceInMillimeters() + Dist_Offset;
	delay(5);
	Value += Sensor_Dict->distanceInMillimeters() + Dist_Offset;
	Value /= 3.0f;
	if (Value < 0) {
		Value = 0;
	}
}

SensorDistClass SensorDist;

这里非常简单粗暴的使用了平均值,直接读数是不太可靠的——我的超声波传感器太廉价了^ ^,更好的做法应该是用一个数组存储多次,然后进行软件滤波。这个传感器在近距离(5cm)时是无法正确读数的,所以安装时要距离水面一段距离,如果是底滤或者侧滤则应该安装在泵室上方,我做的上滤所以需要在底板开孔,为了防止鱼虾跳缸,用钢丝滤网遮蔽一下——这完全没有问题,不会影响传感器测定水面的高度。

4、颜色传感器

// 
// 
// 

#include "SensorColor.h"


void SensorColorClass::begin(HardwareSerial* hwSerial)
{
    Serial_TCS34725 = hwSerial;
    Serial_TCS34725->begin(9600);
    while (!Serial_TCS34725)
    {
        delay(1);
    }
    delay(100);
    Serial_TCS34725->write(TCS34725_Command_DefMode, 3);
}

bool SensorColorClass::read()
{
    PackHead = false;                               //是否找到帧头   
    PackHead_Prt = 0;                               //包头校验指针
    Serial_FrameBuff_Ptr = 0;                       //缓存指针
    Serial_FrameBuff_Sum = result_Head_Sum;                       //校验和
    //循环读取串口数据,得到一个完整帧(缓存中只有r,g,b)
    while (Serial_TCS34725->available() > 0)
    {
        //读1字节到缓存
        TCS34725_Result_Buff[Serial_FrameBuff_Ptr] = (uint8_t)Serial_TCS34725->read();
        if (!PackHead) {                                    //没找到包头时,一直写入缓存的0字节
            //Serial.print((char)Serial_FrameBuff[Serial_FrameBuff_Ptr-1]);     //输出其它信息
            if (TCS34725_Result_Head[PackHead_Prt] == TCS34725_Result_Buff[Serial_FrameBuff_Ptr]) {
                PackHead_Prt++;
            }
            else {
                PackHead_Prt == 0;
            }
            PackHead = PackHead_Prt == sizeof(TCS34725_Result_Head);        //头校验是否通过
        }
        else {          //找到包头的时候,从0字节往后写
            Serial_FrameBuff_Sum += TCS34725_Result_Buff[Serial_FrameBuff_Ptr++];
            if (Serial_FrameBuff_Ptr == 4) {
                Serial_FrameBuff_Sum -= TCS34725_Result_Buff[Serial_FrameBuff_Ptr - 1];
                Serial_FrameBuff_Sum &= 0xff;
                break;
            }
        }
        delay(5);                                           //确保一帧数据完全达到串口
    }
    //发送命令
    Serial_TCS34725->write(TCS34725_Command_ReadMcu, sizeof(TCS34725_Command_ReadMcu));
    //返回
    if (PackHead && (Serial_FrameBuff_Sum == TCS34725_Result_Buff[Serial_FrameBuff_Ptr - 1])) {
        r = TCS34725_Result_Buff[0];
        g = TCS34725_Result_Buff[1];
        b = TCS34725_Result_Buff[2];
        //灰度
        Value = (3.0 * r + 5.9 * g + 1.1 * b) / 100.0;
        return true;
    }
    return false;
}

SensorColorClass SensorColor;

这个并没有实装,它的作用是代替浊度传感器,只需要安装一个LED和一片小镜片,就可以得到水的颜色偏差,但可以想象的是,这玩意比浊度传感器更不容易清理,也经常会呈现绿色、褐色…………

5、最后的部分:各种执行器的控制

如果是一个很简单的控制系统,那if就完了。但是作为一个较简单的控制系统,里面有各种传感器、执行器,if出来很low——不易于调试和修改。所以这里使用一种更好玩的技术——有限状态机。如果你不了解这个东西还想理解下面这个最简单的气泵控制过程是如何工作的,那只能先去看一下有限状态机的理论,然后看看Fsm这个库的范例了。

// ActuatorAirPump.h

#ifndef _ACTUATORAIRPUMP_h
#define _ACTUATORAIRPUMP_h

#if defined(ARDUINO) && ARDUINO >= 100
	#include "arduino.h"
#else
	#include "WProgram.h"
#endif

#include <Fsm.h>			
#include "ParameterConfig.h"

namespace ActuatorAirPump {
	//气泵驱动Pin,配置文件
	void init(int8_t drivePin, ParameterConfigClass* config);
	//运行状态机
	void Run();

	//白天状态
	void On_AirPump_Sunlight_Enter();
	void On_AirPump_Sunlight_State();
	//晚上状态
	void On_AirPump_Moonlight_Enter();
	void On_AirPump_Moonlight_State();

	//引脚
	static uint8_t Actuator_Pin_AirPump ;
	//PWM
	static int Actuator_PWM_AirPump_Sunlight = 64;
	static int Actuator_PWM_AirPump_Moonlight = 72;

	//配置
	static ParameterConfigClass* Param_Config;

	//时段定义
#define SunRises 6.0
#define SunSets 18.0

	//白天状态
	static State State_AirPump_Sunlight(&On_AirPump_Sunlight_Enter, &On_AirPump_Sunlight_State, NULL);
	//夜晚状态
	static State State_AirPump_Moonlight(&On_AirPump_Moonlight_Enter, &On_AirPump_Moonlight_State, NULL);
	//状态机
	static Fsm Fsm_AirPump(&State_AirPump_Sunlight);

}

#endif

// 
// 
// 

#include "ActuatorAirPump.h"

void ActuatorAirPump::init(int8_t drivePin, ParameterConfigClass* config)
{
	Actuator_Pin_AirPump = drivePin;
	Param_Config = config;
	pinMode(Actuator_Pin_AirPump, OUTPUT);
	Fsm_AirPump.add_transition(&State_AirPump_Sunlight, &State_AirPump_Moonlight, 1, NULL);
	Fsm_AirPump.add_transition(&State_AirPump_Moonlight, &State_AirPump_Sunlight, 0, NULL);
}

void ActuatorAirPump::Run()
{
	Fsm_AirPump.run_machine();
}

void ActuatorAirPump::On_AirPump_Sunlight_Enter()
{
	//执行
	analogWrite(Actuator_Pin_AirPump, Actuator_PWM_AirPump_Sunlight);
}

void ActuatorAirPump::On_AirPump_Sunlight_State()
{
	//是否处于晚上时段(更高的PWM)
	if (Param_Config->GetHMIConfigByIndex(HMI_ID_SettingCurTimeHour) < SunRises || Param_Config->GetHMIConfigByIndex(HMI_ID_SettingCurTimeHour) > SunSets) {
		Fsm_AirPump.trigger(1);
	}
	else {
		Fsm_AirPump.trigger(0);
	}
}

void ActuatorAirPump::On_AirPump_Moonlight_Enter()
{
	//执行
	analogWrite(Actuator_Pin_AirPump, Actuator_PWM_AirPump_Moonlight);
}

void ActuatorAirPump::On_AirPump_Moonlight_State()
{
	//是否处于白天时段(较低的PWM)
	if (Param_Config->GetHMIConfigByIndex(HMI_ID_SettingCurTimeHour) >= SunRises && Param_Config->GetHMIConfigByIndex(HMI_ID_SettingCurTimeHour) <= SunSets) {
		Fsm_AirPump.trigger(0);
	}
	else {
		Fsm_AirPump.trigger(1);
	}
}

这个有限状态机的库需要定义一些状态,每个状态有进入、持续、结束几个函数被在对应时刻调用。所以在处理事物时,我们只需要定义状态切换条件和要做什么,而不必if它们——有限状态机是一个成熟的代码它知道自己该调用什么函数。当然,你可能觉得这么简单的东西,我if就完了,那你可能会在温度控制(加热、制冷)和上下水(排水、补水)的时候要多写一会。我的建议还是使用有限状态机这一技术来完成这样的工作。

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清晨曦月

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值