final testing before publishing

This commit is contained in:
RobertJDavis
2025-10-14 15:58:16 -07:00
parent a514780b14
commit 4320322365
9 changed files with 946 additions and 11 deletions

View File

@@ -0,0 +1,48 @@
# Datahall CRAH UMAS 10FSV041206-058-117.00x114.00 TCP
## Brief Introduction
This is first of a kind CRAH, so Modbus maps are different.
BMS Control Source (0:speed, 1:external room temp) and
BMS Enable Source (0:keypad, 1:DI, 2:BMS) sent from PLC/Modscan
PLC code for these CRAHs use Speed Control, i.e. Speed Setpoint sent from PLC/BMS.
Return Air is generally not used for control during normal operation.
Leak Detect alarm is the only alarm that will send unit to FailState.
This is a (9) fan array CRAH unit.
## List of Equipmentt
This cofiguration has been used for these models:
* **Model: 10FSV041206-058-117.00x114.00**: 10-07-25
## Hardware Prerequisites
The code is written for an ESP8266/ESP32-style microcontroller with WiFi capabilities.
* **Microcontroller**: [Firebeetle 2 ESP32.](https://www.dfrobot.com/product-2231.html)
---
## States and Strategies
Provide a brief description of what variables and strategies were used in this configuraiton
### Standby State
* **CW valve**: **Ramp Strategy** ramp to 0
* **Supply Air temperature**: **Ramp Strategy** ramp to 74
* **Return Air temperature**: **Ramp Strategy** ramp to 86
* **Fan speeds**: **Ramp Strategy** ramp to 0
* **Fan amps**: **Ramp Strategy** ramp to 0
* **Fan run status**: set to 0
### Running State
* **Fan run status**: set to 1
* **Fan min speed**: initialized to 30
* **Fan max speed**: initialized to 100
* **Fan speed**: **Ramp Strategy** dynamically ramps to speed setpoint
* **Fan amps**: **Ramp Strategy** ramp to 15
* **Fan operating hours**: initialize totalizers
* **Supply air temperature**: **Saw Strategy** ramps back and forth between 60 and 100 deg
* **CW valve**: **PID Strategy** adjusts until Supply Air Temperature matches Supply Air Temperature setpoint
### Fail State
* **Fan run status**: set to 0
* **CW valve**: **Ramp Strategy** ramp to 0
* **Fan speeds**: **Ramp Strategy** ramp to 0
* **Fan amps**: **Ramp Strategy** ramp to 0

View File

@@ -0,0 +1,122 @@
/**
* @file StateUtils.cpp
* @brief Implementation of the StateUtils class.
* @author Robert J. Davis
* @date 2025-10-03
*
* This file contains implementation of utility functions that are used in multiple States.
*/
#include "Strategies/Strategy_Ramp.h"
#include "Strategies/Strategy_Random.h"
#include "Strategies/Strategy_Saw.h"
#include "Strategies/Strategy_SingleValue.h"
#include "Strategies/Strategy_Square.h"
#include "Strategies/Strategy_PID.h"
#include "ModbusPoints/Modbus_Point.h"
#include "ModbusPoints/Modbus_FloatDecorator.h"
#include "Equipment/Equipment.h"
#include "States/State_Standby.h"
#include "States/State_Running.h"
#include "States/State_Fail.h"
#include "States/State.h"
#include "StateUtils.h"
#include <vector>
#include <string>
#if defined(USE_MODBUS_IP)
#include <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#endif
/**
* @brief This function is used to update the Control Mode (feedback),
* based on the selected BMS Control Source and BMS Enable Source.
*
* This is a function used in the update() of the Standby, Running, and Fail States.
*
*/
void updateControlMode(Equipment<ModbusIP>* equipment){
Modbus_Point<ModbusIP>* BMS_Control_Source_Pt = equipment -> getModbus_Point ("BMS Control Source");
Modbus_Point<ModbusIP>* BMS_Enable_Source_Pt = equipment -> getModbus_Point ("BMS Enable Source");
Modbus_Point<ModbusIP>* Control_Mode_Pt = equipment -> getModbus_Point ("Control Mode Selected");
int BMS_Control_Source = BMS_Control_Source_Pt ? BMS_Control_Source_Pt->getValue() : 0;
int BMS_Enable_Source = BMS_Enable_Source_Pt ? BMS_Enable_Source_Pt->getValue() : 0;
int Control_Mode_Selected = Control_Mode_Pt ? Control_Mode_Pt->getValue() : -1;
if (BMS_Control_Source == 0 && BMS_Enable_Source == 2) {
Control_Mode_Selected = 0;
} else if (BMS_Control_Source == 1 && BMS_Enable_Source == 2) {
Control_Mode_Selected = 1;
} else if (BMS_Enable_Source == 0) {
Control_Mode_Selected = 2;
}
equipment->setModbus_Point("Control Mode Selected", Control_Mode_Selected);
}
/**
* @brief This function will update the Alarm status DI bits according to the Alarm Commands from Coils (Modscan)
* It will also update the Common Alarm: if any alarm is active, the Common alarm will also be active.
*
* This is a function used in the update() of the Standby, Running, and Fail States.
*
*/
void updateAlarms(Equipment<ModbusIP>* equipment){
const std::vector<std::string> alarmDescriptions = {
"Alarm Fan 1", "Alarm Fan 2", "Alarm Fan 3", "Alarm Fan 4",
"Alarm Fan 5", "Alarm Fan 6", "Alarm Fan 7", "Alarm Fan 8",
"Alarm Fan 9", "Alarm Dirty Filter", "Alarm Leak Detect",
"Alarm Condensate Pump", "Alarm Fire", "Alarm Smoke"
};
const std::vector<std::string> alarmCommands = {
"Alarm Fan 1 ON", "Alarm Fan 2 ON", "Alarm Fan 3 ON", "Alarm Fan 4 ON",
"Alarm Fan 5 ON", "Alarm Fan 6 ON", "Alarm Fan 7 ON", "Alarm Fan 8 ON",
"Alarm Fan 9 ON", "Alarm Dirty Filter ON", "Alarm Leak Detect ON",
"Alarm Condensate Pump ON", "Alarm Fire ON", "Alarm Smoke ON"
};
int numAlarms = 0;
for (int i =0; i< alarmCommands.size() && i < alarmDescriptions.size(); ++i) {
Modbus_Point<ModbusIP>* commandPoint = equipment->getModbus_Point(alarmCommands[i]);
Modbus_Point<ModbusIP>* alarmPoint = equipment->getModbus_Point(alarmDescriptions[i]);
if (commandPoint) {
alarmPoint->setValue(commandPoint->getValue());
if (alarmPoint->getValue() == 1) numAlarms++;
}
}
if (numAlarms >= 1) equipment->setModbus_Point("Common Alarm", 1);
else equipment->setModbus_Point("Common Alarm", 0);
}
/**
* @brief The purpose of this function is for testing the RA Temp and RA Humidity alarms (Ignition HMI display)
* User will manually set Alarms through Modscan coils, which will change RA Temp or RA Humidity accordingly.
* The RA Temp alarm limits (reference Ignition UDT) are 72, 100.
* The SA Temp alarm limits (reference Ignition UDT) are 72, 78.
* The RA Humidity alarm limits (reference Ignition UDT) are 20, 60.
* If an associated alarm coil is not active, the analog values are set to a safe, un-alarmed value.
*
* NOTE: These analog alarms will not activate the Common Alarm in the Arduino test.
* Since these alarms will be set in Ignition, will not be sent over Modbus from CRAH to Ignition.
*
* This is a function used in the update() of the Running State.
*
*/
// Note: The Common Alarm is not configured to annunciate with these analog low/high alarms.
void updateAnalogs(Equipment<ModbusIP>* equipment){
if (equipment->getModbus_Point("RA Temp Low Alarm ON")->getValue() ==1){
equipment->setModbus_Point("Return Air Temp", 68.0f);
}
else if (equipment->getModbus_Point("RA Temp High Alarm ON")->getValue()==1){
equipment->setModbus_Point("Return Air Temp", 104.0f);
}
else equipment->setModbus_Point("Return Air Temp", 74.0f);
if (equipment->getModbus_Point("RA Humidity Low Alarm ON")->getValue()==1){
equipment->setModbus_Point("Return Air Humidity", 15.0f);
}
else if (equipment->getModbus_Point("RA Humidity High Alarm ON")->getValue()==1){
equipment->setModbus_Point("Return Air Humidity", 65.0f);
}
else equipment->setModbus_Point("Return Air Humidity", 35.0f);
}

View File

@@ -0,0 +1,50 @@
/**
* @file config.h
* @brief StateUtils class
* @author Robert J Davis
* @date 2025-10-06
*
* Defines the StateUtils class, which contains utility functions used in multiple States.
*/
#pragma once
#include "ModbusPoints/Modbus_Point.h"
#include "ModbusPoints/Modbus_FloatDecorator.h"
#include "Equipment/Equipment.h"
#include "States/State_Standby.h"
#include "States/State_Running.h"
#include "States/State_Fail.h"
#include "States/State.h"
#include <vector>
#include <string>
#if defined(USE_MODBUS_IP)
#include <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#endif
template <typename T>
class State;
/**
* @brief Updates the Control Mode based on BMS signals and writes it back to Modbus.
* @param equipment Pointer to the Equipment instance.
* @return The selected control mode (int).
*/
void updateControlMode(Equipment<ModbusIP>* equipment);
/**
* @brief Checks common alarms (non-fail alarms) and updates the Common Alarm Modbus point.
* @param equipment Pointer to the Equipment instance.
* @return true if any common alarm is active, false otherwise.
*/
void updateAlarms(Equipment<ModbusIP>* equipment);
/**
* @brief Checks common alarms (non-fail alarms) and updates the Common Alarm Modbus point.
* @param equipment Pointer to the Equipment instance.
* @return true if any common alarm is active, false otherwise.
*/
void updateAnalogs(Equipment<ModbusIP>* equipment);

View File

@@ -0,0 +1,130 @@
/**
* @file State_Fail.cpp
* @brief Implementation of the FailState class.
* @author Robert J Davis
* @date 2025-10-06
*
* This file contains the implementation for the FailState, which defines
* the behavior of the equipment when it has entered a fault condition.
* This can only be initiated when the Leak Detection Alarm is active according
* to the UMAS SOO. If leak detection alarm --> close the cooling valve, turn off fans.
* The Control Mode and Alarms can still be updated while in a Failed State.
* Also resets the BMS Command to OFF.
*/
#include "ModbusPoints/Modbus_Point.h"
#include "Equipment/Equipment.h"
#include "Strategies/Strategy_Ramp.h"
#include "Strategies/Strategy_SingleValue.h"
#include "Strategies/Strategy_PID.h"
#include "States/State_Standby.h"
#include "States/State_Running.h"
#include "States/State_Fail.h"
#include "StateUtils.h"
#if defined(USE_MODBUS_IP)
#include <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#endif
/**
* @brief Constructs a new FailState object with a list of active alarms.
*
* This constructor creates strategies to ramp the corresponding Modbus points down to 0.
*
*/
template<>
FailState<ModbusIP>::FailState(const std::vector<std::string>& activeAlarms) {
// The only failure mode is if the Leak Detect Alarm is activated, according to UMAS SOO.
// If leak detection alarm --> close cooling valve, turn off fans
// Fan speed --> 0, Run Status --> 0, Amps --> 0
addStrategy("CW Valve Position", new RampStrategy(0.0f, 5.0f, 1000));
addStrategy("Speed Fan 1", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 2", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 3", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 4", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 5", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 6", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 7", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 8", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 9", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Amps Fan 1", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 2", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 3", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 4", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 5", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 6", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 7", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 8", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 9", new RampStrategy(0.0f, 4.5f, 1000));
}
/**
* @brief Executes the fail state's logic for one update cycle.
*
* This method checks the status of the Leak Detect Alarm Modbus point, if alarm clears --> Standby State
* While the CRAH is in a failed state, the Control Mode and Alarms are still updated,
* but the BMS Command cannot be turned on.
*
* @param equipment Pointer to the Equipment instance.
* @return A pointer to a new State if a transition should occur, otherwise nullptr.
*/
template<>
State<ModbusIP>* FailState<ModbusIP>::update(Equipment<ModbusIP>* equipment) {
Serial.println("Fail update function");
// Still want Control Mode and Alarms to be updated while in Fail State
// Ensure BMS Command is set to Off: want operator to re-start from BMS once Leak Detect Alarm is cleared.
updateControlMode(equipment);
updateAlarms(equipment);
updateAnalogs(equipment);
setPointValue(equipment, "ON/OFF Command By BMS", 0);
// The only way to exit the Fail State is for Leak Detect Alarm to turn off, then enter Standby State.
bool leakDetected = equipment->getModbus_Point("Alarm Leak Detect");
if (leakDetected == 0){
return new StandbyState<ModbusIP>();
}
_applyStrategies(equipment);
return nullptr;
}
/**
* @brief Logic to execute once when entering the fail state.
* When entering failed state, turn all fans off (fan status --> 0) and set BMS Command --> 0
* @param equipment Pointer to the Equipment instance.
*/
template<>
void FailState<ModbusIP>::enterState(Equipment<ModbusIP>* equipment) {
// Logic to run when the equipment enters this state
Serial.println("Enter Fail State...");
const std::vector<std::string> motorStatusDescriptions = {
"Run Status Fan 1", "Run Status Fan 2", "Run Status Fan 3",
"Run Status Fan 4", "Run Status Fan 5", "Run Status Fan 6",
"Run Status Fan 7", "Run Status Fan 8", "Run Status Fan 9"
};
// Loop through and set all motor statuses to 0
for (const auto& desc : motorStatusDescriptions) {
Modbus_Point<ModbusIP>* point = equipment->getModbus_Point(desc);
if (point) {
point->setValue(0);
}
};
// Set BMS On/Off Command to 0
setPointValue(equipment, "ON/OFF Command By BMS", 0);
}
/**
* @brief Logic to execute once when exiting the fail state.
* @param equipment Pointer to the Equipment instance.
*/
template<>
void FailState<ModbusIP>::exitState(Equipment<ModbusIP>* equipment) {
// Cleanup logic to run when the equipment leaves this state
Serial.println("Exit Fail State...");
}

View File

@@ -0,0 +1,198 @@
/**
* @file State_Running.cpp
* @brief Implementation of the RunningState class.
* @author Robert J Davis
* @date 2025-10-03
*
* This file contains the implementation for the RunningState, which defines
* the behavior of the equipment when it is actively running.
*/
#include "ModbusPoints/Modbus_Point.h"
#include "ModbusPoints/Modbus_FloatDecorator.h"
#include "Equipment/Equipment.h"
#include "Strategies/Strategy_Ramp.h"
#include "Strategies/Strategy_Random.h"
#include "Strategies/Strategy_Saw.h"
#include "Strategies/Strategy_SingleValue.h"
#include "Strategies/Strategy_Square.h"
#include "Strategies/Strategy_PID.h"
#include "Strategies/Strategy_Totalizer.h"
#include "States/State_Standby.h"
#include "States/State_Running.h"
#include "States/State_Fail.h"
#include "States/State.h"
#include "StateUtils.h"
#include <vector>
#include <string>
#if defined(USE_MODBUS_IP)
#include <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#endif
/**
* @brief Constructs a new RunningState object.
*
* This constructor initializes behavior strategies active during the running
* state, such as a PID controller for the 'CW Valve Position', totalizers
* for the run-hours of each EC fan, fan speed, amps for each fan. Also sets
* the fan min and max speeds to a default value. The supply air temperature
* is set to a saw strategy between 60 and 100, which will also activate the
* SA high (78) and low (72) temperature alarms.
*/
template<>
RunningState<ModbusIP>::RunningState() {
addStrategy("Fan Min Speed", new SingleValueStrategy(30.0f, 0.0f, 1000));
addStrategy("Fan Max Speed", new SingleValueStrategy(100.0f, 0.0f, 1000));
addStrategy("Speed Fan 1", new RampStrategy(0.0f, 1.0f, 200));
addStrategy("Speed Fan 2", new RampStrategy(0.0f, 1.0f, 200));
addStrategy("Speed Fan 3", new RampStrategy(0.0f, 1.0f, 200));
addStrategy("Speed Fan 4", new RampStrategy(0.0f, 1.0f, 200));
addStrategy("Speed Fan 5", new RampStrategy(0.0f, 1.0f, 200));
addStrategy("Speed Fan 6", new RampStrategy(0.0f, 1.0f, 200));
addStrategy("Speed Fan 7", new RampStrategy(0.0f, 1.0f, 200));
addStrategy("Speed Fan 8", new RampStrategy(0.0f, 1.0f, 200));
addStrategy("Speed Fan 9", new RampStrategy(0.0f, 1.0f, 200));
addStrategy("Amps Fan 1", new RampStrategy(15.0f, 2.5f, 1000));
addStrategy("Amps Fan 2", new RampStrategy(15.0f, 2.5f, 1000));
addStrategy("Amps Fan 3", new RampStrategy(15.0f, 2.5f, 1000));
addStrategy("Amps Fan 4", new RampStrategy(15.0f, 2.5f, 1000));
addStrategy("Amps Fan 5", new RampStrategy(15.0f, 2.5f, 1000));
addStrategy("Amps Fan 6", new RampStrategy(15.0f, 2.5f, 1000));
addStrategy("Amps Fan 7", new RampStrategy(15.0f, 2.5f, 1000));
addStrategy("Amps Fan 8", new RampStrategy(15.0f, 2.5f, 1000));
addStrategy("Amps Fan 9", new RampStrategy(15.0f, 2.5f, 1000));
addStrategy("Operating Hours Fan 1", new TotalizerStrategy(1000)); // Does not retain these values when switching States.
addStrategy("Operating Hours Fan 2", new TotalizerStrategy(1000));
addStrategy("Operating Hours Fan 3", new TotalizerStrategy(1000));
addStrategy("Operating Hours Fan 4", new TotalizerStrategy(1000));
addStrategy("Operating Hours Fan 5", new TotalizerStrategy(1000));
addStrategy("Operating Hours Fan 6", new TotalizerStrategy(1000));
addStrategy("Operating Hours Fan 7", new TotalizerStrategy(1000));
addStrategy("Operating Hours Fan 8", new TotalizerStrategy(1000));
addStrategy("Operating Hours Fan 9", new TotalizerStrategy(1000));
addStrategy("Supply Air Temp", new SawStrategy(60.0f, 100.0f, 2.0f, 1000)); // Won't initialize at lower bound; always initializes at 0 b/c FLOAT; initialize manually via Modscan
addStrategy("Return Air Temp", new SawStrategy(70.0f, 80.0f, 1.0f, 1000));
addStrategy("Filter Differential Pressure", new SawStrategy(0.0f, 5.0f, 0.2f, 1000));
addStrategy("CW Valve Position", new PIDStrategy("Supply Air Temp Setpoint", 1000, "Supply Air Temp")); // SAT must be greater than SAT Setpoint for this PID to work.
}
/**
* @brief Executes the running state's logic for one update cycle.
*
* This method first updates the Control Mode (based on BMS Control Source and BMS Enable Source),
* and then updates the Alarms, setting the Common Alarm to 1 if any alarm is active. Alarms may be
* set using Coils 2-9 in Modscan (for Arduino testing only).
*
* If the Leak Detect alarm is active, the unit will transition to FailState.
* If the BMS Command is set to OFF, the unit will transition to StandbyState.
*
* If the unit is still in a RunningState, the fan speed will dynamically be updated
* to ramp to the speed setpoint sent from the PLC/Modscan.
*
* @param equipment Pointer to the Equipment instance.
* @return A pointer to a new State if a transition should occur, otherwise nullptr.
*/
template<>
State<ModbusIP>* RunningState<ModbusIP>::update(Equipment<ModbusIP>* equipment) {
Serial.println("Running update function");
int On_Off_Command = getPointValue(equipment, "ON/OFF Command By BMS"); // Modscan COIL 1
int BMS_Control_Source = getPointValue(equipment, "BMS Control Source"); // Modscan HR 1
int BMS_Enable_Source = getPointValue(equipment, "BMS Enable Source"); // Modscan HR 2
updateControlMode(equipment);
updateAlarms(equipment);
updateAnalogs(equipment);
// Check to see if Leak Detect alarm is active (only alarm which will make unit FAIL and turn off) --> Send to FailState
bool leakDetect = getPointValue(equipment, "Alarm Leak Detect");
if (leakDetect){
return new FailState<ModbusIP>({"Alarm Leak Detect"});
}
// Check to see if BMS Command set to OFF --> Place unit in Standby
// Removed logic of placing unit on standby if BMS_Enable_Source != 2 for ease in testing Mode Feedback.
if (On_Off_Command == 0){
setPointValue(equipment, "ON/OFF Command By BMS", 0);
return new StandbyState<ModbusIP>();
}
// This will update the Fan Speed Setpoint dynamically while in run mode. Fan Speed Setpoint changed through Modscan.
// This functioanlity matches the UMAS SOO specifically for how it calculates Speed Setpoint.
float BMS_Speed_Setpoint = getPointValue(equipment, "Fan Speed Setpoint");
float BMS_Speed_Setpoint_Pct = BMS_Speed_Setpoint / 100.0f;
float Fan_Min_Speed = getPointValue(equipment, "Fan Min Speed");
float Fan_Max_Speed = getPointValue(equipment, "Fan Max Speed");
float Fan_Speed_Setpoint = BMS_Speed_Setpoint_Pct * (Fan_Max_Speed - Fan_Min_Speed) + Fan_Min_Speed;
for (int i = 0; i < 10; i++){
std::string pointName = "Speed Fan " + std::to_string(i);
Strategy_Behavior* strat = getStrategy(pointName);
if (strat) {
RampStrategy* ramp = static_cast<RampStrategy*>(strat);
if (ramp){
if (BMS_Speed_Setpoint > 100){
Fan_Speed_Setpoint = Fan_Max_Speed;
}
else if (BMS_Speed_Setpoint < 0){
Fan_Speed_Setpoint = Fan_Min_Speed;
}
ramp->setTarget(Fan_Speed_Setpoint);
}
}
}
// Apply any strategies defined for the standby state
_applyStrategies(equipment);
return nullptr;
}
/**
* @brief Logic to execute once when entering the running state.
* Sets the "Run Status" for all EC fans to 1 to indicate they are active.
* @param equipment Pointer to the Equipment instance.
*/
template<>
void RunningState<ModbusIP>::enterState(Equipment<ModbusIP>* equipment) {
// Logic to run when the equipment enters this state
Serial.println("Enter Running State...");
// You could also update a Modbus register to show the "standby" state
const std::vector<std::string> motorStatusDescriptions = {
"Run Status Fan 1", "Run Status Fan 2", "Run Status Fan 3",
"Run Status Fan 4", "Run Status Fan 5", "Run Status Fan 6",
"Run Status Fan 7", "Run Status Fan 8", "Run Status Fan 9"
};
// Loop through and set all motor statuses to 1
for (const auto& desc : motorStatusDescriptions) {
Modbus_Point<ModbusIP>* point = equipment->getModbus_Point(desc);
if (point) {
point->setValue(1);
}
}
}
/**
* @brief Logic to execute once when exiting the running state.
* Sets the "Run Status" for all EC fans to 0 before transitioning to the next state.
* @param equipment Pointer to the Equipment instance.
*/
template<>
void RunningState<ModbusIP>::exitState(Equipment<ModbusIP>* equipment) {
// Cleanup logic to run when the equipment leaves this state
Serial.println("Exit Running State...");
const std::vector<std::string> motorStatusDescriptions = {
"Run Status Fan 1", "Run Status Fan 2", "Run Status Fan 3",
"Run Status Fan 4", "Run Status Fan 5", "Run Status Fan 6",
"Run Status Fan 7", "Run Status Fan 8", "Run Status Fan 9"
};
// Loop through and set all motor statuses to 0
for (const auto& desc : motorStatusDescriptions) {
Modbus_Point<ModbusIP>* point = equipment->getModbus_Point(desc);
if (point) {
point->setValue(0);
}
}
}

View File

@@ -0,0 +1,142 @@
/**
* @file State_Standby.cpp
* @brief Implementation of the StandbyState class.
* @author Robert J. Davis
* @date 2025-10-01
*
* This file contains the implementation for the StandbyState, which defines
* the behavior of the equipment when it is in an idle or standby mode.
*/
#include "ModbusPoints/Modbus_Point.h"
#include "ModbusPoints/Modbus_FloatDecorator.h"
#include "Equipment/Equipment.h"
#include "Strategies/Strategy_Ramp.h"
#include "Strategies/Strategy_Random.h"
#include "Strategies/Strategy_Saw.h"
#include "Strategies/Strategy_SingleValue.h"
#include "Strategies/Strategy_Square.h"
#include "Strategies/Strategy_PID.h"
#include "States/State_Standby.h"
#include "States/State_Running.h"
#include "States/State_Fail.h"
#include "States/State.h"
#include "StateUtils.h"
#include <vector>
#include <string>
#if defined(USE_MODBUS_IP)
#include <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#endif
/**
* @brief Constructs a new StandbyState object.
*
* In this state, the equipment is idle. This constructor initializes strategies
* to bring the system to a safe, idle condition. It ramps to a stable value for
* the SAT and RAT readings and creates ramp strategies to bring the CW valve and all
* EC fan speeds and amps down to zero.
*/
template<>
StandbyState<ModbusIP>::StandbyState() {
// You can add initialization code here if needed.
// These strategies are applied at the end of the update function.
addStrategy("CW Valve Position", new RampStrategy(0.0f, 5.0f, 1000));
addStrategy("Supply Air Temp", new RampStrategy(74.0f, 1.0f, 1000));
addStrategy("Return Air Temp", new RampStrategy(86.0f, 1.0f, 1000));
addStrategy("Speed Fan 1", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 2", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 3", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 4", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 5", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 6", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 7", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 8", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Speed Fan 9", new RampStrategy(0.0f, 10.0f, 1000));
addStrategy("Amps Fan 1", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 2", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 3", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 4", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 5", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 6", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 7", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 8", new RampStrategy(0.0f, 4.5f, 1000));
addStrategy("Amps Fan 9", new RampStrategy(0.0f, 4.5f, 1000));
}
/**
* @brief Executes the standby state's logic for one update cycle.
*
* While in Standby, the Control Mode will be updated (based on BMS Control Source and BMS Enable Source),
* the Alarms will also be updated, with the Common Alarm being set to 1 if any alarm is active.
*
* If the Leak Detect alarm is active, the unit will transition to a FailState.
* If in the correct Control Mode, and BMS Command ON is sent from the PLC/Modscan,
* the unit will transition to a RunningState.
*
* @return A pointer to a new State if a transition should occur, otherwise nullptr.
*/
template<>
State<ModbusIP>* StandbyState<ModbusIP>::update(Equipment<ModbusIP>* equipment) {
// STATE control, add conditions if change to a different state is needed
Serial.println("Standby update function");
int On_Off_Command = getPointValue(equipment, "ON/OFF Command By BMS"); // Modscan COIL 1
int BMS_Control_Source = getPointValue(equipment, "BMS Control Source"); // Modscan HR 1
int BMS_Enable_Source = getPointValue(equipment, "BMS Enable Source"); // Modscan HR 2
updateControlMode(equipment);
updateAlarms(equipment);
updateAnalogs(equipment);
// Check to see if Leak Detect alarm is active (only alarm which will make unit FAIL and turn off) --> Send to FailState
bool leakDetect = getPointValue(equipment, "Alarm Leak Detect");
if (leakDetect){
return new FailState<ModbusIP>({"Alarm Leak Detect"});
}
if (On_Off_Command == 1 && BMS_Control_Source == 0 && BMS_Enable_Source == 2){
return new RunningState<ModbusIP>();
}
// Apply any strategies defined for the standby state
_applyStrategies(equipment);
return nullptr;
}
/**
* @brief Logic to execute once when entering the standby state.
* This method performs cleanup by setting all EC fan run status points to 0.
* The BMS Command is also set to OFF.
* @param equipment Pointer to the Equipment instance.
*/
template<>
void StandbyState<ModbusIP>::enterState(Equipment<ModbusIP>* equipment) {
// Logic to run when the equipment enters this state
// A list of all motor run status descriptions
const std::vector<std::string> motorStatusDescriptions = {
"Run Status Fan 1", "Run Status Fan 2", "Run Status Fan 3",
"Run Status Fan 4", "Run Status Fan 5", "Run Status Fan 6",
"Run Status Fan 7", "Run Status Fan 8", "Run Status Fan 9"
};
// Loop through and set all motor statuses to 0
for (const auto& desc : motorStatusDescriptions) {
Modbus_Point<ModbusIP>* point = equipment->getModbus_Point(desc);
if (point) {
point->setValue(0);
}
};
setPointValue(equipment, "ON/OFF Command By BMS", 0);
}
/**
* @brief Logic to execute once when exiting the standby state.
* @param equipment Pointer to the Equipment instance.
*/
template<>
void StandbyState<ModbusIP>::exitState(Equipment<ModbusIP>* equipment) {
// Cleanup logic to run when the equipment leaves this state
Serial.println("Exit Standby State...");
}

View File

@@ -0,0 +1,160 @@
/**
* @file config.h
* @brief Main configuration file for the UMAS CRAH Unit (TCP) emulator.
* @author Robert J Davis
* @date 2025-10-01
*
* This file contains two important configurations: WiFi network parameters
* and the Modbus register map for the device.
*/
#ifndef CONFIG_H
#define CONFIG_H
#include "core.h"
#include "Equipment/Equipment.h"
#if defined(USE_MODBUS_IP)
/**
* @defgroup ModbusTCPConfig Modbus IP Configuration
* @brief Parameters for Modbus TCP communication.
* @{
*/
#include <ModbusIP_ESP8266.h>
const char *ssid = "TP-Link_D91A"; /**< @brief The SSID of the WiFi network. */
const char *password = "52761492"; /**< @brief The password for the WiFi network. */
IPAddress local_IP(192, 168, 1, 234); /**< @brief The static IP address for the device. */
IPAddress gateway(192, 168, 1, 1); /**< @brief The gateway IP address. */
IPAddress subnet(255, 255, 255, 0); /**< @brief The subnet mask. */
ModbusIP mb;
#else
/**
* @defgroup ModbusRTUConfig Modbus RTU Configuration
* @brief Parameters for serial Modbus RTU communication.
* @{
*/
#include <ModbusRTU.h>
const int BAUDRATE = 19200; /**< @brief The serial communication speed in bits per second. */
const int RX_PIN = 17; /**< @brief The GPIO pin used for receiving data (RX). */
const int TX_PIN = 16; /**< @brief The GPIO pin used for transmitting data (TX). */
const int RST_PIN = 4; /**< @brief The GPIO pin connected to the RS485 driver's DE/RE pins for direction control. */
const int MODBUS_ID = 1; /**< @brief The unique slave ID for this device on the Modbus bus. */
/** @} */
/** @brief Global instance of the Modbus RTU server. */
ModbusRTU mb;
#endif
/**
* @defgroup ModbusMapConfig Modbus Map Configuration
* @brief Defines the Modbus register map and related parameters for the emulator.
* @{
*/
/**
* @brief The Modbus map for the Equipment device.
* This array defines all the Modbus points available on the emulated device.
* The `description` field is crucial as it's used to look up points within the application logic.
*/
modbusMap mb_map[] =
{
{COIL, 0, 0, "ON/OFF Command By BMS"}, // Receive signal from PLC
{COIL, 1, 0, "Alarm Fan 1 ON"}, // Just for Arduino testing. Sets Alarm Fan 1 to 1.
{COIL, 2, 0, "Alarm Fan 2 ON"}, // Just for Arduino testing. Sets Alarm Fan 2 to 1.
{COIL, 3, 0, "Alarm Dirty Filter ON"}, // Just for Arduino testing. Sets Alarm Dirty Filter to 1.
{COIL, 4, 0, "Alarm Leak Detect ON"}, // Just for Arduino testing. Sets Alarm Fan 1 to 1.
{COIL, 5, 0, "RA Temp Low Alarm ON"}, // Just for Arduino testing. Sets RA Temp to 68F (low alarm)
{COIL, 6, 0, "RA Temp High Alarm ON"}, // Just for Arduino testing. Sets RA Temp to 104F (high alarm)
{COIL, 7, 0, "RA Humidity Low Alarm ON"}, // Just for Arduino testing. Sets RA Humidity to 15% (low alarm)
{COIL, 8, 0, "RA Humidity High Alarm ON"}, // Just for Arduino testing. Sets RA Humidity to 65% (high alarm)
{COIL, 9, 0, "Alarm Fan 3 ON"}, // Just for Arduino testing. Sets Alarm Fan 3 to 1.
{COIL, 10, 0, "Alarm Fan 4 ON"}, // Just for Arduino testing. Sets Alarm Fan 4 to 1.
{COIL, 11, 0, "Alarm Fan 5 ON"}, // Just for Arduino testing. Sets Alarm Fan 5 to 1.
{COIL, 12, 0, "Alarm Fan 6 ON"}, // Just for Arduino testing. Sets Alarm Fan 6 to 1.
{COIL, 13, 0, "Alarm Fan 7 ON"}, // Just for Arduino testing. Sets Alarm Fan 7 to 1.
{COIL, 14, 0, "Alarm Fan 8 ON"}, // Just for Arduino testing. Sets Alarm Fan 8 to 1.
{COIL, 15, 0, "Alarm Fan 9 ON"}, // Just for Arduino testing. Sets Alarm Fan 9 to 1.
{COIL, 16, 0, "Alarm Condensate Pump ON"}, // Just for Arduino testing. Sets Alarm Condensate Pump to 1.
{COIL, 17, 0, "Alarm Fire ON"}, // Just for Arduino testing. Sets Alarm Fire to 1.
{COIL, 18, 0, "Alarm Smoke ON"}, // Just for Arduino testing. Sets Alarm Smoke to 1.
{DI, 4, 0, "Alarm Leak Detect"},
{DI, 5, 0, "Alarm Dirty Filter"},
{DI, 12, 0, "Common Alarm"}, // Send to PLC
{DI, 14, 0, "Alarm Condensate Pump"},
{DI, 15, 0, "Alarm Smoke"},
{DI, 16, 0, "Alarm Fire"},
{IR_FLOAT, 1, 0, "Supply Air Temp"},
{IR_FLOAT, 3, 0, "Return Air Humidity"}, // Ignition visual only
{IR_FLOAT, 5, 0, "Return Air Temp"}, // Send to PLC
{IR_FLOAT, 7, 0, "Filter Differential Pressure"}, // sawStrategy between 0 and 5
{IR_FLOAT, 9, 0, "CW Valve Position"},
{IR, 26, 0, "Alarm Fan 1"},
{IR, 30, 0, "Alarm Fan 2"},
{IR, 34, 0, "Alarm Fan 3"},
{IR, 38, 0, "Alarm Fan 4"},
{IR, 42, 0, "Alarm Fan 5"},
{IR, 46, 0, "Alarm Fan 6"},
{IR, 50, 0, "Alarm Fan 7"},
{IR, 54, 0, "Alarm Fan 8"},
{IR, 58, 0, "Alarm Fan 9"},
{IR, 27, 0, "Run Status Fan 1"}, // Send to PLC
{IR, 31, 0, "Run Status Fan 2"}, // Send to PLC
{IR, 35, 0, "Run Status Fan 3"}, // Send to PLC
{IR, 39, 0, "Run Status Fan 4"}, // Send to PLC
{IR, 43, 0, "Run Status Fan 5"}, // Send to PLC
{IR, 47, 0, "Run Status Fan 6"}, // Send to PLC
{IR, 51, 0, "Run Status Fan 7"}, // Send to PLC
{IR, 55, 0, "Run Status Fan 8"}, // Send to PLC
{IR, 59, 0, "Run Status Fan 9"}, // Send to PLC
{IR, 25, 0, "Speed Fan 1"},
{IR, 29, 0, "Speed Fan 2"},
{IR, 33, 0, "Speed Fan 3"},
{IR, 37, 0, "Speed Fan 4"},
{IR, 41, 0, "Speed Fan 5"},
{IR, 45, 0, "Speed Fan 6"},
{IR, 49, 0, "Speed Fan 7"},
{IR, 53, 0, "Speed Fan 8"},
{IR, 57, 0, "Speed Fan 9"},
{IR, 28, 0, "Operating Hours Fan 1"},
{IR, 32, 0, "Operating Hours Fan 2"},
{IR, 36, 0, "Operating Hours Fan 3"},
{IR, 40, 0, "Operating Hours Fan 4"},
{IR, 44, 0, "Operating Hours Fan 5"},
{IR, 48, 0, "Operating Hours Fan 6"},
{IR, 52, 0, "Operating Hours Fan 7"},
{IR, 56, 0, "Operating Hours Fan 8"},
{IR, 60, 0, "Operating Hours Fan 9"},
{IR, 61, 0, "Control Mode Selected"},
{IR_FLOAT, 63, 0, "Amps Fan 1"},
{IR_FLOAT, 65, 0, "Amps Fan 2"},
{IR_FLOAT, 67, 0, "Amps Fan 3"},
{IR_FLOAT, 69, 0, "Amps Fan 4"},
{IR_FLOAT, 71, 0, "Amps Fan 5"},
{IR_FLOAT, 73, 0, "Amps Fan 6"},
{IR_FLOAT, 75, 0, "Amps Fan 7"},
{IR_FLOAT, 77, 0, "Amps Fan 8"},
{IR_FLOAT, 79, 0, "Amps Fan 9"},
{HR_FLOAT, 13, 0, "Fan Speed Setpoint"}, // Receive signal from PLC
{HR_FLOAT, 17, 0, "Supply Air Temp Setpoint"}, // Receive signal from PLC
{HR_FLOAT, 21, 0, "Fan Min Speed"}, // Send to PLC
{HR_FLOAT, 23, 0, "Fan Max Speed"}, // Send to PLC
{HR, 25, 0, "BMS Control Source"}, // Receive signal from PLC
{HR, 26, 0, "BMS Enable Source"}, // Receive signal from PLC
};
//Size of modbus map used in FOR cycles, automatically calculated.
/**
* @brief The total number of entries in the `mb_map` array.
* This is calculated at compile time and used for iterating over the map.
*/
const int map_size = sizeof(mb_map) / sizeof(mb_map[0]);
/** @brief The main loop update interval in milliseconds. */
int interval = 250;
/** @} */ // End of ModbusMapConfig group
#endif // CONFIG_H

View File

@@ -0,0 +1,86 @@
/**
* @file main.cpp
* @brief Main execution program for the CRAH Unit (TCP) Emulator.
* @author Emmanuel Hernandez Cruz
* @date 2025-09-02
*
* @details This file contains the main execution program for an Arduino-based emulator of a CRAH unit.
* The program uses a Wi-Fi connection to communicate via the Modbus IP protocol.
*
* The setup() function initializes the following:
* - Serial communication for debugging.
* - Wi-Fi connection using credentials from config.h.
* - A Modbus TCP server.
* - Modbus points (Coils, Holding Registers, etc.) based on a predefined map in config.h.
*
* The loop() function continuously:
* - Services the Modbus TCP server to handle incoming requests.
* - Periodically calls the main update loop for the emulated equipment, which
* manages state transitions and behavior strategies.
*
* @see config.h for Wi-Fi and Modbus configuration.
* @see Equipment.h for the main equipment logic.
* @see State.h for different equipment states.
* @see Strategies/Strategy_Behavior.h for value generation strategies.
* @see Modbus_Point.h for the base class for all Modbus points.
*/
//=================================================================================================================================
//Libraries and declaration of variables.
#include <WiFi.h>
#include "config.h"
#include "ModbusPoints/Modbus_PointFactory.h"
#if defined(USE_MODBUS_IP)
#include <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#endif
//=================================================================================================================================
/**
* @brief Initializes the application.
* @details This function runs once at startup. It configures the serial communication,
* Wi-Fi, and the Modbus server. It also creates and initializes all the Modbus points
* based on the `mb_map` array in `config.h`.
*/
void setup() {
Serial.begin(115200); //Serial comm start
WiFi.config(local_IP, gateway, subnet); // Wifi service start
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("Connected!!");
mb.server(); //Modbus server start
Serial.println("Server Created");
Serial.println(map_size);
for(int i = 0; i < map_size; i++){
Modbus_Point<ModbusIP>* point = createModbus_Point(&mb, mb_map[i].category, mb_map[i].address, mb_map[i].value, mb_map[i].description);
if (point) {
point->addToModbusServer();
EquipmentInstance.addModbus_Point(mb_map[i].description, point);
}
}
Serial.println("All modbus Points created");
Serial.println("Setup function ended");
}
//=================================================================================================================================
/**
* @brief The main application loop.
* @details This function runs repeatedly after setup() has completed. It performs two main actions:
* 1. It continuously services the Modbus server by calling `mb.task()` to handle
* incoming requests from a Modbus master.
* 2. At a fixed interval (defined in `config.h`), it calls `EquipmentInstance.update()`
* to run the emulator's internal state machine and behavior logic.
*/
void loop() {
mb.task();
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
unsigned long startTime = millis();
EquipmentInstance.update();
unsigned long endTime = millis();
unsigned long elapsedTime = endTime - startTime;
Serial.printf("Control Execution time: %d ms\n", elapsedTime);
}
}