From ac20b8249645330471b92e5baf7d77e0d04750be Mon Sep 17 00:00:00 2001 From: Emmanuel Hernandez Cruz Date: Sun, 14 Sep 2025 12:01:55 -0500 Subject: [PATCH] First stable compilation with ModbusRTU and ModbusTCP --- .gitignore | 3 + include/README | 37 + lib/Core/Categories/ModbusCoil.h | 95 ++ lib/Core/Categories/ModbusFloatDecorator.h | 116 +++ lib/Core/Categories/ModbusHreg.h | 75 ++ lib/Core/Categories/ModbusIreg.h | 82 ++ lib/Core/Categories/ModbusIsts.h | 83 ++ lib/Core/Categories/ModbusLongDecorator.h | 86 ++ lib/Core/Categories/ModbusPoint.h | 101 ++ lib/Core/Categories/ModbusPointDecorator.h | 60 ++ lib/Core/Categories/ModbusPointFactory.h | 103 ++ lib/Core/Categories/ModbusScaleDecorator.h | 50 + lib/Core/Equipment/Equipment.h | 223 +++++ lib/Core/README.md | 113 +++ lib/Core/States/State.h | 167 ++++ lib/Core/States/State_Fail.h | 50 + lib/Core/States/State_Running.h | 47 + lib/Core/States/State_Standby.h | 48 + lib/Core/Strategies/Strategy_Behavior.h | 65 ++ lib/Core/Strategies/Strategy_PID.cpp | 73 ++ lib/Core/Strategies/Strategy_PID.h | 43 + lib/Core/Strategies/Strategy_Ramp.cpp | 51 + lib/Core/Strategies/Strategy_Ramp.h | 48 + lib/Core/Strategies/Strategy_Random.cpp | 36 + lib/Core/Strategies/Strategy_Random.h | 37 + lib/Core/Strategies/Strategy_Saw.cpp | 54 ++ lib/Core/Strategies/Strategy_Saw.h | 54 ++ lib/Core/Strategies/Strategy_SingleValue.cpp | 45 + lib/Core/Strategies/Strategy_SingleValue.h | 44 + lib/Core/Strategies/Strategy_Square.cpp | 44 + lib/Core/Strategies/Strategy_Square.h | 45 + lib/Core/Strategies/Strategy_Totalizer.cpp | 38 + lib/Core/Strategies/Strategy_Totalizer.h | 42 + lib/Core/core.h | 69 ++ lib/README | 46 + lib/modbus-esp8266/Modbus.cpp | 931 +++++++++++++++++++ lib/modbus-esp8266/Modbus.h | 363 ++++++++ lib/modbus-esp8266/ModbusAPI.h | 507 ++++++++++ lib/modbus-esp8266/ModbusEthernet.h | 59 ++ lib/modbus-esp8266/ModbusIP_ESP8266.h | 11 + lib/modbus-esp8266/ModbusRTU.cpp | 329 +++++++ lib/modbus-esp8266/ModbusRTU.h | 99 ++ lib/modbus-esp8266/ModbusSettings.h | 148 +++ lib/modbus-esp8266/ModbusTCP.h | 40 + lib/modbus-esp8266/ModbusTCPTemplate.h | 606 ++++++++++++ lib/modbus-esp8266/ModbusTLS.h | 120 +++ lib/modbus-esp8266/darray.h | 70 ++ platformio.ini | 32 + src/CH_Daikin_AWV026B_RTU/State_Fail.cpp | 90 ++ src/CH_Daikin_AWV026B_RTU/State_Running.cpp | 167 ++++ src/CH_Daikin_AWV026B_RTU/State_Standby.cpp | 106 +++ src/CH_Daikin_AWV026B_RTU/config.h | 99 ++ src/CH_Daikin_AWV026B_RTU/main.cpp | 79 ++ src/CRAH_PAHHC_600_C6_TCP/State_Fail.cpp | 87 ++ src/CRAH_PAHHC_600_C6_TCP/State_Running.cpp | 169 ++++ src/CRAH_PAHHC_600_C6_TCP/State_Standby.cpp | 124 +++ src/CRAH_PAHHC_600_C6_TCP/config.h | 134 +++ src/CRAH_PAHHC_600_C6_TCP/main.cpp | 87 ++ test/README | 11 + 59 files changed, 6841 insertions(+) create mode 100644 .gitignore create mode 100644 include/README create mode 100644 lib/Core/Categories/ModbusCoil.h create mode 100644 lib/Core/Categories/ModbusFloatDecorator.h create mode 100644 lib/Core/Categories/ModbusHreg.h create mode 100644 lib/Core/Categories/ModbusIreg.h create mode 100644 lib/Core/Categories/ModbusIsts.h create mode 100644 lib/Core/Categories/ModbusLongDecorator.h create mode 100644 lib/Core/Categories/ModbusPoint.h create mode 100644 lib/Core/Categories/ModbusPointDecorator.h create mode 100644 lib/Core/Categories/ModbusPointFactory.h create mode 100644 lib/Core/Categories/ModbusScaleDecorator.h create mode 100644 lib/Core/Equipment/Equipment.h create mode 100644 lib/Core/README.md create mode 100644 lib/Core/States/State.h create mode 100644 lib/Core/States/State_Fail.h create mode 100644 lib/Core/States/State_Running.h create mode 100644 lib/Core/States/State_Standby.h create mode 100644 lib/Core/Strategies/Strategy_Behavior.h create mode 100644 lib/Core/Strategies/Strategy_PID.cpp create mode 100644 lib/Core/Strategies/Strategy_PID.h create mode 100644 lib/Core/Strategies/Strategy_Ramp.cpp create mode 100644 lib/Core/Strategies/Strategy_Ramp.h create mode 100644 lib/Core/Strategies/Strategy_Random.cpp create mode 100644 lib/Core/Strategies/Strategy_Random.h create mode 100644 lib/Core/Strategies/Strategy_Saw.cpp create mode 100644 lib/Core/Strategies/Strategy_Saw.h create mode 100644 lib/Core/Strategies/Strategy_SingleValue.cpp create mode 100644 lib/Core/Strategies/Strategy_SingleValue.h create mode 100644 lib/Core/Strategies/Strategy_Square.cpp create mode 100644 lib/Core/Strategies/Strategy_Square.h create mode 100644 lib/Core/Strategies/Strategy_Totalizer.cpp create mode 100644 lib/Core/Strategies/Strategy_Totalizer.h create mode 100644 lib/Core/core.h create mode 100644 lib/README create mode 100644 lib/modbus-esp8266/Modbus.cpp create mode 100644 lib/modbus-esp8266/Modbus.h create mode 100644 lib/modbus-esp8266/ModbusAPI.h create mode 100644 lib/modbus-esp8266/ModbusEthernet.h create mode 100644 lib/modbus-esp8266/ModbusIP_ESP8266.h create mode 100644 lib/modbus-esp8266/ModbusRTU.cpp create mode 100644 lib/modbus-esp8266/ModbusRTU.h create mode 100644 lib/modbus-esp8266/ModbusSettings.h create mode 100644 lib/modbus-esp8266/ModbusTCP.h create mode 100644 lib/modbus-esp8266/ModbusTCPTemplate.h create mode 100644 lib/modbus-esp8266/ModbusTLS.h create mode 100644 lib/modbus-esp8266/darray.h create mode 100644 platformio.ini create mode 100644 src/CH_Daikin_AWV026B_RTU/State_Fail.cpp create mode 100644 src/CH_Daikin_AWV026B_RTU/State_Running.cpp create mode 100644 src/CH_Daikin_AWV026B_RTU/State_Standby.cpp create mode 100644 src/CH_Daikin_AWV026B_RTU/config.h create mode 100644 src/CH_Daikin_AWV026B_RTU/main.cpp create mode 100644 src/CRAH_PAHHC_600_C6_TCP/State_Fail.cpp create mode 100644 src/CRAH_PAHHC_600_C6_TCP/State_Running.cpp create mode 100644 src/CRAH_PAHHC_600_C6_TCP/State_Standby.cpp create mode 100644 src/CRAH_PAHHC_600_C6_TCP/config.h create mode 100644 src/CRAH_PAHHC_600_C6_TCP/main.cpp create mode 100644 test/README diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0c19da --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# .gitignore +.vscode/ +.pio/ \ No newline at end of file diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -0,0 +1,37 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the convention is to give header files names that end with `.h'. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/Core/Categories/ModbusCoil.h b/lib/Core/Categories/ModbusCoil.h new file mode 100644 index 0000000..9122406 --- /dev/null +++ b/lib/Core/Categories/ModbusCoil.h @@ -0,0 +1,95 @@ +/** + * @file ModbusCoil.h + * @brief Defines the ModbusCoil class for handling Modbus coils. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-04 + * + * This file contains the definition of the ModbusCoil class, which is a specific + * implementation of the ModbusPoint for handling coils (digital outputs). + */ +#ifndef ModbusCoil_h +#define ModbusCoil_h + +#include "ModbusPoint.h" + +/** + * @class ModbusCoil + * @brief Represents a Modbus coil point. + * + * This class provides a concrete implementation for a Modbus coil, + * which is a single bit digital output. It inherits from ModbusPoint + * and implements its virtual functions for coil-specific operations. + */ +template +class ModbusCoil : public ModbusPoint{ +public: + /** + * @brief Constructor for the ModbusCoil class. + * @param server Pointer to the ModbusIP server instance. + * @param address The Modbus address of the coil. + * @param value The initial value of the coil. + * @param description A description of the coil. + */ + ModbusCoil(T* server, int address, int value, const char* description); + + /** + * @brief Adds the coil to the Modbus server. + */ + void addToModbusServer() override; + + + + /** + * @brief Sets the internal value of the coil. + * @param value The new value for the coil. + */ + void setValue(int value) override; + + /** + * @brief Gets the current internal value of the coil. + * @return The current value of the coil. + */ + int getValue() const override; + + +}; + +template +ModbusCoil::ModbusCoil(T* server, int address, int value, const char* description) + : ModbusPoint(server, address, value, description){ + +} + +/** + * @brief Adds the coil to the Modbus server's register map. + */ +template +void ModbusCoil::addToModbusServer(){ + this->_server->addCoil(this->_address, this->_value); +} + + +/** + * @brief Sets the internal value of the coil. + * + * If the new value is different from the current value, it updates the + * internal value and marks the point as dirty, indicating it needs to be + * written to the Modbus server. + * + * @param value The new value for the coil. + */ +template +void ModbusCoil::setValue(int value){ + this->_server->Coil(this->_address, value); +} + +/** + * @brief Gets the current internal value of the coil. + * @return The current value. + */ +template +int ModbusCoil::getValue() const{ + return this->_server->Coil(this->_address); +} + +#endif \ No newline at end of file diff --git a/lib/Core/Categories/ModbusFloatDecorator.h b/lib/Core/Categories/ModbusFloatDecorator.h new file mode 100644 index 0000000..25f5115 --- /dev/null +++ b/lib/Core/Categories/ModbusFloatDecorator.h @@ -0,0 +1,116 @@ +/** + * @file ModbusFloatDecorator.h + * @brief Defines the ModbusFloatDecorator class for handling 32-bit float values. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for a decorator that combines two 16-bit + * Modbus registers to represent a single 32-bit floating-point value. + */ +#ifndef ModbusFloatDecorator_h +#define ModbusFloatDecorator_h +#include +#include "ModbusPointDecorator.h" + +/** + * @union cracked_float_t + * @brief A union to easily convert between a 32-bit float and two 16-bit integers. + * + * This union allows type-punning between a `float` and an array of two `int16_t` + * values, which simplifies splitting a float into high and low words for Modbus + * transmission. + */ +typedef union{ + float v; /**< @brief The value as a 32-bit float. */ + int16_t as_int[2]; /**< @brief The value as two 16-bit integers. */ +} cracked_float_t; + +/** + * @class ModbusFloatDecorator + * @brief A decorator that combines two 16-bit registers into a 32-bit float. + * + * This class wraps two consecutive ModbusPoint objects (a low-word and a + * high-word point) and treats them as a single 32-bit float value. It + * overrides the necessary methods to handle reading, writing, and value + * conversion across both underlying registers. + */ +template +class ModbusFloatDecorator : public ModbusPointDecorator { +public: + /** + * @brief Constructs a new ModbusFloatDecorator object. + * @param point A pointer to the ModbusPoint for the low-order word. + * @param highOrderPoint A pointer to the ModbusPoint for the high-order word. + */ + ModbusFloatDecorator(ModbusPoint* point, ModbusPoint* highOrderPoint) + : ModbusPointDecorator(point), _highOrderPoint(highOrderPoint) {} + + /** + * @brief Returns the logical type of the point. + * @return Always returns `PointType::FLOAT`. + */ + PointType getType() const override { return PointType::FLOAT; } + + /** + * @brief Sets the 32-bit float value by casting an integer. + * This method is an override for the base class and calls `setFloatValue`. + * @param value The integer value to set (will be cast to float). + */ + void setValue(int value) override { + setFloatValue(static_cast(value)); + } + /** + * @brief Sets the 32-bit float value by splitting it into two 16-bit words. + * + * This method uses a union to split the float into two 16-bit integers + * and sets the values on the two underlying Modbus points. + * @note The order of `as_int[0]` vs `as_int[1]` might need to be swapped + * depending on the endianness of the target Modbus device. + * + * @param value The 32-bit float value to set. + */ + void setFloatValue(float value) { + cracked_float_t buffer; + buffer.v = value; + this->_point->setValue(buffer.as_int[0]); + _highOrderPoint->setValue(buffer.as_int[1]); + } + + /** + * @brief Gets the 32-bit float value, cast to an integer. + * @return The integer representation of the full float value. + */ + int getValue() const override { + return static_cast(getFloatValue()); + } + /** + * @brief Gets the combined 32-bit float value from the two registers. + * + * This method reads the 16-bit integer values from the two underlying + * points and uses a union to reconstruct the 32-bit float value. + * + * @return The reconstructed 32-bit float value. + */ + float getFloatValue() const { + cracked_float_t buffer; + // Directly read the two 16-bit words from the Modbus server + buffer.as_int[0] = this->_point->getServer()->Hreg(this->_point->getAddress()); + buffer.as_int[1] = _highOrderPoint->getServer()->Hreg(_highOrderPoint->getAddress()); + return buffer.v; + } + + // --- Housekeeping Methods --- + + /** @brief Adds both underlying registers to the Modbus server. */ + void addToModbusServer() override { + this->_point->addToModbusServer(); + _highOrderPoint->addToModbusServer(); + } + + + +private: + ModbusPoint* _highOrderPoint; /**< @brief Pointer to the ModbusPoint for the high-order word. */ +}; + +#endif \ No newline at end of file diff --git a/lib/Core/Categories/ModbusHreg.h b/lib/Core/Categories/ModbusHreg.h new file mode 100644 index 0000000..d315d03 --- /dev/null +++ b/lib/Core/Categories/ModbusHreg.h @@ -0,0 +1,75 @@ +/** + * @file ModbusHreg.h + * @brief Defines the ModbusHreg class for handling Modbus holding registers. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-04 + * + * This file contains the definition of the ModbusHreg class, which is a specific + * implementation of the ModbusPoint for handling holding registers (16-bit). + */ +#ifndef ModbusHreg_h +#define ModbusHreg_h + +#include "ModbusPoint.h" + +/** + * @class ModbusHreg + * @brief Represents a Modbus holding register point. + * + * This class provides a concrete implementation for a Modbus holding register, + * which is a 16-bit read/write register. It inherits from ModbusPoint + * and implements its virtual functions for holding register-specific operations. + */ +template +class ModbusHreg : public ModbusPoint{ +public: + /** + * @brief Constructor for the ModbusHreg class. + * @param server Pointer to the ModbusIP server instance. + * @param address The Modbus address of the holding register. + * @param value The initial value of the holding register. + * @param description A description of the holding register. + */ + ModbusHreg(T* server, int address, int value, const char* description); + + /** + * @brief Adds the holding register to the Modbus server. + */ + void addToModbusServer() override; + + + + /** + * @brief Sets the internal value of the holding register. + * @param value The new value for the holding register. + */ + void setValue(int value) override; + + /** + * @brief Gets the current internal value of the holding register. + * @return The current value of the holding register. + */ + int getValue() const override; + + +}; + +template +ModbusHreg::ModbusHreg(T* server, int address, int value, const char* description) + : ModbusPoint(server, address, value, description) {} + +template +void ModbusHreg::addToModbusServer() { + this->_server->addHreg(this->_address, this->_value); +} + +template +void ModbusHreg::setValue(int value) { + this->_server->Hreg(this->_address, value); +} + +template +int ModbusHreg::getValue() const { + return this->_server->Hreg(this->_address); +} +#endif \ No newline at end of file diff --git a/lib/Core/Categories/ModbusIreg.h b/lib/Core/Categories/ModbusIreg.h new file mode 100644 index 0000000..9c7def8 --- /dev/null +++ b/lib/Core/Categories/ModbusIreg.h @@ -0,0 +1,82 @@ +/** + * @file ModbusIreg.h + * @brief Defines the ModbusIreg class for handling Modbus Input Registers. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-04 + * + * This file contains the definition of the ModbusIreg class, which is a specific + * implementation of the ModbusPoint for handling input registers (16-bit read-only). + */ +#ifndef ModbusIreg_h +#define ModbusIreg_h + +#include "ModbusPoint.h" + +/** + * @class ModbusIreg + * @brief Represents a Modbus Input Register point. + * + * This class provides a concrete implementation for a Modbus input register, + * which is a 16-bit read-only register. It inherits from ModbusPoint + * and implements its virtual functions for input register-specific operations. + */ +template +class ModbusIreg : public ModbusPoint{ +public: + /** + * @brief Constructor for the ModbusIreg class. + * @param server Pointer to the ModbusIP server instance. + * @param address The Modbus address of the input register. + * @param value The initial value of the input register. + * @param description A description of the input register. + */ + ModbusIreg(T* server, int address, int value, const char* description); + + /** + * @brief Adds the input register to the Modbus server. + */ + void addToModbusServer() override; + + /** + * @brief Sets the internal value of the input register. + * @param value The new value for the input register. + */ + void setValue(int value) override; + /** + * @brief Gets the current internal value of the input register. + * @return The current value of the input register. + */ + int getValue() const override; +}; + +template +ModbusIreg::ModbusIreg(T* server, int address, int value, const char* description) + : ModbusPoint(server, address, value, description){ + +} +/** + * @brief Sets the internal value of the input register and marks it as dirty. + * @param value The new value for the input register. + */ +template +void ModbusIreg::setValue(int value){ + this->_server->Ireg(this->_address, value); +} +/** + * @brief Gets the current internal value of the input register. + * @return The current value. + */ +template +int ModbusIreg::getValue() const { + return this->_server->Ireg(this->_address); +} + +/** + * @brief Adds the input register to the Modbus server's register map. + */ +template +void ModbusIreg::addToModbusServer(){ + this->_server->addIreg(this->_address, this->_value); +} + +#endif \ No newline at end of file diff --git a/lib/Core/Categories/ModbusIsts.h b/lib/Core/Categories/ModbusIsts.h new file mode 100644 index 0000000..3b33dfd --- /dev/null +++ b/lib/Core/Categories/ModbusIsts.h @@ -0,0 +1,83 @@ +/** + * @file ModbusIsts.h + * @brief Defines the ModbusIsts class for handling Modbus Input Status (Discrete Inputs). + * @author Emmanuel Hernandez Cruz + * @date 2025-09-04 + * + * This file contains the definition of the ModbusIsts class, which is a specific + * implementation of the ModbusPoint for handling discrete inputs (read-only coils). + */ +#ifndef ModbusIsts_h +#define ModbusIsts_h + +#include "ModbusPoint.h" + +/** + * @class ModbusIsts + * @brief Represents a Modbus Input Status (Discrete Input) point. + * + * This class provides a concrete implementation for a Modbus discrete input, + * which is a single bit read-only value. It inherits from ModbusPoint + * and implements its virtual functions for discrete input-specific operations. + */ +template +class ModbusIsts : public ModbusPoint{ +public: + /** + * @brief Constructor for the ModbusIsts class. + * @param server Pointer to the ModbusIP server instance. + * @param address The Modbus address of the discrete input. + * @param value The initial value of the discrete input. + * @param description A description of the discrete input. + */ + ModbusIsts(T* server, int address, int value, const char* description); + + /** + * @brief Adds the discrete input to the Modbus server. + */ + void addToModbusServer() override; + + /** + * @brief Sets the internal value of the discrete input. + * @param value The new value for the discrete input. + */ + void setValue(int value) override; + /** + * @brief Gets the current internal value of the discrete input. + * @return The current value of the discrete input. + */ + int getValue() const override; + +}; + +template +ModbusIsts::ModbusIsts(T* server, int address, int value, const char* description) + : ModbusPoint(server, address, value, description){ + +} +/** + * @brief Sets the internal value of the discrete input and marks it as dirty. + * @param value The new value for the discrete input. + */ +template +void ModbusIsts::setValue(int value){ + this->_server->Ists(this->_address, value); +} +/**ss + * @brief Gets the current internal value of the discrete input. + * @return The current value. + */ +template +int ModbusIsts::getValue() const{ + return this->_server->Ists(this->_address); +} +/** + * @brief Adds the discrete input to the Modbus server's register map. + */ +template +void ModbusIsts::addToModbusServer(){ + this->_server->addIsts(this->_address, this->_value); +} + + +#endif \ No newline at end of file diff --git a/lib/Core/Categories/ModbusLongDecorator.h b/lib/Core/Categories/ModbusLongDecorator.h new file mode 100644 index 0000000..4e4189a --- /dev/null +++ b/lib/Core/Categories/ModbusLongDecorator.h @@ -0,0 +1,86 @@ +/** + * @file ModbusLongDecorator.h + * @brief Defines the ModbusLongDecorator class for handling 32-bit long values. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for a decorator that combines two 16-bit + * Modbus registers to represent a single 32-bit long integer value. + */ +#ifndef ModbusLongDecorator_h +#define ModbusLongDecorator_h + +#include "ModbusPointDecorator.h" + +/** + * @class ModbusLongDecorator + * @brief A decorator that combines two 16-bit registers into a 32-bit long. + * + * This class wraps two consecutive ModbusPoint objects (a low-word and a + * high-word point) and treats them as a single 32-bit long integer. It + * overrides the necessary methods to handle reading, writing, and value + * conversion across both underlying registers. + */ +template +class ModbusLongDecorator : public ModbusPointDecorator { +public: + /** + * @brief Constructs a new ModbusLongDecorator object. + * @param point A pointer to the ModbusPoint for the low-order word (LSB). + * @param highOrderPoint A pointer to the ModbusPoint for the high-order word (MSB). + */ + ModbusLongDecorator(ModbusPoint* point, ModbusPoint* highOrderPoint) + : ModbusPointDecorator(point), _highOrderPoint(highOrderPoint) {} + + /** + * @brief Returns the logical type of the point. + * @return Always returns `PointType::LONG`. + */ + PointType getType() const override { return PointType::LONG; } + + // --- Housekeeping Methods --- + + /** @brief Adds both underlying registers to the Modbus server. */ + void addToModbusServer() override { + this->_point->addToModbusServer(); + _highOrderPoint->addToModbusServer(); + } + + + + // --- Value Getters and Setters --- + + /** + * @brief Sets the 32-bit long value by splitting it into two 16-bit words. + * This method is an override for the base class and calls `setLongValue`. + * @param value The integer value to set (will be cast to long). + */ + void setValue(int value) override { + setLongValue(static_cast(value)); + } + + /** + * @brief Sets the 32-bit long value by splitting it into two 16-bit words. + * @param value The 32-bit long value to set. + */ + void setLongValue(long value) { + this->_point->setValue(value & 0xFFFF); // Low Word (LSB) + _highOrderPoint->setValue((value >> 16) & 0xFFFF); // High Word (MSB) + } + + /** @brief Gets the combined 32-bit long value from the two registers. */ + long getLongValue() const { + long lsb = this->_point->getValue(); + long msb = _highOrderPoint->getValue(); + return (msb << 16) | lsb; + } + + /** @brief Gets the 32-bit long value, cast to an integer. */ + int getValue() const override { + return static_cast(getLongValue()); + } +private: + ModbusPoint* _highOrderPoint; /**< @brief Pointer to the ModbusPoint for the high-order word. */ +}; + +#endif \ No newline at end of file diff --git a/lib/Core/Categories/ModbusPoint.h b/lib/Core/Categories/ModbusPoint.h new file mode 100644 index 0000000..85adb6a --- /dev/null +++ b/lib/Core/Categories/ModbusPoint.h @@ -0,0 +1,101 @@ +/** + * @file ModbusPoint.h + * @brief Defines the abstract base class for all Modbus points. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-04 + * + * This file contains the definition of the abstract ModbusPoint class, which + * serves as the base for all specific Modbus point types (Coils, Registers, etc.). + * It defines the common interface for interacting with Modbus points. + */ +#ifndef ModbusPoint_h +#define ModbusPoint_h +#include +/** + * @enum PointType + * @brief Identifies the logical type of a Modbus point. + * + * This enum is used to distinguish between simple points and decorated points + * that represent more complex data types like floats or longs. + */ +enum class PointType { + GENERIC, /**< @brief A standard, non-decorated point. */ + FLOAT, /**< @brief A point decorated to handle 32-bit float values. */ + LONG, /**< @brief A point decorated to handle 32-bit long integer values. */ + SCALE /**< @brief A point decorated to apply a scaling factor (e.g., 10x). */ +}; + +/** + * @class ModbusPoint + * @brief Abstract base class representing a single point in the Modbus map. + * + * This class defines the common interface and data for all types of Modbus + * points. Concrete implementations (e.g., ModbusCoil, ModbusHreg) and decorators + * must inherit from this class and implement its pure virtual functions. + */ +template +class ModbusPoint{ +public: + /** + * @brief Constructs a new ModbusPoint object. + * @param server Pointer to the ModbusIP server instance. + * @param address The Modbus address of the point. + * @param value The initial value of the point. + * @param description A descriptive name for the point. + */ + ModbusPoint(T* server, int address, int value, const char* description); + + // --- Getters --- + /** @brief Gets a pointer to the ModbusIP server instance. */ + T* getServer() const { return _server; } + /** @brief Gets the Modbus address of the point. */ + int getAddress() const { return _address; } + /** @brief Gets the initial value assigned to the point at creation. */ + float getInitialValue() const { return _value; } + /** @brief Gets the descriptive name of the point. */ + const char* getDescription() const { return _description; } + + // --- Virtual Interface --- + + /** @brief Returns the logical type of the point (e.g., FLOAT, LONG). */ + virtual PointType getType() const { return PointType::GENERIC; } + /** @brief Pure virtual function to add the point to the Modbus server's registers. */ + virtual void addToModbusServer() = 0; + /** @brief Pure virtual function to read the point's value from the Modbus server. */ + + /** @brief Pure virtual function to set the point's internal value. */ + virtual void setValue(int value) = 0; + /** @brief Pure virtual function to get the point's internal value. */ + virtual int getValue() const = 0; + + // --- Dirty Flag --- + + /** + * @brief Checks if the point's value has changed since the last write. + * @return True if the value is dirty, false otherwise. + */ + bool isDirty() const { return _dirty; } + /** + * @brief Sets the dirty flag for the point. + * @param dirty The new state of the dirty flag. + */ + void setDirty(bool dirty) { _dirty = dirty; } + + /** @brief Virtual destructor to ensure proper cleanup of derived classes. */ + virtual ~ModbusPoint() = default; + +protected: + T* _server; /**< @brief Pointer to the global ModbusIP server instance. */ + int _address; /**< @brief The Modbus address of this point. */ + int _value; /**< @brief The current internal value of this point. */ + char _description[35]; /**< @brief A descriptive name for this point. */ + bool _dirty = false; /**< @brief Flag to track if the value has changed and needs to be written. */ +}; + +template +ModbusPoint::ModbusPoint(T* server, int address, int value, const char* description) + : _server(server), _address(address), _value(value) { + strncpy(_description, description, sizeof(_description) - 1); + _description[sizeof(_description) - 1] = '\0'; +} +#endif \ No newline at end of file diff --git a/lib/Core/Categories/ModbusPointDecorator.h b/lib/Core/Categories/ModbusPointDecorator.h new file mode 100644 index 0000000..858fb6f --- /dev/null +++ b/lib/Core/Categories/ModbusPointDecorator.h @@ -0,0 +1,60 @@ +/** + * @file ModbusPointDecorator.h + * @brief Defines the base decorator class for Modbus points. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for ModbusPointDecorator, which is the + * abstract base class for all decorators in the Decorator design pattern. + * It wraps a ModbusPoint and forwards all calls to it by default. + */ +#ifndef ModbusPointDecorator_h +#define ModbusPointDecorator_h +#include "ModbusPoint.h" + +/** + * @class ModbusPointDecorator + * @brief An abstract base class for decorating ModbusPoint objects. + * + * This class follows the Decorator pattern. It wraps a `ModbusPoint` object + * and provides a default implementation for all virtual methods that simply + * delegate the call to the wrapped object. Concrete decorators should inherit + * from this class and override the specific methods they need to modify. + */ +template +class ModbusPointDecorator : public ModbusPoint{ +public: + /** + * @brief Constructs a new ModbusPointDecorator object. + * + * Initializes the base ModbusPoint with the properties of the wrapped point + * and stores a pointer to the wrapped point. + * + * @param point A pointer to the ModbusPoint object to be decorated. + */ + ModbusPointDecorator(ModbusPoint* point) : ModbusPoint( + point->getServer(), + point->getAddress(), + point->getInitialValue(), + point->getDescription()), + _point(point) {} + + // --- Delegated Methods --- + // These methods simply forward the call to the wrapped _point object. + // Concrete decorators can override them to add new behavior. + + void addToModbusServer() override{ + _point->addToModbusServer(); + } + + void setValue(int value) override { + _point->setValue(value); + } + int getValue() const override{ + return _point->getValue(); + } + +protected: + ModbusPoint* _point; /**< @brief Pointer to the wrapped ModbusPoint object. */ +}; +#endif \ No newline at end of file diff --git a/lib/Core/Categories/ModbusPointFactory.h b/lib/Core/Categories/ModbusPointFactory.h new file mode 100644 index 0000000..7fc1fab --- /dev/null +++ b/lib/Core/Categories/ModbusPointFactory.h @@ -0,0 +1,103 @@ +/** + * @file ModbusPointFactory.h + * @brief Defines the factory function for creating ModbusPoint objects. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file provides the interface for a factory function that simplifies the + * creation of various ModbusPoint types (e.g., Coils, Holding Registers) and + * their decorators (e.g., for scaling, float, or long values). + */ +#ifndef ModbusPointFactory_h +#define ModbusPointFactory_h +#include "ModbusPoint.h" +#include "ModbusCoil.h" +#include "ModbusIsts.h" +#include "ModbusIreg.h" +#include "ModbusHreg.h" +#include "ModbusScaleDecorator.h" +#include "ModbusLongDecorator.h" +#include "ModbusFloatDecorator.h" +#include + + +/** + * @brief Creates a specific ModbusPoint object based on a category code. + * + * This factory function acts as a centralized point for instantiating different + * concrete ModbusPoint classes and applying decorators. It takes a category + * code and other parameters, and returns a pointer to the appropriate object. + * + * @param server Pointer to the ModbusIP server instance. + * @param category An integer code representing the type of Modbus point to create + * (e.g., COIL, HR, IR_FLOAT from config.h). + * @param address The Modbus address for the point. + * @param value The initial value for the point. + * @param description A descriptive name for the point. + * @return A pointer to a newly created ModbusPoint object. The caller is + * responsible for managing the memory of this object. Returns nullptr + * if the category is not recognized. + */ +template +ModbusPoint* createModbusPoint(T* server, int category, int address, int value, const char* description); + +template +ModbusPoint* createModbusPoint(T* server, int category, int address, int value, const char* description) { + switch (category) { + case COIL: + Serial.printf("Creating Coil: %s\n", description); + return new ModbusCoil(server, address, value, description); + + case DI: + Serial.printf("Creating Digital Input: %s\n", description); + return new ModbusIsts(server, address, value, description); + + case IR: + Serial.printf("Creating Input Register: %s\n", description); + return new ModbusIreg(server, address, value, description); + + case IR_10X: { + Serial.printf("Creating Input Register 10x: %s\n", description); + ModbusPoint* point = new ModbusIreg(server, address, value, description); + return new ModbusScaleDecorator(point); + } + case IR_LONG: { + Serial.printf("Creating Input Register Long: %s\n", description); + ModbusPoint* point = new ModbusIreg(server, address, 0, description); + ModbusPoint* highOrderPoint = new ModbusIreg(server, address + 1, 0, ""); + return new ModbusLongDecorator(point, highOrderPoint); + } + case IR_FLOAT: { + Serial.printf("Creating Input Register Float: %s\n", description); + ModbusPoint* point = new ModbusIreg(server, address, 0, description); + ModbusPoint* highOrderPoint = new ModbusIreg(server, address + 1, 0, ""); + return new ModbusFloatDecorator(point, highOrderPoint); + } + case HR: + Serial.printf("Creating Holding Register: %s\n", description); + return new ModbusHreg(server, address, value, description); + + case HR_10x: { + Serial.printf("Creating Holding Register 10x: %s\n", description); + ModbusPoint* point = new ModbusHreg(server, address, value, description); + return new ModbusScaleDecorator(point); + } + case HR_LONG: { + Serial.printf("Creating Holding Register Long: %s\n", description); + ModbusPoint* point = new ModbusHreg(server, address, 0, description); + ModbusPoint* highOrderPoint = new ModbusHreg(server, address + 1 , 0, ""); + return new ModbusLongDecorator(point, highOrderPoint); + } + case HR_FLOAT: { + Serial.printf("Creating Holding Register Float: %s\n", description); + ModbusPoint* point = new ModbusHreg(server, address, 0, description); + ModbusPoint* highOrderPoint = new ModbusHreg(server, address + 1 , 0, ""); + return new ModbusFloatDecorator(point, highOrderPoint); + } + default: + Serial.printf("ERROR: Unknown Modbus category %d for '%s'\n", category, description); + return nullptr; + } +} + +#endif \ No newline at end of file diff --git a/lib/Core/Categories/ModbusScaleDecorator.h b/lib/Core/Categories/ModbusScaleDecorator.h new file mode 100644 index 0000000..281a48b --- /dev/null +++ b/lib/Core/Categories/ModbusScaleDecorator.h @@ -0,0 +1,50 @@ +/** + * @file ModbusScaleDecorator.h + * @brief Defines the ModbusScaleDecorator class for scaling Modbus point values. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for a decorator that applies a 10x scaling + * factor to a Modbus point. This is useful for representing decimal values + * in integer registers (e.g., storing 12.3 as 123). + */ +#ifndef ModbusScaleDecorator_h +#define ModbusScaleDecorator_h + +#include "ModbusPointDecorator.h" + +/** + * @class ModbusScaleDecorator + * @brief A decorator that multiplies/divides a Modbus point's value by 10. + * + * This class wraps a ModbusPoint and intercepts its `getValue` and `setValue` + * calls. When setting a value, it multiplies the input by 10 before storing it. + * When getting a value, it divides the stored value by 10. + */ +template +class ModbusScaleDecorator : public ModbusPointDecorator { +public: + /** + * @brief Constructs a new ModbusScaleDecorator object. + * @param point A pointer to the ModbusPoint object to be decorated. + */ + ModbusScaleDecorator(ModbusPoint* point) : ModbusPointDecorator(point) {} + + /** + * @brief Sets the value of the underlying point after scaling it up by 10. + * @param value The logical value to set (e.g., 12 for 12.0). + */ + void setValue(int value) override { + this->_point->setValue(value * 10); + } + + /** + * @brief Gets the value from the underlying point after scaling it down by 10. + * @return The logical, scaled-down value (e.g., 12 if the stored value is 120). + */ + int getValue() const override { + return this->_point->getValue() / 10; + } +}; + +#endif \ No newline at end of file diff --git a/lib/Core/Equipment/Equipment.h b/lib/Core/Equipment/Equipment.h new file mode 100644 index 0000000..d310e35 --- /dev/null +++ b/lib/Core/Equipment/Equipment.h @@ -0,0 +1,223 @@ +/** + * @file Equipment.h + * @brief Defines the main Equipment class for the device emulator. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-02 + * + * This file contains the definition for the Equipment class, which acts as the + * central context for the State design pattern. It manages the current state + * of the device and holds all of its Modbus points. + */ +#ifndef Equipment_h +#define Equipment_h +#include +#include +#include +#include +#include "States/State_Standby.h" + +// Forward Declarations +template class State; +template class ModbusPoint; + +/** + * @class Equipment + * @brief The main class representing the emulated device. + * + * This class orchestrates the device's behavior. It holds a collection of all + * Modbus points and manages the device's current operational state (e.g., + * Standby, Running) by delegating actions to a concrete State object. + */ +template +class Equipment{ +public: + /** + * @brief Constructs a new Equipment object. + * Initializes the device in the default initial state (Standby). + */ + Equipment(); + Equipment(T* server); + /** @brief The main update loop for the equipment, called repeatedly. Delegates to the current state. */ + void update(); + /** @brief Delegates the enter state logic to the current state object. */ + void enterState(); + /** @brief Delegates the exit state logic to the current state object. */ + void exitState(); + /** + * @brief Sets a simple integer identifier for the current state. + * @param stateId The integer ID representing the state. + */ + void setState(int stateId); + /** + * @brief Gets the simple integer identifier for the current state. + * @return The integer ID of the state. + */ + int getState(); + /** + * @brief Reads all the points generated for the equipment from the Modbus server to the objects + */ + void readAllPoints(); + /** + * @brief Writes all the points generated for the equipment from the objects to the Modbus server + */ + void writeAllPoints(); + /** + * @brief Transitions the equipment to a new state. + * Handles exiting the old state, deleting it, and entering the new one. + * @param newState A pointer to the new State object. The Equipment takes ownership. + */ + void changeState(State* newState); + /** + * @brief Adds a Modbus point to the equipment's internal map. + * @param description The unique string description used as a key. + * @param point A pointer to the ModbusPoint object. + */ + void addModbusPoint(const std::string& description, ModbusPoint* point); + /** + * @brief Retrieves a Modbus point by its description. + * @param description The string key for the Modbus point. + * @return A pointer to the ModbusPoint object, or nullptr if not found. + */ + ModbusPoint* getModbusPoint(const std::string& description); + /** + * @brief Sets the value of a specific Modbus point. + * @param description The string key for the Modbus point. + * @param value The value to set. + */ + void setModbusPoint(const std::string& description, float value); + +private: + State* _state; /**< @brief Pointer to the current state object. */ + std::map*> _points;/**< @brief Map of all Modbus points, keyed by description. */ + int _stateId; /**< @brief A simple integer identifier for the current state. */ + std::vector*> _allPoints; /**< @brief Vector of all Modbus points. */ + T* _server; +}; + +template +Equipment::Equipment() : _server(nullptr) { + this->_state = new StandbyState(); + this->_state->enterState(this); +} + +template +Equipment::Equipment(T* server) + : _server(server) +{ + this->_state = new StandbyState(); + this->_state->enterState(this); +} + +/** + * @brief The main update loop for the equipment. + * + * This method delegates the update logic to the current state object. If the + * state's update method returns a pointer to a new state, this method + * triggers a state transition. + */ +template +void Equipment::update() { + State* newState = this->_state->update(this); + if (newState != nullptr) { + changeState(newState); + } +} + +/** + * @brief Delegates the enter state logic to the current state object. + */ +template +void Equipment::enterState() { + this->_state->enterState(this); +} + +/** + * @brief Delegates the exit state logic to the current state object. + */ +template +void Equipment::exitState() { + this->_state->exitState(this); +} + +/** + * @brief Adds a Modbus point to the equipment's internal collections. + * The point is added to a map for quick lookup by description and to a + * vector for simple iteration. + * @param description The unique string description used as a key. + * @param point A pointer to the ModbusPoint object. + */ +template +void Equipment::addModbusPoint(const std::string& description, ModbusPoint* point) { + this->_points[description] = point; + this->_allPoints.push_back(point); +} + +/** + * @brief Retrieves a Modbus point by its description. + * @param description The string key for the Modbus point. + * @return A pointer to the ModbusPoint object, or nullptr if not found. + */ +template +ModbusPoint* Equipment::getModbusPoint(const std::string& description) { + auto it = this->_points.find(description); + if (it != this->_points.end()) { + return it->second; + } + return nullptr; +} + +/** + * @brief Sets the value of a specific Modbus point. + * @param description The string key for the Modbus point. + * @param value The value to set. + */ +template +void Equipment::setModbusPoint(const std::string& description, float value) { + ModbusPoint* point = getModbusPoint(description); + if (point != nullptr) { + point->setValue(value); + } +} + +/** + * @brief Gets the simple integer identifier for the current state. + * @return The integer ID of the state. + */ +template +int Equipment::getState() { + return _stateId; +} + +/** + * @brief Sets a simple integer identifier for the current state. + * @param stateId The integer ID representing the state. + */ +template +void Equipment::setState(int stateId) { + this->_stateId = stateId; +} + +/** + * @brief Transitions the equipment to a new state. + * + * This method handles the full lifecycle of a state transition: it calls + * `exitState` on the current state, deletes the old state object to prevent + * memory leaks, assigns the new state, and finally calls `enterState` on the + * new state. + * + * @param newState A pointer to the new State object. The Equipment takes ownership. + */ +template +void Equipment::changeState(State* newState) { + if (this->_state != nullptr) { + this->_state->exitState(this); + delete this->_state; + } + this->_state = newState; + + if (this->_state != nullptr) { + this->_state->enterState(this); + } +} + +#endif diff --git a/lib/Core/README.md b/lib/Core/README.md new file mode 100644 index 0000000..9e6463e --- /dev/null +++ b/lib/Core/README.md @@ -0,0 +1,113 @@ +# Industrial Equipment Emulator + +This project is an Arduino-based emulator for an industrial equipment. It simulates the behavior of a real-world device (like a Computer Room Air Handler - CRAH) and communicates over Wi-Fi using the Modbus IP protocol. + +The primary goal is to provide a flexible and extensible virtual device for testing, development, and training purposes without needing physical hardware. + +## Core Concepts for Automation Professionals + +This software is built using a few key Object-Oriented Programming (OOP) concepts that make it powerful and easy to modify. If you think in terms of control systems, these concepts will feel very familiar. + +### 1. The State Pattern: "What is the machine's current operating mode?" + +In industrial automation, a machine has different operating modes: **Standby**, **Running**, **Alarm/Fault**, **Manual Override**, etc. The machine behaves differently in each mode. + +The **State Pattern** organizes the code to mirror these real-world machine modes. + +* **Analogy:** Think of a PLC program. Instead of having one massive ladder logic routine with dozens of branches checking `IF machine_is_running THEN... ELSE IF machine_is_in_standby THEN...`, you create separate routines for each mode. + +* **How it works here:** + * The `Equipment` class is our main "machine". + * We have separate classes for each state: `State_Standby`, `State_Running`, `State_Fail`. + * The `Equipment` object holds onto the *current* state object (e.g., an instance of `State_Running`). + * The main `loop()` simply tells the current state object to `update()`. + * All the logic for the running mode is contained entirely within `State_Running.cpp`. All the logic for standby is in `State_Standby.cpp`. + +* **Key Benefit:** To change how the machine behaves in "Running" mode, you only need to modify the `State_Running.cpp` file. You don't have to touch any other part of the system. To add a new "Maintenance" mode, you just create a new `State_Maintenance.cpp` file. This is much safer and easier than editing a giant `if/else` block. + +* **Files to see:** + * `States/State.h`: The "template" for all state classes. + * `States/State_Running.cpp`: Defines all behavior when the unit is running. + * `States/State_Standby.cpp`: Defines all behavior when the unit is in standby. + * `Equipment/Equipment.cpp`: The main machine that `changeState()`s between different modes. + +--- + +### 2. The Strategy Pattern: "How should this specific value behave?" + +Within a single operating mode (like "Running"), different components might have different behaviors. For example, one fan's speed might be constant, another might ramp up and down, and a sensor reading might fluctuate randomly to simulate real-world conditions. + +The **Strategy Pattern** lets us define these individual behaviors as interchangeable "algorithms" or "strategies". + +* **Analogy:** Think of a function block in a PLC. You might have a `RAMP` block, a `PID` block, or a `SQUARE_WAVE_GENERATOR` block. The Strategy Pattern lets us create these as software objects. We can then "assign" a behavior strategy to a specific Modbus point. + +* **How it works here:** + * We have a family of "Strategy" classes: `RampStrategy`, `SawStrategy`, `RandomStrategy`, `SquareStrategy`, etc. + * Inside a state file like `State_Running.cpp`, we assign these strategies to specific Modbus points. For example: + ```cpp + // From State_Running.cpp + // Assign a ramp behavior to Fan #5 + addStrategy("Speed EC Fan #5", new RampStrategy(100.0f, 1.5f, 2000)); + // Assign a random behavior to Fan #4 + addStrategy("Speed EC Fan #4", new RandomStrategy(1000)); + // Assign a sawtooth wave behavior to Fan #3 + addStrategy("Speed EC Fan #3", new SawStrategy(50.0f, 100.0f, 5.0f, 750)); + ``` + +* **Key Benefit:** This makes the emulator incredibly dynamic. You can easily change the behavior of any point just by swapping out its strategy. Want Fan #4 to have a square wave pattern instead of random? Just change one line in `State_Running.cpp`. You don't have to rewrite any core logic. + +* **Files to see:** + * `Strategies/Strategy_Behavior.h`: The "template" for all behavior strategies. + * `Strategies/Strategy_Ramp.h`, `Strategies/Strategy_Saw.h`, etc.: The specific, reusable behavior algorithms. + * `States/State_Running.cpp`: Where strategies are assigned to Modbus points for that state. + +--- + +### 3. The Decorator Pattern: "How do we handle special data types?" + +Modbus registers are fundamentally just 16-bit integers. However, in the real world, we use these integers to represent many different data types: booleans (coils), scaled integers (e.g., `value * 10`), 32-bit long integers, and 32-bit floating-point numbers. + +The **Decorator Pattern** lets us "wrap" a basic Modbus point to add this extra functionality for handling data types without creating a whole new class for every possible combination. + +* **Analogy:** Think of a basic 4-20mA analog input card. That's your base object. Now, you add a "scaling block" in your PLC to convert the raw 4-20mA signal into a temperature in Celsius. That scaling block is a "Decorator". It doesn't change the input card, it just wraps its output to make it more useful. + +* **How it works here:** + * We start with a basic `ModbusPoint` (like `ModbusHreg` for a Holding Register). + * If a point needs to be treated as a float, we "decorate" or "wrap" it with a `ModbusFloatDecorator`. This decorator knows how to take two 16-bit registers and combine them into a single 32-bit float value, and vice-versa. + * If a point needs to be scaled, we can wrap it with a `ModbusScaleDecorator`. + +* **Key Benefit:** This keeps our code clean and avoids an explosion of classes. We don't need `ModbusFloatHoldingRegister`, `ModbusScaledHoldingRegister`, `ModbusLongInputRegister`, etc. We have our basic point types (`Coil`, `Hreg`, `Ireg`) and we simply "decorate" them with the data handling logic they need. This is all handled automatically by the `ModbusPointFactory`. + +* **Files to see:** + * `Categories/ModbusPoint.h`: The base for all points. + * `Categories/ModbusPointDecorator.h`: The base "wrapper" class. + * `Categories/ModbusFloatDecorator.h`: A specific wrapper that adds floating-point logic. + * `Categories/ModbusPointFactory.cpp`: The factory that automatically creates and decorates points based on the `config.h` map. + +## Project Structure + +* `BaseEmulator.ino`: The main entry point of the Arduino program. It handles Wi-Fi setup, initializes the Modbus server, and runs the main loop. +* `config.h`: The central configuration file. **This is where you define all the Modbus points for the device.** You set the register type, address, and description here. +* `/Equipment`: Contains the `Equipment` class, which represents the overall state machine. +* `/Categories`: Contains the classes for different types of Modbus points (`ModbusCoil`, `ModbusHreg`) and the Decorators (`ModbusFloatDecorator`). +* `/States`: Contains the different operating modes for the `Equipment` (e.g., `State_Running`). +* `/Strategies`: Contains the reusable behavior algorithms for Modbus points (e.g., `Strategy_Ramp`). + +## How to Modify or Extend the Emulator + +1. **To add or change a Modbus point:** + * Open `config.h`. + * Set Wifi parameters to communicate to the network. + * Add a new line to the `mb_map` array, defining its category (e.g., `HR_FLOAT`), address, initial value, and a unique description. + +2. **To change how a point behaves in the "Running" state:** + * Open `States/State_Running.cpp`. + * In the `RunningState()` constructor, find or add an `addStrategy()` call for that point's description. + * Assign it a new or different strategy (e.g., change `new RandomStrategy(...)` to `new SingleValueStrategy(...)`). + +3. **To add a new operating mode (e.g., "Cleaning Cycle"):** + * Create new files: `States/State_Cleaning.h` and `States/State_Cleaning.cpp`. + * Implement the logic for that mode, including adding strategies for how points should behave. + * Update the state-switching logic (e.g., in `State_Running.cpp` or `State_Standby.cpp`) to allow transitioning into your new `CleaningState`. + +--- \ No newline at end of file diff --git a/lib/Core/States/State.h b/lib/Core/States/State.h new file mode 100644 index 0000000..8de12f1 --- /dev/null +++ b/lib/Core/States/State.h @@ -0,0 +1,167 @@ +/** + * @file State.h + * @brief Defines the abstract base class for all device states. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-04 + * + * This file contains the definition of the abstract State class, which is a base + * to implement the State Pattern. Concrete states (Standby, Running, Random, Fail, etc.) + * will inherit from this class. + */ +#ifndef State_h +#define State_h +#include +#include +#include +#include "Strategies/Strategy_PID.h" +#include "Categories/ModbusFloatDecorator.h" +#include "Categories/ModbusPoint.h" + +// Forward Declarations +templateclass Equipment; +class Strategy_Behavior; + +/** + * @class State + * @brief Abstract base class for a state in the State design pattern. + * + * This class defines the interface for all concrete states. It manages a + * collection of "strategies" that define how Modbus points behave while the + * equipment is in this state. + */ +template +class State{ +public: + /** + * @brief Virtual destructor. + * Cleans up all associated Strategy_Behavior objects. + */ + virtual ~State(); + + /** + * @brief Executes the state's logic for one update cycle. + * This method applies the state's strategies and checks for transitions. + * @param equipment Pointer to the Equipment instance. + * @return A pointer to a new State if a transition should occur, otherwise nullptr. + */ + virtual State* update(Equipment* equipment) = 0; + /** + * @brief Logic to execute once when entering this state. + * @param equipment Pointer to the Equipment instance. + */ + virtual void enterState(Equipment* equipment) {} + /** + * @brief Logic to execute once when exiting this state. + * @param equipment Pointer to the Equipment instance. + */ + virtual void exitState(Equipment* equipment) {} + /** + * @brief Logic to apply all strategies created. + * @param equipment Pointer to the Equipment instance. + */ + virtual void _applyStrategies(Equipment* equipment); + + +protected: + /** + * @brief Adds a behavior strategy for a specific Modbus point. + * @param pointDescription The description of the Modbus point to apply the strategy to. + * @param strategy A pointer to the Strategy_Behavior object. The State will take ownership. + */ + float getPointValue(Equipment* equipment, const std::string& pointName); + void setPointValue(Equipment* equipment, const std::string& pointName, float value); + void addStrategy(const std::string& pointDescription, Strategy_Behavior* strategy); + std::map _strategies; +}; + +template +State::~State(){ + for (auto const& pair : this->_strategies) { + delete pair.second; // 'second' is the pointer to Strategy_Behavior + } +} + +/** + * @brief Adds a new strategy to the state's behavior map. + * + * The State object takes ownership of the strategy pointer and will be + * responsible for its deletion. + * + * @param pointDescription The description of the Modbus point this strategy applies to. + * @param strategy A pointer to a Strategy_Behavior object. + */ +template +void State::addStrategy(const std::string& pointDescription, Strategy_Behavior* strategy){ + this->_strategies[pointDescription] = strategy; +} + +template +float State::getPointValue(Equipment* equipment, const std::string& pointName) { + ModbusPoint* point = equipment->getModbusPoint(pointName); + if (!point) return 0.0f; + + if (point->getType() == PointType::FLOAT) { + // If it's a float, cast and get the full float value + return static_cast*>(point)->getFloatValue(); + } else { + // Otherwise, get the standard integer value + return static_cast(point->getValue()); + } +} + +template +void State::setPointValue(Equipment* equipment, const std::string& pointName, float value) { + ModbusPoint* point = equipment->getModbusPoint(pointName); + if (!point) return; + + if (point->getType() == PointType::FLOAT) { + static_cast*>(point)->setFloatValue(value); + } else { + point->setValue(round(value)); + } +} + +/** + * @brief Applies all registered strategies for the current state. + * + * This helper method iterates through all strategies associated with this state. + * For each strategy that is ready to run (based on its internal timer), it + * retrieves the corresponding Modbus point and applies the new value. + * It handles both integer and float point types. + * + * @param equipment A pointer to the main Equipment object. + */ +template +void State::_applyStrategies(Equipment* equipment) { + unsigned long currentTime = millis(); + + // Use the C++11 compatible for-loop for std::map + for (auto const& pair : this->_strategies) { + const std::string& description = pair.first; + Strategy_Behavior* strategy = pair.second; + + if (strategy->isReady(currentTime)) { + ModbusPoint* outputPoint = equipment->getModbusPoint(description); + if (!outputPoint) continue; + + float inputValue; + + if (strategy->isPID()) { + PIDStrategy* pid = static_cast(strategy); + inputValue = getPointValue(equipment, pid->getInputSensorName()); + + ModbusPoint* PIDsetpoint = equipment->getModbusPoint(pid->getSetpointName()); + if (PIDsetpoint) { + pid->setSetpoint(PIDsetpoint->getValue()); + } + } else { + inputValue = getPointValue(equipment, description); + } + + float newValue = strategy->execute(inputValue); + setPointValue(equipment, description, newValue); + } + } +} + +#endif diff --git a/lib/Core/States/State_Fail.h b/lib/Core/States/State_Fail.h new file mode 100644 index 0000000..b05e577 --- /dev/null +++ b/lib/Core/States/State_Fail.h @@ -0,0 +1,50 @@ +/** + * @file State_Fail.h + * @brief Defines the FailState class for the device. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for the FailState, which represents + * a state where the equipment has encountered an error or fault condition. + */ +#ifndef Fail_State_h +#define Fail_State_h + +#include "State.h" // Include the base class header + +#include +#include + +template class Equipment; +/** + * @class FailState + * @brief Represents a failure or alarm state of the equipment. + * + * In this state, the equipment is in a non-operational fault condition. + * It can be configured to apply specific strategies to its Modbus points to + * simulate a particular failure scenario (e.g., setting alarm bits, stopping fans). + * It waits for a command to transition to another state, such as returning to + * Standby after the fault is cleared. + */ +template +class FailState : public State { +public: + /** + * @brief Constructs a new FailState object. + * This is where strategies for failure behavior would be initialized. + */ + FailState(const std::vector& activeAlarms); + + /** + * @brief Executes the fail state's logic for one update cycle. + * This method applies the state's strategies and checks for a state transition command. + * @param equipment Pointer to the Equipment instance. + * @return A pointer to a new State if a transition should occur, otherwise nullptr. + */ + State* update(Equipment* equipment) override; + /** @brief Logic to execute once when entering the fail state. */ + void enterState(Equipment* equipment) override; + /** @brief Logic to execute once when exiting the fail state. */ + void exitState(Equipment* equipment) override; +}; +#endif \ No newline at end of file diff --git a/lib/Core/States/State_Running.h b/lib/Core/States/State_Running.h new file mode 100644 index 0000000..438801f --- /dev/null +++ b/lib/Core/States/State_Running.h @@ -0,0 +1,47 @@ +/** + * @file State_Running.h + * @brief Defines the RunningState class for the device. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for the RunningState, which represents + * the state where the equipment is actively performing its primary function. + */ +#ifndef Running_State_h +#define Running_State_h + +#include "State.h" // Include the base class header +template class Equipment; + +/** + * @class RunningState + * @brief Represents the active running state of the equipment. + * + * In this state, the equipment is fully operational and performing its main + * tasks. It applies a set of predefined strategies to its Modbus points to + * simulate active behavior (e.g., fans running at various speeds) and waits + * for a command to transition to another state. + */ +template +class RunningState : public State { +public: + /** + * @brief Constructs a new RunningState object. + * Initializes the strategies for various Modbus points that are active + * during the running state, such as setting fan speed behaviors. + */ + RunningState(); + + /** + * @brief Executes the running state's logic for one update cycle. + * This method applies the state's strategies and checks for a state transition command. + * @param equipment Pointer to the Equipment instance. + * @return A pointer to a new State if a transition should occur, otherwise nullptr. + */ + State* update(Equipment* equipment) override; + /** @brief Logic to execute once when entering the running state. */ + void enterState(Equipment* equipment) override; + /** @brief Logic to execute once when exiting the running state. */ + void exitState(Equipment* equipment) override; +}; +#endif \ No newline at end of file diff --git a/lib/Core/States/State_Standby.h b/lib/Core/States/State_Standby.h new file mode 100644 index 0000000..979b28b --- /dev/null +++ b/lib/Core/States/State_Standby.h @@ -0,0 +1,48 @@ +/** + * @file State_Standby.h + * @brief Defines the StandbyState class for the device. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for the StandbyState, which represents + * the state where the equipment is idle but ready to start. It defines + * specific behaviors for Modbus points while in this state. + */ +#ifndef Standby_State_h +#define Standby_State_h + +#include "State.h" // Include the base class header + +template class Equipment; +/** + * @class StandbyState + * @brief Represents the standby state of the equipment. + * + * In this state, the equipment is operational but not actively running its + * primary function. It applies a set of predefined strategies to its Modbus + * points to simulate standby behavior and waits for a command to transition + * to another state (e.g., Running). + */ +template +class StandbyState : public State { +public: + /** + * @brief Constructs a new StandbyState object. + * Initializes the strategies for various Modbus points that are active + * during the standby state. + */ + StandbyState(); + + /** + * @brief Executes the standby state's logic for one update cycle. + * This method applies the state's strategies and checks for a state transition command. + * @param equipment Pointer to the Equipment instance. + * @return A pointer to a new State if a transition should occur, otherwise nullptr. + */ + State* update(Equipment* equipment) override; + /** @brief Logic to execute once when entering the standby state. */ + void enterState(Equipment* equipment) override; + /** @brief Logic to execute once when exiting the standby state. */ + void exitState(Equipment* equipment) override; +}; +#endif \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_Behavior.h b/lib/Core/Strategies/Strategy_Behavior.h new file mode 100644 index 0000000..3549f3c --- /dev/null +++ b/lib/Core/Strategies/Strategy_Behavior.h @@ -0,0 +1,65 @@ +/** + * @file Strategy_Behavior.h + * @brief Defines the abstract base class for all behavior strategies. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the interface for the Strategy design pattern. All concrete + * value-generation algorithms (e.g., Ramp, Saw, Random) will inherit from + * the Strategy_Behavior class defined here. + */ +#ifndef strategy_behavior_h +#define strategy_behavior_h + +/** + * @class Strategy_Behavior + * @brief Abstract base class for a value-generation strategy. + * + * This class defines the common interface for all strategies. It includes a + * pure virtual `execute` method that must be implemented by concrete strategies + * and a helper method `isReady` to manage the update timing. + */ +class Strategy_Behavior { +public: + /** + * @brief Constructs a new Strategy_Behavior object. + * @param interval The update interval in milliseconds. The strategy will only + * be ready to execute after this interval has passed. + */ + Strategy_Behavior(unsigned long interval) + : _interval(interval), _previousMillis(0) {} + + /** + * @brief Virtual destructor. + */ + virtual ~Strategy_Behavior() = default; + + /** + * @brief Pure virtual method to execute the strategy's logic. + * Concrete classes must implement this to define their behavior. + * @param currentValue The current value of the point, which can be used by the strategy. + * @return The newly calculated value. + */ + virtual float execute(float currentValue) = 0; + + /** + * @brief Checks if the strategy's update interval has elapsed. + * @param currentMillis The current time from `millis()`. + * @return True if the strategy should be executed, false otherwise. + */ + bool isReady(unsigned long currentMillis) { + if (currentMillis - _previousMillis >= _interval) { + _previousMillis = currentMillis; + return true; + } + return false; + } + + virtual bool isPID() const { return false; } + +protected: + unsigned long _previousMillis; /**< @brief The timestamp of the last execution. */ + unsigned long _interval; /**< @brief How often the strategy should run, in milliseconds. */ +}; + +#endif \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_PID.cpp b/lib/Core/Strategies/Strategy_PID.cpp new file mode 100644 index 0000000..37dd2cd --- /dev/null +++ b/lib/Core/Strategies/Strategy_PID.cpp @@ -0,0 +1,73 @@ +/** + * @file Strategy_PID.cpp + * @brief Implementation of the PIDStrategy class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-08 + */ +#include "Strategy_PID.h" +#include + +// CORRECTED CONSTRUCTOR +// You should pass in your gains here, but for now we'll add setters and initialize to 0. +PIDStrategy::PIDStrategy(const std::string& setpointName, unsigned long interval, const std::string& inputSensorName) + : Strategy_Behavior(interval), _setpointName(setpointName), _inputSensorName(inputSensorName) { + + _kp = -2.0; + _ki = -0.1; + _kd = 0.0; + _input = 0.0; + _lastError = 0.0; + _integral = 0.0; + _lastTime = millis(); +} + +// It's good practice to have methods to set your gains +void PIDStrategy::setGains(float kp, float ki, float kd) { + _kp = kp; + _ki = ki; + _kd = kd; +} + +void PIDStrategy::setSetpoint(float setpoint) { + _setpoint = setpoint; +} + +// This function is less necessary if execute() takes the current value, but can be used for setting an initial state. +void PIDStrategy::setInput(float input) { + _input = input; +} + + +float PIDStrategy::execute(float currentValue) { + unsigned long now = millis(); + float timeChange = (float)(now - _lastTime); + + if (timeChange <= 0) { + Serial.printf("PID strategy delta time 0.\n"); + return 0; + } + + _input = currentValue; + float error = _setpoint - _input; + _integral += error * timeChange; + if (_integral > 100.0) _integral = 100.0; + if (_integral < -100.0) _integral = -100.0; + + float derivative = (error - _lastError) / timeChange; + float output = (_kp * error) + (_ki * _integral) + (_kd * derivative); + _lastError = error; + _lastTime = now; + + if (output > 100.0) output = 100.0; + if (output < 0.0) output = 0.0; + Serial.printf("PID timeChange: %f.\n", timeChange); + Serial.printf("PID _input: %f.\n", _input); + Serial.printf("PID error: %f.\n", error); + Serial.printf("PID _integral: %f.\n", _integral); + Serial.printf("PID derivative: %f.\n", derivative); + Serial.printf("PID _kp: %f.\n", _kp); + Serial.printf("PID _ki: %f.\n", _ki); + Serial.printf("PID _kd: %f.\n", _kd); + Serial.printf("PID output: %f.\n", output); + return output; +} \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_PID.h b/lib/Core/Strategies/Strategy_PID.h new file mode 100644 index 0000000..5a0546c --- /dev/null +++ b/lib/Core/Strategies/Strategy_PID.h @@ -0,0 +1,43 @@ +// In BaseEmulator/Strategies/Strategy_PID.h + +#ifndef PID_Strategy_h +#define PID_Strategy_h + +#include "Strategy_Behavior.h" +#include + +class PIDStrategy : public Strategy_Behavior { +public: + // MODIFIED CONSTRUCTOR: Takes the setpoint, interval, and the name of the input sensor. + PIDStrategy(const std::string& setpointName, unsigned long interval, const std::string& inputSensorName); + + float execute(float currentValue) override; + + // Add a getter for the sensor name + std::string getInputSensorName() const { return _inputSensorName; } + + std::string getSetpointName() const { return _setpointName; } + + // Add this virtual function to easily identify this strategy as a PID + bool isPID() const override { return true; } + + // ... (other methods like setSetpoint, setGains) + void setSetpoint(float setpoint); + void setGains(float kp, float ki, float kd); + void setInput(float input); + + +private: + // ... (PID variables like _kp, _ki, _kd, etc.) + float _kp; + float _ki; + float _kd; + unsigned long _lastTime; + float _setpoint; + float _input; + float _lastError; + float _integral; + std::string _inputSensorName; // <-- Add this to store the name of our input + std::string _setpointName; // <-- Add this to store the name of our input +}; +#endif \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_Ramp.cpp b/lib/Core/Strategies/Strategy_Ramp.cpp new file mode 100644 index 0000000..5c93f40 --- /dev/null +++ b/lib/Core/Strategies/Strategy_Ramp.cpp @@ -0,0 +1,51 @@ +/** + * @file Strategy_Ramp.cpp + * @brief Implementation of the RampStrategy class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + */ +#include "Strategy_Ramp.h" +#include +/** + * @brief Constructs a new RampStrategy object. + * + * Initializes the ramp strategy by passing the update interval to the + * base Strategy_Behavior class and storing the target value and step. + * + * @param targetValue The destination value for the ramp. + * @param step The amount to increment or decrement on each execution. + * @param interval The time in milliseconds between each value change. + */ +RampStrategy::RampStrategy(float targetValue, float step, unsigned long interval) + : Strategy_Behavior(interval), _targetValue(targetValue), _step(step) {} + +/** + * @brief Calculates the next value in the ramp sequence towards a target. + * + * This method compares the current value to the target value and returns + * the current value incremented or decremented by the step amount. To prevent + * overshooting, if the next step would pass the target, it returns the target + * value directly. If the target is already reached, it returns the current value. + * + * @param currentValue The current value, used to determine the next step. + * @return The next value in the ramp sequence. + */ +float RampStrategy::execute(float currentValue) { + if (currentValue > _targetValue) { + // Prevent overshooting the target when decrementing + Serial.println("Ramp strategy going down."); + return (currentValue - _step < _targetValue) ? _targetValue : currentValue - _step; + } else if (currentValue < _targetValue) { + // Prevent overshooting the target when incrementing + Serial.println("Ramp strategy going up."); + return (currentValue + _step > _targetValue) ? _targetValue : currentValue + _step; + } else { // currentValue is already at the target + Serial.println("Ramp strategy at target."); + return _targetValue; + } + +} + +void RampStrategy::setTarget(float targetValue) { + _targetValue = targetValue; +} \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_Ramp.h b/lib/Core/Strategies/Strategy_Ramp.h new file mode 100644 index 0000000..768c050 --- /dev/null +++ b/lib/Core/Strategies/Strategy_Ramp.h @@ -0,0 +1,48 @@ +/** + * @file Strategy_Ramp.h + * @brief Defines the RampStrategy class for ramping a value towards a target. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for a behavior strategy that incrementally + * changes a value until it reaches a specified target value. + */ +#ifndef Ramp_Strategy_h +#define Ramp_Strategy_h + +#include "Strategy_Behavior.h" + +/** + * @class RampStrategy + * @brief A strategy that moves a value towards a target by a fixed step. + * + * This class implements the Strategy_Behavior interface to produce a ramping + * effect. On each `execute` call, it increments or decrements the current + * value by a fixed step until the target value is reached. + */ +class RampStrategy : public Strategy_Behavior { +public: + /** + * @brief Constructs a new RampStrategy object. + * @param targetValue The destination value for the ramp. + * @param step The amount to increment or decrement on each execution. + * @param interval The time in milliseconds between each value change. + */ + RampStrategy(float targetValue, float step, unsigned long interval); + /** + * @brief Executes the strategy to set the target setpoint in the ramp strategy. + * @param targetValue The target value that is going to be set. + */ + void setTarget(float targetValue); + /** + * @brief Executes the strategy to get the next value in the ramp sequence. + * @param currentValue The current value, used to determine the next step. + * @return The next value in the ramp sequence. + */ + float execute(float currentValue) override; + +private: + float _targetValue; /**< @brief The target value that the strategy will ramp towards. */ + float _step; /**< @brief The value to add or subtract on each execution. */ +}; +#endif \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_Random.cpp b/lib/Core/Strategies/Strategy_Random.cpp new file mode 100644 index 0000000..23d386a --- /dev/null +++ b/lib/Core/Strategies/Strategy_Random.cpp @@ -0,0 +1,36 @@ +/** + * @file Strategy_Random.cpp + * @brief Implementation of the RandomStrategy class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + */ +#include "Strategy_Random.h" +#include +#include + +/** + * @brief Constructs a new RandomStrategy object. + * + * Initializes the random strategy by passing the update interval to the + * base Strategy_Behavior class. + * + * @param interval The time in milliseconds between each value generation. + */ +RandomStrategy::RandomStrategy(unsigned long interval) + : Strategy_Behavior(interval) {} + +/** + * @brief Generates a new random value between 0.0 and 100.0. + * + * This method calculates a random integer between 0 and 1000 and divides + * it by 10.0 to produce a floating-point number. The `currentValue` + * parameter is ignored. + * + * @param currentValue The current value of the Modbus point (ignored). + * @return A random floating-point number between 0.0 and 100.0. + */ +float RandomStrategy::execute(float currentValue) { + int randomNum = rand() % 1001; + Serial.println("Random strategy."); + return randomNum / 10.0; +} \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_Random.h b/lib/Core/Strategies/Strategy_Random.h new file mode 100644 index 0000000..8480dd3 --- /dev/null +++ b/lib/Core/Strategies/Strategy_Random.h @@ -0,0 +1,37 @@ +/** + * @file Strategy_Random.h + * @brief Defines the RandomStrategy class for generating random values. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for a behavior strategy that generates + * a random floating-point number between 0.0 and 100.0. + */ +#ifndef Random_Strategy_h +#define Random_Strategy_h + +#include "Strategy_Behavior.h" + +/** + * @class RandomStrategy + * @brief A strategy that generates a random value on each execution. + * + * This class implements the Strategy_Behavior interface to produce a random + * value. Each time `execute` is called, it returns a new random float + * between 0.0 and 100.0. + */ +class RandomStrategy : public Strategy_Behavior { +public: + /** + * @brief Constructs a new RandomStrategy object. + * @param interval The time in milliseconds between each value generation. + */ + RandomStrategy(unsigned long interval); + /** + * @brief Executes the strategy to generate a new random value. + * @param currentValue The current value of the Modbus point (ignored). + * @return A random floating-point number between 0.0 and 100.0. + */ + float execute(float currentValue) override; +}; +#endif \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_Saw.cpp b/lib/Core/Strategies/Strategy_Saw.cpp new file mode 100644 index 0000000..fa9eed5 --- /dev/null +++ b/lib/Core/Strategies/Strategy_Saw.cpp @@ -0,0 +1,54 @@ +/** + * @file Strategy_Saw.cpp + * @brief Implementation of the SawStrategy class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + */ +#include "Strategy_Saw.h" +#include +/** + * @brief Constructs a new SawStrategy object. + * + * Initializes the sawtooth wave strategy by passing the update interval to the + * base Strategy_Behavior class and storing the wave's parameters. + * + * @param minValue The lower bound of the sawtooth wave. + * @param maxValue The upper bound of the sawtooth wave. + * @param step The amount to increment or decrement on each execution. + * @param interval The time in milliseconds between each value change. + */ +SawStrategy::SawStrategy(float minValue, float maxValue, float step, unsigned long interval) + : Strategy_Behavior(interval), _minValue(minValue), _maxValue(maxValue), _step(step) {} + +void SawStrategy::setMinValue(float minValue) { + _minValue = minValue; +} + +void SawStrategy::setMaxValue(float maxValue) { + _maxValue = maxValue; +} +/** + * @brief Calculates the next value in the sawtooth wave sequence. + * + * This method checks if the current value has reached the upper or lower + * bounds and reverses the direction if necessary. It then returns the + * current value incremented or decremented by the step amount. + * + * @param currentValue The current value, used to determine the next step. + * @return The next value in the sawtooth sequence. + */ +float SawStrategy::execute(float currentValue) { + if (currentValue >= _maxValue) { + _goingUp = false; + } else if (currentValue <= _minValue) { + _goingUp = true; + } + + if (_goingUp) { + Serial.println("Saw Strategy going up."); + return currentValue + _step; + } else { + Serial.println("Saw Strategy going down."); + return currentValue - _step; + } +} \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_Saw.h b/lib/Core/Strategies/Strategy_Saw.h new file mode 100644 index 0000000..e0f0a76 --- /dev/null +++ b/lib/Core/Strategies/Strategy_Saw.h @@ -0,0 +1,54 @@ +/** + * @file Strategy_Saw.h + * @brief Defines the SawStrategy class for generating a sawtooth wave pattern. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for a behavior strategy that creates a + * sawtooth (or triangular) wave. The value ramps up from a minimum to a + * maximum and then ramps back down, repeating the cycle. + */ +#ifndef Saw_Strategy_h +#define Saw_Strategy_h + +#include "Strategy_Behavior.h" + +/** + * @class SawStrategy + * @brief A strategy that generates a value that moves up and down between two bounds. + * + * This class implements the Strategy_Behavior interface to produce a sawtooth + * wave. On each `execute` call, it increments or decrements the current value + * by a fixed step. The direction reverses when the value hits the minimum or + * maximum bound. + */ +class SawStrategy : public Strategy_Behavior { +public: + /** + * @brief Constructs a new SawStrategy object. + * @param minValue The lower bound of the sawtooth wave. + * @param maxValue The upper bound of the sawtooth wave. + * @param step The amount to increment or decrement on each execution. + * @param interval The time in milliseconds between each value change. + */ + SawStrategy(float minValue, float maxValue, float step, unsigned long interval); + /** + * @brief Executes the strategy to get the next value in the sawtooth wave. + * @param currentValue The current value, used to determine the next step. + * @return The next value in the sawtooth sequence. + */ + float execute(float currentValue) override; + + void setMinValue(float minValue); + + void setMaxValue(float maxValue); + + + +private: + float _minValue; /**< @brief The lower bound of the sawtooth wave. */ + float _maxValue; /**< @brief The upper bound of the sawtooth wave. */ + float _step; /**< @brief The value to add or subtract on each execution. */ + bool _goingUp = true; /**< @brief Tracks the current direction of the wave (up or down). */ +}; +#endif \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_SingleValue.cpp b/lib/Core/Strategies/Strategy_SingleValue.cpp new file mode 100644 index 0000000..29e2f0c --- /dev/null +++ b/lib/Core/Strategies/Strategy_SingleValue.cpp @@ -0,0 +1,45 @@ +/** + * @file Strategy_SingleValue.cpp + * @brief Implementation of the SingleValueStrategy class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + */ +#include "Strategy_SingleValue.h" +#include +#include +/** + * @brief Constructs a new SingleValueStrategy object. + * + * Initializes the strategy by passing the update interval to the base + * Strategy_Behavior class and storing the base setpoint value. + * + * @param setpoint The base value around which noise will be generated. + * @param interval The time in milliseconds between each value generation. + */ +SingleValueStrategy::SingleValueStrategy(float setpoint, float noiseMagnitude, unsigned long interval) + : Strategy_Behavior(interval), _setpoint(setpoint), _noiseMagnitude(noiseMagnitude) {} + +void SingleValueStrategy::setSetpoint(float setpoint) { + _setpoint = setpoint; +} +/** + * @brief Generates a new value by adding random noise to the setpoint. + * + * This method calculates a random noise value between -10.0 and +10.0 and + * adds it to the stored setpoint. The `currentValue` parameter is ignored. + * + * @param currentValue The current value of the Modbus point (ignored). + * @return The setpoint with added random noise. + */ +float SingleValueStrategy::execute(float currentValue) { + if(_noiseMagnitude <= 0){ + Serial.println("Single value strategy static."); + return _setpoint; + } + + int noiseInt = rand() % 201; + noiseInt -= 100; + float noise = (noiseInt / 100) * _noiseMagnitude; + Serial.println("Single value strategy with noise."); + return _setpoint + noise; +} \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_SingleValue.h b/lib/Core/Strategies/Strategy_SingleValue.h new file mode 100644 index 0000000..3d9624e --- /dev/null +++ b/lib/Core/Strategies/Strategy_SingleValue.h @@ -0,0 +1,44 @@ +/** + * @file Strategy_SingleValue.h + * @brief Defines the SingleValueStrategy class for setup a value with noise. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for a behavior strategy that sets the + * value to a setpoint and the generates simulates noise with a random number. + */ +#ifndef SingleValue_strategy_h +#define SingleValue_strategy_h +#include "Strategy_Behavior.h" + +/** + * @class SingleValueStrategy + * @brief A strategy that sets a value to a setpoint and generates noise around it. + * + * This class implements the Strategy_Behavior interface to produce a noise + * pattern. Each time `execute` is called, it add a random number (+/-10) to + * emulate noise around the value + */ +class SingleValueStrategy : public Strategy_Behavior { +public: + /** + * @brief Constructs a new SquareStrategy object. + * @param setpoiny Target value. + * @param interval The time in milliseconds between each value toggle. + */ + SingleValueStrategy(float setpoint, float noiseMagnitude, unsigned long interval); + /** + * @brief Executes the strategy to get the next random value around setpoint. + * @param currentValue The current value of the Modbus point (ignored in this strategy). + * @return The next value in the sequence, either the lower or upper bound. + */ + float execute(float currentValue) override; + + void setSetpoint(float setpoint); + + +private: + float _setpoint; + float _noiseMagnitude; +}; +#endif \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_Square.cpp b/lib/Core/Strategies/Strategy_Square.cpp new file mode 100644 index 0000000..490d572 --- /dev/null +++ b/lib/Core/Strategies/Strategy_Square.cpp @@ -0,0 +1,44 @@ +/** + * @file Strategy_Square.cpp + * @brief Implementation of the SquareStrategy class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + */ +#include "Strategy_Square.h" +#include +/** + * @brief Constructs a new SquareStrategy object. + * + * Initializes the square wave strategy by passing the update interval to the + * base Strategy_Behavior class and storing the lower and upper bounds. + * + * @param lowerValue The lower value of the square wave. + * @param upperValue The upper value of the square wave. + * @param interval The time in milliseconds between each value toggle. + */ +SquareStrategy::SquareStrategy(float lowerValue, float upperValue, unsigned long interval) + : Strategy_Behavior(interval), _lowerValue(lowerValue), _upperValue(upperValue) {} + +/** + * @brief Toggles between the upper and lower values on each execution. + * @param currentValue The current value of the Modbus point (ignored). + * @return The next value in the square wave sequence. + */ +void SquareStrategy::setLowerValue(float lowerValue) { + _lowerValue = lowerValue; +} + +void SquareStrategy::setUpperValue(float upperValue) { + _upperValue = upperValue; +} + +float SquareStrategy::execute(float currentValue) { + _upState = !_upState; + if(_upState){ + Serial.println("Square Strategy high state."); + return _upperValue; + } else { + Serial.println("Square Strategy low state."); + return _lowerValue; + } +} \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_Square.h b/lib/Core/Strategies/Strategy_Square.h new file mode 100644 index 0000000..586b41b --- /dev/null +++ b/lib/Core/Strategies/Strategy_Square.h @@ -0,0 +1,45 @@ +/** + * @file Strategy_Square.h + * @brief Defines the SquareStrategy class for generating a square wave pattern. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for a behavior strategy that alternates + * between a lower and an upper value, creating a square wave effect. + */ +#ifndef square_strategy_h +#define square_strategy_h +#include "Strategy_Behavior.h" + +/** + * @class SquareStrategy + * @brief A strategy that alternates between a lower and an upper value on each execution. + * + * This class implements the Strategy_Behavior interface to produce a square wave + * pattern. Each time `execute` is called, it toggles between returning the + * `_lowerValue` and the `_upperValue`. + */ +class SquareStrategy : public Strategy_Behavior { +public: + /** + * @brief Constructs a new SquareStrategy object. + * @param lowerValue The lower value of the square wave. + * @param upperValue The upper value of the square wave. + * @param interval The time in milliseconds between each value toggle. + */ + SquareStrategy(float lowerValue, float upperValue, unsigned long interval); + /** + * @brief Executes the strategy to get the next value in the square wave. + * @param currentValue The current value of the Modbus point (ignored in this strategy). + * @return The next value in the sequence, either the lower or upper bound. + */ + float execute(float currentValue) override; + void setLowerValue(float lowerValue); + void setUpperValue(float upperValue); + +private: + float _lowerValue; /**< @brief The lower bound of the square wave. */ + float _upperValue; /**< @brief The upper bound of the square wave. */ + bool _upState = true; /**< @brief The internal state to track whether to return the upper or lower value. */ +}; +#endif \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_Totalizer.cpp b/lib/Core/Strategies/Strategy_Totalizer.cpp new file mode 100644 index 0000000..d70d906 --- /dev/null +++ b/lib/Core/Strategies/Strategy_Totalizer.cpp @@ -0,0 +1,38 @@ +/** + * @file Strategy_Totalizer.cpp + * @brief Implementation of the TotalizerStrategy class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + */ +#include "Strategy_Totalizer.h" +#include +#include +/** + * @brief Constructs a new TotalizerStrategy object. + * + * Initializes the square wave strategy by passing the update interval to the + * base Strategy_Behavior class and storing the lower and upper bounds. + * + * @param lowerValue The lower value of the square wave. + * @param upperValue The upper value of the square wave. + * @param interval The time in milliseconds between each value toggle. + */ +TotalizerStrategy::TotalizerStrategy(unsigned long interval) + : Strategy_Behavior(interval) { + int randomNum = rand() % 1001; + _currentValue = randomNum; + } + +/** + * @brief Toggles between the upper and lower values on each execution. + * @param currentValue The current value of the Modbus point (ignored). + * @return The next value in the square wave sequence. + */ +float TotalizerStrategy::execute(float currentValue) { + _currentValue += 1; + if (_currentValue >60000) { + _currentValue = 0; + } + Serial.println("Totalizer strategy adding up."); + return _currentValue; +} \ No newline at end of file diff --git a/lib/Core/Strategies/Strategy_Totalizer.h b/lib/Core/Strategies/Strategy_Totalizer.h new file mode 100644 index 0000000..9b02f4c --- /dev/null +++ b/lib/Core/Strategies/Strategy_Totalizer.h @@ -0,0 +1,42 @@ +/** + * @file Strategy_Square.h + * @brief Defines the SquareStrategy class for generating a square wave pattern. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the definition for a behavior strategy that alternates + * between a lower and an upper value, creating a square wave effect. + */ +#ifndef totalizer_strategy_h +#define totalizer_strategy_h +#include "Strategy_Behavior.h" + +/** + * @class SquareStrategy + * @brief A strategy that alternates between a lower and an upper value on each execution. + * + * This class implements the Strategy_Behavior interface to produce a square wave + * pattern. Each time `execute` is called, it toggles between returning the + * `_lowerValue` and the `_upperValue`. + */ +class TotalizerStrategy : public Strategy_Behavior { +public: + /** + * @brief Constructs a new SquareStrategy object. + * @param lowerValue The lower value of the square wave. + * @param upperValue The upper value of the square wave. + * @param interval The time in milliseconds between each value toggle. + */ + TotalizerStrategy(unsigned long interval); + /** + * @brief Executes the strategy to get the next value in the square wave. + * @param currentValue The current value of the Modbus point (ignored in this strategy). + * @return The next value in the sequence, either the lower or upper bound. + */ + float execute(float currentValue) override; + +private: + float _currentValue; /**< @brief The lower bound of the square wave. */ + +}; +#endif \ No newline at end of file diff --git a/lib/Core/core.h b/lib/Core/core.h new file mode 100644 index 0000000..dd24064 --- /dev/null +++ b/lib/Core/core.h @@ -0,0 +1,69 @@ +/** + * @file core.h + * @brief Core constants and definitions for the Industrial Emulator project. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-06 + * + * This file contains shared constants, enumerations, and definitions that are + * used across both the main application firmware and the Core library. + * Placing these in a central, shared header prevents duplication and makes + * configuration easier. + * @note This file is intended to be included in the main `.ino` file. + */ +#ifndef CORE_H +#define CORE_H +#include "Equipment/Equipment.h" + +// Include the appropriate Modbus library header based on the build flag. +// Define USE_MODBUS_IP in your build flags (e.g., platformio.ini) for Modbus IP. +// Otherwise, it will default to Modbus RTU. +#if defined(USE_MODBUS_IP) +#include // Or your specific Modbus IP library +#else +#include // Or your specific Modbus RTU library +#endif + +const int COIL = 0; /**< @brief 0x: R/W Coil */ +const int DI = 1; /**< @brief 1x: R Discrete Inputs */ +const int IR = 3; /**< @brief 3x: R Input Register - Single word */ +const int IR_10X = 31; /**< @brief 3x: R Input Register - Single word, 10x scaled */ +const int IR_LONG = 32; /**< @brief 3x: R Input Register - Double word, Long type */ +const int IR_FLOAT = 33; /**< @brief 3x: R Input Register - Double word, Float encoding */ +const int HR = 4; /**< @brief 4x: R/W Holding Register - Single word */ +const int HR_10x = 41; /**< @brief 4x: R/W Holding Register - Single word, 10x scaled */ +const int HR_LONG = 42; /**< @brief 4x: R/W Holding Register - Double word, Long type */ +const int HR_FLOAT = 43; /**< @brief 4x: R/W Holding Register - Double word, Float encoding */ + +/** + * @struct modbusMap + * @brief Defines the structure for a single entry in the Modbus map. + */ +struct modbusMap +{ + int category; /**< @brief The Modbus category (e.g., COIL, HR, IR_FLOAT). */ + int address; /**< @brief The Modbus address (0-9999). */ + int value; /**< @brief The initial value for the point. */ + char description[35];/**< @brief A descriptive name for the point. Used as a key for access. */ +}; + +/** + * @brief The main loop previous milliseconds. + */ +unsigned long previousMillis = 0; + +/** + * @brief Global Modbus object instance. + * The type is determined at compile time based on the USE_MODBUS_IP flag. + */ +#if defined(USE_MODBUS_IP) +extern ModbusIP mb; // Use ModbusIP class +/** @brief An instance of the Equipment class, representing the emulated Equipment unit. */ +Equipment EquipmentInstance(&mb); +#else +extern ModbusRTU mb; // Use ModbusRTU class +/** @brief An instance of the Equipment class, representing the emulated Equipment unit. */ +Equipment EquipmentInstance(&mb); +#endif + + +#endif // CORE_H diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/modbus-esp8266/Modbus.cpp b/lib/modbus-esp8266/Modbus.cpp new file mode 100644 index 0000000..66f9f98 --- /dev/null +++ b/lib/modbus-esp8266/Modbus.cpp @@ -0,0 +1,931 @@ +/* + Modbus Library for Arduino + Core functions + Copyright (C) 2014 Andr� Sarmento Barbosa + 2017-2023 Alexander Emelianov (a.m.emelianov@gmail.com) +*/ +#include "Modbus.h" + +#if defined(MODBUS_GLOBAL_REGS) +#if defined(MODBUS_USE_STL) + std::vector Modbus::_regs; + std::vector Modbus::_callbacks; + #if defined(MODBUS_FILES) + std::function Modbus::_onFile; + #endif +#else + DArray Modbus::_regs; + DArray Modbus::_callbacks; + #if defined(MODBUS_FILES) + cbModbusFileOp Modbus::_onFile = nullptr; + #endif +#endif +#endif + +uint16_t Modbus::callback(TRegister* reg, uint16_t val, TCallback::CallbackType t) { +#define MODBUS_COMPARE_CB [reg, t](TCallback& cb){return cb.address == reg->address && cb.type == t;} + uint16_t newVal = val; +#if defined(MODBUS_USE_STL) + std::vector::iterator it = _callbacks.begin(); + do { + it = std::find_if(it, _callbacks.end(), MODBUS_COMPARE_CB); + if (it != _callbacks.end()) { + newVal = it->cb(reg, newVal); + it++; + } + } while (it != _callbacks.end()); +#else + size_t r = 0; + do { + r = _callbacks.find(MODBUS_COMPARE_CB, r); + if (r < _callbacks.size()) + newVal = _callbacks[r].cb(reg, newVal); + r++; + } while (r < _callbacks.size()); +#endif + return newVal; +} + +TRegister* Modbus::searchRegister(TAddress address) { +#define MODBUS_COMPARE_REG [address](TRegister& addr){return (addr.address == address);} +#if defined(MODBUS_USE_STL) + std::vector::iterator it = std::find_if(_regs.begin(), _regs.end(), MODBUS_COMPARE_REG); + if (it != _regs.end()) return &*it; +#else + size_t r = _regs.find(MODBUS_COMPARE_REG); + if (r < _regs.size()) return _regs.entry(r); +#endif + return nullptr; +} + +bool Modbus::addReg(TAddress address, uint16_t value, uint16_t numregs) { + #if defined(MODBUS_MAX_REGS) + if (_regs.size() + numregs > MODBUS_MAX_REGS) return false; + #endif + if (0xFFFF - address.address < numregs) + numregs = 0xFFFF - address.address; + for (uint16_t i = 0; i < numregs; i++) { + if (!searchRegister(address + i)) + _regs.push_back({address + i, value}); + } + //std::sort(_regs.begin(), _regs.end()); + return true; +} + +bool Modbus::Reg(TAddress address, uint16_t value) { + TRegister* reg; + reg = searchRegister(address); //search for the register address + if (reg) { //if found then assign the register value to the new value. + if (cbEnabled) { + reg->value = callback(reg, value, TCallback::ON_SET); + } else { + reg->value = value; + } + return true; + } else + return false; +} + +uint16_t Modbus::Reg(TAddress address) { + TRegister* reg; + reg = searchRegister(address); + if(reg) + if (cbEnabled) { + return callback(reg, reg->value, TCallback::ON_GET); + } else { + return reg->value; + } + else + return 0; +} + +bool Modbus::removeReg(TAddress address, uint16_t numregs) { + TRegister* reg; + bool atLeastOne = false; + if (0xFFFF - address.address < numregs) + numregs = 0xFFFF - address.address; + for (uint16_t i = 0; i < numregs; i++) { + reg = searchRegister(address + i); + if (reg) { + atLeastOne = true; + removeOnSet(address + i); + removeOnGet(address + i); + #if defined(MODBUS_USE_STL) + _regs.erase(std::remove( _regs.begin(), _regs.end(), *reg), _regs.end() ); + #else + _regs.remove(_regs.find(MODBUS_COMPARE_REG)); + #endif + } + } + return atLeastOne; +} + +bool Modbus::addReg(TAddress address, uint16_t* value, uint16_t numregs) { + if (0xFFFF - address.address < numregs) + numregs = 0xFFFF - address.address; + for (uint16_t k = 0; k < numregs; k++) + addReg(address + k, value[k]); + return true; +} + +void Modbus::slavePDU(uint8_t* frame) { + FunctionCode fcode = (FunctionCode)frame[0]; + uint16_t field1 = (uint16_t)frame[1] << 8 | (uint16_t)frame[2]; + uint16_t field2 = (uint16_t)frame[3] << 8 | (uint16_t)frame[4]; + uint16_t field3 = 0; + uint16_t field4 = 0; + uint16_t bytecount_calc; + uint16_t k; + ResultCode ex; + switch (fcode) { + case FC_WRITE_REG: + //field1 = reg, field2 = value + ex = _onRequest(fcode, {HREG(field1), field2}); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + if (!Reg(HREG(field1), field2)) { //Check Address and execute (reg exists?) + exceptionResponse(fcode, EX_ILLEGAL_ADDRESS); + return; + } + if (Reg(HREG(field1)) != field2) { //Check for failure + exceptionResponse(fcode, EX_SLAVE_FAILURE); + return; + } + _reply = REPLY_ECHO; + _onRequestSuccess(fcode, {HREG(field1), field2}); + break; + + case FC_READ_REGS: + //field1 = startreg, field2 = numregs, header len = 3 + ex = _onRequest(fcode, {HREG(field1), field2}); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + ex = readWords(HREG(field1), field2, fcode); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + _onRequestSuccess(fcode, {HREG(field1), field2}); + break; + + case FC_WRITE_REGS: + //field1 = startreg, field2 = numregs, frame[5] = data lenght, header len = 6 + ex = _onRequest(fcode, {HREG(field1), field2}); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + if (field2 < 0x0001 || field2 > MODBUS_MAX_WORDS || 0xFFFF - field1 < field2 || frame[5] != 2 * field2) { //Check constrains + exceptionResponse(fcode, EX_ILLEGAL_VALUE); + return; + } + for (k = 0; k < field2; k++) { //Check Address (startreg...startreg + numregs) + if (!searchRegister(HREG(field1) + k)) { + exceptionResponse(fcode, EX_ILLEGAL_ADDRESS); + return; + } + } + if (!setMultipleWords((uint16_t*)(frame + 6), HREG(field1), field2)) { + exceptionResponse(fcode, EX_SLAVE_FAILURE); + return; + } + successResponce(HREG(field1), field2, fcode); + _reply = REPLY_NORMAL; + _onRequestSuccess(fcode, {HREG(field1), field2}); + break; + + case FC_READ_COILS: + //field1 = startreg, field2 = numregs + ex = _onRequest(fcode, {COIL(field1), field2}); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + ex = readBits(COIL(field1), field2, fcode); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + _onRequestSuccess(fcode, {COIL(field1), field2}); + break; + + case FC_READ_INPUT_STAT: + //field1 = startreg, field2 = numregs + ex = _onRequest(fcode, {ISTS(field1), field2}); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + ex = readBits(ISTS(field1), field2, fcode); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + _onRequestSuccess(fcode, {ISTS(field1), field2}); + break; + + case FC_READ_INPUT_REGS: + //field1 = startreg, field2 = numregs + ex = _onRequest(fcode, {IREG(field1), field2}); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + ex = readWords(IREG(field1), field2, fcode); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + _onRequestSuccess(fcode, {IREG(field1), field2}); + break; + + case FC_WRITE_COIL: + //field1 = reg, field2 = status, header len = 3 + ex = _onRequest(fcode, {COIL(field1), field2}); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + if (field2 != 0xFF00 && field2 != 0x0000) { //Check value (status) + exceptionResponse(fcode, EX_ILLEGAL_VALUE); + return; + } + if (!Reg(COIL(field1), field2)) { //Check Address and execute (reg exists?) + exceptionResponse(fcode, EX_ILLEGAL_ADDRESS); + return; + } + if (Reg(COIL(field1)) != field2) { //Check for failure + exceptionResponse(fcode, EX_SLAVE_FAILURE); + return; + } + _reply = REPLY_ECHO; + _onRequestSuccess(fcode, {COIL(field1), field2}); + break; + + case FC_WRITE_COILS: + //field1 = startreg, field2 = numregs, frame[5] = bytecount, header len = 6 + ex = _onRequest(fcode, {COIL(field1), field2}); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + bytecount_calc = field2 / 8; + if (field2%8) bytecount_calc++; + if (field2 < 0x0001 || field2 > MODBUS_MAX_BITS || 0xFFFF - field1 < field2 || frame[5] != bytecount_calc) { //Check registers range and data size maches + exceptionResponse(fcode, EX_ILLEGAL_VALUE); + return; + } + for (k = 0; k < field2; k++) { //Check Address (startreg...startreg + numregs) + if (!searchRegister(COIL(field1) + k)) { + exceptionResponse(fcode, EX_ILLEGAL_ADDRESS); + return; + } + } + if (!setMultipleBits(frame + 6, COIL(field1), field2)) { + exceptionResponse(fcode, EX_SLAVE_FAILURE); + return; + } + successResponce(COIL(field1), field2, fcode); + _reply = REPLY_NORMAL; + _onRequestSuccess(fcode, {COIL(field1), field2}); + break; + #if defined(MODBUS_FILES) + case FC_READ_FILE_REC: + if (frame[1] < 0x07 || frame[1] > 0xF5) { // Wrong request data size + exceptionResponse(fcode, EX_ILLEGAL_VALUE); + return; + } + { + uint8_t bufSize = 2; // 2 bytes for frame header + uint8_t* recs = frame + 2; // Begin of sub-recs blocks + uint8_t recsCount = frame[1] / 7; // Count of sub-rec blocks + for (uint8_t p = 0; p < recsCount; p++) { // Calc output buffer size required + //uint16_t fileNum = (uint16_t)recs[1] << 8 | (uint16_t)recs[2]; + uint16_t recNum = (uint16_t)recs[3] << 8 | (uint16_t)recs[4]; + uint16_t recLen = (uint16_t)recs[5] << 8 | (uint16_t)recs[6]; + //Serial.printf("%d, %d, %d\n", fileNum, recNum, recLen); + if (recs[0] != 0x06 || recNum > 0x270F) { // Wrong ref type or count of records + exceptionResponse(fcode, EX_ILLEGAL_ADDRESS); + return; + } + bufSize += recLen * 2 + 2; // 4 bytes for header + data + recs += 7; + } +// if (bufSize > MODBUS_MAX_FRAME) { // Frame to return too large +// exceptionResponse(fcode, EX_ILLEGAL_ADDRESS); +// return; +// } + uint8_t* srcFrame = _frame; + _frame = (uint8_t*)malloc(bufSize); + if (!_frame) { + free(srcFrame); + exceptionResponse(fcode, EX_SLAVE_FAILURE); + return; + } + _len = bufSize; + recs = frame + 2; // Begin of sub-recs blocks + uint8_t* data = _frame + 2; + for (uint8_t p = 0; p < recsCount; p++) { + uint16_t fileNum = (uint16_t)recs[1] << 8 | (uint16_t)recs[2]; + uint16_t recNum = (uint16_t)recs[3] << 8 | (uint16_t)recs[4]; + uint16_t recLen = (uint16_t)recs[5] << 8 | (uint16_t)recs[6]; + ResultCode res = fileOp(fcode, fileNum, recNum, recLen, data + 2); + if (res != EX_SUCCESS) { // File read failed + free(srcFrame); + exceptionResponse(fcode, res); + return; + } + data[0] = recLen * 2 + 1; + data[1] = 0x06; + data += recLen * 2 + 2; + recs += 7; + } + _frame[0] = fcode; + _frame[1] = bufSize; + _reply = REPLY_NORMAL; + free(srcFrame); + } + break; + case FC_WRITE_FILE_REC: { + if (frame[1] < 0x09 || frame[1] > 0xFB) { // Wrong request data size + exceptionResponse(fcode, EX_ILLEGAL_VALUE); + return; + } + uint8_t* recs = frame + 2; // Begin of sub-recs blocks + while (recs < frame + frame[1]) { + if (recs[0] != 0x06) { + exceptionResponse(fcode, EX_ILLEGAL_ADDRESS); + return; + } + uint16_t fileNum = (uint16_t)recs[1] << 8 | (uint16_t)recs[2]; + uint16_t recNum = (uint16_t)recs[3] << 8 | (uint16_t)recs[4]; + uint16_t recLen = (uint16_t)recs[5] << 8 | (uint16_t)recs[6]; + if (recs + recLen * 2 > frame + frame[1]) { + exceptionResponse(fcode, EX_ILLEGAL_ADDRESS); + return; + } + ResultCode res = fileOp(fcode, fileNum, recNum, recLen, recs + 7); + if (res != EX_SUCCESS) { // File write failed + exceptionResponse(fcode, res); + return; + } + recs += 7 + recLen * 2; + } + } + _reply = REPLY_ECHO; + break; + #endif + case FC_MASKWRITE_REG: + //field1 = reg, field2 = AND mask, field3 = OR mask + // Result = (Current Contents AND And_Mask) OR (Or_Mask AND (NOT And_Mask)) + field3 = (uint16_t)frame[5] << 8 | (uint16_t)frame[6]; + ex = _onRequest(fcode, {HREG(field1), field2, field3}); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + field4 = Reg(HREG(field1)); + field4 = (field4 & field2) | (field3 & ~field2); + if (!Reg(HREG(field1), field4)) { //Check Address and execute (reg exists?) + exceptionResponse(fcode, EX_ILLEGAL_ADDRESS); + return; + } + if (Reg(HREG(field1)) != field4) { //Check for failure + exceptionResponse(fcode, EX_SLAVE_FAILURE); + return; + } + _reply = REPLY_ECHO; + _onRequestSuccess(fcode, {HREG(field1), field2, field3}); + break; + case FC_READWRITE_REGS: + //field1 = readreg, field2 = read count, frame[9] = data lenght, header len = 10 + //field3 = wtitereg, field4 = write count + field3 = (uint16_t)frame[5] << 8 | (uint16_t)frame[6]; + field4 = (uint16_t)frame[7] << 8 | (uint16_t)frame[8]; + ex = _onRequest(fcode, {HREG(field1), field2, HREG(field3), field4}); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + if (field2 < 0x0001 || field2 > MODBUS_MAX_WORDS || + field4 < 0x0001 || field4 > MODBUS_MAX_WORDS || + 0xFFFF - field1 < field2 || 0xFFFF - field1 < field2 || + frame[9] != 2 * field4) { //Check value + exceptionResponse(fcode, EX_ILLEGAL_VALUE); + return; + } + if (!setMultipleWords((uint16_t*)(frame + 10), HREG(field3), field4)) { + exceptionResponse(fcode, EX_SLAVE_FAILURE); + return; + } + ex = readWords(HREG(field1), field2, fcode); + if (ex != EX_SUCCESS) { + exceptionResponse(fcode, ex); + return; + } + _onRequestSuccess(fcode, {HREG(field1), field2, HREG(field3), field4}); + break; + + default: + ex = _onRequest(fcode, {frame + 1}); + if (ex != EX_PASSTHROUGH) { + exceptionResponse(fcode, EX_ILLEGAL_FUNCTION); + } + return; + } +} + +void Modbus::successResponce(TAddress startreg, uint16_t numoutputs, FunctionCode fn) { + free(_frame); + _len = 5; + _frame = (uint8_t*) malloc(_len); + if (!_frame) { + _reply = REPLY_OFF; + return; + } + _frame[0] = fn; + _frame[1] = startreg.address >> 8; + _frame[2] = startreg.address & 0x00FF; + _frame[3] = numoutputs >> 8; + _frame[4] = numoutputs & 0x00FF; +} + +void Modbus::exceptionResponse(FunctionCode fn, ResultCode excode) { + free(_frame); + _len = 2; + _frame = (uint8_t*) malloc(_len); + if (!_frame) { + _reply = REPLY_OFF; + return; + } + _frame[0] = fn + 0x80; + _frame[1] = excode; + _reply = REPLY_NORMAL; +} + +void Modbus::getMultipleBits(uint8_t* frame, TAddress startreg, uint16_t numregs) { + uint8_t bitn = 0; + uint16_t i = 0; + while (numregs--) { + if (BIT_BOOL(Reg(startreg))) + bitSet(frame[i], bitn); + else + bitClear(frame[i], bitn); + bitn++; //increment the bit index + if (bitn == 8) { + i++; + bitn = 0; + } + startreg++; //increment the register + } +} + +void Modbus::getMultipleWords(uint16_t* frame, TAddress startreg, uint16_t numregs) { + for (uint8_t i = 0; i < numregs; i++) { + frame[i] = __swap_16(Reg(startreg + i)); + } +} + +Modbus::ResultCode Modbus::readBits(TAddress startreg, uint16_t numregs, FunctionCode fn) { + if (numregs < 0x0001 || numregs > MODBUS_MAX_BITS || (0xFFFF - startreg.address) < numregs) + return EX_ILLEGAL_ADDRESS; + //Check Address + //Check only startreg. Is this correct? + //When I check all registers in range I got errors in ScadaBR + //I think that ScadaBR request more than one in the single request + //when you have more then one datapoint configured from same type. +#if defined(MODBUS_STRICT_REG) + for (k = 0; k < numregs; k++) { //Check Address (startreg...startreg + numregs) + if (!searchRegister(startreg + k)) + return EX_ILLEGAL_ADDRESS; + } +#else + if (!searchRegister(startreg)) + return EX_ILLEGAL_ADDRESS; +#endif + free(_frame); + //Determine the message length = function type, byte count and + //for each group of 8 registers the message length increases by 1 + _len = 2 + numregs/8; + if (numregs % 8) _len++; //Add 1 to the message length for the partial byte. + _frame = (uint8_t*) malloc(_len); + if (!_frame) + return EX_SLAVE_FAILURE; + _frame[0] = fn; + _frame[1] = _len - 2; //byte count (_len - function code and byte count) + _frame[_len - 1] = 0; //Clean last probably partial byte + getMultipleBits(_frame+2, startreg, numregs); + _reply = REPLY_NORMAL; + return EX_SUCCESS; +} + +Modbus::ResultCode Modbus::readWords(TAddress startreg, uint16_t numregs, FunctionCode fn) { + //Check value (numregs) + if (numregs < 0x0001 || numregs > MODBUS_MAX_WORDS || 0xFFFF - startreg.address < numregs) + return EX_ILLEGAL_ADDRESS; +#if defined(MODBUS_STRICT_REG) + for (k = 0; k < numregs; k++) { //Check Address (startreg...startreg + numregs) + if (!searchRegister(startreg + k)) + return EX_ILLEGAL_ADDRESS; + } +#else + if (!searchRegister(startreg)) + return EX_ILLEGAL_ADDRESS; +#endif + free(_frame); + _len = 2 + numregs * 2; //calculate the query reply message length. 2 bytes per register + 2 bytes for header + _frame = (uint8_t*) malloc(_len); + if (!_frame) + return EX_SLAVE_FAILURE; + _frame[0] = fn; + _frame[1] = _len - 2; //byte count + getMultipleWords((uint16_t*)(_frame + 2), startreg, numregs); + _reply = REPLY_NORMAL; + return EX_SUCCESS; +} + +bool Modbus::setMultipleBits(uint8_t* frame, TAddress startreg, uint16_t numoutputs) { + uint8_t bitn = 0; + uint16_t i = 0; + bool result = true; + while (numoutputs--) { + Reg(startreg, BIT_VAL(bitRead(frame[i], bitn))); + if (Reg(startreg) != BIT_VAL(bitRead(frame[i], bitn))) + result = false; + bitn++; //increment the bit index + if (bitn == 8) { + i++; + bitn = 0; + } + startreg++; //increment the register + } + return result; +} + +bool Modbus::setMultipleWords(uint16_t* frame, TAddress startreg, uint16_t numregs) { + bool result = true; + for (uint8_t i = 0; i < numregs; i++) { + Reg(startreg + i, __swap_16(frame[i])); + if (Reg(startreg + i) != __swap_16(frame[i])) + result = false; + } + return result; +} + +bool Modbus::onGet(TAddress address, cbModbus cb, uint16_t numregs) { + TRegister* reg; + bool atLeastOne = false; + if (!cb) { + return removeOnGet(address, nullptr, numregs); + } + while (numregs > 0) { + reg = searchRegister(address); + if (reg) { + _callbacks.push_back({TCallback::ON_GET, address, cb}); + atLeastOne = true; + } + address++; + numregs--; + } + return atLeastOne; +} +bool Modbus::onSet(TAddress address, cbModbus cb, uint16_t numregs) { + TRegister* reg; + bool atLeastOne = false; + if (!cb) { + return removeOnSet(address, nullptr, numregs); + } + while (numregs > 0) { + reg = searchRegister(address); + if (reg) { + _callbacks.push_back({TCallback::ON_SET, address, cb}); + atLeastOne = true; + } + address++; + numregs--; + } + return atLeastOne; +} + +bool Modbus::removeOn(TCallback::CallbackType t, TAddress address, cbModbus cb, uint16_t numregs) { + size_t s = _callbacks.size(); + #if defined(MODBUS_USE_STL) + #define MODBUS_COMPARE_ON [t, address, cb](const TCallback entry){\ + return entry.type == t && entry.address == address \ + && (!cb || std::addressof(cb) == std::addressof(entry.cb));} + while(numregs--) { + _callbacks.erase(remove_if(_callbacks.begin(), _callbacks.end(), MODBUS_COMPARE_ON), _callbacks.end()); + address++; + } + #else + #define MODBUS_COMPARE_ON [t, address, cb](const TCallback entry){ \ + return entry.type == t && entry.address == address \ + && (!cb || entry.cb == cb);} + while(numregs--) { + size_t r = 0; + do { + r = _callbacks.find(MODBUS_COMPARE_ON); + _callbacks.remove(r); + } while (r < _callbacks.size()); + address++; + } + #endif + return s == _callbacks.size(); +} +bool Modbus::removeOnSet(TAddress address, cbModbus cb, uint16_t numregs) { + return removeOn(TCallback::ON_SET, address, cb, numregs); +} + +bool Modbus::removeOnGet(TAddress address, cbModbus cb, uint16_t numregs) { + return removeOn(TCallback::ON_GET, address, cb, numregs); +} + +bool Modbus::readSlave(uint16_t address, uint16_t numregs, FunctionCode fn) { + free(_frame); + _len = 5; + _frame = (uint8_t*) malloc(_len); + if (!_frame) { + _reply = REPLY_OFF; + return false; + } + _frame[0] = fn; + _frame[1] = address >> 8; + _frame[2] = address & 0x00FF; + _frame[3] = numregs >> 8; + _frame[4] = numregs & 0x00FF; + return true; +} + +bool Modbus::writeSlaveBits(TAddress startreg, uint16_t to, uint16_t numregs, FunctionCode fn, bool* data) { + free(_frame); + _len = 6 + numregs/8; + if (numregs % 8) _len++; //Add 1 to the message length for the partial byte. + _frame = (uint8_t*) malloc(_len); + if (!_frame) { + _reply = REPLY_OFF; + return false; + } + _frame[0] = fn; + _frame[1] = to >> 8; + _frame[2] = to & 0x00FF; + _frame[3] = numregs >> 8; + _frame[4] = numregs & 0x00FF; + _frame[5] = _len - 6; + _frame[_len - 1] = 0; //Clean last probably partial byte + if (data) { + boolToBits(_frame + 6, data, numregs); + } else { + getMultipleBits(_frame + 6, startreg, numregs); + } + _reply = REPLY_NORMAL; + return true; +} + +bool Modbus::writeSlaveWords(TAddress startreg, uint16_t to, uint16_t numregs, FunctionCode fn, uint16_t* data) { + free(_frame); + _len = 6 + 2 * numregs; + _frame = (uint8_t*) malloc(_len); + if (!_frame) { + _reply = REPLY_OFF; + return false; + } + _frame[0] = fn; + _frame[1] = to >> 8; + _frame[2] = to & 0x00FF; + _frame[3] = numregs >> 8; + _frame[4] = numregs & 0x00FF; + _frame[5] = _len - 6; + if (data) { + uint16_t* frame = (uint16_t*)(_frame + 6); + for (uint8_t i = 0; i < numregs; i++) { + frame[i] = __swap_16(data[i]); + } + } else { + getMultipleWords((uint16_t*)(_frame + 6), startreg, numregs); + } + return true; +} + +void Modbus::boolToBits(uint8_t* dst, bool* src, uint16_t numregs) { + uint8_t bitn = 0; + uint16_t i = 0; + uint16_t j = 0; + while (numregs--) { + if (src[j]) + bitSet(dst[i], bitn); + else + bitClear(dst[i], bitn); + bitn++; //increment the bit index + if (bitn == 8) { + i++; + bitn = 0; + } + j++; //increment the register + } +} + +void Modbus::bitsToBool(bool* dst, uint8_t* src, uint16_t numregs) { + uint8_t bitn = 0; + uint16_t i = 0; + uint16_t j = 0; + while (numregs--) { + dst[j] = bitRead(src[i], bitn); + bitn++; //increment the bit index + if (bitn == 8) { + i++; + bitn = 0; + } + j++; //increment the register + } +} + +void Modbus::masterPDU(uint8_t* frame, uint8_t* sourceFrame, TAddress startreg, uint8_t* output) { + uint8_t fcode = frame[0]; + if ((fcode & 0x80) != 0) { // Check if error responce + _reply = frame[1]; + return; + } + if (fcode != sourceFrame[0]) { // Check if responce matches the request + _reply = EX_DATA_MISMACH; + return; + } + _reply = EX_SUCCESS; + uint16_t field2 = (uint16_t)sourceFrame[3] << 8 | (uint16_t)sourceFrame[4]; + uint8_t bytecount_calc; + switch (fcode) { + case FC_READ_REGS: + case FC_READ_INPUT_REGS: + case FC_READWRITE_REGS: + //field2 = numregs, frame[1] = data lenght, header len = 2 + if (frame[1] != 2 * field2) { //Check if data size matches + _reply = EX_DATA_MISMACH; + break; + } + if (output) { + uint16_t* from = (uint16_t*)(frame + 2); + uint16_t* to = (uint16_t*)output; + while(field2--) { + *(to++) = __swap_16(*(from++)); + } + } else { + setMultipleWords((uint16_t*)(frame + 2), startreg, field2); + } + break; + case FC_READ_COILS: + case FC_READ_INPUT_STAT: + //field2 = numregs, frame[1] = data length, header len = 2 + bytecount_calc = field2 / 8; + if (field2 % 8) bytecount_calc++; + if (frame[1] != bytecount_calc) { // check if data size matches + _reply = EX_DATA_MISMACH; + break; + } + if (output) { + bitsToBool((bool*)output, frame + 2, field2); + } else { + setMultipleBits(frame + 2, startreg, field2); + } + break; + #if defined(MODBUS_FILES) + case FC_READ_FILE_REC: + // Should check if byte order swap needed + if (frame[1] < 0x07 || frame[1] > 0xF5) { // Wrong request data size + _reply = EX_ILLEGAL_VALUE; + return; + } + { + uint8_t* data = frame + 2; + uint8_t* eoFrame = frame + frame[1]; + while (data < eoFrame) { + //data[0] - sub-req length + //data[1] = 0x06 + if (data[1] != 0x06 || data[0] < 0x07 || data[0] > 0xF5 || data + data[0] > eoFrame) { // Wrong request data size + _reply = EX_ILLEGAL_VALUE; + return; + } + memcpy(output, data + 2, data[0]); + data += data[0] + 1; + output += data[0] - 1; + } + } + break; + case FC_WRITE_FILE_REC: + #endif + case FC_WRITE_REG: + case FC_WRITE_REGS: + case FC_WRITE_COIL: + case FC_WRITE_COILS: + case FC_MASKWRITE_REG: + break; + + default: + _reply = EX_GENERAL_FAILURE; + } +} + +bool Modbus::cbEnable(const bool state) { + const bool old_state = state; + cbEnabled = state; + return old_state; +} +bool Modbus::cbDisable() { + return cbEnable(false); +} +Modbus::~Modbus() { + free(_frame); +} + +#if defined(MODBUS_FILES) +#if defined(MODBUS_USE_STL) +bool Modbus::onFile(std::function cb) { +#else +bool Modbus::onFile(Modbus::ResultCode (*cb)(Modbus::FunctionCode, uint16_t, uint16_t, uint16_t, uint8_t*)) { +#endif + _onFile = cb; + return true; +} +Modbus::ResultCode Modbus::fileOp(Modbus::FunctionCode fc, uint16_t fileNum, uint16_t recNum, uint16_t recLen, uint8_t* frame) { + if (!_onFile) return EX_ILLEGAL_ADDRESS; + return _onFile(fc, fileNum, recNum, recLen, frame); +} + + bool Modbus::readSlaveFile(uint16_t* fileNum, uint16_t* startRec, uint16_t* len, uint8_t count, FunctionCode fn) { + _len = count * 7 + 2; + if (_len > MODBUS_MAX_FRAME) return false; + free(_frame); + _frame = (uint8_t*) malloc(_len); + if (!_frame) return false; + _frame[0] = fn; + _frame[1] = _len - 2; + uint8_t* subReq = _frame + 2; + for (uint8_t i = 0; i < count; i++) { + subReq[0] = 0x06; + subReq[1] = fileNum[i] >> 8; + subReq[2] = fileNum[i] & 0x00FF; + subReq[3] = startRec[i] >> 8; + subReq[4] = startRec[i] & 0x00FF; + subReq[5] = len[i] >> 8; + subReq[6] = len[i] & 0x00FF; + subReq += 7; + } + return true; + } + bool Modbus::writeSlaveFile(uint16_t* fileNum, uint16_t* startRec, uint16_t* len, uint8_t count, FunctionCode fn, uint8_t* data) { + _len = 2; + for (uint8_t i = 0; i < count; i++) { + _len += len[i] * 2 + 7; + } + if (_len > MODBUS_MAX_FRAME) return false; + free(_frame); + _frame = (uint8_t*) malloc(_len); + if (!_frame) return false; + _frame[0] = fn; + _frame[1] = _len - 2; + uint8_t* subReq = _frame + 2; + for (uint8_t i = 0; i < count; i++) { + subReq[0] = 0x06; + subReq[1] = fileNum[i] >> 8; + subReq[2] = fileNum[i] & 0x00FF; + subReq[3] = startRec[i] >> 8; + subReq[4] = startRec[i] & 0x00FF; + subReq[5] = len[i] >> 8; + subReq[6] = len[i] & 0x00FF; + uint8_t clen = len[i] * 2; + memcpy(subReq + 7, data, clen); + subReq += 7 + clen; + data += clen; + } + return true; + } + #endif + +bool Modbus::onRaw(cbRaw cb) { + _cbRaw = cb; + return true; +} +Modbus::ResultCode Modbus::_onRequestDefault(Modbus::FunctionCode fc, const RequestData data) { + return EX_SUCCESS; +} +bool Modbus::onRequest(cbRequest cb) { + _onRequest = cb; + return true; +} +#if defined (MODBUSAPI_OPTIONAL) +bool Modbus::onRequestSuccess(cbRequest cb) { + _onRequestSuccess = cb; + return true; +} +#endif + +#if defined(ARDUINO_SAM_DUE_STL) +namespace std { + void __throw_bad_function_call() { + Serial.println(F("STL ERROR - __throw_bad_function_call")); + __builtin_unreachable(); + } +} +#endif \ No newline at end of file diff --git a/lib/modbus-esp8266/Modbus.h b/lib/modbus-esp8266/Modbus.h new file mode 100644 index 0000000..75aeb17 --- /dev/null +++ b/lib/modbus-esp8266/Modbus.h @@ -0,0 +1,363 @@ +/* + Modbus Library for Arduino + Core functions + Copyright (C) 2014 Andr� Sarmento Barbosa + 2017-2022 Alexander Emelianov (a.m.emelianov@gmail.com) +*/ +#pragma once +#include "ModbusSettings.h" +#include "Arduino.h" +#if defined(MODBUS_USE_STL) + #include + #include + #include + #include +#else + #include "darray.h" +#endif + +static inline uint16_t __swap_16(uint16_t num) { return (num >> 8) | (num << 8); } + +#define COIL(n) (TAddress){TAddress::COIL, n} +#define ISTS(n) (TAddress){TAddress::ISTS, n} +#define IREG(n) (TAddress){TAddress::IREG, n} +#define HREG(n) (TAddress){TAddress::HREG, n} +#define NULLREG (TAddress){TAddress::NONE, 0xFFFF} +#define BIT_VAL(v) (v?0xFF00:0x0000) +#define BIT_BOOL(v) (v==0xFF00) +#define COIL_VAL(v) (v?0xFF00:0x0000) +#define COIL_BOOL(v) (v==0xFF00) +#define ISTS_VAL(v) (v?0xFF00:0x0000) +#define ISTS_BOOL(v) (v==0xFF00) + +// For depricated (v1.xx) onSet/onGet format compatibility +#define cbDefault nullptr + +struct TRegister; +#if defined(MODBUS_USE_STL) +typedef std::function cbModbus; // Callback function Type +#else +typedef uint16_t (*cbModbus)(TRegister* reg, uint16_t val); // Callback function Type +#endif + +struct TAddress { + enum RegType {COIL, ISTS, IREG, HREG, NONE = 0xFF}; + RegType type; + uint16_t address; + bool operator==(const TAddress &obj) const { // TAddress == TAddress + return type == obj.type && address == obj.address; + } + bool operator!=(const TAddress &obj) const { // TAddress != TAddress + return type != obj.type || address != obj.address; + } + TAddress& operator++() { // ++TAddress + address++; + return *this; + } + TAddress operator++(int) { // TAddress++ + TAddress result(*this); + ++(*this); + return result; + } + TAddress& operator+=(const int& inc) { // TAddress += integer + address += inc; + return *this; + } + const TAddress operator+(const int& inc) const { // TAddress + integer + TAddress result(*this); + result.address += inc; + return result; + } + bool isCoil() { + return type == COIL; + } + bool isIsts() { + return type == ISTS; + } + bool isIreg() { + return type == IREG; + } + bool isHreg() { + return type == HREG; + } +}; + +struct TCallback { + enum CallbackType {ON_SET, ON_GET}; + CallbackType type; + TAddress address; + cbModbus cb; +}; + +struct TRegister { + TAddress address; + uint16_t value; + bool operator ==(const TRegister &obj) const { + return address == obj.address; + } +}; + +class Modbus { + public: + //Function Codes + enum FunctionCode { + FC_READ_COILS = 0x01, // Read Coils (Output) Status + FC_READ_INPUT_STAT = 0x02, // Read Input Status (Discrete Inputs) + FC_READ_REGS = 0x03, // Read Holding Registers + FC_READ_INPUT_REGS = 0x04, // Read Input Registers + FC_WRITE_COIL = 0x05, // Write Single Coil (Output) + FC_WRITE_REG = 0x06, // Preset Single Register + FC_DIAGNOSTICS = 0x08, // Not implemented. Diagnostics (Serial Line only) + FC_WRITE_COILS = 0x0F, // Write Multiple Coils (Outputs) + FC_WRITE_REGS = 0x10, // Write block of contiguous registers + FC_READ_FILE_REC = 0x14, // Read File Record + FC_WRITE_FILE_REC = 0x15, // Write File Record + FC_MASKWRITE_REG = 0x16, // Mask Write Register + FC_READWRITE_REGS = 0x17 // Read/Write Multiple registers + }; + //Exception Codes + //Custom result codes used internally and for callbacks but never used for Modbus responce + enum ResultCode { + EX_SUCCESS = 0x00, // Custom. No error + EX_ILLEGAL_FUNCTION = 0x01, // Function Code not Supported + EX_ILLEGAL_ADDRESS = 0x02, // Output Address not exists + EX_ILLEGAL_VALUE = 0x03, // Output Value not in Range + EX_SLAVE_FAILURE = 0x04, // Slave or Master Device Fails to process request + EX_ACKNOWLEDGE = 0x05, // Not used + EX_SLAVE_DEVICE_BUSY = 0x06, // Not used + EX_MEMORY_PARITY_ERROR = 0x08, // Not used + EX_PATH_UNAVAILABLE = 0x0A, // Not used + EX_DEVICE_FAILED_TO_RESPOND = 0x0B, // Not used + EX_GENERAL_FAILURE = 0xE1, // Custom. Unexpected master error + EX_DATA_MISMACH = 0xE2, // Custom. Inpud data size mismach + EX_UNEXPECTED_RESPONSE = 0xE3, // Custom. Returned result doesn't mach transaction + EX_TIMEOUT = 0xE4, // Custom. Operation not finished within reasonable time + EX_CONNECTION_LOST = 0xE5, // Custom. Connection with device lost + EX_CANCEL = 0xE6, // Custom. Transaction/request canceled + EX_PASSTHROUGH = 0xE7, // Custom. Raw callback. Indicate to normal processing on callback exit + EX_FORCE_PROCESS = 0xE8 // Custom. Raw callback. Indicate to force processing on callback exit + }; + union RequestData { + struct { + TAddress reg; + uint16_t regCount; + }; + struct { + TAddress regRead; + uint16_t regReadCount; + TAddress regWrite; + uint16_t regWriteCount; + }; + struct { + TAddress regMask; + uint16_t andMask; + uint16_t orMask; + }; + uint8_t* data; + RequestData(TAddress r1, uint16_t c1) { + reg = r1; + regCount = c1; + }; + RequestData(TAddress r1, uint16_t c1, TAddress r2, uint16_t c2) { + regRead = r1; + regReadCount = c1; + regWrite = r2; + regWriteCount = c2; + }; + RequestData(TAddress r1, uint16_t m1, uint16_t m2) { + regMask = r1; + andMask = m1; + orMask = m2; + }; + RequestData(uint8_t* d) { + data = d; + }; + }; + + struct frame_arg_t { + bool to_server; + union { + uint8_t slaveId; + struct { + uint8_t unitId; + uint32_t ipaddr; + uint16_t transactionId; + }; + }; + frame_arg_t(uint8_t s, bool m = false) { + slaveId = s; + to_server = m; + }; + frame_arg_t(uint8_t u, uint32_t a, uint16_t t, bool m = false) { + unitId = u; + ipaddr = a; + transactionId = t; + to_server = m; + }; + }; + + ~Modbus(); + + bool cbEnable(const bool state = true); + bool cbDisable(); + + private: + ResultCode readBits(TAddress startreg, uint16_t numregs, FunctionCode fn); + ResultCode readWords(TAddress startreg, uint16_t numregs, FunctionCode fn); + + bool setMultipleBits(uint8_t* frame, TAddress startreg, uint16_t numoutputs); + bool setMultipleWords(uint16_t* frame, TAddress startreg, uint16_t numoutputs); + + void getMultipleBits(uint8_t* frame, TAddress startreg, uint16_t numregs); + void getMultipleWords(uint16_t* frame, TAddress startreg, uint16_t numregs); + + void bitsToBool(bool* dst, uint8_t* src, uint16_t numregs); + void boolToBits(uint8_t* dst, bool* src, uint16_t numregs); + + protected: + //Reply Types + enum ReplyCode { + REPLY_OFF = 0x01, + REPLY_ECHO = 0x02, + REPLY_NORMAL = 0x03, + REPLY_ERROR = 0x04, + REPLY_UNEXPECTED = 0x05 + }; + #if defined(MODBUS_USE_STL) + #if defined(MODBUS_GLOBAL_REGS) + static std::vector _regs; + static std::vector _callbacks; + #if defined(MODBUS_FILES) + static std::function _onFile; + #endif + #else + std::vector _regs; + std::vector _callbacks; + #if defined(MODBUS_FILES) + std::function _onFile; + #endif + #endif + #else + #if defined(MODBUS_GLOBAL_REGS) + static DArray _regs; + static DArray _callbacks; + #if defined(MODBUS_FILES) + static ResultCode (*_onFile)(FunctionCode, uint16_t, uint16_t, uint16_t, uint8_t*); + #endif + #else + DArray _regs; + DArray _callbacks; + #if defined(MODBUS_FILES) + ResultCode (*_onFile)(FunctionCode, uint16_t, uint16_t, uint16_t, uint8_t*)= nullptr; + #endif + #endif + #endif + + uint8_t* _frame = nullptr; + uint16_t _len = 0; + uint8_t _reply = 0; + bool cbEnabled = true; + uint16_t callback(TRegister* reg, uint16_t val, TCallback::CallbackType t); + virtual TRegister* searchRegister(TAddress addr); + void exceptionResponse(FunctionCode fn, ResultCode excode); // Fills _frame with response + void successResponce(TAddress startreg, uint16_t numoutputs, FunctionCode fn); // Fills frame with response + void slavePDU(uint8_t* frame); //For Slave + void masterPDU(uint8_t* frame, uint8_t* sourceFrame, TAddress startreg, uint8_t* output = nullptr); //For Master + // frame - data received form slave + // sourceFrame - data have sent fo slave + // startreg - local register to start put data to + // output - if not null put data to the buffer insted local registers. output assumed to by array of uint16_t or boolean + + bool readSlave(uint16_t address, uint16_t numregs, FunctionCode fn); + bool writeSlaveBits(TAddress startreg, uint16_t to, uint16_t numregs, FunctionCode fn, bool* data = nullptr); + bool writeSlaveWords(TAddress startreg, uint16_t to, uint16_t numregs, FunctionCode fn, uint16_t* data = nullptr); + // startreg - local register to get data from + // to - slave register to write data to + // numregs - number of registers + // fn - Modbus function + // data - if null use local registers. Otherwise use data from array to erite to slave + bool removeOn(TCallback::CallbackType t, TAddress address, cbModbus cb = nullptr, uint16_t numregs = 1); + public: + bool addReg(TAddress address, uint16_t value = 0, uint16_t numregs = 1); + bool Reg(TAddress address, uint16_t value); + uint16_t Reg(TAddress address); + bool removeReg(TAddress address, uint16_t numregs = 1); + bool addReg(TAddress address, uint16_t* value, uint16_t numregs = 1); + bool Reg(TAddress address, uint16_t* value, uint16_t numregs = 1); + + bool onGet(TAddress address, cbModbus cb = nullptr, uint16_t numregs = 1); + bool onSet(TAddress address, cbModbus cb = nullptr, uint16_t numregs = 1); + bool removeOnSet(TAddress address, cbModbus cb = nullptr, uint16_t numregs = 1); + bool removeOnGet(TAddress address, cbModbus cb = nullptr, uint16_t numregs = 1); + + virtual uint32_t eventSource() {return 0;} + #if defined(MODBUS_USE_STL) + typedef std::function cbRequest; // Callback function Type + typedef std::function cbRaw; // Callback function Type + #else + typedef ResultCode (*cbRequest)(FunctionCode fc, const RequestData data); // Callback function Type + typedef ResultCode (*cbRaw)(uint8_t*, uint8_t, void*); // Callback function Type + #endif + + protected: + cbRaw _cbRaw = nullptr; + static ResultCode _onRequestDefault(FunctionCode fc, const RequestData data); + cbRequest _onRequest = _onRequestDefault; + public: + bool onRaw(cbRaw cb = nullptr); + bool onRequest(cbRequest cb = _onRequestDefault); + #if defined (MODBUSAPI_OPTIONAL) + protected: + cbRequest _onRequestSuccess = _onRequestDefault; + public: + bool onRequestSuccess(cbRequest cb = _onRequestDefault); + #endif + + #if defined(MODBUS_FILES) + public: + #if defined(MODBUS_USE_STL) + bool onFile(std::function); + #else + bool onFile(ResultCode (*cb)(FunctionCode, uint16_t, uint16_t, uint16_t, uint8_t*)); + #endif + private: + ResultCode fileOp(FunctionCode fc, uint16_t fileNum, uint16_t recNum, uint16_t recLen, uint8_t* frame); + protected: + bool readSlaveFile(uint16_t* fileNum, uint16_t* startRec, uint16_t* len, uint8_t count, FunctionCode fn); + // fileNum - sequental array of files numbers to read + // startRec - array of strart records for each file + // len - array of counts of records to read in terms of register size (2 bytes) for each file + // count - count of records to be compose in the single request + // fn - Modbus function. Assumed to be 0x14 + bool writeSlaveFile(uint16_t* fileNum, uint16_t* startRec, uint16_t* len, uint8_t count, FunctionCode fn, uint8_t* data); + // fileNum - sequental array of files numbers to read + // startRec - array of strart records for each file + // len - array of counts of records to read in terms of register size (2 bytes) for each file + // count - count of records to be compose in the single request + // fn - Modbus function. Assumed to be 0x15 + // data - sequental set of data records + #endif + +}; + +#if defined(MODBUS_USE_STL) +typedef std::function cbTransaction; // Callback skeleton for requests +#else +typedef bool (*cbTransaction)(Modbus::ResultCode event, uint16_t transactionId, void* data); // Callback skeleton for requests +#endif +//typedef Modbus::ResultCode (*cbRequest)(Modbus::FunctionCode func, TRegister* reg, uint16_t regCount); // Callback function Type +#if defined(MODBUS_FILES) +// Callback skeleton for file read/write +#if defined(MODBUS_USE_STL) +typedef std::function cbModbusFileOp; +#else +typedef Modbus::ResultCode (*cbModbusFileOp)(Modbus::FunctionCode func, uint16_t fileNum, uint16_t recNumber, uint16_t recLength, uint8_t* frame); +#endif +#endif + +#if defined(ARDUINO_SAM_DUE_STL) +// Arduino Due STL workaround +namespace std { + void __throw_bad_function_call(); +} +#endif \ No newline at end of file diff --git a/lib/modbus-esp8266/ModbusAPI.h b/lib/modbus-esp8266/ModbusAPI.h new file mode 100644 index 0000000..8c31a93 --- /dev/null +++ b/lib/modbus-esp8266/ModbusAPI.h @@ -0,0 +1,507 @@ +/* + Modbus Library for Arduino + Modbus public API implementation + Copyright (C) 2014 Andr� Sarmento Barbosa + 2017-2021 Alexander Emelianov (a.m.emelianov@gmail.com) +*/ +#pragma once +#include "Modbus.h" + +template +class ModbusAPI : public T { + public: + // Alternative API + template + uint16_t read(TYPEID id, TAddress reg, uint16_t* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t read(TYPEID id, TAddress reg, bool* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t write(TYPEID id, TAddress reg, uint16_t value, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t write(TYPEID id, TAddress reg, bool value, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t write(TYPEID id, TAddress reg, uint16_t* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t write(TYPEID id, TAddress reg, bool* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); +/* + template + uint16_t push(TYPEID id, TAddress to, TAddress from, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t pull(TYPEID id, TAddress from, TAddress to, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); +*/ + // Classic API + bool Hregs(uint16_t offset, uint16_t* value, uint16_t numregs = 1) {return this->Reg(HREG(offset), value);} + bool Coils(uint16_t offset, bool* value, uint16_t numregs = 1) {return this->Reg(COIL(offset), value);} + bool Istss(uint16_t offset, bool* value, uint16_t numregs = 1) {return this->Reg(ISTS(offset), value);} + bool Iregs(uint16_t offset, uint16_t* value, uint16_t numregs = 1) {return this->Reg(IREG(offset), value);} + + //bool addHreg(uint16_t offset, uint16_t* value, uint16_t numregs = 1) {return this->addReg(HREG(offset), value);} + //bool addCoil(uint16_t offset, bool* value, uint16_t numregs = 1) {return this->addReg(COIL(offset), value);} + //bool addIsts(uint16_t offset, bool* value, uint16_t numregs = 1) {return this->addReg(ISTS(offset), value);} + //bool addIreg(uint16_t offset, uint16_t* value, uint16_t numregs = 1) {return this->addReg(IREG(offset), value);} + + bool addHreg(uint16_t offset, uint16_t value = 0, uint16_t numregs = 1); + bool addCoil(uint16_t offset, bool value = false, uint16_t numregs = 1); + bool addIsts(uint16_t offset, bool value = false, uint16_t numregs = 1); + bool addIreg(uint16_t offset, uint16_t value = 0, uint16_t numregs = 1); + + bool Hreg(uint16_t offset, uint16_t value); + bool Coil(uint16_t offset, bool value); + bool Ists(uint16_t offset, bool value); + bool Ireg(uint16_t offset, uint16_t value); + + bool Coil(uint16_t offset); + bool Ists(uint16_t offset); + uint16_t Ireg(uint16_t offset); + uint16_t Hreg(uint16_t offset); + + bool removeCoil(uint16_t offset, uint16_t numregs = 1); + bool removeIsts(uint16_t offset, uint16_t numregs = 1); + bool removeIreg(uint16_t offset, uint16_t numregs = 1); + bool removeHreg(uint16_t offset, uint16_t numregs = 1); + + bool onGetCoil(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool onSetCoil(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool onGetHreg(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool onSetHreg(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool onGetIsts(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool onSetIsts(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool onGetIreg(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool onSetIreg(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + + bool removeOnGetCoil(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool removeOnSetCoil(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool removeOnGetHreg(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool removeOnSetHreg(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool removeOnGetIsts(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool removeOnSetIsts(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool removeOnGetIreg(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + bool removeOnSetIreg(uint16_t offset, cbModbus cb = nullptr, uint16_t numregs = 1); + + template + uint16_t writeCoil(TYPEID id, uint16_t offset, bool value, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t writeCoil(TYPEID id, uint16_t offset, bool* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t readCoil(TYPEID id, uint16_t offset, bool* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t writeHreg(TYPEID id, uint16_t offset, uint16_t value, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t writeHreg(TYPEID id, uint16_t offset, uint16_t* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t readIsts(TYPEID id, uint16_t offset, bool* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t readHreg(TYPEID id, uint16_t offset, uint16_t* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t readIreg(TYPEID id, uint16_t offset, uint16_t* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + + template + uint16_t pushCoil(TYPEID id, uint16_t to, uint16_t from, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t pullCoil(TYPEID id, uint16_t from, uint16_t to, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t pullIsts(TYPEID id, uint16_t from, uint16_t to, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t pushHreg(TYPEID id, uint16_t to, uint16_t from, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t pullHreg(TYPEID id, uint16_t from, uint16_t to, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t pullIreg(TYPEID id, uint16_t from, uint16_t to, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + + template + uint16_t pullHregToIreg(TYPEID id, uint16_t offset, uint16_t startreg, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t pullCoilToIsts(TYPEID id, uint16_t offset, uint16_t startreg, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t pushIstsToCoil(TYPEID id, uint16_t to, uint16_t from, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t pushIregToHreg(TYPEID id, uint16_t to, uint16_t from, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + + template + uint16_t readFileRec(TYPEID slaveId, uint16_t fileNum, uint16_t startRec, uint16_t len, uint8_t* data, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t writeFileRec(TYPEID slaveId, uint16_t fileNum, uint16_t startRec, uint16_t len, uint8_t* data, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + + template + uint16_t maskHreg(TYPEID slaveId, uint16_t offset, uint16_t andMask, uint16_t orMask, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t readWriteHreg(TYPEID slaveId, uint16_t readOffset, uint16_t* readValue, uint16_t readNumregs, uint16_t writeOffset, uint16_t* writeValue, uint16_t writeNumregs, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + + template + uint16_t rawRequest(TYPEID ip, const uint8_t* data, uint16_t len, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t rawResponce(TYPEID ip, const uint8_t* data, uint16_t len, uint8_t unit = MODBUSIP_UNIT); + template + uint16_t errorResponce(TYPEID ip, Modbus::FunctionCode fn, Modbus::ResultCode excode, uint8_t unit = MODBUSIP_UNIT); +}; + +// FNAME writeCoil, writeIsts, writeHreg, writeIreg +// REG COIL, ISTS, HREG, IREG +// FUNC Modbus function +// MAXNUM Register count limit +// VALTYPE bool, uint16_t +// VALUE +#define IMPLEMENT_WRITEREG(FNAME, REG, FUNC, VALUE, VALTYPE) \ +template \ +template \ +uint16_t ModbusAPI::FNAME(TYPEID ip, uint16_t offset, VALTYPE value, cbTransaction cb, uint8_t unit) { \ + this->readSlave(offset, VALUE(value), Modbus::FUNC); \ + return this->send(ip, REG(offset), cb, unit); \ +} +IMPLEMENT_WRITEREG(writeCoil, COIL, FC_WRITE_COIL, COIL_VAL, bool) +IMPLEMENT_WRITEREG(writeHreg, HREG, FC_WRITE_REG, , uint16_t) + +#define IMPLEMENT_WRITEREGS(FNAME, REG, FUNC, VALUE, MAXNUM, VALTYPE) \ +template \ +template \ +uint16_t ModbusAPI::FNAME(TYPEID ip, uint16_t offset, VALTYPE* value, uint16_t numregs, cbTransaction cb, uint8_t unit) { \ + if (numregs < 0x0001 || numregs > MAXNUM) return false; \ + this->VALUE(REG(offset), offset, numregs, Modbus::FUNC, value); \ + return this->send(ip, REG(offset), cb, unit); \ +} +IMPLEMENT_WRITEREGS(writeCoil, COIL, FC_WRITE_COILS, writeSlaveBits, MODBUS_MAX_BITS, bool) +IMPLEMENT_WRITEREGS(writeHreg, HREG, FC_WRITE_REGS, writeSlaveWords, MODBUS_MAX_WORDS, uint16_t) + +#define IMPLEMENT_READREGS(FNAME, REG, FUNC, MAXNUM, VALTYPE) \ +template \ +template \ +uint16_t ModbusAPI::FNAME(TYPEID ip, uint16_t offset, VALTYPE* value, uint16_t numregs, cbTransaction cb, uint8_t unit) { \ + if (numregs < 0x0001 || numregs > MAXNUM) return false; \ + this->readSlave(offset, numregs, Modbus::FUNC); \ + return this->send(ip, REG(offset), cb, unit, (uint8_t*)value); \ +} +IMPLEMENT_READREGS(readCoil, COIL, FC_READ_COILS, MODBUS_MAX_BITS, bool) +IMPLEMENT_READREGS(readHreg, HREG, FC_READ_REGS, MODBUS_MAX_WORDS, uint16_t) +IMPLEMENT_READREGS(readIsts, ISTS, FC_READ_INPUT_STAT, MODBUS_MAX_BITS, bool) +IMPLEMENT_READREGS(readIreg, IREG, FC_READ_INPUT_REGS, MODBUS_MAX_WORDS, uint16_t) + +#if defined(MODBUS_ADD_REG) +#define ADDREG(R) this->addReg(R(to), (uint16_t)0, numregs); +#else +#define ADDREG(R) ; +#endif +#define IMPLEMENT_PULL(FNAME, REG, FUNC, MAXNUM) \ +template \ +template \ +uint16_t ModbusAPI::FNAME(TYPEID ip, uint16_t from, uint16_t to, uint16_t numregs, cbTransaction cb, uint8_t unit) { \ + if (numregs < 0x0001 || numregs > MAXNUM) return false; \ + ADDREG(REG) \ + this->readSlave(from, numregs, Modbus::FUNC); \ + return this->send(ip, REG(to), cb, unit); \ +} +IMPLEMENT_PULL(pullCoil, COIL, FC_READ_COILS, MODBUS_MAX_BITS) +IMPLEMENT_PULL(pullIsts, ISTS, FC_READ_INPUT_STAT, MODBUS_MAX_BITS) +IMPLEMENT_PULL(pullHreg, HREG, FC_READ_REGS, MODBUS_MAX_WORDS) +IMPLEMENT_PULL(pullIreg, IREG, FC_READ_INPUT_REGS, MODBUS_MAX_WORDS) +IMPLEMENT_PULL(pullHregToIreg, IREG, FC_READ_REGS, MODBUS_MAX_WORDS) +IMPLEMENT_PULL(pullCoilToIsts, ISTS, FC_READ_COILS, MODBUS_MAX_BITS) + +#define IMPLEMENT_PUSH(FNAME, REG, FUNC, MAXNUM, FINT) \ +template \ +template \ +uint16_t ModbusAPI::FNAME(TYPEID ip, uint16_t to, uint16_t from, uint16_t numregs, cbTransaction cb, uint8_t unit) { \ + if (numregs < 0x0001 || numregs > MAXNUM) return false; \ + if (!this->searchRegister(REG(from))) return false; \ + this->FINT(REG(from), to, numregs, Modbus::FUNC); \ + return this->send(ip, REG(from), cb, unit); \ +} +IMPLEMENT_PUSH(pushCoil, COIL, FC_WRITE_COILS, MODBUS_MAX_BITS, writeSlaveBits) +IMPLEMENT_PUSH(pushHreg, HREG, FC_WRITE_REGS, MODBUS_MAX_WORDS, writeSlaveWords) +IMPLEMENT_PUSH(pushIregToHreg, IREG, FC_WRITE_REGS, MODBUS_MAX_WORDS, writeSlaveWords) +IMPLEMENT_PUSH(pushIstsToCoil, ISTS, FC_WRITE_COILS, MODBUS_MAX_BITS, writeSlaveBits) + +template +template +uint16_t ModbusAPI::read(TYPEID id, TAddress reg, uint16_t* value, uint16_t numregs, cbTransaction cb, uint8_t unit) { + switch (reg.type) { + case TAddress::HREG: + return readHreg(id, reg.address, value, numregs, cb, unit); + case TAddress::IREG: + return readIreg(id, reg.address, value, numregs, cb, unit); + default: + return 0; + } +} +template +template +uint16_t ModbusAPI::read(TYPEID id, TAddress reg, bool* value, uint16_t numregs, cbTransaction cb, uint8_t unit) { + switch (reg.type) { + case TAddress::COIL: + return readCoil(id, reg.address, value, numregs, cb, unit); + case TAddress::ISTS: + return readIsts(id, reg.address, value, numregs, cb, unit); + default: + return 0; + } +} +template +template +uint16_t ModbusAPI::write(TYPEID id, TAddress reg, uint16_t value, cbTransaction cb, uint8_t unit) { + switch (reg.type) { + case TAddress::COIL: + return writeCoil(id, reg.address, value, cb, unit); + case TAddress::HREG: + return writeHreg(id, reg.address, value, cb, unit); + default: + return 0; + } +} +template +template +uint16_t ModbusAPI::write(TYPEID id, TAddress reg, bool value, cbTransaction cb, uint8_t unit) { + switch (reg.type) { + case TAddress::COIL: + return writeCoil(id, reg.address, value, cb, unit); + default: + return 0; + } +} +template +template +uint16_t ModbusAPI::write(TYPEID id, TAddress reg, uint16_t* value, uint16_t numregs, cbTransaction cb, uint8_t unit) { + switch (reg.type) { + case TAddress::COIL: + return writeCoil(id, reg.address, value, numregs, cb, unit); + case TAddress::HREG: + return writeHreg(id, reg.address, value, numregs, cb, unit); + default: + return 0; + } +} +template +template +uint16_t ModbusAPI::write(TYPEID id, TAddress reg, bool* value, uint16_t numregs, cbTransaction cb, uint8_t unit) { + switch (reg.type) { + case TAddress::COIL: + return writeCoil(id, reg.address, value, cb, numregs, unit); + default: + return 0; + } +} + +template \ +bool ModbusAPI::addHreg(uint16_t offset, uint16_t value, uint16_t numregs) { + return this->addReg(HREG(offset), value, numregs); +} +template \ +bool ModbusAPI::Hreg(uint16_t offset, uint16_t value) { + return this->Reg(HREG(offset), value); +} +template \ +uint16_t ModbusAPI::Hreg(uint16_t offset) { + return this->Reg(HREG(offset)); +} +template \ +bool ModbusAPI::removeHreg(uint16_t offset, uint16_t numregs) { + return this->removeReg(HREG(offset), numregs); +} +template \ +bool ModbusAPI::addCoil(uint16_t offset, bool value, uint16_t numregs) { + return this->addReg(COIL(offset), COIL_VAL(value), numregs); +} +template \ +bool ModbusAPI::addIsts(uint16_t offset, bool value, uint16_t numregs) { + return this->addReg(ISTS(offset), ISTS_VAL(value), numregs); +} +template \ +bool ModbusAPI::addIreg(uint16_t offset, uint16_t value, uint16_t numregs) { + return this->addReg(IREG(offset), value, numregs); +} +template \ +bool ModbusAPI::Coil(uint16_t offset, bool value) { + return this->Reg(COIL(offset), COIL_VAL(value)); +} +template \ +bool ModbusAPI::Ists(uint16_t offset, bool value) { + return this->Reg(ISTS(offset), ISTS_VAL(value)); +} +template \ +bool ModbusAPI::Ireg(uint16_t offset, uint16_t value) { + return this->Reg(IREG(offset), value); +} +template \ +bool ModbusAPI::Coil(uint16_t offset) { + return COIL_BOOL(this->Reg(COIL(offset))); +} +template \ +bool ModbusAPI::Ists(uint16_t offset) { + return ISTS_BOOL(this->Reg(ISTS(offset))); +} +template \ +uint16_t ModbusAPI::Ireg(uint16_t offset) { + return this->Reg(IREG(offset)); +} +template \ +bool ModbusAPI::removeCoil(uint16_t offset, uint16_t numregs) { + return this->removeReg(COIL(offset), numregs); +} +template \ +bool ModbusAPI::removeIsts(uint16_t offset, uint16_t numregs) { + return this->removeReg(ISTS(offset), numregs); +} +template \ +bool ModbusAPI::removeIreg(uint16_t offset, uint16_t numregs) { + return this->removeReg(IREG(offset), numregs); +} +template \ +bool ModbusAPI::onGetCoil(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->onGet(COIL(offset), cb, numregs); +} +template \ +bool ModbusAPI::onSetCoil(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->onSet(COIL(offset), cb, numregs); +} +template \ +bool ModbusAPI::onGetHreg(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->onGet(HREG(offset), cb, numregs); +} +template \ +bool ModbusAPI::onSetHreg(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->onSet(HREG(offset), cb, numregs); +} +template \ +bool ModbusAPI::onGetIsts(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->onGet(ISTS(offset), cb, numregs); +} +template \ +bool ModbusAPI::onSetIsts(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->onSet(ISTS(offset), cb, numregs); +} +template \ +bool ModbusAPI::onGetIreg(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->onGet(IREG(offset), cb, numregs); +} +template \ +bool ModbusAPI::onSetIreg(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->onSet(IREG(offset), cb, numregs); +} +template \ +bool ModbusAPI::removeOnGetCoil(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->removeOnGet(COIL(offset), cb, numregs); +} +template \ +bool ModbusAPI::removeOnSetCoil(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->removeOnSet(COIL(offset), cb, numregs); +} +template \ +bool ModbusAPI::removeOnGetHreg(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->removeOnGet(HREG(offset), cb, numregs); +} +template \ +bool ModbusAPI::removeOnSetHreg(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->removeOnSet(HREG(offset), cb, numregs); +} +template \ +bool ModbusAPI::removeOnGetIsts(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->removeOnGet(ISTS(offset), cb, numregs); +} +template \ +bool ModbusAPI::removeOnSetIsts(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->removeOnSet(ISTS(offset), cb, numregs); +} +template \ +bool ModbusAPI::removeOnGetIreg(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->removeOnGet(IREG(offset), cb, numregs); +} +template \ +bool ModbusAPI::removeOnSetIreg(uint16_t offset, cbModbus cb, uint16_t numregs) { + return this->removeOnSet(IREG(offset), cb, numregs); +} +template \ +template \ +uint16_t ModbusAPI::readFileRec(TYPEID slaveId, uint16_t fileNum, uint16_t startRec, uint16_t len, uint8_t* data, cbTransaction cb, uint8_t unit) { + if (startRec > MODBUS_MAX_FILES) return 0; + if (!this->readSlaveFile(&fileNum, &startRec, &len, 1, Modbus::FC_READ_FILE_REC)) return 0; + return this->send(slaveId, NULLREG, cb, unit, data); +}; +template \ +template \ +uint16_t ModbusAPI::writeFileRec(TYPEID slaveId, uint16_t fileNum, uint16_t startRec, uint16_t len, uint8_t* data, cbTransaction cb, uint8_t unit) { + if (startRec > MODBUS_MAX_FILES) return 0; + if (!this->writeSlaveFile(&fileNum, &startRec, &len, 1, Modbus::FC_WRITE_FILE_REC, data)) return 0; + return this->send(slaveId, NULLREG, cb, unit); +}; +template \ +template \ +uint16_t ModbusAPI::maskHreg(TYPEID slaveId, uint16_t offset, uint16_t andMask, uint16_t orMask, cbTransaction cb, uint8_t unit) { + free(this->_frame); + this->_len = 7; + this->_frame = (uint8_t*) malloc(this->_len); + this->_frame[0] = Modbus::FC_MASKWRITE_REG; + this->_frame[1] = offset >> 8; + this->_frame[2] = offset & 0x00FF; + this->_frame[3] = andMask >> 8; + this->_frame[4] = andMask & 0x00FF; + this->_frame[5] = orMask >> 8; + this->_frame[6] = orMask & 0x00FF; + return this->send(slaveId, HREG(offset), cb, unit); +}; + +template \ +template \ +uint16_t ModbusAPI::readWriteHreg(TYPEID ip, \ + uint16_t readOffset, uint16_t* readValue, uint16_t readNumregs, \ + uint16_t writeOffset, uint16_t* writeValue, uint16_t writeNumregs, \ + cbTransaction cb, uint8_t unit) { + const uint8_t _header = 10; + if (readNumregs < 0x0001 || readNumregs > MODBUS_MAX_WORDS || writeNumregs < 0x0001 || writeNumregs > 0X0079 || !readValue || !writeValue) return 0; + + free(this->_frame); + this->_len = _header + 2 * writeNumregs; + this->_frame = (uint8_t*) malloc(this->_len); + if (!this->_frame) { + this->_reply = Modbus::REPLY_OFF; + return 0; + } + this->_frame[0] = Modbus::FC_READWRITE_REGS; + this->_frame[1] = readOffset >> 8; + this->_frame[2] = readOffset & 0x00FF; + this->_frame[3] = readNumregs >> 8; + this->_frame[4] = readNumregs & 0x00FF; + + this->_frame[5] = writeOffset >> 8; + this->_frame[6] = writeOffset & 0x00FF; + this->_frame[7] = writeNumregs >> 8; + this->_frame[8] = writeNumregs & 0x00FF; + this->_frame[9] = this->_len - _header; + + uint16_t* frame = (uint16_t*)(this->_frame + _header); + for (uint8_t i = 0; i < writeNumregs; i++) { + frame[i] = __swap_16(writeValue[i]); + } + return this->send(ip, HREG(readOffset), cb, unit, (uint8_t*)readValue); +}; + +template +template +uint16_t ModbusAPI::rawRequest(TYPEID ip, \ + const uint8_t* data, uint16_t len, + cbTransaction cb, uint8_t unit) { + free(this->_frame); + this->_frame = (uint8_t*)malloc(len); + if (!this->_frame) + return 0; + this->_len = len; + memcpy(this->_frame, data, len); + return this->send(ip, NULLREG, cb, unit); +}; + +template +template +uint16_t ModbusAPI::rawResponce(TYPEID ip, \ + const uint8_t* data, uint16_t len, uint8_t unit) { + free(this->_frame); + this->_frame = (uint8_t*)malloc(len); + if (!this->_frame) + return 0; + this->_len = len; + memcpy(this->_frame, data, len); + return this->send(ip, NULLREG, nullptr, unit, nullptr, false); +}; + +template +template +uint16_t ModbusAPI::errorResponce(TYPEID ip, Modbus::FunctionCode fn, Modbus::ResultCode excode, uint8_t unit) { + this->exceptionResponse(fn, excode); + return this->send(ip, NULLREG, nullptr, unit, nullptr, false); +} diff --git a/lib/modbus-esp8266/ModbusEthernet.h b/lib/modbus-esp8266/ModbusEthernet.h new file mode 100644 index 0000000..301432f --- /dev/null +++ b/lib/modbus-esp8266/ModbusEthernet.h @@ -0,0 +1,59 @@ +/* + Modbus Library for Arduino + ModbusTCP for W5x00 Ethernet + Copyright (C) 2022 Alexander Emelianov (a.m.emelianov@gmail.com) +*/ + +#pragma once +#if defined(MODBUSIP_USE_DNS) +#include +#endif +#include "ModbusAPI.h" +#include "ModbusTCPTemplate.h" +#if defined(ARDUINO_PORTENTA_H7_M4) || defined(ARDUINO_PORTENTA_H7_M7) || defined(ARDUINO_PORTENTA_X8) +#define MODBUS_ETH_WRAP_ACCEPT +#undef MODBUS_ETH_WRAP_BEGIN +#elif defined(ESP32) +#undef MODBUS_ETH_WRAP_ACCEPT +#define MODBUS_ETH_WRAP_BEGIN +#else +#undef MODBUS_ETH_WRAP_ACCEPT +#undef MODBUS_ETH_WRAP_BEGIN +#endif +// Ethernet class wrapper to be able to compile for ESP32 +class EthernetServerWrapper : public EthernetServer { + public: + EthernetServerWrapper(uint16_t port) : EthernetServer(port) { + + } +#if defined(MODBUS_ETH_WRAP_BEGIN) + void begin(uint16_t port=0) { + EthernetServer::begin(); + } +#endif +#if defined(MODBUS_ETH_WRAP_ACCEPT) + inline EthernetClient accept() { + return available(); + } +#endif +}; + +class ModbusEthernet : public ModbusAPI> { +#if defined(MODBUSIP_USE_DNS) + private: + static IPAddress resolver (const char* host) { + DNSClient dns; + IPAddress ip; + + dns.begin(Ethernet.dnsServerIP()); + if (dns.getHostByName(host, ip) == 1) + return ip; + else + return IPADDR_NONE; + } + public: + ModbusEthernet() : ModbusAPI() { + resolve = resolver; + } +#endif +}; diff --git a/lib/modbus-esp8266/ModbusIP_ESP8266.h b/lib/modbus-esp8266/ModbusIP_ESP8266.h new file mode 100644 index 0000000..2c83146 --- /dev/null +++ b/lib/modbus-esp8266/ModbusIP_ESP8266.h @@ -0,0 +1,11 @@ +/* + Modbus Library for Arduino + ModbusIP class compatibility wrapper + Copyright (C) 2014 Andr� Sarmento Barbosa + 2017-2020 Alexander Emelianov (a.m.emelianov@gmail.com) +*/ + +#pragma once +#include "ModbusTCP.h" + +class ModbusIP : public ModbusTCP {}; \ No newline at end of file diff --git a/lib/modbus-esp8266/ModbusRTU.cpp b/lib/modbus-esp8266/ModbusRTU.cpp new file mode 100644 index 0000000..a00d609 --- /dev/null +++ b/lib/modbus-esp8266/ModbusRTU.cpp @@ -0,0 +1,329 @@ +/* + Modbus Library for Arduino + ModbusRTU implementation + Copyright (C) 2019-2022 Alexander Emelianov (a.m.emelianov@gmail.com) + https://github.com/emelianov/modbus-esp8266 + This code is licensed under the BSD New License. See LICENSE.txt for more info. +*/ +#include "ModbusRTU.h" + +// Table of CRC values +static const uint16_t _auchCRC[] PROGMEM = { + 0x0000, 0xC1C0, 0x81C1, 0x4001, 0x01C3, 0xC003, 0x8002, 0x41C2, 0x01C6, 0xC006, 0x8007, 0x41C7, 0x0005, 0xC1C5, 0x81C4, + 0x4004, 0x01CC, 0xC00C, 0x800D, 0x41CD, 0x000F, 0xC1CF, 0x81CE, 0x400E, 0x000A, 0xC1CA, 0x81CB, 0x400B, 0x01C9, 0xC009, + 0x8008, 0x41C8, 0x01D8, 0xC018, 0x8019, 0x41D9, 0x001B, 0xC1DB, 0x81DA, 0x401A, 0x001E, 0xC1DE, 0x81DF, 0x401F, 0x01DD, + 0xC01D, 0x801C, 0x41DC, 0x0014, 0xC1D4, 0x81D5, 0x4015, 0x01D7, 0xC017, 0x8016, 0x41D6, 0x01D2, 0xC012, 0x8013, 0x41D3, + 0x0011, 0xC1D1, 0x81D0, 0x4010, 0x01F0, 0xC030, 0x8031, 0x41F1, 0x0033, 0xC1F3, 0x81F2, 0x4032, 0x0036, 0xC1F6, 0x81F7, + 0x4037, 0x01F5, 0xC035, 0x8034, 0x41F4, 0x003C, 0xC1FC, 0x81FD, 0x403D, 0x01FF, 0xC03F, 0x803E, 0x41FE, 0x01FA, 0xC03A, + 0x803B, 0x41FB, 0x0039, 0xC1F9, 0x81F8, 0x4038, 0x0028, 0xC1E8, 0x81E9, 0x4029, 0x01EB, 0xC02B, 0x802A, 0x41EA, 0x01EE, + 0xC02E, 0x802F, 0x41EF, 0x002D, 0xC1ED, 0x81EC, 0x402C, 0x01E4, 0xC024, 0x8025, 0x41E5, 0x0027, 0xC1E7, 0x81E6, 0x4026, + 0x0022, 0xC1E2, 0x81E3, 0x4023, 0x01E1, 0xC021, 0x8020, 0x41E0, 0x01A0, 0xC060, 0x8061, 0x41A1, 0x0063, 0xC1A3, 0x81A2, + 0x4062, 0x0066, 0xC1A6, 0x81A7, 0x4067, 0x01A5, 0xC065, 0x8064, 0x41A4, 0x006C, 0xC1AC, 0x81AD, 0x406D, 0x01AF, 0xC06F, + 0x806E, 0x41AE, 0x01AA, 0xC06A, 0x806B, 0x41AB, 0x0069, 0xC1A9, 0x81A8, 0x4068, 0x0078, 0xC1B8, 0x81B9, 0x4079, 0x01BB, + 0xC07B, 0x807A, 0x41BA, 0x01BE, 0xC07E, 0x807F, 0x41BF, 0x007D, 0xC1BD, 0x81BC, 0x407C, 0x01B4, 0xC074, 0x8075, 0x41B5, + 0x0077, 0xC1B7, 0x81B6, 0x4076, 0x0072, 0xC1B2, 0x81B3, 0x4073, 0x01B1, 0xC071, 0x8070, 0x41B0, 0x0050, 0xC190, 0x8191, + 0x4051, 0x0193, 0xC053, 0x8052, 0x4192, 0x0196, 0xC056, 0x8057, 0x4197, 0x0055, 0xC195, 0x8194, 0x4054, 0x019C, 0xC05C, + 0x805D, 0x419D, 0x005F, 0xC19F, 0x819E, 0x405E, 0x005A, 0xC19A, 0x819B, 0x405B, 0x0199, 0xC059, 0x8058, 0x4198, 0x0188, + 0xC048, 0x8049, 0x4189, 0x004B, 0xC18B, 0x818A, 0x404A, 0x004E, 0xC18E, 0x818F, 0x404F, 0x018D, 0xC04D, 0x804C, 0x418C, + 0x0044, 0xC184, 0x8185, 0x4045, 0x0187, 0xC047, 0x8046, 0x4186, 0x0182, 0xC042, 0x8043, 0x4183, 0x0041, 0xC181, 0x8180, + 0x4040, 0x0000 +}; + +uint16_t ModbusRTUTemplate::crc16(uint8_t address, uint8_t* frame, uint8_t pduLen) { + uint8_t i = 0xFF ^ address; + uint16_t val = pgm_read_word(_auchCRC + i); + uint8_t CRCHi = 0xFF ^ highByte(val); // Hi + uint8_t CRCLo = lowByte(val); //Low + while (pduLen--) { + i = CRCHi ^ *frame++; + val = pgm_read_word(_auchCRC + i); + CRCHi = CRCLo ^ highByte(val); // Hi + CRCLo = lowByte(val); //Low + } + return (CRCHi << 8) | CRCLo; +} +/* +uint16_t ModbusRTUTemplate::crc16_alt(uint8_t address, uint8_t* frame, uint8_t pduLen) { + uint16_t temp, temp2, flag; + temp = 0xFFFF ^ address; + for (uint8_t i = 0; i < pduLen; i++) + { + temp = temp ^ frame[i]; + for (uint8_t j = 1; j <= 8; j++) + { + flag = temp & 0x0001; + temp >>= 1; + if (flag) + temp ^= 0xA001; + } + } + // Reverse byte order. + temp2 = temp >> 8; + temp = (temp << 8) | temp2; + temp &= 0xFFFF; + return temp; +} +*/ + +uint32_t ModbusRTUTemplate::charSendTime(uint32_t baud, uint8_t char_bits) { + return (uint32_t)char_bits * 1000000UL / baud; +} + +uint32_t ModbusRTUTemplate::calculateMinimumInterFrameTime(uint32_t baud, uint8_t char_bits) { + // baud = baudrate of the serial port + // char_bits = size of 1 modbus character (defined a 11 bits in modbus specificacion) + // Returns: The minimum time between frames (defined as 3.5 characters time in modbus specification) + + // According to standard, the Modbus frame is always 11 bits long: + // 1 start + 8 data + 1 parity + 1 stop + // 1 start + 8 data + 2 stops + // And the minimum time between frames is defined as 3.5 characters time in modbus specification. + // This means the time between frames (in microseconds) should be calculated as follows: + // _t = 3.5 x 11 x 1000000 / baudrate = 38500000 / baudrate + + // Eg: For 9600 baudrate _t = 38500000 / 9600 = 4010 us + // For baudrates grater than 19200 the _t should be fixed at 1750 us. + + // If the used modbus frame length is 10 bits (out of standard - 1 start + 8 data + 1 stop), then + // it can be set using char_bits = 10. + + if (baud > 19200) { + return 1750UL; + } else { + return 3.5 * charSendTime(baud, char_bits); + } +} + +// Kept for backward compatibility +void ModbusRTUTemplate::setBaudrate(uint32_t baud) { + setInterFrameTime(calculateMinimumInterFrameTime(baud)); +} + +void ModbusRTUTemplate::setInterFrameTime(uint32_t t_us) { + // This function sets the inter frame time. This time is the time that task() waits before considering that the frame being transmitted on the RS485 bus has finished. + // If the interframe calculated by calculateMinimumInterFrameTime() is not enough, you can set the interframe time manually with this function. + // The time must be set in micro seconds. + // This is useful when you are receiving data as a slave and you notice that the slave is dividing a frame in two or more pieces (and obviously the CRC is failing on all pieces). + // This is because it is detecting an interframe time inbetween bytes of the frame and thus it interprets one single frame as two or more frames. + // In that case it is useful to be able to set a more "permissive" interframe time. + _t = t_us; +} + +bool ModbusRTUTemplate::begin(Stream* port, int16_t txEnablePin, bool txEnableDirect) { + _port = port; + _t = 1750UL; +#if defined(MODBUSRTU_FLUSH_DELAY) + _t1 = charSendTime(0); +#endif + if (txEnablePin >= 0) { + _txEnablePin = txEnablePin; + _direct = txEnableDirect; + pinMode(_txEnablePin, OUTPUT); + digitalWrite(_txEnablePin, _direct?LOW:HIGH); + } + return true; +} + +bool ModbusRTUTemplate::rawSend(uint8_t slaveId, uint8_t* frame, uint8_t len) { + uint16_t newCrc = crc16(slaveId, frame, len); +#if defined(MODBUSRTU_DEBUG) + for (uint8_t i=0 ; i < _len ; i++) { + Serial.print(_frame[i], HEX); + Serial.print(" "); + } + Serial.println(); +#endif +#if defined(MODBUSRTU_REDE) + if (_txEnablePin >= 0 || _rxPin >= 0) { + if (_txEnablePin >= 0) + digitalWrite(_txEnablePin, _direct?HIGH:LOW); + if (_rxPin >= 0) + digitalWrite(_rxPin, _direct?HIGH:LOW); +#if !defined(ESP32) + delayMicroseconds(MODBUSRTU_REDE_SWITCH_US); +#endif + } +#else + if (_txEnablePin >= 0) { + digitalWrite(_txEnablePin, _direct?HIGH:LOW); +#if !defined(ESP32) + delayMicroseconds(MODBUSRTU_REDE_SWITCH_US); +#endif + } +#endif +#if defined(ESP32) + vTaskDelay(0); +#endif + _port->write(slaveId); //Send slaveId + _port->write(frame, len); // Send PDU + _port->write(newCrc >> 8); //Send CRC + _port->write(newCrc & 0xFF);//Send CRC + _port->flush(); +#if defined(MODBUSRTU_REDE) + if (_txEnablePin >= 0 || _rxPin >= 0) { +#if defined(MODBUSRTU_FLUSH_DELAY) + delayMicroseconds(_t1 * MODBUSRTU_FLUSH_DELAY); +#endif + if (_txEnablePin >= 0) + digitalWrite(_txEnablePin, _direct?LOW:HIGH); + if (_rxPin >= 0) + digitalWrite(_rxPin, _direct?LOW:HIGH); + } +#else + if (_txEnablePin >= 0) { +#if defined(MODBUSRTU_FLUSH_DELAY) + delayMicroseconds(_t1 * MODBUSRTU_FLUSH_DELAY); +#endif + digitalWrite(_txEnablePin, _direct?LOW:HIGH); + } +#endif + return true; +} + +uint16_t ModbusRTUTemplate::send(uint8_t slaveId, TAddress startreg, cbTransaction cb, uint8_t unit, uint8_t* data, bool waitResponse) { + bool result = false; + if ((!isMaster || !_slaveId) && _len && _frame) { // Check if waiting for previous request result and _frame filled + //if (_len && _frame) { // Check if waiting for previous request result and _frame filled + rawSend(slaveId, _frame, _len); + if (waitResponse && slaveId) { + _slaveId = slaveId; + _timestamp = micros(); + _cb = cb; + _data = data; + _sentFrame = _frame; + _sentReg = startreg; + _frame = nullptr; + } + result = true; + } + free(_frame); + _frame = nullptr; + _len = 0; + return result; +} + +void ModbusRTUTemplate::task() { +#if defined(ESP32) + vTaskDelay(0); +#endif + if (_port->available() > _len) { + _len = _port->available(); + t = micros(); + } + if (_len == 0) { + if (isMaster) cleanup(); + return; + } + if (isMaster) { + if (micros() - t < _t) { + return; + } + } + else { // For slave wait for whole message to come (unless MODBUSRTU_MAX_READMS reached) + uint32_t taskStart = micros(); + while (micros() - t < _t) { // Wait data whitespace + if (_port->available() > _len) { + _len = _port->available(); + t = micros(); + } + if (micros() - taskStart > MODBUSRTU_MAX_READ_US) { // Prevent from task() executed too long + return; + } + } + } + + bool valid_frame = true; + address = _port->read(); //first byte of frame = address + _len--; // Decrease by slaveId byte + if (isMaster && _slaveId == 0) { // Check if slaveId is set + valid_frame = false; + } + if (address != MODBUSRTU_BROADCAST && address != _slaveId) { // SlaveId Check + valid_frame = false; + } + if (!valid_frame && !_cbRaw) { + for (uint8_t i=0 ; i < _len ; i++) _port->read(); // Skip packet if SlaveId doesn't mach + _len = 0; + if (isMaster) cleanup(); + return; + } + + free(_frame); //Just in case + _frame = (uint8_t*) malloc(_len); + if (!_frame) { // Fail to allocate buffer + for (uint8_t i=0 ; i < _len ; i++) _port->read(); // Skip packet if can't allocate buffer + _len = 0; + if (isMaster) cleanup(); + return; + } + for (uint8_t i=0 ; i < _len ; i++) { + _frame[i] = _port->read(); // read data + crc + #if defined(MODBUSRTU_DEBUG) + Serial.print(_frame[i], HEX); + Serial.print(" "); + #endif + } + #if defined(MODBUSRTU_DEBUG) + Serial.println(); + #endif + //_port->readBytes(_frame, _len); + uint16_t frameCrc = ((_frame[_len - 2] << 8) | _frame[_len - 1]); // Last two byts = crc + _len = _len - 2; // Decrease by CRC 2 bytes + if (frameCrc != crc16(address, _frame, _len)) { // CRC Check + goto cleanup; + } + _reply = EX_PASSTHROUGH; + if (_cbRaw) { + frame_arg_t header_data = { address, !isMaster }; + _reply = _cbRaw(_frame, _len, (void*)&header_data); + } + if (!valid_frame && _reply != EX_FORCE_PROCESS) { + goto cleanup; + } + if (isMaster) { + if ((_frame[0] & 0x7F) == _sentFrame[0]) { // Check if function code the same as requested + // Procass incoming frame as master + if (_reply == EX_PASSTHROUGH || _reply == EX_FORCE_PROCESS) + masterPDU(_frame, _sentFrame, _sentReg, _data); + if (_cb) { + _cb((ResultCode)_reply, 0, nullptr); + _cb = nullptr; + } + free(_sentFrame); + _sentFrame = nullptr; + _data = nullptr; + _slaveId = 0; + } + _reply = Modbus::REPLY_OFF; // No reply if master + } else { + if (_reply == EX_PASSTHROUGH || _reply == EX_FORCE_PROCESS) { + slavePDU(_frame); + if (address == MODBUSRTU_BROADCAST) + _reply = Modbus::REPLY_OFF; // No reply for Broadcasts + if (_reply != Modbus::REPLY_OFF) + rawSend(address, _frame, _len); + } + } + // Cleanup +cleanup: + free(_frame); + _frame = nullptr; + _len = 0; + if (isMaster) cleanup(); +} + +bool ModbusRTUTemplate::cleanup() { + // Remove timeouted request and forced event + if (_slaveId && (micros() - _timestamp > MODBUSRTU_TIMEOUT_US)) { + if (_cb) { + _cb(Modbus::EX_TIMEOUT, 0, nullptr); + _cb = nullptr; + } + free(_sentFrame); + _sentFrame = nullptr; + _data = nullptr; + _slaveId = 0; + return true; + } + return false; +} \ No newline at end of file diff --git a/lib/modbus-esp8266/ModbusRTU.h b/lib/modbus-esp8266/ModbusRTU.h new file mode 100644 index 0000000..5d27562 --- /dev/null +++ b/lib/modbus-esp8266/ModbusRTU.h @@ -0,0 +1,99 @@ +/* + Modbus Library for Arduino + ModbusRTU + Copyright (C) 2019-2022 Alexander Emelianov (a.m.emelianov@gmail.com) + https://github.com/emelianov/modbus-esp8266 + This code is licensed under the BSD New License. See LICENSE.txt for more info. +*/ +#pragma once +#include "ModbusAPI.h" + +class ModbusRTUTemplate : public Modbus { + protected: + Stream* _port; + int16_t _txEnablePin = -1; +#if defined(MODBUSRTU_REDE) + int16_t _rxPin = -1; +#endif + bool _direct = true; // Transmit control logic (true=txEnableDirect, false=inverse) + uint32_t _t; // inter-frame delay in uS +#if defined(MODBUSRTU_FLUSH_DELAY) + uint32_t _t1; // char send time +#endif + uint32_t t = 0; // time sience last data byte arrived + bool isMaster = false; + uint8_t _slaveId; + uint32_t _timestamp = 0; + cbTransaction _cb = nullptr; + uint8_t* _data = nullptr; + uint8_t* _sentFrame = nullptr; + TAddress _sentReg = COIL(0); + uint16_t maxRegs = MODBUS_MAX_WORDS; + uint8_t address = 0; + + uint16_t send(uint8_t slaveId, TAddress startreg, cbTransaction cb, uint8_t unit = MODBUSIP_UNIT, uint8_t* data = nullptr, bool waitResponse = true); + // Prepare and send ModbusRTU frame. _frame buffer and _len should be filled with Modbus data + // slaveId - slave id + // startreg - first local register to save returned data to (miningless for write to slave operations) + // cb - transaction callback function + // data - if not null use buffer to save returned data instead of local registers + bool rawSend(uint8_t slaveId, uint8_t* frame, uint8_t len); + bool cleanup(); // Free clients if not connected and remove timedout transactions and transaction with forced events + uint16_t crc16(uint8_t address, uint8_t* frame, uint8_t pdulen); + uint16_t crc16_alt(uint8_t address, uint8_t* frame, uint8_t pduLen); + public: + void setBaudrate(uint32_t baud = -1); + uint32_t calculateMinimumInterFrameTime(uint32_t baud, uint8_t char_bits = 11); + void setInterFrameTime(uint32_t t_us); + uint32_t charSendTime(uint32_t baud, uint8_t char_bits = 11); + template + bool begin(T* port, int16_t txEnablePin = -1, bool txEnableDirect = true); +#if defined(MODBUSRTU_REDE) + template + bool begin(T* port, int16_t txEnablePin, int16_t rxEnablePin, bool txEnableDirect); +#endif + bool begin(Stream* port, int16_t txEnablePin = -1, bool txEnableDirect = true); + void task(); + void client() { isMaster = true; }; + inline void master() {client();} + void server(uint8_t serverId) {_slaveId = serverId;}; + inline void slave(uint8_t slaveId) {server(slaveId);} + uint8_t server() { return _slaveId; } + inline uint8_t slave() { return server(); } + uint32_t eventSource() override {return address;} +}; + +template +bool ModbusRTUTemplate::begin(T* port, int16_t txEnablePin, bool txEnableDirect) { + uint32_t baud = 0; + #if defined(ESP32) || defined(ESP8266) // baudRate() only available with ESP32+ESP8266 + baud = port->baudRate(); + #else + baud = 9600; + #endif + setInterFrameTime(calculateMinimumInterFrameTime(baud)); +#if defined(MODBUSRTU_FLUSH_DELAY) + _t1 = charSendTime(baud); +#endif + _port = port; + if (txEnablePin >= 0) { + _txEnablePin = txEnablePin; + _direct = txEnableDirect; + pinMode(_txEnablePin, OUTPUT); + digitalWrite(_txEnablePin, _direct?LOW:HIGH); + } + return true; +} +#if defined(MODBUSRTU_REDE) +template +bool ModbusRTUTemplate::begin(T* port, int16_t txEnablePin, int16_t rxEnablePin, bool txEnableDirect) { + begin(port, txEnablePin, txEnableDirect); + if (rxEnablePin > 0) { + _rxPin = rxEnablePin; + pinMode(_rxPin, OUTPUT); + digitalWrite(_rxPin, _direct?LOW:HIGH); + } + return true; +} +#endif +class ModbusRTU : public ModbusAPI {}; diff --git a/lib/modbus-esp8266/ModbusSettings.h b/lib/modbus-esp8266/ModbusSettings.h new file mode 100644 index 0000000..0a26644 --- /dev/null +++ b/lib/modbus-esp8266/ModbusSettings.h @@ -0,0 +1,148 @@ + +/* + Modbus Library for Arduino + + Copyright (C) 2019-2022 Alexander Emelianov (a.m.emelianov@gmail.com) + https://github.com/emelianov/modbus-esp8266 + This code is licensed under the BSD New License. See LICENSE.txt for more info. + +Prefixes: +MODBUS_ Global library settings +MODBUSIP_ Settings for TCP and TLS both +MODBUSTCP_ Settings for TCP +MODBUSTLS_ Settings for TLS +MODBUSRTU_ Settings for RTU +MODBUSAPI_ Settings for API +*/ +#pragma once + +/* +#define MODBUS_GLOBAL_REGS +If defined Modbus registers will be shared across all Modbus* instances. +If not defined each Modbus object will have own registers set. +*/ +#define MODBUS_GLOBAL_REGS +//#define MODBUS_FREE_REGS + +/* +#define ARDUINO_SAM_DUE_STL +Use STL with Arduino Due. Was able to use with Arduino IDE but not with PlatformIO +Also note STL issue workaround code in Modbus.cpp +*/ +#if defined(ARDUINO_SAM_DUE) +//#define ARDUINO_SAM_DUE_STL +#endif + +/* +#define MODBUS_USE_STL +If defined C STL will be used. +*/ +#if defined(ESP8266) || defined(ESP32) || defined(ARDUINO_ARCH_STM32) || defined(ARDUINO_SAM_DUE_STL) +#define MODBUS_USE_STL +#endif + +/* +#define MODBUS_MAX_REGS 32 +If defined regisers count will be limited. +*/ +// Add limitation for specific STL implementation +#if defined(MODBUS_USE_STL) && (defined(ESP8266) || defined(ESP32)) +#undef MODBUS_MAX_REGS +#define MODBUS_MAX_REGS 4000 +#endif + +#define MODBUS_ADD_REG +//#define MODBUS_STRICT_REG +#define MODBUS_MAX_FRAME 256 +//#define MODBUS_STATIC_FRAME +#define MODBUS_MAX_WORDS 0x007D +#define MODBUS_MAX_BITS 0x07D0 +#define MODBUS_FILES +#define MODBUS_MAX_FILES 0x270F +#define MODBUSTCP_PORT 502 +#define MODBUSTLS_PORT 802 +#define MODBUSIP_MINFRAME 2 +#define MODBUSIP_MAXFRAME 200 + +/* +ModbusTCP and ModbusTLS timeouts +#define MODBUSIP_TIMEOUT 1000 +Outgoing request timeout +#define MODBUSIP_CONNECT_TIMEOUT 1000 +ESP32 only. Outgoing connection attempt timeout +*/ +#define MODBUSIP_TIMEOUT 1000 +//#define MODBUSIP_CONNECT_TIMEOUT 1000 + +#define MODBUSIP_UNIT 255 +#define MODBUSIP_MAX_TRANSACTIONS 16 +#if defined(ESP32) +#define MODBUSIP_MAX_CLIENTS 8 +#else +#define MODBUSIP_MAX_CLIENTS 4 +#endif +#define MODBUSIP_UNIQUE_CLIENTS +#define MODBUSIP_MAX_READMS 100 + +/* +Use available() instead of accept() to get TCP client +#define MODBUSIP_USE_AVAILABLE +Used to wrap variation in Ethernet/WiFi client API implementations +*/ +//#define MODBUSIP_USE_AVAILABLE + +#define MODBUSIP_FULL +//#define MODBUSIP_DEBUG +/* +Allows to use DNS names as target +Otherwise IP addresses only must be used +#define MODBUS_IP_USE_DNS +*/ +//#define MODBUS_IP_USE_DNS + +//#define MODBUSRTU_DEBUG +#define MODBUSRTU_BROADCAST 0 +#define MB_RESERVE 248 +#define MB_SERIAL_BUFFER 128 +#ifndef MODBUSRTU_TIMEOUT +#define MODBUSRTU_TIMEOUT 1000 +#endif +#define MODBUSRTU_MAX_READMS 100 +/* +#define MODBUSRTU_REDE +Enable using separate pins for RE DE +*/ +//#define MODBUSRTU_REDE + +// Define for internal use. Do not change. +#define MODBUSRTU_TIMEOUT_US 1000UL * MODBUSRTU_TIMEOUT +#define MODBUSRTU_MAX_READ_US 1000UL * MODBUSRTU_MAX_READMS + +/* +#defone MODBUSRTU_FLUSH_DELAY 1 +Set extraa delay after serial buffer flush before changing RE/DE pin state. +Specified in chars. That is 1 is means to add delay enough to send 1 char at current port baudrate +*/ +//#define MODBUSRTU_FLUSH_DELAY 1 + +#define MODBUSRTU_REDE_SWITCH_US 1000 + +#define MODBUSAPI_LEGACY +#define MODBUSAPI_OPTIONAL + +// Workaround for RP2040 flush() bug +#if defined(ARDUINO_ARCH_RP2040) +#define MODBUSRTU_FLUSH_DELAY 1 +#endif + +// Limit resources usage for entry level boards +#if defined(ARDUINO_UNO) || defined(ARDUINO_LEONARDO) +#undef MODBUS_MAX_REGS +#undef MODBUSIP_MAX_TRANSACTIONS +#undef MODBUS_MAX_WORDS +#undef MODBUS_MAX_BITS +#define MODBUS_MAX_REGS 32 +#define MODBUSIP_MAX_TRANSACTIONS 4 +#define MODBUS_MAX_WORDS 0x0020 +#define MODBUS_MAX_BITS 0x0200 +#endif \ No newline at end of file diff --git a/lib/modbus-esp8266/ModbusTCP.h b/lib/modbus-esp8266/ModbusTCP.h new file mode 100644 index 0000000..fed76c3 --- /dev/null +++ b/lib/modbus-esp8266/ModbusTCP.h @@ -0,0 +1,40 @@ +/* + Modbus Library for Arduino + ModbusTCP for ESP8266/ESP32 + Copyright (C) 2020 Alexander Emelianov (a.m.emelianov@gmail.com) +*/ + +#pragma once +#if defined(ESP8266) +#include +#elif defined(ESP32) +#include +#endif + +#include "ModbusAPI.h" +#include "ModbusTCPTemplate.h" + +class WiFiServerESPWrapper : public WiFiServer { + public: + WiFiServerESPWrapper(uint16_t port) : WiFiServer(port) {} + inline WiFiClient accept() { + return available(); + } +}; + +class ModbusTCP : public ModbusAPI> { +#if defined(MODBUSIP_USE_DNS) + private: + static IPAddress resolver(const char *host) { + IPAddress remote_addr; + if (WiFi.hostByName(host, remote_addr)) + return remote_addr; + return IPADDR_NONE; + } + + public: + ModbusTCP() : ModbusAPI() { + resolve = resolver; + } +#endif +}; diff --git a/lib/modbus-esp8266/ModbusTCPTemplate.h b/lib/modbus-esp8266/ModbusTCPTemplate.h new file mode 100644 index 0000000..c78a43d --- /dev/null +++ b/lib/modbus-esp8266/ModbusTCPTemplate.h @@ -0,0 +1,606 @@ +/* + Modbus Library for Arduino + ModbusTCP general implementation + Copyright (C) 2014 Andr� Sarmento Barbosa + 2017-2020 Alexander Emelianov (a.m.emelianov@gmail.com) +*/ + +#pragma once +#include "Modbus.h" + +#define BIT_SET(a,b) ((a) |= (1ULL<<(b))) +#define BIT_CLEAR(a,b) ((a) &= ~(1ULL<<(b))) +#define BIT_CHECK(a,b) (!!((a) & (1ULL<<(b)))) // '!!' to make sure this returns 0 or 1 +#ifndef IPADDR_NONE +#define IPADDR_NONE ((uint32_t)0xffffffffUL) +#endif +// Callback function Type +#if defined(MODBUS_USE_STL) +typedef std::function cbModbusConnect; +typedef std::function cbModbusResolver; +#else +typedef bool (*cbModbusConnect)(IPAddress ip); +typedef IPAddress (*cbModbusResolver)(const char*); +#endif + +struct TTransaction { + uint16_t transactionId; + uint32_t timestamp; + cbTransaction cb = nullptr; + uint8_t* _frame = nullptr; + uint8_t* data = nullptr; + TAddress startreg; + Modbus::ResultCode forcedEvent = Modbus::EX_SUCCESS; // EX_SUCCESS means no forced event here. Forced EX_SUCCESS is not possible. + bool operator ==(const TTransaction &obj) const { + return transactionId == obj.transactionId; + } +}; + +template +class ModbusTCPTemplate : public Modbus { + protected: + union MBAP_t { + struct { + uint16_t transactionId; + uint16_t protocolId; + uint16_t length; + uint8_t unitId; + }; + uint8_t raw[7]; + }; + cbModbusConnect cbConnect = nullptr; + cbModbusConnect cbDisconnect = nullptr; + SERVER* tcpserver = nullptr; + CLIENT* tcpclient[MODBUSIP_MAX_CLIENTS]; + #if MODBUSIP_MAX_CLIENTS <= 8 + uint8_t tcpServerConnection = 0; + #elif MODBUSIP_MAX_CLIENTS <= 16 + uint16_t tcpServerConnection = 0; + #else + uint32_t tcpServerConnection = 0; + #endif + #if defined(MODBUS_USE_STL) + std::vector _trans; + #else + DArray _trans; + #endif + int16_t transactionId = 1; // Last started transaction. Increments on unsuccessful transaction start too. + int8_t n = -1; + bool autoConnectMode = false; + uint16_t serverPort = 0; + uint16_t defaultPort = MODBUSTCP_PORT; + cbModbusResolver resolve = nullptr; + TTransaction* searchTransaction(uint16_t id); + void cleanupConnections(); // Free clients if not connected + void cleanupTransactions(); // Remove timedout transactions and forced event + + int8_t getFreeClient(); // Returns free slot position + int8_t getSlave(IPAddress ip); + int8_t getMaster(IPAddress ip); + public: + uint16_t send(String host, TAddress startreg, cbTransaction cb, uint8_t unit = MODBUSIP_UNIT, uint8_t* data = nullptr, bool waitResponse = true); + uint16_t send(const char* host, TAddress startreg, cbTransaction cb, uint8_t unit = MODBUSIP_UNIT, uint8_t* data = nullptr, bool waitResponse = true); + uint16_t send(IPAddress ip, TAddress startreg, cbTransaction cb, uint8_t unit = MODBUSIP_UNIT, uint8_t* data = nullptr, bool waitResponse = true); + // Prepare and send ModbusIP frame. _frame buffer and _len should be filled with Modbus data + // ip - slave ip address + // startreg - first local register to save returned data to (miningless for write to slave operations) + // cb - transaction callback function + // unit - slave modbus unit id + // data - if not null use buffer to save returned data instead of local registers + public: + ModbusTCPTemplate(); + ~ModbusTCPTemplate(); + bool isTransaction(uint16_t id); +#if defined(MODBUSIP_USE_DNS) + bool isConnected(String host); + bool isConnected(const char* host); + bool connect(String host, uint16_t port = 0); + bool connect(const char* host, uint16_t port = 0); + bool disconnect(String host); + bool disconnect(const char* host); +#endif + bool isConnected(IPAddress ip); + bool connect(IPAddress ip, uint16_t port = 0); + bool disconnect(IPAddress ip); + // ModbusTCP + void server(uint16_t port = 0); + // ModbusTCP depricated + inline void slave(uint16_t port = 0) { server(port); } // Depricated + inline void master() { client(); } // Depricated + inline void begin() { server(); }; // Depricated + void client(); + void task(); + void onConnect(cbModbusConnect cb = nullptr); + void onDisconnect(cbModbusConnect cb = nullptr); + uint32_t eventSource() override; + void autoConnect(bool enabled = true); + void dropTransactions(); + uint16_t setTransactionId(uint16_t); + #if defined(MODBUS_USE_STL) + static IPAddress defaultResolver(const char*) {return IPADDR_NONE;} + #else + static IPAddress defaultResolver(const char*) {return IPADDR_NONE;} + #endif +}; + +template +ModbusTCPTemplate::ModbusTCPTemplate() { + //_trans.reserve(MODBUSIP_MAX_TRANSACIONS); + for (uint8_t i = 0; i < MODBUSIP_MAX_CLIENTS; i++) + tcpclient[i] = nullptr; + resolve = defaultResolver; +} + +template +void ModbusTCPTemplate::client() { + +} + +template +void ModbusTCPTemplate::server(uint16_t port) { + if (port) + serverPort = port; + else + serverPort = defaultPort; + tcpserver = new SERVER(serverPort); + tcpserver->begin(); +} + +#if defined(MODBUSIP_USE_DNS) +template +bool ModbusTCPTemplate::connect(String host, uint16_t port) { + return connect(resolve(host.c_str()), port); +} + +template +bool ModbusTCPTemplate::connect(const char* host, uint16_t port) { + return connect(resolve(host), port); +} +#endif + +template +bool ModbusTCPTemplate::connect(IPAddress ip, uint16_t port) { + //cleanupConnections(); + if (!ip) + return false; + if(getSlave(ip) != -1) + return true; + int8_t p = getFreeClient(); + if (p == -1) + return false; + tcpclient[p] = new CLIENT(); + BIT_CLEAR(tcpServerConnection, p); +#if defined(ESP32) && defined(MODBUSIP_CONNECT_TIMEOUT) + if (!tcpclient[p]->connect(ip, port?port:defaultPort, MODBUSIP_CONNECT_TIMEOUT)) { +#else + if (!tcpclient[p]->connect(ip, port?port:defaultPort)) { +#endif + delete(tcpclient[p]); + tcpclient[p] = nullptr; + return false; + } + return true; +} + +template +uint32_t ModbusTCPTemplate::eventSource() { // Returns IP of current processing client query + if (n >= 0 && n < MODBUSIP_MAX_CLIENTS && tcpclient[n]) + #if !defined(ethernet_h) + return (uint32_t)tcpclient[n]->remoteIP(); + #else + return 1; + #endif + return (uint32_t)INADDR_NONE; +} + +template +TTransaction* ModbusTCPTemplate::searchTransaction(uint16_t id) { +#define MODBUSIP_COMPARE_TRANS [id](TTransaction& trans){return trans.transactionId == id;} + #if defined(MODBUS_USE_STL) + std::vector::iterator it = std::find_if(_trans.begin(), _trans.end(), MODBUSIP_COMPARE_TRANS); + if (it != _trans.end()) return &*it; + return nullptr; + #else + return _trans.entry(_trans.find(MODBUSIP_COMPARE_TRANS)); + #endif +} + +template +void ModbusTCPTemplate::task() { + MBAP_t _MBAP; + uint32_t taskStart = millis(); + cleanupConnections(); + if (tcpserver) { + CLIENT c; + // WiFiServer.available() == Ethernet.accept() and should wrapped to get code to be compatible with Ethernet library (See ModbusTCP.h code). + // WiFiServer.available() != Ethernet.available() internally +#if defined(MODBUSIP_USE_AVAILABLE) + while (millis() - taskStart < MODBUSIP_MAX_READMS && (c = tcpserver->available())) { +#else + while (millis() - taskStart < MODBUSIP_MAX_READMS && (c = tcpserver->accept())) { +#endif +#if defined(MODBUSIP_DEBUG) + Serial.println("IP: Accepted"); +#endif + CLIENT* currentClient = new CLIENT(c); + if (!currentClient || !currentClient->connected()) { + delete currentClient; + continue; + } +#if defined(MODBUSIP_DEBUG) + Serial.println("IP: Connected"); +#endif + if (cbConnect == nullptr || cbConnect(currentClient->remoteIP())) { + #if defined(MODBUSIP_UNIQUE_CLIENTS) + // Disconnect previous connection from same IP if present + n = getMaster(currentClient->remoteIP()); + if (n != -1) { + tcpclient[n]->flush(); + delete tcpclient[n]; + tcpclient[n] = nullptr; + } + #endif + n = getFreeClient(); + if (n > -1) { + tcpclient[n] = currentClient; + BIT_SET(tcpServerConnection, n); +#if defined(MODBUSIP_DEBUG) + Serial.print("IP: Conn "); + Serial.println(n); +#endif +#if defined(MODBUSIP_USE_AVAILABLE) + break; // while +#else + continue; // while +#endif + } + } + // Close connection if callback returns false or MODBUSIP_MAX_CLIENTS reached + delete currentClient; + } + } + for (n = 0; n < MODBUSIP_MAX_CLIENTS; n++) { + if (!tcpclient[n]) continue; + if (!tcpclient[n]->connected()) continue; + while ((size_t)tcpclient[n]->available() > sizeof(_MBAP) && millis() - taskStart < MODBUSIP_MAX_READMS) { +#if defined(MODBUSIP_DEBUG) + Serial.print(n); + Serial.print(": Bytes available "); + Serial.println(tcpclient[n]->available()); +#endif + tcpclient[n]->readBytes(_MBAP.raw, sizeof(_MBAP.raw)); // Get MBAP + + if (__swap_16(_MBAP.protocolId) != 0) { // Check if MODBUSIP packet. __swap is usless there. + while (tcpclient[n]->available()) // Drop all incoming if wrong packet + tcpclient[n]->read(); + continue; + } + _len = __swap_16(_MBAP.length); + if (_len < MODBUSIP_MINFRAME) { // Length is shorter than MODBUSIP_MINFRAME + Modbus::FunctionCode fc = FC_READ_COILS; // Just placeholder + while (tcpclient[n]->available()) // Drop rest of the packet + tcpclient[n]->read(); + exceptionResponse(fc, EX_ILLEGAL_VALUE); + } + _len--; // Do not count with last byte from MBAP + if (_len > MODBUSIP_MAXFRAME) { // Length is over MODBUSIP_MAXFRAME + Modbus::FunctionCode fc = (Modbus::FunctionCode)tcpclient[n]->read(); + _len--; // Subtract for read byte + for (uint8_t i = 0; tcpclient[n]->available() && i < _len; i++) // Drop rest of the packet + tcpclient[n]->read(); + exceptionResponse(fc, EX_SLAVE_FAILURE); + } + else { + free(_frame); + _frame = (uint8_t*) malloc(_len); + if (!_frame) { + Modbus::FunctionCode fc = (Modbus::FunctionCode)tcpclient[n]->read(); + _len--; // Subtract for read byte + for (uint8_t i = 0; tcpclient[n]->available() && i < _len; i++) // Drop rest of the packet + tcpclient[n]->read(); + exceptionResponse(fc, EX_SLAVE_FAILURE); + } + else { + if (tcpclient[n]->readBytes(_frame, _len) < _len) { // Try to read MODBUS frame + exceptionResponse((Modbus::FunctionCode)_frame[0], EX_ILLEGAL_VALUE); + //while (tcpclient[n]->available()) // Drop all incoming (if any) + // tcpclient[n]->read(); + } + else { + _reply = EX_PASSTHROUGH; + // Note on _reply usage + // it's used and set as ReplyCode by slavePDU and as exceptionCode by masterPDU + if (_cbRaw) { + frame_arg_t transData = { _MBAP.unitId, tcpclient[n]->remoteIP(), __swap_16(_MBAP.transactionId), BIT_CHECK(tcpServerConnection, n) }; + _reply = _cbRaw(_frame, _len, &transData); + } + if (BIT_CHECK(tcpServerConnection, n)) { + if (_reply == EX_PASSTHROUGH) + slavePDU(_frame); // Process incoming frame as slave + else + _reply = REPLY_OFF; + } + else { + // Process reply to master request + TTransaction* trans = searchTransaction(__swap_16(_MBAP.transactionId)); + if (trans) { // if valid transaction id + if ((_frame[0] & 0x7F) == trans->_frame[0]) { // Check if function code the same as requested + if (_reply == EX_PASSTHROUGH) + masterPDU(_frame, trans->_frame, trans->startreg, trans->data); // Process incoming frame as master + } + else { + _reply = EX_UNEXPECTED_RESPONSE; + } + if (trans->cb) { + trans->cb((ResultCode)_reply, trans->transactionId, nullptr); + } + free(trans->_frame); + #if defined(MODBUS_USE_STL) + //_trans.erase(std::remove(_trans.begin(), _trans.end(), *trans), _trans.end() ); + std::vector::iterator it = std::find(_trans.begin(), _trans.end(), *trans); + if (it != _trans.end()) + _trans.erase(it); + #else + size_t r = _trans.find([trans](TTransaction& t){return *trans == t;}); + _trans.remove(r); + #endif + } + } + } + } + } + if (!BIT_CHECK(tcpServerConnection, n)) _reply = REPLY_OFF; // No replay if it was responce to master + if (_reply != REPLY_OFF) { + _MBAP.length = __swap_16(_len+1); // _len+1 for last byte from MBAP + size_t send_len = (uint16_t)_len + sizeof(_MBAP.raw); + uint8_t sbuf[send_len]; + memcpy(sbuf, _MBAP.raw, sizeof(_MBAP.raw)); + memcpy(sbuf + sizeof(_MBAP.raw), _frame, _len); + tcpclient[n]->write(sbuf, send_len); + //tcpclient[n]->flush(); + } + if (_frame) { + free(_frame); + _frame = nullptr; + } + _len = 0; + } + } + n = -1; + cleanupTransactions(); +} + +template +uint16_t ModbusTCPTemplate::send(String host, TAddress startreg, cbTransaction cb, uint8_t unit, uint8_t* data, bool waitResponse) { + return send(resolve(host.c_str()), startreg, cb, unit, data, waitResponse); +} + +template +uint16_t ModbusTCPTemplate::send(const char* host, TAddress startreg, cbTransaction cb, uint8_t unit, uint8_t* data, bool waitResponse) { + return send(resolve(host), startreg, cb, unit, data, waitResponse); +} + +template +uint16_t ModbusTCPTemplate::send(IPAddress ip, TAddress startreg, cbTransaction cb, uint8_t unit, uint8_t* data, bool waitResponse) { + MBAP_t _MBAP; + uint16_t result = 0; + int8_t p; +#if defined(MODBUSIP_MAX_TRANSACTIONS) + if (_trans.size() >= MODBUSIP_MAX_TRANSACTIONS) + goto cleanup; +#endif + if (!ip) + return 0; + if (tcpserver) { + p = getMaster(ip); + } else { + p = getSlave(ip); + } + if (p == -1 || !tcpclient[p]->connected()) { + if (!autoConnectMode) + goto cleanup; + if (!connect(ip)) + goto cleanup; + } + _MBAP.transactionId = __swap_16(transactionId); + _MBAP.protocolId = __swap_16(0); + _MBAP.length = __swap_16(_len+1); //_len+1 for last byte from MBAP + _MBAP.unitId = unit; + bool writeResult; + { // for sbuf isolation + size_t send_len = _len + sizeof(_MBAP.raw); + uint8_t sbuf[send_len]; + memcpy(sbuf, _MBAP.raw, sizeof(_MBAP.raw)); + memcpy(sbuf + sizeof(_MBAP.raw), _frame, _len); + writeResult = (tcpclient[p]->write(sbuf, send_len) == send_len); + } + if (!writeResult) + goto cleanup; + //tcpclient[p]->flush(); + if (waitResponse) { + TTransaction tmp; + tmp.transactionId = transactionId; + tmp.timestamp = millis(); + tmp.cb = cb; + tmp.data = data; // BUG: Should data be saved? It may lead to memory leak or double free. + tmp._frame = _frame; + tmp.startreg = startreg; + _trans.push_back(tmp); + _frame = nullptr; + } + result = transactionId; + transactionId++; + if (!transactionId) + transactionId = 1; + cleanup: + free(_frame); + _frame = nullptr; + _len = 0; + return result; +} + +template +void ModbusTCPTemplate::onConnect(cbModbusConnect cb) { + cbConnect = cb; +} + +template +void ModbusTCPTemplate::onDisconnect(cbModbusConnect cb) { + cbDisconnect = cb; +} + +template +void ModbusTCPTemplate::cleanupConnections() { + for (uint8_t i = 0; i < MODBUSIP_MAX_CLIENTS; i++) { + if (tcpclient[i] && !tcpclient[i]->connected()) { + //IPAddress ip = tcpclient[i]->remoteIP(); + tcpclient[i]->stop(); + delete tcpclient[i]; + tcpclient[i] = nullptr; + if (cbDisconnect && cbEnabled) + cbDisconnect(IPADDR_NONE); + } + } +} + +template +void ModbusTCPTemplate::cleanupTransactions() { + #if defined(MODBUS_USE_STL) + for (auto it = _trans.begin(); it != _trans.end();) { + if (millis() - it->timestamp > MODBUSIP_TIMEOUT || it->forcedEvent != Modbus::EX_SUCCESS) { + Modbus::ResultCode res = (it->forcedEvent != Modbus::EX_SUCCESS)?it->forcedEvent:Modbus::EX_TIMEOUT; + if (it->cb) + it->cb(res, it->transactionId, nullptr); + free(it->_frame); + it = _trans.erase(it); + } else + it++; + } + #else + size_t i = 0; + while (i < _trans.size()) { + TTransaction t = _trans[i]; + if (millis() - t.timestamp > MODBUSIP_TIMEOUT || t.forcedEvent != Modbus::EX_SUCCESS) { + Modbus::ResultCode res = (t.forcedEvent != Modbus::EX_SUCCESS)?t.forcedEvent:Modbus::EX_TIMEOUT; + if (t.cb) + t.cb(res, t.transactionId, nullptr); + free(t._frame); + _trans.remove(i); + } else + i++; + } + #endif +} + +template +int8_t ModbusTCPTemplate::getFreeClient() { + for (uint8_t i = 0; i < MODBUSIP_MAX_CLIENTS; i++) + if (!tcpclient[i]) + return i; + return -1; +} + +template +int8_t ModbusTCPTemplate::getSlave(IPAddress ip) { + for (uint8_t i = 0; i < MODBUSIP_MAX_CLIENTS; i++) + if (tcpclient[i] && tcpclient[i]->connected() && tcpclient[i]->remoteIP() == ip && !BIT_CHECK(tcpServerConnection, i)) + return i; + return -1; +} + +template +int8_t ModbusTCPTemplate::getMaster(IPAddress ip) { + for (uint8_t i = 0; i < MODBUSIP_MAX_CLIENTS; i++) + if (tcpclient[i] && tcpclient[i]->connected() && tcpclient[i]->remoteIP() == ip && BIT_CHECK(tcpServerConnection, i)) + return i; + return -1; +} + +template +bool ModbusTCPTemplate::isTransaction(uint16_t id) { + return searchTransaction(id) != nullptr; +} +#if defined(MODBUSIP_USE_DNS) +template +bool ModbusTCPTemplate::isConnected(String host) { + return isConnected(resolve(host.c_str())); +} + +template +bool ModbusTCPTemplate::isConnected(const char* host) { + return isConnected(resolve(host)); +} +#endif + +template +bool ModbusTCPTemplate::isConnected(IPAddress ip) { + if (!ip) + return false; + int8_t p = getSlave(ip); + return p != -1 && tcpclient[p]->connected(); +} + +template +void ModbusTCPTemplate::autoConnect(bool enabled) { + autoConnectMode = enabled; +} + +#if defined(MODBUSIP_USE_DNS) +template +bool ModbusTCPTemplate::disconnect(String host) { + return disconnect(resolve(host.c_str())); +} + +template +bool ModbusTCPTemplate::disconnect(const char* host) { + return disconnect(resolve(host)); +} +#endif + +template +bool ModbusTCPTemplate::disconnect(IPAddress ip) { + if (!ip) + return false; + int8_t p = getSlave(ip); + if (p != -1) { + tcpclient[p]->stop(); + delete tcpclient[p]; + tcpclient[p] = nullptr; + return true; + } + return false; +} + +template +void ModbusTCPTemplate::dropTransactions() { + #if defined(MODBUS_USE_STL) + for (auto &t : _trans) t.forcedEvent = EX_CANCEL; + #else + for (size_t i = 0; i < _trans.size(); i++) + _trans.entry(i)->forcedEvent = EX_CANCEL; + #endif +} + +template +ModbusTCPTemplate::~ModbusTCPTemplate() { + free(_frame); + _frame = nullptr; + dropTransactions(); + cleanupConnections(); + cleanupTransactions(); + delete tcpserver; + tcpserver = nullptr; + for (uint8_t i = 0; i < MODBUSIP_MAX_CLIENTS; i++) { + delete tcpclient[i]; + tcpclient[i] = nullptr; + } +} + +template +uint16_t ModbusTCPTemplate::setTransactionId(uint16_t t) { + transactionId = t; + if (!transactionId) + transactionId = 1; + return transactionId; +} + diff --git a/lib/modbus-esp8266/ModbusTLS.h b/lib/modbus-esp8266/ModbusTLS.h new file mode 100644 index 0000000..7f0fdf8 --- /dev/null +++ b/lib/modbus-esp8266/ModbusTLS.h @@ -0,0 +1,120 @@ +/* + Modbus Library for Arduino + ModbusTLS - ModbusTCP Security for ESP8266 + Copyright (C) 2020 Alexander Emelianov (a.m.emelianov@gmail.com) +*/ +#pragma once +#if !defined(ESP8266) && !defined(ESP32) +#error Unsupported architecture +#endif +#include +#if defined(ESP8266) +#include +#else +// Just emty stub +class WiFiServerSecure { +public: + WiFiServerSecure(uint16_t){} + WiFiClientSecure available(){} + void begin(); + inline WiFiClientSecure accept() { + return available(); + } +}; +#endif +#include "ModbusTCPTemplate.h" +#include "ModbusAPI.h" + +class ModbusTLS : public ModbusAPI> { + private: + int8_t _connect(IPAddress ip, uint16_t port, const char* client_cert = nullptr, const char* client_private_key = nullptr) { + int8_t p = getFreeClient(); + if (p < 0) + return p; + tcpclient[p] = new WiFiClientSecure(); + BIT_CLEAR(tcpServerConnection, p); + #if defined(ESP8266) + BearSSL::X509List *clientCertList = new BearSSL::X509List(client_cert); + BearSSL::PrivateKey *clientPrivKey = new BearSSL::PrivateKey(client_private_key); + tcpclient[p]->setClientRSACert(clientCertList, clientPrivKey); + tcpclient[p]->setBufferSizes(512, 512); + #else + tcpclient[p]->setCertificate(client_cert); + tcpclient[p]->setPrivateKey(client_private_key); + #endif + return p; + } +#if defined(MODBUSIP_USE_DNS) + static IPAddress resolver (const char* host) { + IPAddress remote_addr; + if (WiFi.hostByName(host, remote_addr)) + return remote_addr; + return IPADDR_NONE; + } +#endif + public: + ModbusTLS() : ModbusAPI() { + defaultPort = MODBUSTLS_PORT; +#if defined(MODBUSIP_USE_DNS) + resolve = resolver; +#endif + } + #if defined(ESP8266) + void server(uint16_t port, const char* server_cert = nullptr, const char* server_private_key = nullptr, const char* ca_cert = nullptr) { + serverPort = port; + tcpserver = new WiFiServerSecure(serverPort); + BearSSL::X509List *serverCertList = new BearSSL::X509List(server_cert); + BearSSL::PrivateKey *serverPrivKey = new BearSSL::PrivateKey(server_private_key); + tcpserver->setRSACert(serverCertList, serverPrivKey); + if (ca_cert) { + BearSSL::X509List *trustedCA = new BearSSL::X509List(ca_cert); + tcpserver->setClientTrustAnchor(trustedCA); + } + //tcpserver->setBufferSizes(512, 512); + tcpserver->begin(); + } + + bool connectWithKnownKey(IPAddress ip, uint16_t port, const char* client_cert = nullptr, const char* client_private_key = nullptr, const char* key = nullptr) { + if(getSlave(ip) >= 0) + return true; + int8_t p = _connect(ip, port, client_cert, client_private_key); + BearSSL::PublicKey *clientPublicKey = new BearSSL::PublicKey(key); + tcpclient[p]->setKnownKey(clientPublicKey); + return tcpclient[p]->connect(ip, port); + } + + #endif +#if defined(MODBUSIP_USE_DNS) + bool connect(String host, uint16_t port, const char* client_cert = nullptr, const char* client_private_key = nullptr, const char* ca_cert = nullptr) { + return connect(resolver(host.c_str()), port, client_cert, client_private_key, ca_cert); + } + bool connect(const char* host, uint16_t port, const char* client_cert = nullptr, const char* client_private_key = nullptr, const char* ca_cert = nullptr) { + return connect(resolver(host), port, client_cert, client_private_key, ca_cert); + } +#endif + bool connect(IPAddress ip, uint16_t port, const char* client_cert = nullptr, const char* client_private_key = nullptr, const char* ca_cert = nullptr) { + if (!ip) + return false; + if(getSlave(ip) >= 0) + return false; + int8_t p = _connect(ip, port, client_cert, client_private_key); + if (p < 0) + return false; + #if defined(ESP8266) + if (ca_cert) { + BearSSL::X509List *trustedCA = new BearSSL::X509List(ca_cert); + tcpclient[p]->setTrustAnchors(trustedCA); + } else { + tcpclient[p]->setInsecure(); + } + #else + if (ca_cert) { + tcpclient[p]->setCACert(ca_cert); + } + #endif + //return tcpclient[p]->connect(ip, port); + if (!tcpclient[p]->connect(ip, port)) + return false; + return true; + } +}; diff --git a/lib/modbus-esp8266/darray.h b/lib/modbus-esp8266/darray.h new file mode 100644 index 0000000..0b204fb --- /dev/null +++ b/lib/modbus-esp8266/darray.h @@ -0,0 +1,70 @@ + +/* + Very Basic Dynamic Array + + Copyright (C) 2020 Alexander Emelianov (a.m.emelianov@gmail.com) + https://github.com/emelianov/modbus-esp8266 + This code is licensed under the BSD New License. See LICENSE.txt for more info. +*/ +template +class DArray { + public: + typedef bool (*Compare)(T); + T* data = nullptr; + size_t resSize = 0; + size_t last = 0; + bool isEmpty = true; + DArray(size_t i = SIZE) { + data = (T*)malloc(i * sizeof(T)); + if (data) resSize = i; + } + size_t push_back(const T& v) { + if (!data) { + data = (T*)malloc(resSize * sizeof(T)); + if (!data) return 1; + } + if (last >= resSize - 1) { + if (INCREMENT == 0) return last + 1; + void* tmp = realloc(data, (resSize + INCREMENT) * sizeof(T)); + if (!tmp) return last + 1; + resSize += INCREMENT; + data = (T*)tmp; + } + if (!isEmpty) + last++; + else + isEmpty = false; + data[last] = v; + return last; + } + size_t size() { + if (isEmpty) return 0; + return last + 1; + } + template + size_t find(UnaryPredicate func, size_t i = 0) { + if (isEmpty) return 1; + for (; i <= last; i++) + if (func(data[i])) break; + return i; + } + + void remove(size_t i) { + if (isEmpty) return; + if (i > last) return; + if (last == 0) { + isEmpty = true; + return; + } + if (i < last) + memcpy(&data[i], &data[i + 1], (last - i) * sizeof(T)); + last --; + } + T operator[](int i) { + return data[i]; + } + T* entry(size_t i) { + if (i > last) return nullptr; + return &data[i]; + } +}; \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..b230a01 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,32 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +default_envs = CRAH_PAHHC_600_C6_TCP + +[common_env_options] +framework = arduino +monitor_speed = 115200 +lib_ldf_mode = chain+ +lib_compat_mode = soft + +[env:CH_Daikin_AWV026B_RTU] +platform = espressif32 +board = dfrobot_firebeetle2_esp32e +extends = common_env_options +build_src_filter = -<*> + + +[env:CRAH_PAHHC_600_C6_TCP] +platform = espressif32 +board = dfrobot_firebeetle2_esp32e +extends = common_env_options +build_flags = + -D USE_MODBUS_IP +build_src_filter = -<*> + diff --git a/src/CH_Daikin_AWV026B_RTU/State_Fail.cpp b/src/CH_Daikin_AWV026B_RTU/State_Fail.cpp new file mode 100644 index 0000000..f9d5820 --- /dev/null +++ b/src/CH_Daikin_AWV026B_RTU/State_Fail.cpp @@ -0,0 +1,90 @@ +/** + * @file State_Fail.cpp + * @brief Implementation of the FailState class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the implementation for the FailState, which defines + * the behavior of the equipment when it has entered a fault condition. + */ +#include "States/State_Fail.h" +#include "States/State_Standby.h" + +#include "Categories/ModbusPoint.h" +#include "Equipment/Equipment.h" +#include "Strategies/Strategy_SingleValue.h" +#include "Strategies/Strategy_PID.h" + +#include +#include + +#if defined(USE_MODBUS_IP) + #include +#else + #include +#endif + +/** + * @brief Constructs a new FailState object. + * + * This constructor initializes behavior strategies to simulate a failure + * scenario. In this example, it sets a common alarm bit, triggers a specific + * alarm for "EC Fan #1", and ramps down all fan speeds to zero. + */ +template<> +FailState::FailState(const std::vector& activeAlarms) { + // Simulate a failure: set common alarm and a specific fan alarm. + + for (const auto& alarmName : activeAlarms){ + addStrategy(alarmName, new SingleValueStrategy(1.0f, 0.0f, 1000)); + } + addStrategy("CW Valve Position", new PIDStrategy("RAT Setpoint", 1000, "RAT")); +} + +/** + * @brief Executes the fail state's logic for one update cycle. + * + * This method checks the "State Control" Modbus point for a command to + * transition back to Standby, which would typically happen after a fault + * is cleared. If no transition is requested, it applies the failure + * strategies (e.g., keeping fans off and alarms active). + * + * @param equipment Pointer to the Equipment instance. + * @return A pointer to a new State if a transition should occur, otherwise nullptr. + */ +template<> +State* FailState::update(Equipment* equipment) { + // STATE control, add conditions if change to a different state is needed + Serial.println("Fail update function"); + ModbusPoint* alarmReset = equipment->getModbusPoint("Alarm Reset"); + int nextStateId = alarmReset ? alarmReset->getValue() : 0; + if (nextStateId == 1){ + return new StandbyState(); + } + _applyStrategies(equipment); + return nullptr; +} + +/** + * @brief Logic to execute once when entering the fail state. + * @param equipment Pointer to the Equipment instance (unused in this implementation). + */ +template<> +void FailState::enterState(Equipment* equipment) { + // Logic to run when the equipment enters this state + Serial.println("Enter Fail State..."); + ModbusPoint* alarm_common = equipment->getModbusPoint("Alarm Common"); + alarm_common->setValue(1); +} + +/** + * @brief Logic to execute once when exiting the fail state. + * @param equipment Pointer to the Equipment instance (unused in this implementation). + */ +template<> +void FailState::exitState(Equipment* equipment) { + // Cleanup logic to run when the equipment leaves this state + Serial.println("Exit Fail State..."); + ModbusPoint* alarm_common = equipment->getModbusPoint("Alarm Common"); + alarm_common->setValue(0); +} \ No newline at end of file diff --git a/src/CH_Daikin_AWV026B_RTU/State_Running.cpp b/src/CH_Daikin_AWV026B_RTU/State_Running.cpp new file mode 100644 index 0000000..192ac8c --- /dev/null +++ b/src/CH_Daikin_AWV026B_RTU/State_Running.cpp @@ -0,0 +1,167 @@ +/** + * @file State_Running.cpp + * @brief Implementation of the RunningState class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the implementation for the RunningState, which defines + * the behavior of the equipment when it is actively running. + */ +#include "States/State_Running.h" +#include "States/State_Standby.h" +#include "States/State_Fail.h" +#include "Strategies/Strategy_Behavior.h" +#include "Strategies/Strategy_PID.h" +#include "Strategies/Strategy_Totalizer.h" +#include "Equipment/Equipment.h" +#include "Categories/ModbusPoint.h" +#include "Categories/ModbusFloatDecorator.h" + +#include +#include + +#if defined(USE_MODBUS_IP) + #include +#else + #include +#endif + +/** + * @brief Constructs a new RunningState object. + * + * This constructor initializes the behavior strategies for various Modbus points + * that are active during the running state. For example, it sets different + * dynamic behaviors for the speeds of EC fans 1 through 5. + */ +template<> +RunningState::RunningState() { + addStrategy("CW Valve Position", new PIDStrategy("RAT Setpoint", 1000, "RAT")); + addStrategy("Operating Hours EC Fan #1", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #2", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #3", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #4", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #5", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #6", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #7", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #8", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #9", new TotalizerStrategy(10000)); +} + +/** + * @brief Executes the running state's logic for one update cycle. + * + * This method checks the "State Control" Modbus point for a command to + * transition to a different state (e.g., back to Standby). If no transition + * is requested, it applies the strategies defined for the running state. + * + * @param equipment Pointer to the Equipment instance. + * @return A pointer to a new State if a transition should occur, otherwise nullptr. + */ +template<> +State* RunningState::update(Equipment* equipment) { + // STATE control, add conditions if change to a different state is needed + Serial.println("Running update function"); + ModbusPoint* On_Off_Command = equipment->getModbusPoint("ON/OFF Command By BMS"); + int nextStateId = On_Off_Command ? On_Off_Command->getValue() : 0; + Serial.println(nextStateId); + if (nextStateId == 0){ + return new StandbyState(); + } + + ModbusPoint* faultCode = equipment->getModbusPoint("Fault Code"); + int faultCodeValue = faultCode ? faultCode->getValue() : 0; + switch (faultCodeValue){ + case 1: + return new FailState({"Alarm SAT Sensor Fault"}); + case 2: + return new FailState({"Alarm RAH Sensor Fault"}); + case 3: + return new FailState({"Alarm RAT Sensor Fault"}); + case 4: + return new FailState({"Alarm Filter DP Sensor Fault"}); + case 5: + return new FailState({"Alarm Flooding"}); + case 6: + return new FailState({"Alarm Dirty Filter"}); + case 7: + return new FailState({"Alarm High RAT"}); + case 8: + return new FailState({"Alarm Low RAT"}); + case 9: + return new FailState({"Alarm High SAT"}); + case 10: + return new FailState({"Alarm Low SAT"}); + case 11: + return new FailState({"Alarm High RAH"}); + case 12: + return new FailState({"Alarm Low RAH"}); + case 13: + return new FailState({"Alarm Phase Failure"}); + case 14: + return new FailState({"Alarm Condensate Pump"}); + case 15: + return new FailState({"Alarm Smoke"}); + case 16: + return new FailState({"Alarm Fire"}); + case 17: + return new FailState({"Alarm EC Fan #1"}); + case 18: + return new FailState({"Alarm EC Fan #2"}); + case 19: + return new FailState({"Alarm EC Fan #3"}); + case 20: + return new FailState({"Alarm EC Fan #4"}); + case 21: + return new FailState({"Alarm EC Fan #5"}); + case 22: + return new FailState({"Alarm EC Fan #6"}); + case 23: + return new FailState({"Alarm EC Fan #7"}); + case 24: + return new FailState({"Alarm EC Fan #8"}); + case 25: + return new FailState({"Alarm EC Fan #9"}); + default: + break; + } + ModbusPoint* point = equipment->getModbusPoint("Setting the EC Fan Max Speed"); + point->setValue(45); + // Apply any strategies defined for the standby state + _applyStrategies(equipment); + return nullptr; +} + +/** + * @brief Logic to execute once when entering the running state. + * @param equipment Pointer to the Equipment instance (unused in this implementation). + */ +template<> +void RunningState::enterState(Equipment* 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 motorStatusDescriptions = { + "Run Status EC Fan #1", "Run Status EC Fan #2", "Run Status EC Fan #3", + "Run Status EC Fan #4", "Run Status EC Fan #5", "Run Status EC Fan #6", + "Run Status EC Fan #7", "Run Status EC Fan #8", "Run Status EC Fan #9" + }; + + // Loop through and set all motor statuses to 0 + for (const auto& desc : motorStatusDescriptions) { + ModbusPoint* point = equipment->getModbusPoint(desc); + if (point) { + point->setValue(1); + } + } +} + +/** + * @brief Logic to execute once when exiting the running state. + * @param equipment Pointer to the Equipment instance (unused in this implementation). + */ +template<> +void RunningState::exitState(Equipment* equipment) { + // Cleanup logic to run when the equipment leaves this state + Serial.println("Exit Running State..."); +} \ No newline at end of file diff --git a/src/CH_Daikin_AWV026B_RTU/State_Standby.cpp b/src/CH_Daikin_AWV026B_RTU/State_Standby.cpp new file mode 100644 index 0000000..7083ad0 --- /dev/null +++ b/src/CH_Daikin_AWV026B_RTU/State_Standby.cpp @@ -0,0 +1,106 @@ +/** + * @file State_Standby.cpp + * @brief Implementation of the StandbyState class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * 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 "States/State_Standby.h" +#include "Categories/ModbusPoint.h" +#include "Categories/ModbusFloatDecorator.h" +#include "Equipment/Equipment.h" +#include "Strategies/Strategy_Random.h" + +#include +#include +#if defined(USE_MODBUS_IP) + #include +#else + #include +#endif + +/** + * @brief Constructs a new StandbyState object. + * + * In this state, the equipment is idle. This constructor can be used to + * define specific behaviors for Modbus points that should occur during standby, + * such as setting fan speeds to zero. + */ +template<> +StandbyState::StandbyState() { + // You can add initialization code here if needed + /* + addStrategy("SAT Reading", new SingleValueStrategy(100.0f, 0.1f, 1000)); + addStrategy("CW Valve Position", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #1", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #2", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #3", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #4", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #5", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #6", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #7", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #8", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #9", new RampStrategy(0.0f, 5.0f, 1000)); + */ + addStrategy("Chiller Local-Network", new RandomStrategy(1000)); + addStrategy("Chiller Enable Output", new RandomStrategy(1000)); + addStrategy("Run Enabled", new RandomStrategy(1000)); + addStrategy("Chiller Capacity Limited", new RandomStrategy(1000)); + addStrategy("Alm Digital Output", new RandomStrategy(1000)); + +} + +/** + * @brief Executes the running state's logic for one update cycle. + * + * This method checks the "State Control" Modbus point for a command to + * transition to a different state (e.g., back to Standby). If no transition + * is requested, it applies the strategies defined for the running state. + * + * @param equipment Pointer to the Equipment instance. + * @return A pointer to a new State if a transition should occur, otherwise nullptr. + */ +template<> +State* StandbyState::update(Equipment* equipment) { + // STATE control, add conditions if change to a different state is needed + Serial.println("Standby update function"); + /* + ModbusPoint* On_Off_Command = equipment->getModbusPoint("ON/OFF Command By BMS"); + int nextStateId = On_Off_Command ? On_Off_Command->getValue() : 0; + Serial.println(nextStateId); + if (nextStateId == 1){ + return new RunningState(); + } + */ + // 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::enterState(Equipment* equipment) { + // Logic to run when the equipment enters this state + // A list of all alarm descriptions + Serial.println("Enter Standby State..."); + + + int CH_ON_OFF = getPointValue(equipment, "Chiller On-Off"); + int Ch_Sts = getPointValue(equipment, "Chiller Sts"); + +} + +/** + * @brief Logic to execute once when exiting the standby state. + * @param equipment Pointer to the Equipment instance. + */ +template<> +void StandbyState::exitState(Equipment* equipment) { + // Cleanup logic to run when the equipment leaves this state + Serial.println("Exit Standby State..."); +} \ No newline at end of file diff --git a/src/CH_Daikin_AWV026B_RTU/config.h b/src/CH_Daikin_AWV026B_RTU/config.h new file mode 100644 index 0000000..9cccbaa --- /dev/null +++ b/src/CH_Daikin_AWV026B_RTU/config.h @@ -0,0 +1,99 @@ +/** + * @file config.h + * @brief Main configuration file for the Equipment emulator. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-02 + * + * This file contains two important configurations: WiFi network parameters + * and the Modbus register map for the device. + */ + +#ifndef CONFIG_H +#define CONFIG_H +#include +#include "core.h" +#include "Equipment/Equipment.h" + +/** + * @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[] = +{ + {HR, 100, 0, "State Control"}, //Internal to control from Modscan + {HR, 101, 0, "Fault Code"}, + {HR_FLOAT, 102, 0, "Supply Temp"}, + {HR, 0, 0, "Chiller Local-Network"}, + {HR, 1, 0, "Chiller Enable Output"}, + {HR, 2, 0, "Run Enabled"}, + {HR, 3, 0, "Chiller Capacity Limited"}, + {HR, 4, 0, "Alm Digital Output"}, + {HR, 6, 0, "Evap Flow Switch Sts"}, + {HR, 7, 0, "Cond Flow Switch Sts"}, + {HR, 8, 0, "Chiller On-Off"}, + {HR, 9, 0, "Chiller Enable SP"}, + {HR, 10, 0, "Clear Alm"}, + {HR, 11, 0, "Chiller Mode Output"}, + {HR_10x, 12, 0, "Active SP"}, + {HR_10x, 13, 0, "Actual Capacity"}, + {HR_10x, 14, 0, "Active Capacity Limit"}, + {HR, 15, 0, "Chiller Sts"}, + {HR_10x, 16, 0, "Evap Entering Fluid Temp"}, + {HR_10x, 17, 0, "Evap Leaving Fluid Temp"}, + {HR, 18, 0, "Evap Fluid Flow Rate"}, + {HR_10x, 19, 0, "Cond Entering Fluid Temp"}, + {HR_10x, 20, 0, "Cond Leaving Fluid Temp"}, + {HR, 21, 0, "Cond Fluid Flow Rate"}, + {HR_10x, 24, 0, "Outdoor Air Temp"}, + {HR, 25, 0, "Chiller Current"}, + {HR, 27, 0, "Total Kw"}, + {HR, 28, 0, "Warning Alm Idx"}, + {HR, 29, 0, "Problem Alm Idx"}, + {HR, 30, 0, "Fault Alm Idx"}, + {HR, 31, 0, "Warning Alm Code"}, + {HR, 32, 0, "Problem Alm Code"}, + {HR, 33, 0, "Fault Alm Code"}, + {HR, 34, 0, "Chiller Mode SP"}, + {HR_10x, 35, 0, "Cool SP - Network"}, + {HR_10x, 36, 0, "Ice SP"}, + {HR_10x, 38, 0, "Capacity Limit SP"}, + {HR_10x, 39, 0, "Cond Refrig Pressure"}, + {HR_10x, 40, 0, "Cond Saturated Refrig Temp"}, + {HR_10x, 41, 0, "Evap Refrig Pressure"}, + {HR_10x, 42, 0, "Evap Saturated Refrig Temp"}, + {HR, 65, 0, "Comp Suction Refrig Temp"}, + {HR_10x, 68, 0, "Comp Discharge Refrig Temp"}, + {HR, 69, 0, "Comp1 Percent RLA"}, + {HR, 70, 0, "Comp1 Current"}, + {HR, 71, 0, "Comp Voltage"}, + {HR, 72, 0, "Comp Power"}, + {HR, 73, 0, "Comp Starts"}, + {HR, 74, 0, "Comp Run Hours"}, + {HR, 75, 0, "Comp Run Hours"}, + {HR, 82, 0, "Comp2 Percent RLA"}, + {HR, 303, 0, "Evap Pump Run Hours"}, + {HR, 304, 0, "Evap Pump Run Hours"}, + {HR, 305, 0, "Evap Pump Sts"}, + {HR, 316, 0, "Units"}, + {HR, 317, 0, "Chiller Model"}, + {HR, 1849, 0, "Oil Feed Pessure"}, + {HR, 1854, 0, "Wtrside Econo State"}, + {HR, 1855, 0, "Wtrside Econo En SP"}, + +}; +//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; + + +ModbusRTU mb; +#endif // CONFIG_H diff --git a/src/CH_Daikin_AWV026B_RTU/main.cpp b/src/CH_Daikin_AWV026B_RTU/main.cpp new file mode 100644 index 0000000..5526236 --- /dev/null +++ b/src/CH_Daikin_AWV026B_RTU/main.cpp @@ -0,0 +1,79 @@ +/** + * @file BaseEmulator.ino + * @brief Main execution program for the Arduino Emulator. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-02 + * + * @details This file contains the main execution program for an Arduino-based emulator of a equipment 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 IP server. + * - Modbus points (Coils, Holding Registers, etc.) based on a predefined map in config.h. + * + * The loop() function continuously: + * - Services the Modbus IP server. + * - Reads values from the Modbus server into internal data structures. + * - Updates the state of the emulated equipment. + * - Writes updated values back to the Modbus server. + * + * @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 different value generation strategies. + * @see ModbusPoint.h for the base class for all Modbus points. + */ +//================================================================================================================================= +//Libraries and declaration of variables. +#include +#include "config.h" +#include "Categories/ModbusPointFactory.h" + +//================================================================================================================================= +/** + * @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`. + */ +const int rtsPin = 4; +void setup() { + Serial.begin(115200); + Serial.println("Setup function started"); + + Serial2.begin(19200, SERIAL_8N1, 17, 16); + mb.begin(&Serial2, 4); // Start the server + mb.slave(1); // Set the slave ID + + for(int i = 0; i < map_size; i++){ + ModbusPoint* point = createModbusPoint(&mb, mb_map[i].category, mb_map[i].address, mb_map[i].value, mb_map[i].description); + if (point) { + point->addToModbusServer(); + EquipmentInstance.addModbusPoint(mb_map[i].description, point); + } + } + Serial.println("Setup function ended"); +} +//================================================================================================================================= +/** + * @brief The main application loop. + * @details This function runs repeatedly after setup() has completed. It performs the following actions in order: + * 1. Services the Modbus server by calling `mb.task()`. + * 2. Reads the current values from the Modbus registers into the `ModbusPoint` objects by calling `readRegisters()`. + * 3. After a specified interval, it updates the equipment's state by calling `EquipmentInstance.update()`. + * 4. Writes any changed values from the `ModbusPoint` objects back to the Modbus registers by calling `writeRegisters()`. + */ +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); + } +} diff --git a/src/CRAH_PAHHC_600_C6_TCP/State_Fail.cpp b/src/CRAH_PAHHC_600_C6_TCP/State_Fail.cpp new file mode 100644 index 0000000..87c27f8 --- /dev/null +++ b/src/CRAH_PAHHC_600_C6_TCP/State_Fail.cpp @@ -0,0 +1,87 @@ +/** + * @file State_Fail.cpp + * @brief Implementation of the FailState class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the implementation for the FailState, which defines + * the behavior of the equipment when it has entered a fault condition. + */ +#include "Categories/ModbusPoint.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" +#if defined(USE_MODBUS_IP) + #include +#else + #include +#endif + +/** + * @brief Constructs a new FailState object. + * + * This constructor initializes behavior strategies to simulate a failure + * scenario. In this example, it sets a common alarm bit, triggers a specific + * alarm for "EC Fan #1", and ramps down all fan speeds to zero. + */ +template<> +FailState::FailState(const std::vector& activeAlarms) { + // Simulate a failure: set common alarm and a specific fan alarm. + + for (const auto& alarmName : activeAlarms){ + addStrategy(alarmName, new SingleValueStrategy(1.0f, 0.0f, 1000)); + } + addStrategy("CW Valve Position", new PIDStrategy("RAT Setpoint", 1000, "RAT")); +} + +/** + * @brief Executes the fail state's logic for one update cycle. + * + * This method checks the "State Control" Modbus point for a command to + * transition back to Standby, which would typically happen after a fault + * is cleared. If no transition is requested, it applies the failure + * strategies (e.g., keeping fans off and alarms active). + * + * @param equipment Pointer to the Equipment instance. + * @return A pointer to a new State if a transition should occur, otherwise nullptr. + */ +template<> +State* FailState::update(Equipment* equipment) { + // STATE control, add conditions if change to a different state is needed + Serial.println("Fail update function"); + ModbusPoint* alarmReset = equipment->getModbusPoint("Alarm Reset"); + int nextStateId = alarmReset ? alarmReset->getValue() : 0; + if (nextStateId == 1){ + return new StandbyState(); + } + _applyStrategies(equipment); + return nullptr; +} + +/** + * @brief Logic to execute once when entering the fail state. + * @param equipment Pointer to the Equipment instance (unused in this implementation). + */ +template<> +void FailState::enterState(Equipment* equipment) { + // Logic to run when the equipment enters this state + Serial.println("Enter Fail State..."); + ModbusPoint* alarm_common = equipment->getModbusPoint("Alarm Common"); + alarm_common->setValue(1); +} + +/** + * @brief Logic to execute once when exiting the fail state. + * @param equipment Pointer to the Equipment instance (unused in this implementation). + */ +template<> +void FailState::exitState(Equipment* equipment) { + // Cleanup logic to run when the equipment leaves this state + Serial.println("Exit Fail State..."); + ModbusPoint* alarm_common = equipment->getModbusPoint("Alarm Common"); + alarm_common->setValue(0); +} \ No newline at end of file diff --git a/src/CRAH_PAHHC_600_C6_TCP/State_Running.cpp b/src/CRAH_PAHHC_600_C6_TCP/State_Running.cpp new file mode 100644 index 0000000..083972b --- /dev/null +++ b/src/CRAH_PAHHC_600_C6_TCP/State_Running.cpp @@ -0,0 +1,169 @@ +/** + * @file State_Running.cpp + * @brief Implementation of the RunningState class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * This file contains the implementation for the RunningState, which defines + * the behavior of the equipment when it is actively running. + */ +#include "Categories/ModbusPoint.h" +#include "Categories/ModbusFloatDecorator.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 +#include +#if defined(USE_MODBUS_IP) + #include +#else + #include +#endif + +/** + * @brief Constructs a new RunningState object. + * + * This constructor initializes the behavior strategies for various Modbus points + * that are active during the running state. For example, it sets different + * dynamic behaviors for the speeds of EC fans 1 through 5. + */ +template<> +RunningState::RunningState() { + addStrategy("CW Valve Position", new PIDStrategy("RAT Setpoint", 1000, "RAT")); + addStrategy("Operating Hours EC Fan #1", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #2", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #3", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #4", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #5", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #6", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #7", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #8", new TotalizerStrategy(10000)); + addStrategy("Operating Hours EC Fan #9", new TotalizerStrategy(10000)); +} + +/** + * @brief Executes the running state's logic for one update cycle. + * + * This method checks the "State Control" Modbus point for a command to + * transition to a different state (e.g., back to Standby). If no transition + * is requested, it applies the strategies defined for the running state. + * + * @param equipment Pointer to the Equipment instance. + * @return A pointer to a new State if a transition should occur, otherwise nullptr. + */ +template<> +State* RunningState::update(Equipment* equipment) { + // STATE control, add conditions if change to a different state is needed + Serial.println("Running update function"); + ModbusPoint* On_Off_Command = equipment->getModbusPoint("ON/OFF Command By BMS"); + int nextStateId = On_Off_Command ? On_Off_Command->getValue() : 0; + Serial.println(nextStateId); + if (nextStateId == 0){ + return new StandbyState(); + } + + ModbusPoint* faultCode = equipment->getModbusPoint("Fault Code"); + int faultCodeValue = faultCode ? faultCode->getValue() : 0; + switch (faultCodeValue){ + case 1: + return new FailState({"Alarm SAT Sensor Fault"}); + case 2: + return new FailState({"Alarm RAH Sensor Fault"}); + case 3: + return new FailState({"Alarm RAT Sensor Fault"}); + case 4: + return new FailState({"Alarm Filter DP Sensor Fault"}); + case 5: + return new FailState({"Alarm Flooding"}); + case 6: + return new FailState({"Alarm Dirty Filter"}); + case 7: + return new FailState({"Alarm High RAT"}); + case 8: + return new FailState({"Alarm Low RAT"}); + case 9: + return new FailState({"Alarm High SAT"}); + case 10: + return new FailState({"Alarm Low SAT"}); + case 11: + return new FailState({"Alarm High RAH"}); + case 12: + return new FailState({"Alarm Low RAH"}); + case 13: + return new FailState({"Alarm Phase Failure"}); + case 14: + return new FailState({"Alarm Condensate Pump"}); + case 15: + return new FailState({"Alarm Smoke"}); + case 16: + return new FailState({"Alarm Fire"}); + case 17: + return new FailState({"Alarm EC Fan #1"}); + case 18: + return new FailState({"Alarm EC Fan #2"}); + case 19: + return new FailState({"Alarm EC Fan #3"}); + case 20: + return new FailState({"Alarm EC Fan #4"}); + case 21: + return new FailState({"Alarm EC Fan #5"}); + case 22: + return new FailState({"Alarm EC Fan #6"}); + case 23: + return new FailState({"Alarm EC Fan #7"}); + case 24: + return new FailState({"Alarm EC Fan #8"}); + case 25: + return new FailState({"Alarm EC Fan #9"}); + default: + break; + } + + // Apply any strategies defined for the standby state + _applyStrategies(equipment); + return nullptr; +} + +/** + * @brief Logic to execute once when entering the running state. + * @param equipment Pointer to the Equipment instance (unused in this implementation). + */ +template<> +void RunningState::enterState(Equipment* 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 motorStatusDescriptions = { + "Run Status EC Fan #1", "Run Status EC Fan #2", "Run Status EC Fan #3", + "Run Status EC Fan #4", "Run Status EC Fan #5", "Run Status EC Fan #6", + "Run Status EC Fan #7", "Run Status EC Fan #8", "Run Status EC Fan #9" + }; + + // Loop through and set all motor statuses to 0 + for (const auto& desc : motorStatusDescriptions) { + ModbusPoint* point = equipment->getModbusPoint(desc); + if (point) { + point->setValue(1); + } + } +} + +/** + * @brief Logic to execute once when exiting the running state. + * @param equipment Pointer to the Equipment instance (unused in this implementation). + */ +template<> +void RunningState::exitState(Equipment* equipment) { + // Cleanup logic to run when the equipment leaves this state + Serial.println("Exit Running State..."); +} \ No newline at end of file diff --git a/src/CRAH_PAHHC_600_C6_TCP/State_Standby.cpp b/src/CRAH_PAHHC_600_C6_TCP/State_Standby.cpp new file mode 100644 index 0000000..239413e --- /dev/null +++ b/src/CRAH_PAHHC_600_C6_TCP/State_Standby.cpp @@ -0,0 +1,124 @@ +/** + * @file State_Standby.cpp + * @brief Implementation of the StandbyState class. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-05 + * + * 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 "Categories/ModbusPoint.h" +#include "Categories/ModbusFloatDecorator.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 +#include +#if defined(USE_MODBUS_IP) + #include +#else + #include +#endif +/** + * @brief Constructs a new StandbyState object. + * + * In this state, the equipment is idle. This constructor can be used to + * define specific behaviors for Modbus points that should occur during standby, + * such as setting fan speeds to zero. + */ +template<> +StandbyState::StandbyState() { + // You can add initialization code here if needed + addStrategy("SAT Reading", new SingleValueStrategy(100.0f, 0.1f, 1000)); + addStrategy("CW Valve Position", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #1", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #2", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #3", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #4", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #5", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #6", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #7", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #8", new RampStrategy(0.0f, 5.0f, 1000)); + addStrategy("Speed EC Fan #9", new RampStrategy(0.0f, 5.0f, 1000)); + +} + +/** + * @brief Executes the running state's logic for one update cycle. + * + * This method checks the "State Control" Modbus point for a command to + * transition to a different state (e.g., back to Standby). If no transition + * is requested, it applies the strategies defined for the running state. + * + * @param equipment Pointer to the Equipment instance. + * @return A pointer to a new State if a transition should occur, otherwise nullptr. + */ +template<> +State* StandbyState::update(Equipment* equipment) { + // STATE control, add conditions if change to a different state is needed + Serial.println("Standby update function"); + + // 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::enterState(Equipment* equipment) { + // Logic to run when the equipment enters this state +// A list of all alarm descriptions + const std::vector alarmDescriptions = { + "Alarm SAT Sensor Fault", "Alarm RAH Sensor Fault", "Alarm RAT Sensor Fault", + "Alarm Filter DP Sensor Fault", "Alarm Flooding", "Alarm Dirty Filter", + "Alarm High RAT", "Alarm Low RAT", "Alarm High SAT", "Alarm Low SAT", + "Alarm High RAH", "Alarm Low RAH", "Alarm Common", "Alarm Phase Failure", + "Alarm Condensate Pump", "Alarm Smoke", "Alarm Fire", "Alarm EC Fan #1", + "Alarm EC Fan #2", "Alarm EC Fan #3", "Alarm EC Fan #4", "Alarm EC Fan #5", + "Alarm EC Fan #6", "Alarm EC Fan #7", "Alarm EC Fan #8", "Alarm EC Fan #9" + }; + + // A list of all motor run status descriptions + const std::vector motorStatusDescriptions = { + "Run Status EC Fan #1", "Run Status EC Fan #2", "Run Status EC Fan #3", + "Run Status EC Fan #4", "Run Status EC Fan #5", "Run Status EC Fan #6", + "Run Status EC Fan #7", "Run Status EC Fan #8", "Run Status EC Fan #9" + }; + + // Loop through and set all alarms to 0 + for (const auto& desc : alarmDescriptions) { + ModbusPoint* point = equipment->getModbusPoint(desc); + if (point) { + point->setValue(0); + } + } + + // Loop through and set all motor statuses to 0 + for (const auto& desc : motorStatusDescriptions) { + ModbusPoint* point = equipment->getModbusPoint(desc); + if (point) { + point->setValue(0); + } + } +} + +/** + * @brief Logic to execute once when exiting the standby state. + * @param equipment Pointer to the Equipment instance. + */ +template<> +void StandbyState::exitState(Equipment* equipment) { + // Cleanup logic to run when the equipment leaves this state + Serial.println("Exit Standby State..."); +} \ No newline at end of file diff --git a/src/CRAH_PAHHC_600_C6_TCP/config.h b/src/CRAH_PAHHC_600_C6_TCP/config.h new file mode 100644 index 0000000..e9c5728 --- /dev/null +++ b/src/CRAH_PAHHC_600_C6_TCP/config.h @@ -0,0 +1,134 @@ +/** + * @file config.h + * @brief Main configuration file for the Equipment emulator. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-02 + * + * This file contains two important configurations: WiFi network parameters + * and the Modbus register map for the device. + */ + +/** + * @defgroup WiFiConfig WiFi Configuration + * @brief Network parameters for WiFi connection. + * @{ + */ + +#ifndef CONFIG_H +#define CONFIG_H + +#include "core.h" +#include "Equipment/Equipment.h" + +#if defined(USE_MODBUS_IP) + #include + const char *ssid = "esrlok_portable"; /**< @brief The SSID of the WiFi network. */ + const char *password = "m7g6eNMe?cy8S@z"; /**< @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 + #include +#endif + + +/** + * @brief The main loop update interval in milliseconds. + */ +int interval = 250; + +/** + * @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[] = +{ + {HR, 15, 0, "State Control"}, //Internal to control from Modscan + {HR, 16, 0, "Fault Code"}, + {HR_FLOAT, 18, 0, "RAT"}, //Internal Fault code from Modscan + {HR_FLOAT, 1, 0, "SAT Setpoint"}, + {HR_FLOAT, 681, 0, "RAT Setpoint"}, + {HR_FLOAT, 111, 0, "High RAT Limit"}, + {HR_FLOAT, 114, 0, "Low RAT Limit"}, + {HR_FLOAT, 118, 0, "High SAT Limit"}, + {HR_FLOAT, 122, 0, "Low SAT Limit"}, + {HR_FLOAT, 685, 0, "High RAH Limit"}, + {HR_FLOAT, 689, 0, "Low RAH Limit"}, + {HR, 5, 0, "Setting the EC Fan Max Speed"}, + {HR, 695, 0, "Setting the EC Fan Min Speed"}, + {HR_FLOAT, 693, 0, "Setting Room Temp"}, + {HR, 691, 0, "Setting EC Fan Speed "}, + {DI, 146, 0, "Alarm SAT Sensor Fault"}, + {DI, 1246, 0, "Alarm RAH Sensor Fault"}, + {DI, 1245, 0, "Alarm RAT Sensor Fault"}, + {DI, 1250, 0, "Alarm Filter DP Sensor Fault"}, + {DI, 51, 0, "Alarm Flooding"}, + {DI, 1096, 0, "Alarm Dirty Filter"}, + {DI, 1367, 0, "Alarm High RAT"}, + {DI, 1099, 0, "Alarm Low RAT"}, + {DI, 118, 0, "Alarm High SAT"}, + {DI, 122, 0, "Alarm Low SAT"}, + {DI, 1307, 0, "Alarm High RAH"}, + {DI, 1308, 0, "Alarm Low RAH"}, + {DI, 1342, 0, "Alarm Common"}, + {DI, 148, 0, "Alarm Phase Failure"}, + {DI, 1370, 0, "Alarm Condensate Pump"}, + {DI, 1368, 0, "Alarm Smoke"}, + {DI, 1369, 0, "Alarm Fire"}, + {DI, 131, 0, "Alarm EC Fan #1"}, + {DI, 132, 0, "Alarm EC Fan #2"}, + {DI, 133, 0, "Alarm EC Fan #3"}, + {DI, 134, 0, "Alarm EC Fan #4"}, + {DI, 135, 0, "Alarm EC Fan #5"}, + {DI, 136, 0, "Alarm EC Fan #6"}, + {DI, 1360, 0, "Alarm EC Fan #7"}, + {DI, 1361, 0, "Alarm EC Fan #8"}, + {DI, 1362, 0, "Alarm EC Fan #9"}, + {DI, 138, 0, "Run Status EC Fan #1"}, + {DI, 139, 0, "Run Status EC Fan #2"}, + {DI, 140, 0, "Run Status EC Fan #3"}, + {DI, 141, 0, "Run Status EC Fan #4"}, + {DI, 142, 0, "Run Status EC Fan #5"}, + {DI, 143, 0, "Run Status EC Fan #6"}, + {DI, 1363, 0, "Run Status EC Fan #7"}, + {DI, 1364, 0, "Run Status EC Fan #8"}, + {DI, 1365, 0, "Run Status EC Fan #9"}, + {IR_FLOAT, 99, 0, "SAT Reading"}, + {IR_FLOAT, 70, 0, "RAH Reading"}, + {IR_FLOAT, 101, 0, "RAT Reading"}, + {IR_FLOAT, 106, 0, "Filter DP Reading"}, + {IR_FLOAT, 496, 0, "CW Valve Position"}, + {IR, 53, 0, "Speed EC Fan #1"}, + {IR, 228, 0, "Speed EC Fan #2"}, + {IR, 229, 0, "Speed EC Fan #3"}, + {IR, 230, 0, "Speed EC Fan #4"}, + {IR, 231, 0, "Speed EC Fan #5"}, + {IR, 232, 0, "Speed EC Fan #6"}, + {IR, 678, 0, "Speed EC Fan #7"}, + {IR, 679, 0, "Speed EC Fan #8"}, + {IR, 680, 0, "Speed EC Fan #9"}, + {IR, 274, 0, "Operating Hours EC Fan #1"}, + {IR, 233, 0, "Operating Hours EC Fan #2"}, + {IR, 244, 0, "Operating Hours EC Fan #3"}, + {IR, 235, 0, "Operating Hours EC Fan #4"}, + {IR, 236, 0, "Operating Hours EC Fan #5"}, + {IR, 245, 0, "Operating Hours EC Fan #6"}, + {IR, 486, 0, "Operating Hours EC Fan #7"}, + {IR, 487, 0, "Operating Hours EC Fan #8"}, + {IR, 488, 0, "Operating Hours EC Fan #9"}, + {COIL, 301, 0, "ON/OFF Command By BMS"}, + {COIL, 302, 0, "Enable Off By Supervisory"}, + {COIL, 264, 0, "Alarm Reset"} +}; +//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]); +/** @} */ + +#endif // CONFIG_H diff --git a/src/CRAH_PAHHC_600_C6_TCP/main.cpp b/src/CRAH_PAHHC_600_C6_TCP/main.cpp new file mode 100644 index 0000000..2ffb050 --- /dev/null +++ b/src/CRAH_PAHHC_600_C6_TCP/main.cpp @@ -0,0 +1,87 @@ +/** + * @file BaseEmulator.ino + * @brief Main execution program for the Arduino Emulator. + * @author Emmanuel Hernandez Cruz + * @date 2025-09-02 + * + * @details This file contains the main execution program for an Arduino-based emulator of a equipment 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 IP server. + * - Modbus points (Coils, Holding Registers, etc.) based on a predefined map in config.h. + * + * The loop() function continuously: + * - Services the Modbus IP server. + * - Reads values from the Modbus server into internal data structures. + * - Updates the state of the emulated equipment. + * - Writes updated values back to the Modbus server. + * + * @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 Strategy_Behavior.h for different value generation strategies. + * @see ModbusPoint.h for the base class for all Modbus points. + */ +//================================================================================================================================= +//Libraries and declaration of variables. +#include +#include "config.h" +#include "Categories/ModbusPointFactory.h" +#if defined(USE_MODBUS_IP) + #include +#else + #include +#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++){ + ModbusPoint* point = createModbusPoint(&mb, mb_map[i].category, mb_map[i].address, mb_map[i].value, mb_map[i].description); + if (point) { + point->addToModbusServer(); + EquipmentInstance.addModbusPoint(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 the following actions in order: + * 1. Services the Modbus server by calling `mb.task()`. + * 2. Reads the current values from the Modbus registers into the `ModbusPoint` objects by calling `readRegisters()`. + * 3. After a specified interval, it updates the equipment's state by calling `EquipmentInstance.update()`. + * 4. Writes any changed values from the `ModbusPoint` objects back to the Modbus registers by calling `writeRegisters()`. + */ +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); + } +} diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html