First stable compilation with ModbusRTU and ModbusTCP

This commit is contained in:
2025-09-14 12:01:55 -05:00
commit ac20b82496
59 changed files with 6841 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# .gitignore
.vscode/
.pio/

37
include/README Normal file
View File

@@ -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

View File

@@ -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<typename T>
class ModbusCoil : public ModbusPoint<T>{
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<typename T>
ModbusCoil<T>::ModbusCoil(T* server, int address, int value, const char* description)
: ModbusPoint<T>(server, address, value, description){
}
/**
* @brief Adds the coil to the Modbus server's register map.
*/
template<typename T>
void ModbusCoil<T>::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<typename T>
void ModbusCoil<T>::setValue(int value){
this->_server->Coil(this->_address, value);
}
/**
* @brief Gets the current internal value of the coil.
* @return The current value.
*/
template<typename T>
int ModbusCoil<T>::getValue() const{
return this->_server->Coil(this->_address);
}
#endif

View File

@@ -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 <stdint.h>
#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<typename T>
class ModbusFloatDecorator : public ModbusPointDecorator<T> {
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<T>* point, ModbusPoint<T>* highOrderPoint)
: ModbusPointDecorator<T>(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<float>(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<int>(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<T>* _highOrderPoint; /**< @brief Pointer to the ModbusPoint for the high-order word. */
};
#endif

View File

@@ -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<typename T>
class ModbusHreg : public ModbusPoint<T>{
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<typename T>
ModbusHreg<T>::ModbusHreg(T* server, int address, int value, const char* description)
: ModbusPoint<T>(server, address, value, description) {}
template<typename T>
void ModbusHreg<T>::addToModbusServer() {
this->_server->addHreg(this->_address, this->_value);
}
template<typename T>
void ModbusHreg<T>::setValue(int value) {
this->_server->Hreg(this->_address, value);
}
template<typename T>
int ModbusHreg<T>::getValue() const {
return this->_server->Hreg(this->_address);
}
#endif

View File

@@ -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<typename T>
class ModbusIreg : public ModbusPoint<T>{
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<typename T>
ModbusIreg<T>::ModbusIreg(T* server, int address, int value, const char* description)
: ModbusPoint<T>(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<typename T>
void ModbusIreg<T>::setValue(int value){
this->_server->Ireg(this->_address, value);
}
/**
* @brief Gets the current internal value of the input register.
* @return The current value.
*/
template<typename T>
int ModbusIreg<T>::getValue() const {
return this->_server->Ireg(this->_address);
}
/**
* @brief Adds the input register to the Modbus server's register map.
*/
template<typename T>
void ModbusIreg<T>::addToModbusServer(){
this->_server->addIreg(this->_address, this->_value);
}
#endif

View File

@@ -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<typename T>
class ModbusIsts : public ModbusPoint<T>{
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<typename T>
ModbusIsts<T>::ModbusIsts(T* server, int address, int value, const char* description)
: ModbusPoint<T>(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<typename T>
void ModbusIsts<T>::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<typename T>
int ModbusIsts<T>::getValue() const{
return this->_server->Ists(this->_address);
}
/**
* @brief Adds the discrete input to the Modbus server's register map.
*/
template<typename T>
void ModbusIsts<T>::addToModbusServer(){
this->_server->addIsts(this->_address, this->_value);
}
#endif

View File

@@ -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<typename T>
class ModbusLongDecorator : public ModbusPointDecorator<T> {
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<T>* point, ModbusPoint<T>* highOrderPoint)
: ModbusPointDecorator<T>(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<long>(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<int>(getLongValue());
}
private:
ModbusPoint<T>* _highOrderPoint; /**< @brief Pointer to the ModbusPoint for the high-order word. */
};
#endif

View File

@@ -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 <string.h>
/**
* @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<typename T>
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<typename T>
ModbusPoint<T>::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

View File

@@ -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<typename T>
class ModbusPointDecorator : public ModbusPoint<T>{
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<T>* point) : ModbusPoint<T>(
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<T>* _point; /**< @brief Pointer to the wrapped ModbusPoint object. */
};
#endif

View File

@@ -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 <Arduino.h>
/**
* @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<typename T>
ModbusPoint<T>* createModbusPoint(T* server, int category, int address, int value, const char* description);
template<typename T>
ModbusPoint<T>* 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<T>(server, address, value, description);
case DI:
Serial.printf("Creating Digital Input: %s\n", description);
return new ModbusIsts<T>(server, address, value, description);
case IR:
Serial.printf("Creating Input Register: %s\n", description);
return new ModbusIreg<T>(server, address, value, description);
case IR_10X: {
Serial.printf("Creating Input Register 10x: %s\n", description);
ModbusPoint<T>* point = new ModbusIreg<T>(server, address, value, description);
return new ModbusScaleDecorator<T>(point);
}
case IR_LONG: {
Serial.printf("Creating Input Register Long: %s\n", description);
ModbusPoint<T>* point = new ModbusIreg<T>(server, address, 0, description);
ModbusPoint<T>* highOrderPoint = new ModbusIreg<T>(server, address + 1, 0, "");
return new ModbusLongDecorator<T>(point, highOrderPoint);
}
case IR_FLOAT: {
Serial.printf("Creating Input Register Float: %s\n", description);
ModbusPoint<T>* point = new ModbusIreg<T>(server, address, 0, description);
ModbusPoint<T>* highOrderPoint = new ModbusIreg<T>(server, address + 1, 0, "");
return new ModbusFloatDecorator<T>(point, highOrderPoint);
}
case HR:
Serial.printf("Creating Holding Register: %s\n", description);
return new ModbusHreg<T>(server, address, value, description);
case HR_10x: {
Serial.printf("Creating Holding Register 10x: %s\n", description);
ModbusPoint<T>* point = new ModbusHreg<T>(server, address, value, description);
return new ModbusScaleDecorator<T>(point);
}
case HR_LONG: {
Serial.printf("Creating Holding Register Long: %s\n", description);
ModbusPoint<T>* point = new ModbusHreg<T>(server, address, 0, description);
ModbusPoint<T>* highOrderPoint = new ModbusHreg<T>(server, address + 1 , 0, "");
return new ModbusLongDecorator<T>(point, highOrderPoint);
}
case HR_FLOAT: {
Serial.printf("Creating Holding Register Float: %s\n", description);
ModbusPoint<T>* point = new ModbusHreg<T>(server, address, 0, description);
ModbusPoint<T>* highOrderPoint = new ModbusHreg<T>(server, address + 1 , 0, "");
return new ModbusFloatDecorator<T>(point, highOrderPoint);
}
default:
Serial.printf("ERROR: Unknown Modbus category %d for '%s'\n", category, description);
return nullptr;
}
}
#endif

View File

@@ -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<typename T>
class ModbusScaleDecorator : public ModbusPointDecorator<T> {
public:
/**
* @brief Constructs a new ModbusScaleDecorator object.
* @param point A pointer to the ModbusPoint object to be decorated.
*/
ModbusScaleDecorator<T>(ModbusPoint<T>* point) : ModbusPointDecorator<T>(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

View File

@@ -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 <Arduino.h>
#include <map>
#include <string>
#include <vector>
#include "States/State_Standby.h"
// Forward Declarations
template<typename T> class State;
template<typename T> 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<typename T>
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<T>* 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<T>* 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<T>* 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<T>* _state; /**< @brief Pointer to the current state object. */
std::map<std::string, ModbusPoint<T>*> _points;/**< @brief Map of all Modbus points, keyed by description. */
int _stateId; /**< @brief A simple integer identifier for the current state. */
std::vector<ModbusPoint<T>*> _allPoints; /**< @brief Vector of all Modbus points. */
T* _server;
};
template<typename T>
Equipment<T>::Equipment() : _server(nullptr) {
this->_state = new StandbyState<T>();
this->_state->enterState(this);
}
template<typename T>
Equipment<T>::Equipment(T* server)
: _server(server)
{
this->_state = new StandbyState<T>();
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<typename T>
void Equipment<T>::update() {
State<T>* newState = this->_state->update(this);
if (newState != nullptr) {
changeState(newState);
}
}
/**
* @brief Delegates the enter state logic to the current state object.
*/
template<typename T>
void Equipment<T>::enterState() {
this->_state->enterState(this);
}
/**
* @brief Delegates the exit state logic to the current state object.
*/
template<typename T>
void Equipment<T>::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<typename T>
void Equipment<T>::addModbusPoint(const std::string& description, ModbusPoint<T>* 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<typename T>
ModbusPoint<T>* Equipment<T>::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<typename T>
void Equipment<T>::setModbusPoint(const std::string& description, float value) {
ModbusPoint<T>* 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<typename T>
int Equipment<T>::getState() {
return _stateId;
}
/**
* @brief Sets a simple integer identifier for the current state.
* @param stateId The integer ID representing the state.
*/
template<typename T>
void Equipment<T>::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<typename T>
void Equipment<T>::changeState(State<T>* newState) {
if (this->_state != nullptr) {
this->_state->exitState(this);
delete this->_state;
}
this->_state = newState;
if (this->_state != nullptr) {
this->_state->enterState(this);
}
}
#endif

113
lib/Core/README.md Normal file
View File

@@ -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`.
---

167
lib/Core/States/State.h Normal file
View File

@@ -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 <string>
#include <Arduino.h>
#include <map>
#include "Strategies/Strategy_PID.h"
#include "Categories/ModbusFloatDecorator.h"
#include "Categories/ModbusPoint.h"
// Forward Declarations
template<typename T>class 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<typename T>
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<T>* equipment) = 0;
/**
* @brief Logic to execute once when entering this state.
* @param equipment Pointer to the Equipment instance.
*/
virtual void enterState(Equipment<T>* equipment) {}
/**
* @brief Logic to execute once when exiting this state.
* @param equipment Pointer to the Equipment instance.
*/
virtual void exitState(Equipment<T>* equipment) {}
/**
* @brief Logic to apply all strategies created.
* @param equipment Pointer to the Equipment instance.
*/
virtual void _applyStrategies(Equipment<T>* 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<T>* equipment, const std::string& pointName);
void setPointValue(Equipment<T>* equipment, const std::string& pointName, float value);
void addStrategy(const std::string& pointDescription, Strategy_Behavior* strategy);
std::map<std::string, Strategy_Behavior*> _strategies;
};
template<typename T>
State<T>::~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<typename T>
void State<T>::addStrategy(const std::string& pointDescription, Strategy_Behavior* strategy){
this->_strategies[pointDescription] = strategy;
}
template<typename T>
float State<T>::getPointValue(Equipment<T>* equipment, const std::string& pointName) {
ModbusPoint<T>* 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<ModbusFloatDecorator<T>*>(point)->getFloatValue();
} else {
// Otherwise, get the standard integer value
return static_cast<float>(point->getValue());
}
}
template<typename T>
void State<T>::setPointValue(Equipment<T>* equipment, const std::string& pointName, float value) {
ModbusPoint<T>* point = equipment->getModbusPoint(pointName);
if (!point) return;
if (point->getType() == PointType::FLOAT) {
static_cast<ModbusFloatDecorator<T>*>(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<typename T>
void State<T>::_applyStrategies(Equipment<T>* 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<T>* outputPoint = equipment->getModbusPoint(description);
if (!outputPoint) continue;
float inputValue;
if (strategy->isPID()) {
PIDStrategy* pid = static_cast<PIDStrategy*>(strategy);
inputValue = getPointValue(equipment, pid->getInputSensorName());
ModbusPoint<T>* 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

View File

@@ -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 <vector>
#include <string>
template<typename T> 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<typename T>
class FailState : public State<T> {
public:
/**
* @brief Constructs a new FailState object.
* This is where strategies for failure behavior would be initialized.
*/
FailState(const std::vector<std::string>& 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<T>* update(Equipment<T>* equipment) override;
/** @brief Logic to execute once when entering the fail state. */
void enterState(Equipment<T>* equipment) override;
/** @brief Logic to execute once when exiting the fail state. */
void exitState(Equipment<T>* equipment) override;
};
#endif

View File

@@ -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<typename T> 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<typename T>
class RunningState : public State<T> {
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<T>* update(Equipment<T>* equipment) override;
/** @brief Logic to execute once when entering the running state. */
void enterState(Equipment<T>* equipment) override;
/** @brief Logic to execute once when exiting the running state. */
void exitState(Equipment<T>* equipment) override;
};
#endif

View File

@@ -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<typename T> 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<typename T>
class StandbyState : public State<T> {
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<T>* update(Equipment<T>* equipment) override;
/** @brief Logic to execute once when entering the standby state. */
void enterState(Equipment<T>* equipment) override;
/** @brief Logic to execute once when exiting the standby state. */
void exitState(Equipment<T>* equipment) override;
};
#endif

View File

@@ -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

View File

@@ -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 <Arduino.h>
// 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;
}

View File

@@ -0,0 +1,43 @@
// In BaseEmulator/Strategies/Strategy_PID.h
#ifndef PID_Strategy_h
#define PID_Strategy_h
#include "Strategy_Behavior.h"
#include <string>
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

View File

@@ -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 <Arduino.h>
/**
* @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;
}

View File

@@ -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

View File

@@ -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 <cstdlib>
#include <Arduino.h>
/**
* @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;
}

View File

@@ -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

View File

@@ -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 <Arduino.h>
/**
* @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;
}
}

View File

@@ -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

View File

@@ -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 <cstdlib>
#include <Arduino.h>
/**
* @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;
}

View File

@@ -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

View File

@@ -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 <Arduino.h>
/**
* @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;
}
}

View File

@@ -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

View File

@@ -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 <cstdlib>
#include <Arduino.h>
/**
* @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;
}

View File

@@ -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

69
lib/Core/core.h Normal file
View File

@@ -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 <ModbusIP_ESP8266.h> // Or your specific Modbus IP library
#else
#include <ModbusRTU.h> // 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<ModbusIP> EquipmentInstance(&mb);
#else
extern ModbusRTU mb; // Use ModbusRTU class
/** @brief An instance of the Equipment class, representing the emulated Equipment unit. */
Equipment<ModbusRTU> EquipmentInstance(&mb);
#endif
#endif // CORE_H

46
lib/README Normal file
View File

@@ -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 <Foo.h>
#include <Bar.h>
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

View File

@@ -0,0 +1,931 @@
/*
Modbus Library for Arduino
Core functions
Copyright (C) 2014 Andr<64> 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<TRegister> Modbus::_regs;
std::vector<TCallback> Modbus::_callbacks;
#if defined(MODBUS_FILES)
std::function<Modbus::ResultCode(Modbus::FunctionCode, uint16_t, uint16_t, uint16_t, uint8_t*)> Modbus::_onFile;
#endif
#else
DArray<TRegister, 1, 1> Modbus::_regs;
DArray<TCallback, 1, 1> 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<TCallback>::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<TRegister>::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<Modbus::ResultCode(Modbus::FunctionCode, uint16_t, uint16_t, uint16_t, uint8_t*)> 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

363
lib/modbus-esp8266/Modbus.h Normal file
View File

@@ -0,0 +1,363 @@
/*
Modbus Library for Arduino
Core functions
Copyright (C) 2014 Andr<64> 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 <vector>
#include <algorithm>
#include <functional>
#include <memory>
#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<uint16_t(TRegister* reg, uint16_t val)> 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<TRegister> _regs;
static std::vector<TCallback> _callbacks;
#if defined(MODBUS_FILES)
static std::function<ResultCode(FunctionCode, uint16_t, uint16_t, uint16_t, uint8_t*)> _onFile;
#endif
#else
std::vector<TRegister> _regs;
std::vector<TCallback> _callbacks;
#if defined(MODBUS_FILES)
std::function<ResultCode(FunctionCode, uint16_t, uint16_t, uint16_t, uint8_t*)> _onFile;
#endif
#endif
#else
#if defined(MODBUS_GLOBAL_REGS)
static DArray<TRegister, 1, 1> _regs;
static DArray<TCallback, 1, 1> _callbacks;
#if defined(MODBUS_FILES)
static ResultCode (*_onFile)(FunctionCode, uint16_t, uint16_t, uint16_t, uint8_t*);
#endif
#else
DArray<TRegister, 1, 1> _regs;
DArray<TCallback, 1, 1> _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<ResultCode(FunctionCode, const RequestData)> cbRequest; // Callback function Type
typedef std::function<ResultCode(uint8_t*, uint8_t, void*)> 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<ResultCode(FunctionCode, uint16_t, uint16_t, uint16_t, uint8_t*)>);
#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<bool(Modbus::ResultCode, uint16_t, void*)> 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<Modbus::ResultCode(Modbus::FunctionCode, uint16_t, uint16_t, uint16_t, uint8_t*)> 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

View File

@@ -0,0 +1,507 @@
/*
Modbus Library for Arduino
Modbus public API implementation
Copyright (C) 2014 Andr<64> Sarmento Barbosa
2017-2021 Alexander Emelianov (a.m.emelianov@gmail.com)
*/
#pragma once
#include "Modbus.h"
template <class T>
class ModbusAPI : public T {
public:
// Alternative API
template <typename TYPEID>
uint16_t read(TYPEID id, TAddress reg, uint16_t* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t read(TYPEID id, TAddress reg, bool* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t write(TYPEID id, TAddress reg, uint16_t value, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t write(TYPEID id, TAddress reg, bool value, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t write(TYPEID id, TAddress reg, uint16_t* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t write(TYPEID id, TAddress reg, bool* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
/*
template <typename TYPEID>
uint16_t push(TYPEID id, TAddress to, TAddress from, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
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 <typename TYPEID>
uint16_t writeCoil(TYPEID id, uint16_t offset, bool value, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t writeCoil(TYPEID id, uint16_t offset, bool* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t readCoil(TYPEID id, uint16_t offset, bool* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t writeHreg(TYPEID id, uint16_t offset, uint16_t value, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t writeHreg(TYPEID id, uint16_t offset, uint16_t* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t readIsts(TYPEID id, uint16_t offset, bool* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t readHreg(TYPEID id, uint16_t offset, uint16_t* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t readIreg(TYPEID id, uint16_t offset, uint16_t* value, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t pushCoil(TYPEID id, uint16_t to, uint16_t from, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t pullCoil(TYPEID id, uint16_t from, uint16_t to, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t pullIsts(TYPEID id, uint16_t from, uint16_t to, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t pushHreg(TYPEID id, uint16_t to, uint16_t from, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t pullHreg(TYPEID id, uint16_t from, uint16_t to, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t pullIreg(TYPEID id, uint16_t from, uint16_t to, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t pullHregToIreg(TYPEID id, uint16_t offset, uint16_t startreg, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t pullCoilToIsts(TYPEID id, uint16_t offset, uint16_t startreg, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t pushIstsToCoil(TYPEID id, uint16_t to, uint16_t from, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t pushIregToHreg(TYPEID id, uint16_t to, uint16_t from, uint16_t numregs = 1, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
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 <typename TYPEID>
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 <typename TYPEID>
uint16_t maskHreg(TYPEID slaveId, uint16_t offset, uint16_t andMask, uint16_t orMask, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
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 <typename TYPEID>
uint16_t rawRequest(TYPEID ip, const uint8_t* data, uint16_t len, cbTransaction cb = nullptr, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
uint16_t rawResponce(TYPEID ip, const uint8_t* data, uint16_t len, uint8_t unit = MODBUSIP_UNIT);
template <typename TYPEID>
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 <class T> \
template <typename TYPEID> \
uint16_t ModbusAPI<T>::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 <class T> \
template <typename TYPEID> \
uint16_t ModbusAPI<T>::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 <class T> \
template <typename TYPEID> \
uint16_t ModbusAPI<T>::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 <class T> \
template <typename TYPEID> \
uint16_t ModbusAPI<T>::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 <class T> \
template <typename TYPEID> \
uint16_t ModbusAPI<T>::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 <class T>
template <typename TYPEID>
uint16_t ModbusAPI<T>::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 <class T>
template <typename TYPEID>
uint16_t ModbusAPI<T>::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 <class T>
template <typename TYPEID>
uint16_t ModbusAPI<T>::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 <class T>
template <typename TYPEID>
uint16_t ModbusAPI<T>::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 <class T>
template <typename TYPEID>
uint16_t ModbusAPI<T>::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 <class T>
template <typename TYPEID>
uint16_t ModbusAPI<T>::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 <class T> \
bool ModbusAPI<T>::addHreg(uint16_t offset, uint16_t value, uint16_t numregs) {
return this->addReg(HREG(offset), value, numregs);
}
template <class T> \
bool ModbusAPI<T>::Hreg(uint16_t offset, uint16_t value) {
return this->Reg(HREG(offset), value);
}
template <class T> \
uint16_t ModbusAPI<T>::Hreg(uint16_t offset) {
return this->Reg(HREG(offset));
}
template <class T> \
bool ModbusAPI<T>::removeHreg(uint16_t offset, uint16_t numregs) {
return this->removeReg(HREG(offset), numregs);
}
template <class T> \
bool ModbusAPI<T>::addCoil(uint16_t offset, bool value, uint16_t numregs) {
return this->addReg(COIL(offset), COIL_VAL(value), numregs);
}
template <class T> \
bool ModbusAPI<T>::addIsts(uint16_t offset, bool value, uint16_t numregs) {
return this->addReg(ISTS(offset), ISTS_VAL(value), numregs);
}
template <class T> \
bool ModbusAPI<T>::addIreg(uint16_t offset, uint16_t value, uint16_t numregs) {
return this->addReg(IREG(offset), value, numregs);
}
template <class T> \
bool ModbusAPI<T>::Coil(uint16_t offset, bool value) {
return this->Reg(COIL(offset), COIL_VAL(value));
}
template <class T> \
bool ModbusAPI<T>::Ists(uint16_t offset, bool value) {
return this->Reg(ISTS(offset), ISTS_VAL(value));
}
template <class T> \
bool ModbusAPI<T>::Ireg(uint16_t offset, uint16_t value) {
return this->Reg(IREG(offset), value);
}
template <class T> \
bool ModbusAPI<T>::Coil(uint16_t offset) {
return COIL_BOOL(this->Reg(COIL(offset)));
}
template <class T> \
bool ModbusAPI<T>::Ists(uint16_t offset) {
return ISTS_BOOL(this->Reg(ISTS(offset)));
}
template <class T> \
uint16_t ModbusAPI<T>::Ireg(uint16_t offset) {
return this->Reg(IREG(offset));
}
template <class T> \
bool ModbusAPI<T>::removeCoil(uint16_t offset, uint16_t numregs) {
return this->removeReg(COIL(offset), numregs);
}
template <class T> \
bool ModbusAPI<T>::removeIsts(uint16_t offset, uint16_t numregs) {
return this->removeReg(ISTS(offset), numregs);
}
template <class T> \
bool ModbusAPI<T>::removeIreg(uint16_t offset, uint16_t numregs) {
return this->removeReg(IREG(offset), numregs);
}
template <class T> \
bool ModbusAPI<T>::onGetCoil(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->onGet(COIL(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::onSetCoil(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->onSet(COIL(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::onGetHreg(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->onGet(HREG(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::onSetHreg(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->onSet(HREG(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::onGetIsts(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->onGet(ISTS(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::onSetIsts(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->onSet(ISTS(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::onGetIreg(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->onGet(IREG(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::onSetIreg(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->onSet(IREG(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::removeOnGetCoil(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->removeOnGet(COIL(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::removeOnSetCoil(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->removeOnSet(COIL(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::removeOnGetHreg(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->removeOnGet(HREG(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::removeOnSetHreg(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->removeOnSet(HREG(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::removeOnGetIsts(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->removeOnGet(ISTS(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::removeOnSetIsts(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->removeOnSet(ISTS(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::removeOnGetIreg(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->removeOnGet(IREG(offset), cb, numregs);
}
template <class T> \
bool ModbusAPI<T>::removeOnSetIreg(uint16_t offset, cbModbus cb, uint16_t numregs) {
return this->removeOnSet(IREG(offset), cb, numregs);
}
template <class T> \
template <typename TYPEID> \
uint16_t ModbusAPI<T>::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 <class T> \
template <typename TYPEID> \
uint16_t ModbusAPI<T>::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 <class T> \
template <typename TYPEID> \
uint16_t ModbusAPI<T>::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 <class T> \
template <typename TYPEID> \
uint16_t ModbusAPI<T>::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 <class T>
template <typename TYPEID>
uint16_t ModbusAPI<T>::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 <class T>
template <typename TYPEID>
uint16_t ModbusAPI<T>::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 <class T>
template <typename TYPEID>
uint16_t ModbusAPI<T>::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);
}

View File

@@ -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 <Dns.h>
#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<ModbusTCPTemplate<EthernetServerWrapper, EthernetClient>> {
#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
};

View File

@@ -0,0 +1,11 @@
/*
Modbus Library for Arduino
ModbusIP class compatibility wrapper
Copyright (C) 2014 Andr<64> Sarmento Barbosa
2017-2020 Alexander Emelianov (a.m.emelianov@gmail.com)
*/
#pragma once
#include "ModbusTCP.h"
class ModbusIP : public ModbusTCP {};

View File

@@ -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;
}

View File

@@ -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 <class T>
bool begin(T* port, int16_t txEnablePin = -1, bool txEnableDirect = true);
#if defined(MODBUSRTU_REDE)
template <class T>
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 <class T>
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 <class T>
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<ModbusRTUTemplate> {};

View File

@@ -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

View File

@@ -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 <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#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<ModbusTCPTemplate<WiFiServerESPWrapper, WiFiClient>> {
#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
};

View File

@@ -0,0 +1,606 @@
/*
Modbus Library for Arduino
ModbusTCP general implementation
Copyright (C) 2014 Andr<64> 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<bool(IPAddress)> cbModbusConnect;
typedef std::function<IPAddress(const char*)> 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 SERVER, class CLIENT>
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<TTransaction> _trans;
#else
DArray<TTransaction, 2, 2> _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 <class SERVER, class CLIENT>
ModbusTCPTemplate<SERVER, CLIENT>::ModbusTCPTemplate() {
//_trans.reserve(MODBUSIP_MAX_TRANSACIONS);
for (uint8_t i = 0; i < MODBUSIP_MAX_CLIENTS; i++)
tcpclient[i] = nullptr;
resolve = defaultResolver;
}
template <class SERVER, class CLIENT>
void ModbusTCPTemplate<SERVER, CLIENT>::client() {
}
template <class SERVER, class CLIENT>
void ModbusTCPTemplate<SERVER, CLIENT>::server(uint16_t port) {
if (port)
serverPort = port;
else
serverPort = defaultPort;
tcpserver = new SERVER(serverPort);
tcpserver->begin();
}
#if defined(MODBUSIP_USE_DNS)
template <class SERVER, class CLIENT>
bool ModbusTCPTemplate<SERVER, CLIENT>::connect(String host, uint16_t port) {
return connect(resolve(host.c_str()), port);
}
template <class SERVER, class CLIENT>
bool ModbusTCPTemplate<SERVER, CLIENT>::connect(const char* host, uint16_t port) {
return connect(resolve(host), port);
}
#endif
template <class SERVER, class CLIENT>
bool ModbusTCPTemplate<SERVER, CLIENT>::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 <class SERVER, class CLIENT>
uint32_t ModbusTCPTemplate<SERVER, CLIENT>::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 <class SERVER, class CLIENT>
TTransaction* ModbusTCPTemplate<SERVER, CLIENT>::searchTransaction(uint16_t id) {
#define MODBUSIP_COMPARE_TRANS [id](TTransaction& trans){return trans.transactionId == id;}
#if defined(MODBUS_USE_STL)
std::vector<TTransaction>::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 <class SERVER, class CLIENT>
void ModbusTCPTemplate<SERVER, CLIENT>::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<TTransaction>::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 <class SERVER, class CLIENT>
uint16_t ModbusTCPTemplate<SERVER, CLIENT>::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 <class SERVER, class CLIENT>
uint16_t ModbusTCPTemplate<SERVER, CLIENT>::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 <class SERVER, class CLIENT>
uint16_t ModbusTCPTemplate<SERVER, CLIENT>::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 <class SERVER, class CLIENT>
void ModbusTCPTemplate<SERVER, CLIENT>::onConnect(cbModbusConnect cb) {
cbConnect = cb;
}
template <class SERVER, class CLIENT>
void ModbusTCPTemplate<SERVER, CLIENT>::onDisconnect(cbModbusConnect cb) {
cbDisconnect = cb;
}
template <class SERVER, class CLIENT>
void ModbusTCPTemplate<SERVER, CLIENT>::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 <class SERVER, class CLIENT>
void ModbusTCPTemplate<SERVER, CLIENT>::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 <class SERVER, class CLIENT>
int8_t ModbusTCPTemplate<SERVER, CLIENT>::getFreeClient() {
for (uint8_t i = 0; i < MODBUSIP_MAX_CLIENTS; i++)
if (!tcpclient[i])
return i;
return -1;
}
template <class SERVER, class CLIENT>
int8_t ModbusTCPTemplate<SERVER, CLIENT>::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 <class SERVER, class CLIENT>
int8_t ModbusTCPTemplate<SERVER, CLIENT>::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 <class SERVER, class CLIENT>
bool ModbusTCPTemplate<SERVER, CLIENT>::isTransaction(uint16_t id) {
return searchTransaction(id) != nullptr;
}
#if defined(MODBUSIP_USE_DNS)
template <class SERVER, class CLIENT>
bool ModbusTCPTemplate<SERVER, CLIENT>::isConnected(String host) {
return isConnected(resolve(host.c_str()));
}
template <class SERVER, class CLIENT>
bool ModbusTCPTemplate<SERVER, CLIENT>::isConnected(const char* host) {
return isConnected(resolve(host));
}
#endif
template <class SERVER, class CLIENT>
bool ModbusTCPTemplate<SERVER, CLIENT>::isConnected(IPAddress ip) {
if (!ip)
return false;
int8_t p = getSlave(ip);
return p != -1 && tcpclient[p]->connected();
}
template <class SERVER, class CLIENT>
void ModbusTCPTemplate<SERVER, CLIENT>::autoConnect(bool enabled) {
autoConnectMode = enabled;
}
#if defined(MODBUSIP_USE_DNS)
template <class SERVER, class CLIENT>
bool ModbusTCPTemplate<SERVER, CLIENT>::disconnect(String host) {
return disconnect(resolve(host.c_str()));
}
template <class SERVER, class CLIENT>
bool ModbusTCPTemplate<SERVER, CLIENT>::disconnect(const char* host) {
return disconnect(resolve(host));
}
#endif
template <class SERVER, class CLIENT>
bool ModbusTCPTemplate<SERVER, CLIENT>::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 <class SERVER, class CLIENT>
void ModbusTCPTemplate<SERVER, CLIENT>::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 <class SERVER, class CLIENT>
ModbusTCPTemplate<SERVER, CLIENT>::~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 <class SERVER, class CLIENT>
uint16_t ModbusTCPTemplate<SERVER, CLIENT>::setTransactionId(uint16_t t) {
transactionId = t;
if (!transactionId)
transactionId = 1;
return transactionId;
}

View File

@@ -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 <WiFiClientSecure.h>
#if defined(ESP8266)
#include <WiFiServerSecure.h>
#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<ModbusTCPTemplate<WiFiServerSecure, WiFiClientSecure>> {
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;
}
};

View File

@@ -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 <typename T, int SIZE, int INCREMENT>
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 <class UnaryPredicate>
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];
}
};

32
platformio.ini Normal file
View File

@@ -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 = -<*> +<CH_Daikin_AWV026B_RTU>
[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 = -<*> +<CRAH_PAHHC_600_C6_TCP>

View File

@@ -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 <vector>
#include <string>
#if defined(USE_MODBUS_IP)
#include <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#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<ModbusRTU>::FailState(const std::vector<std::string>& 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<ModbusRTU>* FailState<ModbusRTU>::update(Equipment<ModbusRTU>* equipment) {
// STATE control, add conditions if change to a different state is needed
Serial.println("Fail update function");
ModbusPoint<ModbusRTU>* alarmReset = equipment->getModbusPoint("Alarm Reset");
int nextStateId = alarmReset ? alarmReset->getValue() : 0;
if (nextStateId == 1){
return new StandbyState<ModbusRTU>();
}
_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<ModbusRTU>::enterState(Equipment<ModbusRTU>* equipment) {
// Logic to run when the equipment enters this state
Serial.println("Enter Fail State...");
ModbusPoint<ModbusRTU>* 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<ModbusRTU>::exitState(Equipment<ModbusRTU>* equipment) {
// Cleanup logic to run when the equipment leaves this state
Serial.println("Exit Fail State...");
ModbusPoint<ModbusRTU>* alarm_common = equipment->getModbusPoint("Alarm Common");
alarm_common->setValue(0);
}

View File

@@ -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 <vector>
#include <string>
#if defined(USE_MODBUS_IP)
#include <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#endif
/**
* @brief Constructs a new RunningState object.
*
* This constructor initializes 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<ModbusRTU>::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<ModbusRTU>* RunningState<ModbusRTU>::update(Equipment<ModbusRTU>* equipment) {
// STATE control, add conditions if change to a different state is needed
Serial.println("Running update function");
ModbusPoint<ModbusRTU>* 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<ModbusRTU>();
}
ModbusPoint<ModbusRTU>* faultCode = equipment->getModbusPoint("Fault Code");
int faultCodeValue = faultCode ? faultCode->getValue() : 0;
switch (faultCodeValue){
case 1:
return new FailState<ModbusRTU>({"Alarm SAT Sensor Fault"});
case 2:
return new FailState<ModbusRTU>({"Alarm RAH Sensor Fault"});
case 3:
return new FailState<ModbusRTU>({"Alarm RAT Sensor Fault"});
case 4:
return new FailState<ModbusRTU>({"Alarm Filter DP Sensor Fault"});
case 5:
return new FailState<ModbusRTU>({"Alarm Flooding"});
case 6:
return new FailState<ModbusRTU>({"Alarm Dirty Filter"});
case 7:
return new FailState<ModbusRTU>({"Alarm High RAT"});
case 8:
return new FailState<ModbusRTU>({"Alarm Low RAT"});
case 9:
return new FailState<ModbusRTU>({"Alarm High SAT"});
case 10:
return new FailState<ModbusRTU>({"Alarm Low SAT"});
case 11:
return new FailState<ModbusRTU>({"Alarm High RAH"});
case 12:
return new FailState<ModbusRTU>({"Alarm Low RAH"});
case 13:
return new FailState<ModbusRTU>({"Alarm Phase Failure"});
case 14:
return new FailState<ModbusRTU>({"Alarm Condensate Pump"});
case 15:
return new FailState<ModbusRTU>({"Alarm Smoke"});
case 16:
return new FailState<ModbusRTU>({"Alarm Fire"});
case 17:
return new FailState<ModbusRTU>({"Alarm EC Fan #1"});
case 18:
return new FailState<ModbusRTU>({"Alarm EC Fan #2"});
case 19:
return new FailState<ModbusRTU>({"Alarm EC Fan #3"});
case 20:
return new FailState<ModbusRTU>({"Alarm EC Fan #4"});
case 21:
return new FailState<ModbusRTU>({"Alarm EC Fan #5"});
case 22:
return new FailState<ModbusRTU>({"Alarm EC Fan #6"});
case 23:
return new FailState<ModbusRTU>({"Alarm EC Fan #7"});
case 24:
return new FailState<ModbusRTU>({"Alarm EC Fan #8"});
case 25:
return new FailState<ModbusRTU>({"Alarm EC Fan #9"});
default:
break;
}
ModbusPoint<ModbusRTU>* 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<ModbusRTU>::enterState(Equipment<ModbusRTU>* equipment) {
// Logic to run when the equipment enters this state
Serial.println("Enter Running State...");
// You could also update a Modbus register to show the "standby" state
const std::vector<std::string> motorStatusDescriptions = {
"Run Status 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<ModbusRTU>* 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<ModbusRTU>::exitState(Equipment<ModbusRTU>* equipment) {
// Cleanup logic to run when the equipment leaves this state
Serial.println("Exit Running State...");
}

View File

@@ -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 <vector>
#include <string>
#if defined(USE_MODBUS_IP)
#include <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#endif
/**
* @brief Constructs a new StandbyState object.
*
* In this state, the equipment is idle. This constructor can be used to
* define specific behaviors for Modbus points that should occur during standby,
* such as setting fan speeds to zero.
*/
template<>
StandbyState<ModbusRTU>::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<ModbusRTU>* StandbyState<ModbusRTU>::update(Equipment<ModbusRTU>* 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<ModbusRTU>::enterState(Equipment<ModbusRTU>* 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<ModbusRTU>::exitState(Equipment<ModbusRTU>* equipment) {
// Cleanup logic to run when the equipment leaves this state
Serial.println("Exit Standby State...");
}

View File

@@ -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 <ModbusRTU.h>
#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

View File

@@ -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 <Arduino.h>
#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<ModbusRTU>* 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);
}
}

View File

@@ -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 <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#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<ModbusIP>::FailState(const std::vector<std::string>& 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<ModbusIP>* FailState<ModbusIP>::update(Equipment<ModbusIP>* equipment) {
// STATE control, add conditions if change to a different state is needed
Serial.println("Fail update function");
ModbusPoint<ModbusIP>* alarmReset = equipment->getModbusPoint("Alarm Reset");
int nextStateId = alarmReset ? alarmReset->getValue() : 0;
if (nextStateId == 1){
return new StandbyState<ModbusIP>();
}
_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<ModbusIP>::enterState(Equipment<ModbusIP>* equipment) {
// Logic to run when the equipment enters this state
Serial.println("Enter Fail State...");
ModbusPoint<ModbusIP>* 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<ModbusIP>::exitState(Equipment<ModbusIP>* equipment) {
// Cleanup logic to run when the equipment leaves this state
Serial.println("Exit Fail State...");
ModbusPoint<ModbusIP>* alarm_common = equipment->getModbusPoint("Alarm Common");
alarm_common->setValue(0);
}

View File

@@ -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 <vector>
#include <string>
#if defined(USE_MODBUS_IP)
#include <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#endif
/**
* @brief Constructs a new RunningState object.
*
* This constructor initializes 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<ModbusIP>::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<ModbusIP>* RunningState<ModbusIP>::update(Equipment<ModbusIP>* equipment) {
// STATE control, add conditions if change to a different state is needed
Serial.println("Running update function");
ModbusPoint<ModbusIP>* 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<ModbusIP>();
}
ModbusPoint<ModbusIP>* faultCode = equipment->getModbusPoint("Fault Code");
int faultCodeValue = faultCode ? faultCode->getValue() : 0;
switch (faultCodeValue){
case 1:
return new FailState<ModbusIP>({"Alarm SAT Sensor Fault"});
case 2:
return new FailState<ModbusIP>({"Alarm RAH Sensor Fault"});
case 3:
return new FailState<ModbusIP>({"Alarm RAT Sensor Fault"});
case 4:
return new FailState<ModbusIP>({"Alarm Filter DP Sensor Fault"});
case 5:
return new FailState<ModbusIP>({"Alarm Flooding"});
case 6:
return new FailState<ModbusIP>({"Alarm Dirty Filter"});
case 7:
return new FailState<ModbusIP>({"Alarm High RAT"});
case 8:
return new FailState<ModbusIP>({"Alarm Low RAT"});
case 9:
return new FailState<ModbusIP>({"Alarm High SAT"});
case 10:
return new FailState<ModbusIP>({"Alarm Low SAT"});
case 11:
return new FailState<ModbusIP>({"Alarm High RAH"});
case 12:
return new FailState<ModbusIP>({"Alarm Low RAH"});
case 13:
return new FailState<ModbusIP>({"Alarm Phase Failure"});
case 14:
return new FailState<ModbusIP>({"Alarm Condensate Pump"});
case 15:
return new FailState<ModbusIP>({"Alarm Smoke"});
case 16:
return new FailState<ModbusIP>({"Alarm Fire"});
case 17:
return new FailState<ModbusIP>({"Alarm EC Fan #1"});
case 18:
return new FailState<ModbusIP>({"Alarm EC Fan #2"});
case 19:
return new FailState<ModbusIP>({"Alarm EC Fan #3"});
case 20:
return new FailState<ModbusIP>({"Alarm EC Fan #4"});
case 21:
return new FailState<ModbusIP>({"Alarm EC Fan #5"});
case 22:
return new FailState<ModbusIP>({"Alarm EC Fan #6"});
case 23:
return new FailState<ModbusIP>({"Alarm EC Fan #7"});
case 24:
return new FailState<ModbusIP>({"Alarm EC Fan #8"});
case 25:
return new FailState<ModbusIP>({"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<ModbusIP>::enterState(Equipment<ModbusIP>* equipment) {
// Logic to run when the equipment enters this state
Serial.println("Enter Running State...");
// You could also update a Modbus register to show the "standby" state
const std::vector<std::string> motorStatusDescriptions = {
"Run Status 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<ModbusIP>* 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<ModbusIP>::exitState(Equipment<ModbusIP>* equipment) {
// Cleanup logic to run when the equipment leaves this state
Serial.println("Exit Running State...");
}

View File

@@ -0,0 +1,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 <vector>
#include <string>
#if defined(USE_MODBUS_IP)
#include <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#endif
/**
* @brief Constructs a new StandbyState object.
*
* In this state, the equipment is idle. This constructor can be used to
* define specific behaviors for Modbus points that should occur during standby,
* such as setting fan speeds to zero.
*/
template<>
StandbyState<ModbusIP>::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<ModbusIP>* StandbyState<ModbusIP>::update(Equipment<ModbusIP>* 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<ModbusIP>::enterState(Equipment<ModbusIP>* equipment) {
// Logic to run when the equipment enters this state
// A list of all alarm descriptions
const std::vector<std::string> 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<std::string> 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<ModbusIP>* point = equipment->getModbusPoint(desc);
if (point) {
point->setValue(0);
}
}
// Loop through and set all motor statuses to 0
for (const auto& desc : motorStatusDescriptions) {
ModbusPoint<ModbusIP>* 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<ModbusIP>::exitState(Equipment<ModbusIP>* equipment) {
// Cleanup logic to run when the equipment leaves this state
Serial.println("Exit Standby State...");
}

View File

@@ -0,0 +1,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 <ModbusIP_ESP8266.h>
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 <ModbusRTU.h>
#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

View File

@@ -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 <WiFi.h>
#include "config.h"
#include "Categories/ModbusPointFactory.h"
#if defined(USE_MODBUS_IP)
#include <ModbusIP_ESP8266.h>
#else
#include <ModbusRTU.h>
#endif
//=================================================================================================================================
/**
* @brief Initializes the application.
* @details This function runs once at startup. It configures the serial communication,
* Wi-Fi, and the Modbus server. It also creates and initializes all the Modbus points
* based on the `mb_map` array in `config.h`.
*/
void setup() {
Serial.begin(115200); //Serial comm start
WiFi.config(local_IP, gateway, subnet); // Wifi service start
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("Connected!!");
mb.server(); //Modbus server start
Serial.println("Server Created");
Serial.println(map_size);
for(int i = 0; i < map_size; i++){
ModbusPoint<ModbusIP>* 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);
}
}

11
test/README Normal file
View File

@@ -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