Added HUM DriSteem RTS RX36 TCP

This commit is contained in:
RobertJDavis
2025-10-28 15:11:45 -07:00
parent 3046bf3f1f
commit f5bfd67727
9 changed files with 711 additions and 3 deletions

View File

@@ -11,10 +11,10 @@
[platformio]
default_envs = PHX3_VFD_ABB_ACH580_RTU ; Select here the name of the configuration you want to download
default_envs = HUM_DriSteem_RTS_RX36_TCP ; Select here the name of the configuration you want to download
[env]
upload_port = COM50
upload_port = COM9
[common_env_options]
framework = arduino
@@ -206,3 +206,10 @@ platform = espressif32
board = dfrobot_firebeetle2_esp32e
extends = common_env_options
build_src_filter = -<*> +<BMS/VFD/PHX3_VFD_ABB_ACH580_RTU>
[env:HUM_DriSteem_RTS_RX36_TCP]
platform = espressif32
board = dfrobot_firebeetle2_esp32e
extends = common_env_options
build_flags = -D USE_MODBUS_IP
build_src_filter = -<*> +<BMS/HUM/HUM_DriSteem_RTS_RX36_TCP>

View File

@@ -0,0 +1,51 @@
# Humidifier Dri-Steem RTS RX-36-1 TCP
## Brief Introduction
This humidifier receives on/off commands and RH Setpoint from the PLC.
The Space RH register is not used, since there will not be a Space RH sensor wired to the HUM unit.
The RH Setpoint will be determined based on dewpoints in the datahall. See QTS SOO for details.
## List of Equipment
This configuration has been used for these models:
* **RTS RX-36-1**: 10-28-2025
## 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
Run Mode = 3 (system standby)
Duct RH = 35 +/- 5
Fill Valve, Drain Valve = 0
Steam Demand Mass/Pct = 0
Steam Output Mass/Pct = 0
If any alarms active or safety interlock = 0 --> FailState
Checks for Run Mode = 1 AND Air Proving Switch = 1 --> RunningState
### Running State
Run Mode = 1 (auto)
If any alarms active or safety interlock = 0 --> FailState
If Run Mode = 3 or loss of airflow --> StandbyState
Reads RH Setpoint from PLC
DuctRH will dynamically ramp to RH Setpoint
Fill Valve and Drain Valve switch between 0 and 1 (squareStrategy)
Steam Demand Mass between 3-6 (sawStrategy)
Steam Demand Percent between 50-80% (sawStrategy)
Tank Temp = 80 +/- 3
Steam Output Mass = 4 +/- 1
Steam Output Percent = 65 +/- 10
Water Until ADS/Service will ramp down to 0 (initializes at 1500 and 10000)
### Fail State
Run Mode = 3 (system standby)
Duct RH = 35 +/- 5
Fill Valve, Drain Valve = 0
Steam Demand Mass/Pct = 0
Steam Output Mass/Pct = 0
When all alarms are cleared and safety interlock = 1 --> StandbyState

View File

@@ -0,0 +1,91 @@
/**
* @file StateUtils.cpp
* @brief Implementation of the StateUtils class.
* @author Robert J. Davis
* @date 2025-10-28
*
* 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 will update the Alarm bits and Safety Interlock state (based on Safety Interlock ON coil - for testing only)
* If the "Clear All Active Alarms" coil is activate, all alarms will be cleared, the Safety Interlock will be set to 1 (ready to operate),
* and the "Manual Clear Alarm Exists" bit will be set to 1.
* The "Alarms Present" (DI 10) will be set to 1 if any alarm is active (or Safety Interlock = 0). This is a register used for
* testing only, and will be used in Standby and Running States to send to FailState.
*
* This function is used in the update() of the Standby, Running, and Fail States.
*
*/
void updateAlarms(Equipment<ModbusIP>* equipment){
const std::vector<std::string> alarmDescriptions = {
"Tank Temp Sensor Fail", "Tank Overtemp", "Input RH Out of Range", "Duct RH Out of Range",
"Water Probe Check", "Water Probe Faulty", "Fill Time Excessive", "Refill Time Excessive",
"Tank Not Draining", "Boil Time Excessive"
};
// update Safety Interlock state (note: Safety Interlock = 0 means the equipment cannot run- fail safe)
if (equipment->getModbus_Point("Safety Interlock ON")->getValue() == 1){
equipment->setModbus_Point("Safety Interlock", 0);
}
else equipment->setModbus_Point("Safety Interlock", 1);
// if "Clear All Active Alarms" bit is 1 --> clear all alarms as well as safety interlock
// if "Clear All Active Alarms" bit is 0 --> if any alarms present set "Alarms Present" register to 1
if (equipment->getModbus_Point("Clear All Active Alarms")->getValue() == 1){
for (int i =0; i < alarmDescriptions.size(); ++i) {
equipment->setModbus_Point(alarmDescriptions[i], 0);
}
equipment->setModbus_Point("Safety Interlock ON", 0);
equipment->setModbus_Point("Safety Interlock", 1);
equipment->setModbus_Point("Alarms Present", 0);
equipment->setModbus_Point("Manual Clear Alarm Exists", 1); // the only way to set this back to 0 is manually via Modscan
}
else {
int numAlarms = 0;
for (int i =0; i < alarmDescriptions.size(); ++i) {
Modbus_Point<ModbusIP>* alarmPoint = equipment->getModbus_Point(alarmDescriptions[i]);
if (alarmPoint->getValue() == 1) numAlarms++;
}
if (equipment->getModbus_Point("Safety Interlock")->getValue() == 0) numAlarms++;
if (numAlarms >= 1) equipment->setModbus_Point("Alarms Present", 1);
else equipment->setModbus_Point("Alarms Present", 0);
}
}
/**
* @brief This function is used to update the Airflow Proving Switch state (DI 1)
* based on the Safety Interlock ON coil (Coil 2) - this is used for testing purposes only.
*
* This function is used in the update() of the Standby, Running, and Fail States.
*
*/
void updateAirflow(Equipment<ModbusIP>* equipment){
if (equipment->getModbus_Point("Airflow ON")->getValue() == 1){
equipment->setModbus_Point("Airflow Proving Switch", 1);
}
else equipment->setModbus_Point("Airflow Proving Switch", 0);
}

View File

@@ -0,0 +1,43 @@
/**
* @file config.h
* @brief StateUtils class
* @author Robert J Davis
* @date 2025-10-28
*
* 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 all alarms, safety interlock, alarms present register.
* @param equipment Pointer to the Equipment instance.
* @return void
*/
void updateAlarms(Equipment<ModbusIP>* equipment);
/**
* @brief Updates the Airflow Proving Switch state based on Airflow ON state.
* @param equipment Pointer to the Equipment instance.
* @return void
*/
void updateAirflow(Equipment<ModbusIP>* equipment);

View File

@@ -0,0 +1,90 @@
/**
* @file State_Fail.cpp
* @brief Implementation of the FailState class.
* @author Robert J Davis
* @date 2025-10-28
*
* This file contains the implementation for the FailState, which defines
* the behavior of the equipment when it has entered a fault condition.
*/
#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 will have the Duct RH fluctuate around 35% (for visualization purposes).
*
* @param activeAlarms A vector of strings, where each string is the
* description of a Modbus point to be set as an active alarm.
* This parameter is not used in this implementation of the Fail State.
*/
template<>
FailState<ModbusIP>::FailState(const std::vector<std::string>& activeAlarms) {
addStrategy("Duct RH", new SingleValueStrategy(35.0f, 5.0f, 1000));
}
/**
* @brief Executes the fail state's logic for one update cycle.
*
* Update Alarms states. Stays in FailState until all alarms are cleared --> Standby State.
*
* @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) {
// STATE control, add conditions if change to a different state is needed
Serial.println("Fail update function");
updateAlarms(equipment);
bool alarmsPresent = getPointValue(equipment, "Alarms Present");
if (!alarmsPresent){
return new StandbyState<ModbusIP>();
}
_applyStrategies(equipment);
return nullptr;
}
/**
* @brief Logic to execute once when entering the fail state.
* Sets the Run Mode to 3 (system standby), and appropriate analogs to 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...");
// Ensure Run Mode set to 3 (standby)
setPointValue(equipment, "Run Mode", 3);
setPointValue(equipment, "Fill Valve", 0);
setPointValue(equipment, "Drain Valve", 0);
setPointValue(equipment, "Steam Demand Mass", 0);
setPointValue(equipment, "Steam Demand Percent", 0);
setPointValue(equipment, "Steam Output Mass", 0);
setPointValue(equipment, "Steam Output Percent", 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,123 @@
/**
* @file State_Running.cpp
* @brief Implementation of the RunningState class.
* @author Robert J Davis
* @date 2025-10-28
*
* 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 including various analog values. Fill and Drain Valves switch between 0 and 1.
* Water Until ADS/Service will ramp down to 0, initialized at 1500 and 10000, respectively.
*/
template<>
RunningState<ModbusIP>::RunningState() {
addStrategy("Duct RH", new RampStrategy(40.0f, 1.0f, 2000));
addStrategy("Fill Valve", new SquareStrategy(1.0f, 0.0f, 5000));
addStrategy("Drain Valve", new SquareStrategy(0.0f, 1.0f, 4500));
addStrategy("Steam Demand Mass", new SawStrategy(3.0f, 6.0f, 1.0f, 2000)); // these values are semi-random for visualization
addStrategy("Steam Demand Percent", new SawStrategy(50.0f, 80.0f, 5.0f, 1000)); // these values are semi-random for visualization
addStrategy("Tank Temp", new SingleValueStrategy(80.0f, 3.0f, 1000)); // these values are semi-random for visualization
addStrategy("Steam Output Mass", new SingleValueStrategy(4.0f, 1.0f, 1000)); // these values are semi-random for visualization
addStrategy("Steam Output Percent", new SingleValueStrategy(65.0f, 10.0f, 1000)); // these values are semi-random for visualization
addStrategy("Water Until ADS", new RampStrategy(0.0f, 1.0f, 2000)); // ramping down to 0 from 1500
addStrategy("Water Until Service", new RampStrategy(0.0f, 1.0f, 2000)); // ramping down to 0 from 10000
}
/**
* @brief Executes the running state's logic for one update cycle.
*
* This method first checks if there are any active alarms --> FailState.
* Also updates Airflow state according to Airflow ON (Coil 1- for testing use only).
* If no alarms are active, checks for loss of airflow or "Run Mode" = 3 (Modscan, but will be from PLC)
* to transition to the Standby state. If no transition is triggered, it updates the
* rampStrategy targetValue of the Duct RH to dynamically ramp up to the Space RH Setpoint (sent from PLC).
*
* @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) {
// Update alarms states and airflow switch state
updateAlarms(equipment);
updateAirflow(equipment);
// if Alarms are present (as updated in updateAlarms function) --> FailState
bool alarmsPresent = getPointValue(equipment, "Alarms Present");
if (alarmsPresent){
std::vector<std::string> activeAlarmsDesc = {}; // sending a blank string to FailState, b/c that parameter not used in FailState implementation.
return new FailState<ModbusIP>(activeAlarmsDesc);
}
// Check for Run Mode and Airflow. If Run Mode = 3 OR Airflow stopped --> StandbyState
int runMode_Command = getPointValue(equipment, "Run Mode"); // Set by PLC
int airflow = getPointValue(equipment, "Airflow Proving Switch");
if (runMode_Command == 3 || airflow == 0) {
return new StandbyState<ModbusIP>();
}
// Set the Duct RH ramp target value equal to the Space RH Setpoint
float Space_RH_Setpoint = getPointValue(equipment, "Space RH Setpoint");
float ductRH = getPointValue(equipment, "Duct RH");
Strategy_Behavior* DuctRH_strat = getStrategy("Duct RH");
if (DuctRH_strat){
static_cast<RampStrategy*>(DuctRH_strat)->setTarget(Space_RH_Setpoint);
}
// Apply any strategies defined for the standby state
_applyStrategies(equipment);
return nullptr;
}
/**
* @brief Logic to execute once when entering the running state.
*
* Note: do not need to set Run Mode = 1 (auto) since that is required to
* send the unit to Run Mode in the first place. Run Mode will already = 1.
*
* @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...");
}
/**
* @brief Logic to execute once when exiting the running 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...");
}

View File

@@ -0,0 +1,109 @@
/**
* @file State_Standby.cpp
* @brief Implementation of the StandbyState class.
* @author Robert J Davis
* @date 2025-10-28
*
* 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 will have
* Duct RH fluctuate around 35% for visualization purposes only.
*
*/
template<>
StandbyState<ModbusIP>::StandbyState() {
addStrategy("Duct RH", new SingleValueStrategy(35.0f, 5.0f, 1000));
}
/**
* @brief Executes the standby state's logic for one update cycle.
*
* This method first checks for state transition commands:
* 1. Updates Alarm states
* 2. Updates Airflow Switch state (based on Airflow ON command - used just for simulation purposes)
* If any Alarms are active or Safety Interlock = 0, send to FailState.
*
* 3. Check if Run Mode = 1 and Airflow Switch = 1, then send to Running State.
*
* If no transition occurs, it applies the strategies defined for the standby state.
*
* @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");
updateAlarms(equipment);
updateAirflow(equipment);
// if Alarms are present (as updated in updateAlarms function) --> FailState
bool alarmsPresent = getPointValue(equipment, "Alarms Present");
if (alarmsPresent){
std::vector<std::string> activeAlarmsDesc = {}; // sending a blank string to FailState, b/c that parameter not used in FailState implementation.
return new FailState<ModbusIP>(activeAlarmsDesc);
}
// Check for Run Mode and Airflow Proving Switch. If Run Mode = 1 and there is Airflow --> RunningState
int runMode_Command = getPointValue(equipment, "Run Mode"); // Set by PLC
int airflow = getPointValue(equipment, "Airflow Proving Switch");
if (airflow == 1 && runMode_Command == 1) {
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.
* @param equipment Pointer to the Equipment instance.
*/
template<>
void StandbyState<ModbusIP>::enterState(Equipment<ModbusIP>* equipment) {
Serial.println("Enter Standby State...");
// Set Run Mode to 3 (standby), just in case entered Standby on loss of airflow
setPointValue(equipment, "Run Mode", 3);
setPointValue(equipment, "Fill Valve", 0);
setPointValue(equipment, "Drain Valve", 0);
setPointValue(equipment, "Steam Demand Mass", 0);
setPointValue(equipment, "Steam Demand Percent", 0);
setPointValue(equipment, "Steam Output Mass", 0);
setPointValue(equipment, "Steam Output Percent", 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,108 @@
/**
* @file config.h
* @brief Main configuration file for the DriSteem Humidifier (TCP) emulator.
* @author Robert J Davis
* @date 2025-10-27
*
* 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, "Airflow ON"}, // Used for Modscan testing only to set Airflow Proving Switch
{COIL, 1, 0, "Safety Interlock ON"}, // Used for Modscan testing only to trip Safety Interlock
{COIL, 2, 0, "Manual Clear Alarm Exists"},
{COIL, 3, 0, "Clear All Active Alarms"}, // OCmd_Reset
{COIL, 4, 0, "Tank Temp Sensor Fail"},
{COIL, 5, 0, "Tank Overtemp"},
{COIL, 6, 0, "Input RH Out of Range"},
{COIL, 7, 0, "Duct RH Out of Range"},
{COIL, 9, 0, "Water Probe Check"},
{COIL, 10, 0, "Water Probe Faulty"},
{COIL, 11, 0, "Fill Time Excessive"},
{COIL, 12, 0, "Refill Time Excessive"},
{COIL, 13, 0, "Tank Not Draining"},
{COIL, 14, 0, "Boil Time Excessive"},
{DI, 0, 0, "Airflow Proving Switch"}, // 0:open, 1:closed
{DI, 2, 1, "Safety Interlock"}, // 0:open, 1:closed
{DI, 7, 0, "Fill Valve"}, // 0:closed, 1:open
{DI, 8, 0, "Drain Valve"}, // 0:not draining, 1:draining
{DI, 9, 0, "Alarms Present"}, // Used for Modscan testing only - not part of vendor Modbus table
{IR, 0, 0, "Space RH"}, // Relative_Humidity
{IR, 2, 0, "Duct RH"}, // OSet_CV
{IR, 3, 0, "Steam Demand Mass"},
{IR, 4, 0, "Steam Demand Percent"},
{IR, 6, 0, "Tank Temp"},
{IR, 7, 0, "Steam Output Mass"},
{IR, 8, 0, "Steam Output Percent"},
{IR_10x, 9, 1500, "Water Until ADS"}, // 1 = 100 lbs (I know this is 10x function only)
{IR_10x, 10, 10000, "Water Until Service"}, // 1 = 100 lbs (I know this is 10x function only)
{HR, 0, 3, "Run Mode"}, // Operation_Mode, 1:auto, 2:local standby, 3:system standby, 4:manual drain
{HR, 1, 0, "Space RH Setpoint"}, // Relative_Humidity_SP
{HR, 3, 85, "Duct High Limit Setpoint"},
};
//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);
}
}