β Production-Ready Examples
Complete, real-world examples showing how to implement Modbus Digital Twin Simulator in various scenarios.
Industrial HMI Simulator
// Complete industrial HMI simulator with multiple devices
public class IndustrialHmiSimulator
{
private readonly List devices = new();
private readonly ModbusLogger logger;
private readonly Timer simulationTimer;
private bool isRunning = false;
public IndustrialHmiSimulator()
{
logger = new ModbusLogger("industrial_hmi.log");
// Create multiple devices simulating different systems
CreateDevices();
// Start simulation timer
simulationTimer = new Timer(UpdateSimulation, null,
TimeSpan.Zero, TimeSpan.FromSeconds(1));
}
private void CreateDevices()
{
// Device 1: Main PLC (TCP)
var mainPlc = ModbusDeviceFactory.CreateTcpDevice(1, 502);
SetupMainPlc(mainPlc);
devices.Add(mainPlc);
// Device 2: Motor Controller (TCP)
var motorController = ModbusDeviceFactory.CreateTcpDevice(2, 5020);
SetupMotorController(motorController);
devices.Add(motorController);
// Device 3: Temperature Controller (RTU)
if (SerialPort.GetPortNames().Contains("COM3"))
{
var tempController = ModbusDeviceFactory.CreateRtuDevice(3, "COM3");
SetupTemperatureController(tempController);
devices.Add(tempController);
}
// Attach logging to all devices
foreach (var device in devices)
{
logger.AttachToDevice(device);
}
}
private void SetupMainPlc(ModbusDeviceBase plc)
{
// Main PLC memory layout:
// Coils 0-99: System outputs (pumps, valves, etc.)
// Discrete Inputs 0-99: System inputs (sensors, switches)
// Holding Registers 0-49: Setpoints and configuration
// Input Registers 0-99: Process variables
// Initialize system outputs
for (int i = 0; i < 100; i++)
{
plc.SetCoil(i, false); // All outputs OFF initially
}
// Initialize system inputs (sensors)
for (int i = 0; i < 100; i++)
{
plc.SetDiscreteInput(i, false); // All inputs normal initially
}
// Initialize setpoints
plc.SetHoldingRegister(0, 250); // Temperature setpoint: 25.0Β°C
plc.SetHoldingRegister(1, 1000); // Pressure setpoint: 10.00 bar
plc.SetHoldingRegister(2, 500); // Flow setpoint: 50.0 L/min
plc.SetHoldingRegister(3, 1); // Auto/Manual mode: Auto
// Initialize process variables
for (int i = 0; i < 100; i++)
{
plc.SetInputRegister(i, (ushort)(200 + i)); // Sample values
}
}
private void SetupMotorController(ModbusDeviceBase motor)
{
// Motor controller memory layout:
// Coils 0-9: Motor commands (start, stop, reset, etc.)
// Discrete Inputs 0-9: Motor status (running, fault, ready)
// Holding Registers 0-19: Motor parameters
// Input Registers 0-19: Motor measurements
// Motor parameters
motor.SetHoldingRegister(0, 1500); // Speed setpoint: 1500 RPM
motor.SetHoldingRegister(1, 100); // Acceleration time: 10.0 seconds
motor.SetHoldingRegister(2, 100); // Deceleration time: 10.0 seconds
motor.SetHoldingRegister(3, 800); // Max current: 80.0A
// Motor status
motor.SetDiscreteInput(0, true); // Ready
motor.SetDiscreteInput(1, false); // Running
motor.SetDiscreteInput(2, false); // Fault
// Motor measurements
motor.SetInputRegister(0, 0); // Actual speed: 0 RPM
motor.SetInputRegister(1, 0); // Actual current: 0.0A
motor.SetInputRegister(2, 25); // Temperature: 25Β°C
motor.SetInputRegister(3, 0); // Power: 0 kW
}
private void SetupTemperatureController(ModbusDeviceBase tempCtrl)
{
// Temperature controller memory layout:
// Holding Registers 0-9: Temperature setpoints
// Input Registers 0-9: Temperature measurements
for (int i = 0; i < 10; i++)
{
tempCtrl.SetHoldingRegister(i, 250); // 25.0Β°C setpoint
tempCtrl.SetInputRegister(i, (ushort)(245 + i)); // Varying temperatures
}
}
public async Task StartAsync()
{
if (isRunning) return;
Console.WriteLine("Starting Industrial HMI Simulator...");
foreach (var device in devices)
{
await device.StartAsync();
}
isRunning = true;
Console.WriteLine($"Started {devices.Count} Modbus devices");
Console.WriteLine("Press any key to stop the simulator...");
}
public async Task StopAsync()
{
if (!isRunning) return;
Console.WriteLine("Stopping Industrial HMI Simulator...");
foreach (var device in devices)
{
await device.StopAsync();
}
simulationTimer?.Dispose();
isRunning = false;
Console.WriteLine("Simulator stopped");
}
private void UpdateSimulation(object state)
{
if (!isRunning) return;
try
{
UpdateMainPlcSimulation();
UpdateMotorControllerSimulation();
UpdateTemperatureControllerSimulation();
}
catch (Exception ex)
{
Console.WriteLine($"Simulation update error: {ex.Message}");
}
}
private void UpdateMainPlcSimulation()
{
var plc = devices.FirstOrDefault(d => d.DeviceId == 1);
if (plc == null) return;
var random = new Random();
// Simulate process variables with realistic variations
for (int i = 0; i < 20; i++)
{
var baseValue = 200 + i * 10;
var variation = random.Next(-20, 21); // Β±2.0 variation
plc.SetInputRegister(i, (ushort)Math.Max(0, baseValue + variation));
}
// Simulate some alarm conditions
var temp = plc.GetInputRegister(0);
var tempSetpoint = plc.GetHoldingRegister(0);
var tempAlarm = Math.Abs(temp - tempSetpoint) > 50; // Β±5.0Β°C
plc.SetDiscreteInput(0, tempAlarm);
}
private void UpdateMotorControllerSimulation()
{
var motor = devices.FirstOrDefault(d => d.DeviceId == 2);
if (motor == null) return;
var startCommand = motor.GetCoil(0);
var isRunning = motor.GetDiscreteInput(1);
var currentSpeed = motor.GetInputRegister(0);
var speedSetpoint = motor.GetHoldingRegister(0);
if (startCommand && !isRunning)
{
// Motor starting up
motor.SetDiscreteInput(1, true); // Set running status
motor.SetCoil(0, false); // Clear start command
}
if (isRunning)
{
// Ramp speed towards setpoint
var newSpeed = currentSpeed;
if (currentSpeed < speedSetpoint)
{
newSpeed = (ushort)Math.Min(speedSetpoint, currentSpeed + 50);
}
else if (currentSpeed > speedSetpoint)
{
newSpeed = (ushort)Math.Max(speedSetpoint, currentSpeed - 50);
}
motor.SetInputRegister(0, newSpeed);
motor.SetInputRegister(1, (ushort)(newSpeed * 0.8)); // Current proportional to speed
motor.SetInputRegister(3, (ushort)(newSpeed * 15 / 1000)); // Power calculation
}
}
private void UpdateTemperatureControllerSimulation()
{
var tempCtrl = devices.FirstOrDefault(d => d.DeviceId == 3);
if (tempCtrl == null) return;
var random = new Random();
for (int i = 0; i < 10; i++)
{
var setpoint = tempCtrl.GetHoldingRegister(i);
var current = tempCtrl.GetInputRegister(i);
// Simple temperature control simulation
var error = setpoint - current;
var change = error > 0 ? random.Next(1, 5) : random.Next(-5, 0);
var newTemp = Math.Max(0, Math.Min(1000, current + change));
tempCtrl.SetInputRegister(i, (ushort)newTemp);
}
}
}
// Usage example
public class Program
{
public static async Task Main(string[] args)
{
var simulator = new IndustrialHmiSimulator();
try
{
await simulator.StartAsync();
Console.ReadKey();
}
finally
{
await simulator.StopAsync();
}
}
}
Testing Framework Integration
// Unit testing with Modbus Digital Twin Simulator
[TestClass]
public class ModbusDeviceTests
{
private ModbusTcpDevice device;
private ModbusClient client; // Your Modbus client implementation
[TestInitialize]
public async Task Setup()
{
// Create test device
device = new ModbusTcpDevice(1, 50200); // Use non-standard port for testing
await device.StartAsync();
// Create test client
client = new ModbusClient("127.0.0.1", 50200);
await client.ConnectAsync();
}
[TestCleanup]
public async Task Cleanup()
{
await client?.DisconnectAsync();
await device?.StopAsync();
}
[TestMethod]
public async Task ReadCoils_ValidRange_ReturnsCorrectValues()
{
// Arrange
device.SetCoil(0, true);
device.SetCoil(1, false);
device.SetCoil(2, true);
// Act
var result = await client.ReadCoilsAsync(0, 3);
// Assert
Assert.AreEqual(3, result.Length);
Assert.IsTrue(result[0]);
Assert.IsFalse(result[1]);
Assert.IsTrue(result[2]);
}
[TestMethod]
public async Task WriteHoldingRegister_ValidValue_UpdatesMemory()
{
// Arrange
const ushort testValue = 12345;
// Act
await client.WriteSingleRegisterAsync(100, testValue);
// Assert
var actualValue = device.GetHoldingRegister(100);
Assert.AreEqual(testValue, actualValue);
}
[TestMethod]
public async Task ReadHoldingRegisters_LargeQuantity_ReturnsException()
{
// Act & Assert
await Assert.ThrowsExceptionAsync(
() => client.ReadHoldingRegistersAsync(0, 200)); // Exceeds 125 limit
}
[TestMethod]
public void EventHandling_RequestReceived_FiresCorrectly()
{
// Arrange
ModbusRequestEventArgs receivedArgs = null;
device.OnRequestReceived += (sender, args) => receivedArgs = args;
// Act
var task = client.ReadCoilsAsync(0, 1);
task.Wait(5000); // Wait up to 5 seconds
// Assert
Assert.IsNotNull(receivedArgs);
Assert.AreEqual(ModbusFunctionCode.ReadCoils, receivedArgs.FunctionCode);
Assert.AreEqual(1, receivedArgs.DeviceId);
}
}
Configuration-Driven Device Factory
// Configuration-based device creation
public class ConfigurableModbusSimulator
{
public class DeviceConfig
{
public byte DeviceId { get; set; }
public string Protocol { get; set; } // "TCP" or "RTU"
public int? Port { get; set; } // For TCP
public string? SerialPort { get; set; } // For RTU
public int BaudRate { get; set; } = 9600;
public string Parity { get; set; } = "None";
public MemoryConfig Memory { get; set; } = new();
public List InitialValues { get; set; } = new();
}
public class MemoryConfig
{
public int CoilCount { get; set; } = 1000;
public int DiscreteInputCount { get; set; } = 1000;
public int HoldingRegisterCount { get; set; } = 1000;
public int InputRegisterCount { get; set; } = 1000;
}
public class InitialValue
{
public string Type { get; set; } // "Coil", "DiscreteInput", "HoldingRegister", "InputRegister"
public int Address { get; set; }
public object Value { get; set; }
}
public static async Task> CreateFromConfigAsync(string configFile)
{
var json = await File.ReadAllTextAsync(configFile);
var configs = System.Text.Json.JsonSerializer.Deserialize>(json);
var devices = new List();
foreach (var config in configs)
{
var device = CreateDevice(config);
ConfigureMemory(device, config);
SetInitialValues(device, config);
devices.Add(device);
}
return devices;
}
private static ModbusDeviceBase CreateDevice(DeviceConfig config)
{
return config.Protocol.ToUpper() switch
{
"TCP" => new ModbusTcpDevice(config.DeviceId, config.Port ?? 502),
"RTU" => new ModbusRtuDevice(
config.DeviceId,
config.SerialPort ?? "COM1",
config.BaudRate,
ParseParity(config.Parity),
8,
StopBits.One),
_ => throw new ArgumentException($"Unknown protocol: {config.Protocol}")
};
}
private static void ConfigureMemory(ModbusDeviceBase device, DeviceConfig config)
{
device.Memory.InitializeMemory(
config.Memory.CoilCount,
config.Memory.DiscreteInputCount,
config.Memory.HoldingRegisterCount,
config.Memory.InputRegisterCount);
}
private static void SetInitialValues(ModbusDeviceBase device, DeviceConfig config)
{
foreach (var initialValue in config.InitialValues)
{
switch (initialValue.Type.ToLower())
{
case "coil":
device.SetCoil(initialValue.Address, Convert.ToBoolean(initialValue.Value));
break;
case "discreteinput":
device.SetDiscreteInput(initialValue.Address, Convert.ToBoolean(initialValue.Value));
break;
case "holdingregister":
device.SetHoldingRegister(initialValue.Address, Convert.ToUInt16(initialValue.Value));
break;
case "inputregister":
device.SetInputRegister(initialValue.Address, Convert.ToUInt16(initialValue.Value));
break;
}
}
}
private static Parity ParseParity(string parity)
{
return parity.ToLower() switch
{
"none" => Parity.None,
"even" => Parity.Even,
"odd" => Parity.Odd,
_ => Parity.None
};
}
}
// Example configuration file (devices.json):
/*
[
{
"DeviceId": 1,
"Protocol": "TCP",
"Port": 502,
"Memory": {
"CoilCount": 2000,
"HoldingRegisterCount": 500
},
"InitialValues": [
{ "Type": "HoldingRegister", "Address": 0, "Value": 1234 },
{ "Type": "HoldingRegister", "Address": 1, "Value": 5678 },
{ "Type": "Coil", "Address": 0, "Value": true }
]
},
{
"DeviceId": 2,
"Protocol": "RTU",
"SerialPort": "COM3",
"BaudRate": 19200,
"Parity": "Even"
}
]
*/