113 lines
8.2 KiB
Markdown
113 lines
8.2 KiB
Markdown
# 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 `Modbus_Point` (like `Modbus_Hreg` for a Holding Register).
|
|
* If a point needs to be treated as a float, we "decorate" or "wrap" it with a `Modbus_FloatDecorator`. 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 `Modbus_ScaleDecorator`.
|
|
|
|
* **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 `Modbus_PointFactory`.
|
|
|
|
* **Files to see:**
|
|
* `Modbus_Points/Modbus_Point.h`: The base for all points.
|
|
* `Modbus_Points/Modbus_PointDecorator.h`: The base "wrapper" class.
|
|
* `Modbus_Points/Modbus_FloatDecorator.h`: A specific wrapper that adds floating-point logic.
|
|
* `Modbus_Points/Modbus_PointFactory.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.
|
|
* `/Modbus_Points`: Contains the classes for different types of Modbus points (`Modbus_Coil`, `Modbus_Hreg`) and the Decorators (`Modbus_FloatDecorator`).
|
|
* `/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`.
|
|
|
|
--- |