一直在完善之前的全自动鱼缸。在开发过程中遇到各种各样的问题:3D模型设计、硬件设计、电路设计、走线、HMI程序、ESP8266程序、Arduino程序……有时候一个东西重新设计两三次,有的时候重新3D打印两三次,因为才疏学浅所以会各种困难吧。好在硬件和软件都进入最后测试阶段(电导率传感器只在面包板上实验了一下还没加入),这是一个比较“大”的控制系统,当然这个大是指复杂程度,整个系统分为两个子系统:信息交换子系统和本地控制系统。控制系统包含传感器(温度、水位、浊度等)、控制器(Arduino mega)、执行器(水泵、进水管路控制电磁阀、出水管辂控制电磁阀、散热风扇、加热器等)。
在ESP8266程序中,是在ino文件里面写的代码,因为做的事情简单——只有信息交换,所以只有二三百行代码,逻辑也不混乱;但是,在Arduino程序中包含信息交换、读取传感器、信息和状态分析以及全部决策、控制执行器等。所以,写了若干个类和模块,但是执行器的动作多数比较复杂,如果采取古老的模式用if语句来做,肯定焦头烂额,于是使用了状态机。
我没有自己写通用状态机而使用了arduino-fsm这个库,就以两个栗子来说明一下Arduino-fsm的使用:
一、LED控制
这个例子很简单,LED控制有三种途径:点击HMI屏幕、远程控制、按设置时间自动。归结起来实际上只有两个:
1、手动控制:点击屏幕、APP控制、MQTT服务器页面控制……无论哪种,最终都由Arduino保存在数组中。
2、自动控制:到达设定时间自动开关。时间设置保存在Arduino数组中,而当前时间是由NTP同步后Arduino计时来的。
首先,我们分析其状态有几个:
1、关闭(默认状态)
2、打开
在fsm库中,每一个状态对应3个回调:进入、持续、离开,在控制LED时,我们只需要用到两个:进入——切换管脚状态,持续——检测按键控制和时间控制。这里需要明确的是:进入和离开只执行一次,而持续是一直被执行的。这部分的定义就像下面这样:
//默认LED状态(LED关)
void On_LED_Off_Enter();
void On_LED_Off_State();
//打开状态
void On_LED_On_Enter();
void On_LED_On_State();
//关闭状态
static State State_LED_Off(&On_LED_Off_Enter, &On_LED_Off_State, NULL);
//打开状态
static State State_LED_On(&On_LED_On_Enter, &On_LED_On_State, NULL);
然后,是状态机本身:
//状态机
static Fsm Fsm_LED(&State_LED_Off);
初始化状态机使用的状态就是状态机的默认状态,不需要用额外的代码切换到这一状态。在初始化函数中,我们给状态机添加状态切换:
void ActuatorLED::init(int8_t drivePin, ParameterConfigClass* config)
{
//Actuator_Pin_LED = drivePin;
//pinMode(Actuator_Pin_LED, OUTPUT);
//Param_Config = config;
Fsm_LED.add_transition(&State_LED_Off, &State_LED_On, 1, NULL);
Fsm_LED.add_transition(&State_LED_On, &State_LED_Off, 0, NULL);
}
不必关心我注释掉的部分和形参。这里添加了两个状态切换路径,第1个参数是从哪个状态开始变化,第2个状态是要变化到的状态,第3个参数称为事件,更规范的写法是#define Event_Off2On 1,第4个参数是开始切换状态时的回调,在整个程序中都没有用到这个参数。这个参数使得我们可以在一个状态通往另一个状态时做一些额外处理。
介绍完整个框架之后,我们来看一下如何具体实现状态的切换:
void ActuatorLED::Run()
{
Fsm_LED.run_machine();
}
void ActuatorLED::On_LED_Off_Enter()
{
digitalWrite(Actuator_Pin_LED, LOW);
//设置HMI
HMIMessage::SetVal(HMI_ID_ControlButtonType, HMI_ID_ButtonLED, 0);
//设置ESP
ESPMessage::SendClickButton(HMI_ID_ButtonLED, 0);
}
void ActuatorLED::On_LED_Off_State()
{
//当前时间等于开始时间
if (Param_Config->GetHMIConfigByIndex(HMI_ID_SettingCurTimeHour)== Param_Config->GetHMIConfigByIndex(HMI_ID_SettingLEDStartHour) &&
Param_Config->GetHMIConfigByIndex(HMI_ID_SettingCurTimeMinute)== Param_Config->GetHMIConfigByIndex(HMI_ID_SettingLEDStartMinute)) {
if (HMIMessage::LoadControlButtonValue(HMI_ID_ButtonLED)==0) { //防止多次调用HMI设置
//设置HMI
HMIMessage::SetVal(HMI_ID_ControlButtonType, HMI_ID_ButtonLED, 1);
//设置ESP
ESPMessage::SendClickButton(HMI_ID_ButtonLED, 1);
//设置缓存按钮值。防闪烁(On_LED_On_State中的按钮判断导致闪烁)
HMIMessage::SaveControlButtonValue(HMI_ID_ButtonLED, 1);
}
}
if (HMIMessage::LoadControlButtonValue(HMI_ID_ButtonLED) == 1) {
Fsm_LED.trigger(1);
}
}
run()方法是在轮询过程中被调用的,就是放在Arduino的loop()过程里。首先,看一下On_Led_Off_Enter()方法:设定管脚,修改屏幕和MQTT服务器上的值,这非常简单,它只在状态进入时被执行。然后,On_LED_Off_State()方法:如果处在State_LED_Off状态,那么这个方法每次运行Fsm_LED.run_machine()时都被调用。我们在这个方法中检查时间和按键,当时间到达或按键被按下时,调用Fsm_LED.trigger()方法,其参数就是Event,前面已经提到过,事件=1时将会从State_LED_Off状态切换到State_LED_On状态。接下来的具体切换过程就是由通用状态机的库来完成的,它会依次执行:从Off状态退出的回调(我们没有定义),进入On状态的回调,这样状态就被切换了。下次运行Fsm_LED.run_machine()时执行将会On_Led_On_State()回调函数。
简单的总结一下设计过程:
1、有哪些状态:定义状态
2、每个状态进、出、持续时需要做什么:定义每个状态对应的回调
3、哪些状态之间可以相互切换:添加状态转换的事件
4、在状态持续函数中进行状态转换判定
5、在Loop循环中运行run_machine()方法
二、换水时的混水控制
这里除了水泵之外,还涉及到另外两个执行器:控制进水管路的三通电磁阀、控制出水管路的三通电磁阀。通过这两个电磁阀,可以分别控制水泵进出水管路从而实现鱼缸进水、循环、出水(当然,也可以和鱼缸没关系,作为一个外进外出的水泵)。所以这个控制过程相对复杂一些,但是和上面的LED区别不大。所以,下面要说的并不是整个有限状态机,而是一个子状态机,这个状态机用于在补水操作时每隔一定时间从鱼缸内进水——这当然会降低效率,但水质变化会更缓慢,对于硝化细菌、水草、鱼都是有好处的。arduino-fsm除了提供上面所说的事件驱动的状态切换,还提供了按时间的状态切换:
//添加子状态机状态转换
SubFsm_WaterPump_Flooding.add_timed_transition(&SubState_Flooding, &SubState_Cycle, 3000, NULL); //从补水到循环
SubFsm_WaterPump_Flooding.add_timed_transition(&SubState_Cycle, &SubState_Flooding, 3000, NULL); //从循环到补水
这样一来,我们就可以实现在补水的时候,不断的:外部进水3秒——鱼缸循环3秒,直到水位达到预定值。使用有限状态机来实现控制可以让逻辑更加容易实现。