2 Protocols
4 Menory Areas
8 Function Codes
100% Standard Compliant
πŸ”
Complete User Manual & Implementation Guide

β˜‘ Table of Contents

β˜‘ Overview

The ULTRAMEGA Modbus Digital Twin Simulator is a comprehensive library for simulating Modbus devices supporting both TCP and RTU protocols. It provides complete implementation of standard Modbus function codes and memory management for testing and development purposes.

What is Modbus Digital Twin Simulator?

  • Device Simulator: Creates virtual Modbus devices for testing
  • Protocol Support: Both Modbus TCP and RTU protocols
  • Complete Implementation: All standard function codes
  • Memory Simulation: Full device memory model
  • Event-Driven: Comprehensive logging and event handling
  • Real-time Data: Dynamic data simulation capabilities

Use Cases

  • Testing Modbus client applications
  • Simulating industrial control systems
  • Training and education
  • Protocol validation and debugging
  • Load testing and performance evaluation
  • Development environment setup

β˜‘ Architecture

The Modbus Digital Twin Simulator library follows a modular, object-oriented architecture designed for easy integration and extensibility:

Public API Components

  • ModbusDeviceBase: Abstract base class for all devices (inherit for custom behavior)
  • ModbusTcpDevice: Ready-to-use TCP/IP implementation
  • ModbusRtuDevice: Ready-to-use serial RTU implementation
  • ModbusMemory: Accessible memory management interface
  • ModbusDeviceFactory: Static factory methods for easy device creation
  • Event Classes: ModbusRequestEventArgs for event handling

Library Features

πŸ“¦ What's Included
  • Complete Implementation: All Modbus functions work out-of-the-box
  • Thread-Safe: All public methods are thread-safe
  • Event-Driven: Comprehensive event system for monitoring
  • Self-Contained: No external dependencies beyond .NET framework
  • Extensible: Inherit from ModbusDeviceBase for custom devices

Protocol Support

πŸ“‘ Supported Protocols
  • Modbus TCP: Ethernet-based communication (Port 502 default)
  • Modbus RTU: Serial communication (RS-232/485)

β˜‘ Key Features

Protocol Features

  • Dual Protocol Support: TCP and RTU in same codebase
  • Standard Compliance: Full Modbus specification compliance
  • Multiple Clients: TCP supports multiple simultaneous connections
  • CRC Validation: Automatic CRC16 calculation for RTU
  • Error Handling: Complete exception code support

Memory Management

  • Four Memory Areas: Coils, Discrete Inputs, Holding Registers, Input Registers
  • Dynamic Allocation: Configurable memory sizes
  • Direct Access: Public API for memory manipulation
  • Data Simulation: Built-in data generation

Development Features

  • Event-Driven: Comprehensive event system
  • Logging Support: Built-in message logging
  • Asynchronous: Full async/await pattern support
  • Cancellation Support: Graceful shutdown with CancellationToken

β˜‘ Supported Function Codes

Code Name Description Memory Area
0x01Read CoilsRead 1-2000 coil valuesCoils (Read/Write)
0x02Read Discrete InputsRead 1-2000 discrete input valuesDiscrete Inputs (Read Only)
0x03Read Holding RegistersRead 1-125 holding register valuesHolding Registers (Read/Write)
0x04Read Input RegistersRead 1-125 input register valuesInput Registers (Read Only)
0x05Write Single CoilWrite single coil valueCoils
0x06Write Single RegisterWrite single holding registerHolding Registers
0x0FWrite Multiple CoilsWrite 1-1968 coil valuesCoils
0x10Write Multiple RegistersWrite 1-123 holding registersHolding Registers

β˜‘ Memory Model

The Modbus Digital Twin Simulator implements the standard Modbus memory model with four distinct areas:

Memory Area Data Type Access Size Usage
CoilsBooleanRead/Write1-65536 bitsDigital outputs, relays
Discrete InputsBooleanRead Only1-65536 bitsDigital inputs, switches
Holding Registers16-bit WordRead/Write1-65536 wordsAnalog outputs, parameters
Input Registers16-bit WordRead Only1-65536 wordsAnalog inputs, measurements
πŸ’‘ Memory Addressing

Modbus Digital Twin Simulator uses zero-based addressing internally. Address 0 in the simulator corresponds to Modbus address 1 in the protocol.

Getting Started Guide Quick Start

β˜‘ Installation & Setup

Get up and running with Modbus Digital Twin Simulator in minutes.

Prerequisites

  • .NET Framework 4.7.2 or later / .NET Core 3.1 or later
  • System.IO.Ports package (automatically referenced for RTU support)
  • Network access (for TCP testing)
  • Serial port access (for RTU testing)

Library Installation

// 1. Add reference to ULTRAMEGA.Modbus.DTS.dll in your project
// 2. Include the necessary using statement in your code

using System;
using System.Threading.Tasks;
using ULTRAMEGA.Modbus.DTS;

// The library includes all required dependencies
// No additional packages need to be installed manually

Adding the Library Reference

πŸ“¦ Installation Methods
  • Visual Studio: Right-click References β†’ Add Reference β†’ Browse to ULTRAMEGA.Modbus.DTS.dll
  • Package Manager: Install via NuGet if available in your repository
  • .NET CLI: dotnet add reference path/to/ULTRAMEGA.Modbus.DTS.dll
  • Project File: Add <Reference Include="ULTRAMEGA.Modbus.DTS" />

β˜‘ Your First Modbus Device

Let's create your first Modbus TCP device simulator:

// Create a simple TCP device
var tcpDevice = ModbusDeviceFactory.CreateTcpDevice(deviceId: 1, port: 502);

// Set up event handlers for logging
tcpDevice.OnLogMessage += (sender, message) => 
{
    Console.WriteLine($"[TCP] {message}");
};

tcpDevice.OnRequestReceived += (sender, args) => 
{
    Console.WriteLine($"Received request: Device {args.DeviceId}, Function {args.FunctionCode}");
};

// Start the device
await tcpDevice.StartAsync();
Console.WriteLine("Modbus TCP device is running on port 502...");

// Keep running until key press
Console.ReadKey();

// Clean shutdown
await tcpDevice.StopAsync();

β˜‘ Your First RTU Device

Create a Modbus RTU device on a serial port:

// Create RTU device on COM3
var rtuDevice = ModbusDeviceFactory.CreateRtuDevice(
    deviceId: 2, 
    portName: "COM3", 
    baudRate: 9600, 
    parity: Parity.None, 
    dataBits: 8, 
    stopBits: StopBits.One
);

// Set up logging
rtuDevice.OnLogMessage += (sender, message) => 
{
    Console.WriteLine($"[RTU] {message}");
};

// Start the device
await rtuDevice.StartAsync();
Console.WriteLine("Modbus RTU device is running on COM3...");

// Keep running
Console.ReadKey();

// Clean shutdown
await rtuDevice.StopAsync();

β˜‘ Setting Initial Values

Configure your device with initial data:

// Set some initial coil values
device.SetCoil(0, true);    // Coil 0 = ON
device.SetCoil(1, false);   // Coil 1 = OFF

// Set holding register values
device.SetHoldingRegister(0, 1234);   // Register 0 = 1234
device.SetHoldingRegister(1, 5678);   // Register 1 = 5678

// Set input register values (sensor readings)
device.SetInputRegister(0, 250);     // Temperature: 25.0Β°C
device.SetInputRegister(1, 1013);    // Pressure: 1013 mbar

// Set discrete inputs (switches/sensors)
device.SetDiscreteInput(0, true);    // Door sensor: OPEN
device.SetDiscreteInput(1, false);   // Emergency stop: OK

β˜‘ Testing Your Device

Use any Modbus client to test your simulator:

πŸ“‹ Recommended Test Tools
  • QModMaster: Free, cross-platform Modbus master
  • ModbusPoll: Professional Windows Modbus tester
  • pyModbusTCP: Python library for quick testing
  • modbus-cli: Command-line testing tool

Basic Test Sequence

  1. Connect: Point your client to 127.0.0.1:502 (TCP) or your COM port (RTU)
  2. Read Coils: Function 0x01, Address 0, Count 10
  3. Read Registers: Function 0x03, Address 0, Count 10
  4. Write Values: Function 0x06, Address 0, Value 9999
  5. Verify: Read back to confirm write operations
πŸ’‘ Quick Test

The library includes built-in data generation. Input registers and discrete inputs change automatically to simulate live sensor data.

β˜‘ Deployment Considerations

When deploying applications that use Modbus Digital Twin Simulator:

Required Files

  • ULTRAMEGA.Modbus.DTS.dll - Main library file
  • System.IO.Ports.dll - Required for RTU support (if used)
  • Your application files - Your compiled application

Deployment Options

Method Description Best For
Copy LocalInclude DLL in application folderSimple deployments
GAC InstallInstall in Global Assembly CacheSystem-wide access
NuGet PackageDistribute via private NuGet feedTeam/enterprise use
MSI InstallerInclude in installer packageProfessional distribution
⚠️ Licensing Note

Ensure you have proper licensing for distribution. Contact ULTRAMEGA for commercial deployment licensing if required.

TCP Implementation Guide Modbus TCP

β˜‘ Modbus TCP Overview

Modbus TCP encapsulates Modbus protocol in TCP/IP packets, making it suitable for Ethernet networks.

TCP Frame Format

Field Size Description Value
Transaction ID2 bytesClient-generated identifier0x0000-0xFFFF
Protocol ID2 bytesAlways 0 for Modbus0x0000
Length2 bytesRemaining bytes in frameVariable
Unit ID1 byteDevice address1-255
Function Code1 byteModbus function0x01-0x10
DataVariableFunction-specific dataVaries

β˜‘ Creating TCP Devices

Basic TCP Device

// Create TCP device with default settings
var tcpDevice = new ModbusTcpDevice(deviceId: 1, port: 502);

// Or use the factory
var tcpDevice = ModbusDeviceFactory.CreateTcpDevice(deviceId: 1, port: 502);

Advanced TCP Configuration

// Create device with custom settings
var tcpDevice = new ModbusTcpDevice(deviceId: 10, port: 5020);

// Set up comprehensive event handling
tcpDevice.OnLogMessage += (sender, message) => 
{
    var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
    Console.WriteLine($"[{timestamp}] {message}");
};

tcpDevice.OnRequestReceived += (sender, args) => 
{
    Console.WriteLine($"Request from client: " +
                     $"Device={args.DeviceId}, " +
                     $"Function={args.FunctionCode}, " +
                     $"Time={DateTime.Now}");
    
    // Log raw request data if needed
    Console.WriteLine($"Raw data: {BitConverter.ToString(args.Request)}");
};

// Initialize with custom memory layout
tcpDevice.Memory.InitializeMemory(
    coilCount: 2000,
    discreteInputCount: 2000,
    holdingRegisterCount: 1000,
    inputRegisterCount: 1000
);

β˜‘ Connection Management

ModbusTcpDevice supports multiple simultaneous client connections:

// Start the TCP server
await tcpDevice.StartAsync();

// The device will:
// 1. Listen on the specified port
// 2. Accept multiple client connections
// 3. Handle each client in a separate task
// 4. Process requests concurrently
// 5. Maintain connection state per client

// Monitor connection events through logging
tcpDevice.OnLogMessage += (sender, message) => 
{
    if (message.Contains("Client connected"))
    {
        // New client connected
        Console.WriteLine("New client connected");
    }
    else if (message.Contains("Client disconnected"))
    {
        // Client disconnected
        Console.WriteLine("Client disconnected");
    }
};

TCP-Specific Features

  • Multiple Clients: Handles concurrent connections
  • No CRC: TCP handles error detection
  • MBAP Header: Automatic header processing
  • Keep-Alive: Maintains persistent connections
  • Broadcast Support: Device ID 0 for broadcast messages

β˜‘ Error Handling

// Robust error handling
try 
{
    await tcpDevice.StartAsync();
    Console.WriteLine("TCP device started successfully");
}
catch (SocketException ex)
{
    Console.WriteLine($"Socket error: {ex.Message}");
    // Port might be in use
}
catch (Exception ex)
{
    Console.WriteLine($"Unexpected error: {ex.Message}");
}

// Graceful shutdown with timeout
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try 
{
    await tcpDevice.StopAsync();
}
catch (OperationCanceledException)
{
    Console.WriteLine("Shutdown timed out");
}

β˜‘ Performance Considerations

⚑ TCP Performance Tips
  • Port Selection: Use standard port 502 for compatibility
  • Connection Pooling: Clients should reuse connections
  • Request Batching: Use multiple register reads when possible
  • Memory Pre-allocation: Initialize memory to expected size
  • Event Handler Efficiency: Keep event handlers lightweight

Monitoring TCP Performance

// Track performance metrics
private int requestCount = 0;
private DateTime startTime = DateTime.Now;

tcpDevice.OnRequestReceived += (sender, args) => 
{
    Interlocked.Increment(ref requestCount);
    
    if (requestCount % 1000 == 0)
    {
        var elapsed = DateTime.Now - startTime;
        var requestsPerSecond = requestCount / elapsed.TotalSeconds;
        Console.WriteLine($"Performance: {requestsPerSecond:F1} requests/second");
    }
};
RTU Implementation Guide Modbus RTU

β˜‘ Modbus RTU Overview

Modbus RTU (Remote Terminal Unit) is the serial implementation of Modbus protocol, commonly used in industrial automation.

RTU Frame Format

Field Size Description Notes
Device Address1 byteTarget device ID1-255 (0 = broadcast)
Function Code1 byteModbus function0x01-0x10
DataVariableFunction-specific data0-252 bytes
CRC-162 bytesError checkingLow byte first

β˜‘ Creating RTU Devices

Basic RTU Device

// Create RTU device with default settings (9600, N, 8, 1)
var rtuDevice = ModbusDeviceFactory.CreateRtuDevice(
    deviceId: 1, 
    portName: "COM3"
);

// Start the device
await rtuDevice.StartAsync();

Advanced RTU Configuration

// Create RTU device with custom serial parameters
var rtuDevice = new ModbusRtuDevice(
    deviceId: 5,
    portName: "COM1",
    baudRate: 19200,
    parity: Parity.Even,
    dataBits: 8,
    stopBits: StopBits.One
);

// Configure timeouts
rtuDevice.ReadTimeout = 1000;   // 1 second read timeout
rtuDevice.WriteTimeout = 1000;  // 1 second write timeout

// Set up event handlers
rtuDevice.OnLogMessage += (sender, message) => 
{
    Console.WriteLine($"[RTU-{DateTime.Now:HH:mm:ss}] {message}");
};

rtuDevice.OnRequestReceived += (sender, args) => 
{
    Console.WriteLine($"RTU Request: Device {args.DeviceId}, " +
                     $"Function {args.FunctionCode:X2}");
};

β˜‘ Serial Port Configuration

Common Serial Settings

Parameter Common Values Default Notes
Baud Rate9600, 19200, 38400, 57600, 1152009600Must match network setting
ParityNone, Even, OddNoneError detection method
Data Bits7, 88Character frame size
Stop Bits1, 21Frame termination

Serial Port Validation

// Validate serial port before creating device
public static bool ValidateSerialPort(string portName)
{
    try 
    {
        // Check if port exists
        var availablePorts = SerialPort.GetPortNames();
        if (!availablePorts.Contains(portName))
        {
            Console.WriteLine($"Port {portName} not found.");
            Console.WriteLine($"Available ports: {string.Join(", ", availablePorts)}");
            return false;
        }

        // Test port access
        using (var testPort = new SerialPort(portName))
        {
            testPort.Open();
            testPort.Close();
        }

        return true;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Port validation failed: {ex.Message}");
        return false;
    }
}

// Use validation before creating device
if (ValidateSerialPort("COM3"))
{
    var rtuDevice = ModbusDeviceFactory.CreateRtuDevice(1, "COM3");
}

β˜‘ CRC-16 Implementation

RTU uses CRC-16 for error detection. The simulator handles this automatically:

// CRC-16 calculation is automatic, but here's how it works:
protected static ushort CalculateCRC16(byte[] data, int length)
{
    ushort crc = 0xFFFF;
    for (int i = 0; i < length; i++)
    {
        crc ^= data[i];
        for (int j = 0; j < 8; j++)
        {
            if ((crc & 0x0001) == 1)
                crc = (ushort)((crc >> 1) ^ 0xA001);
            else
                crc >>= 1;
        }
    }
    return crc;
}

// The simulator automatically:
// 1. Validates incoming CRC
// 2. Calculates outgoing CRC
// 3. Rejects frames with bad CRC
// 4. Logs CRC errors

β˜‘ RTU Timing Requirements

Modbus RTU has specific timing requirements:

⏱️ RTU Timing Rules
  • Inter-frame Gap: 3.5 character times silence between frames
  • Inter-character Gap: Maximum 1.5 character times within frame
  • Response Timeout: Slave must respond within timeout period
  • Turnaround Delay: Minimum delay before slave can transmit
// RTU timing is handled automatically, but you can monitor it:
rtuDevice.OnLogMessage += (sender, message) => 
{
    if (message.Contains("CRC error"))
    {
        Console.WriteLine("CRC error - possible timing issue");
    }
    else if (message.Contains("frame"))
    {
        Console.WriteLine($"Frame processed: {message}");
    }
};

β˜‘ Multi-drop Networks

RTU supports multi-drop networks with multiple devices on one serial line:

// Create multiple RTU devices on same port (simulation)
// In real deployment, each device would be on separate hardware

var devices = new List();

for (byte deviceId = 1; deviceId <= 5; deviceId++)
{
    var device = new ModbusRtuDevice(
        deviceId: deviceId,
        portName: $"COM{deviceId}", // Each on different port for simulation
        baudRate: 9600
    );
    
    // Configure device-specific data
    device.SetHoldingRegister(0, (ushort)(1000 + deviceId * 100));
    device.SetInputRegister(0, (ushort)(deviceId * 10)); // Simulated sensor
    
    devices.Add(device);
}

// Start all devices
foreach (var device in devices)
{
    await device.StartAsync();
}
Function Codes Reference 8 Functions

β˜‘ Complete Function Code Implementation

Modbus Digital Twin Simulator supports all standard Modbus function codes with full validation and error handling.

Read Functions (0x01-0x04)
Function Code Description Address Range Quantity Limits
Read Coils 0x01 Read 1 to 2000 contiguous coils 0x0000-0xFFFF 1-2000
Read Discrete Inputs 0x02 Read 1 to 2000 contiguous discrete inputs 0x0000-0xFFFF 1-2000
Read Holding Registers 0x03 Read 1 to 125 contiguous holding registers 0x0000-0xFFFF 1-125
Read Input Registers 0x04 Read 1 to 125 contiguous input registers 0x0000-0xFFFF 1-125
Write Functions (0x05-0x06, 0x0F-0x10)
Function Code Description Data Format Quantity Limits
Write Single Coil 0x05 Write single coil value 0x0000 (OFF) or 0xFF00 (ON) 1
Write Single Register 0x06 Write single holding register 16-bit value (0x0000-0xFFFF) 1
Write Multiple Coils 0x0F Write multiple coils Packed bit array 1-1968
Write Multiple Registers 0x10 Write multiple holding registers Array of 16-bit values 1-123

β˜‘ Function Code Examples

Read Coils (0x01)

// Set up some coil values for testing
device.SetCoil(0, true);    // Address 0 = ON
device.SetCoil(1, false);   // Address 1 = OFF  
device.SetCoil(2, true);    // Address 2 = ON
device.SetCoil(3, false);   // Address 3 = OFF

// Client request: Read 4 coils starting at address 0
// Request: [Device ID][0x01][Start Hi][Start Lo][Qty Hi][Qty Lo][CRC]
// Example: [01][01][00][00][00][04][CRC]

// Response will contain:
// - 1 byte count (number of data bytes)
// - Packed coil values: 0x05 (binary: 00000101 = coils 0,2 ON)

// Manual testing with coils
for (int i = 0; i < 16; i++)
{
    device.SetCoil(i, i % 3 == 0); // Every third coil ON
}

// This creates pattern: ON,OFF,OFF,ON,OFF,OFF,ON,OFF,OFF...

Read Holding Registers (0x03)

// Set up register values
device.SetHoldingRegister(0, 1234);   // Temperature: 12.34Β°C
device.SetHoldingRegister(1, 5678);   // Pressure: 56.78 bar
device.SetHoldingRegister(2, 9999);   // Flow rate: 99.99 L/min

// Client request: Read 3 registers starting at address 0
// Response contains:
// - 1 byte count (6 bytes for 3 registers)
// - 6 bytes of register data (high byte first)

// Example register patterns
device.SetHoldingRegister(100, 0x1234); // Hex value
device.SetHoldingRegister(101, 65535);   // Maximum value
device.SetHoldingRegister(102, 0);       // Minimum value

Write Single Coil (0x05)

// Function 0x05 automatically validates the coil value
// Valid values: 0x0000 (OFF) or 0xFF00 (ON)
// Any other value returns exception 0x03 (Illegal Data Value)

// Test the validation:
device.OnRequestReceived += (sender, args) => 
{
    if (args.FunctionCode == ModbusFunctionCode.WriteSingleCoil)
    {
        Console.WriteLine("Write Single Coil request received");
        
        // The device automatically validates the value
        // and sets the coil if valid
    }
};

// The response echoes the request if successful
// or returns an exception response if invalid

Write Multiple Registers (0x10)

// Function 0x10 writes multiple contiguous registers
// Validates byte count matches register count

// Monitor multiple register writes
device.OnRequestReceived += (sender, args) => 
{
    if (args.FunctionCode == ModbusFunctionCode.WriteMultipleRegisters)
    {
        Console.WriteLine("Write Multiple Registers request");
        
        // After write, read back the values to verify
        Task.Run(async () => 
        {
            await Task.Delay(100); // Let write complete
            
            // Log updated register values
            for (int i = 0; i < 10; i++)
            {
                var value = device.GetHoldingRegister(i);
                Console.WriteLine($"Register {i}: {value}");
            }
        });
    }
};

β˜‘ Exception Codes

Modbus Digital Twin Simulator implements complete exception handling:

Code Name Description Common Causes
0x01Illegal FunctionFunction code not supportedWrong function code
0x02Illegal Data AddressAddress not availableAddress out of range
0x03Illegal Data ValueInvalid data valueWrong coil value, count too large
0x04Slave Device FailureInternal device errorException during processing
0x05AcknowledgeLong operation in progressNot used in simulator
0x06Slave Device BusyDevice cannot process nowNot used in simulator
⚠️ Exception Response Format

Exception responses have the high bit set in the function code (e.g., 0x81 for Read Coils exception) followed by the exception code.

Memory Management Guide 4 Memory Areas

β˜‘ Memory Architecture

Modbus Digital Twin Simulator implements the standard Modbus memory model with four distinct areas, each optimized for specific data types and access patterns.

Memory Area Overview

Area Data Type Access Function Codes Typical Use
Coils Boolean Read/Write 0x01, 0x05, 0x0F Digital outputs, relays, valves
Discrete Inputs Boolean Read Only 0x02 Digital inputs, switches, sensors
Holding Registers 16-bit Word Read/Write 0x03, 0x06, 0x10 Analog outputs, setpoints, config
Input Registers 16-bit Word Read Only 0x04 Analog inputs, measurements

β˜‘ Memory Initialization

Default Initialization

// Modbus Digital Twin Simulator automatically initializes memory with default sizes
var device = new ModbusTcpDevice(deviceId: 1, port: 502);

// Default initialization creates:
// - 1000 Coils (all false)
// - 1000 Discrete Inputs (all false) 
// - 1000 Holding Registers (all 0)
// - 1000 Input Registers (sample data: i % 65536)

Custom Memory Configuration

// Configure custom memory sizes
device.Memory.InitializeMemory(
    coilCount: 2000,              // 2000 coils
    discreteInputCount: 1500,     // 1500 discrete inputs
    holdingRegisterCount: 500,    // 500 holding registers
    inputRegisterCount: 800       // 800 input registers
);

// Or access memory properties directly
var memory = device.Memory;
Console.WriteLine($"Coils: {memory.Coils.Count}");
Console.WriteLine($"Discrete Inputs: {memory.DiscreteInputs.Count}");
Console.WriteLine($"Holding Registers: {memory.HoldingRegisters.Count}");
Console.WriteLine($"Input Registers: {memory.InputRegisters.Count}");

β˜‘ Memory Access Methods

Direct Memory Access

// Set individual values
device.SetCoil(0, true);                    // Coil 0 = ON
device.SetDiscreteInput(0, false);          // Input 0 = OFF
device.SetHoldingRegister(0, 1234);         // Register 0 = 1234
device.SetInputRegister(0, 5678);           // Input reg 0 = 5678

// Read individual values
bool coilValue = device.GetCoil(0);                    // Read coil 0
bool inputValue = device.GetDiscreteInput(0);          // Read input 0
ushort holdingValue = device.GetHoldingRegister(0);    // Read holding reg 0
ushort inputRegValue = device.GetInputRegister(0);     // Read input reg 0

// Handle non-existent addresses gracefully
// Returns false for non-existent coils/inputs
// Returns 0 for non-existent registers

Bulk Memory Operations

// Set multiple coils
for (int i = 0; i < 10; i++)
{
    device.SetCoil(i, i % 2 == 0); // Even addresses ON, odd OFF
}

// Initialize holding registers with pattern
for (int i = 0; i < 100; i++)
{
    device.SetHoldingRegister(i, (ushort)(1000 + i * 10));
}

// Set up sensor simulation data
for (int i = 0; i < 20; i++)
{
    // Temperature sensors: 200-250 (20.0-25.0Β°C)
    device.SetInputRegister(i, (ushort)(200 + i * 2));
    
    // Corresponding discrete inputs for alarm states
    device.SetDiscreteInput(i, false); // No alarms initially
}

β˜‘ Memory Validation

Address Range Checking

// The simulator automatically validates addresses during Modbus operations
// but you can also check programmatically

public static bool IsValidCoilAddress(ModbusDeviceBase device, int address)
{
    return device.Memory.Coils.ContainsKey(address);
}

public static bool IsValidRegisterAddress(ModbusDeviceBase device, int address)
{
    return device.Memory.HoldingRegisters.ContainsKey(address);
}

// Safe memory access with validation
public static void SafeSetCoil(ModbusDeviceBase device, int address, bool value)
{
    if (address >= 0 && address < 65536)
    {
        device.SetCoil(address, value);
        Console.WriteLine($"Set coil {address} to {value}");
    }
    else
    {
        Console.WriteLine($"Invalid coil address: {address}");
    }
}

β˜‘ Data Simulation Features

Built-in Data Simulation

// Modbus Digital Twin Simulator includes automatic data simulation
// Override SimulateData() for custom behavior

public class CustomTcpDevice : ModbusTcpDevice
{
    private Random random = new Random();
    private int simulationStep = 0;

    public CustomTcpDevice(byte deviceId, int port) : base(deviceId, port) { }

    public override void SimulateData()
    {
        simulationStep++;

        // Simulate temperature sensors (registers 0-9)
        for (int i = 0; i < 10; i++)
        {
            // Temperature range: 18.0-28.0Β°C (180-280 in 0.1Β°C units)
            var temp = 200 + (int)(50 * Math.Sin(simulationStep * 0.1 + i));
            SetInputRegister(i, (ushort)Math.Max(180, Math.Min(280, temp)));
        }

        // Simulate digital inputs (door sensors, switches)
        for (int i = 0; i < 8; i++)
        {
            // Random state changes with 5% probability
            if (random.NextDouble() < 0.05)
            {
                var currentState = GetDiscreteInput(i);
                SetDiscreteInput(i, !currentState);
            }
        }

        // Simulate alarm coils based on temperature
        for (int i = 0; i < 10; i++)
        {
            var temp = GetInputRegister(i);
            var alarm = temp > 270 || temp < 190; // Alarm if outside 19-27Β°C
            SetCoil(i + 100, alarm); // Alarm coils at offset 100
        }

        base.SimulateData(); // Call base implementation
    }
}

External Data Integration

// Integrate with external data sources
public class DatabaseConnectedDevice : ModbusTcpDevice
{
    private Timer dataUpdateTimer;

    public DatabaseConnectedDevice(byte deviceId, int port) : base(deviceId, port)
    {
        // Update from database every 5 seconds
        dataUpdateTimer = new Timer(UpdateFromDatabase, null, 
                                   TimeSpan.Zero, TimeSpan.FromSeconds(5));
    }

    private async void UpdateFromDatabase(object state)
    {
        try
        {
            // Example: Read from database
            var sensorData = await GetSensorDataFromDatabase();
            
            foreach (var sensor in sensorData)
            {
                SetInputRegister(sensor.Address, sensor.Value);
                SetDiscreteInput(sensor.Address, sensor.Value > sensor.AlarmThreshold);
            }
        }
        catch (Exception ex)
        {
            LogMessage($"Database update failed: {ex.Message}");
        }
    }

    private async Task> GetSensorDataFromDatabase()
    {
        // Implement your database query here
        // Return sensor data from your database
        return new List();
    }
}

public class SensorData
{
    public int Address { get; set; }
    public ushort Value { get; set; }
    public ushort AlarmThreshold { get; set; }
}

β˜‘ Memory Performance Optimization

⚑ Memory Performance Tips
  • Pre-allocate: Initialize memory to expected maximum size
  • Batch Operations: Group multiple memory updates together
  • Efficient Simulation: Keep SimulateData() method lightweight
  • Address Validation: Validate addresses once, not repeatedly
  • Dictionary Access: ContainsKey() is O(1) for address checking
// Efficient bulk memory updates
public static void BulkUpdateRegisters(ModbusDeviceBase device, 
                                     int startAddress, ushort[] values)
{
    for (int i = 0; i < values.Length; i++)
    {
        device.SetHoldingRegister(startAddress + i, values[i]);
    }
}

// Efficient memory pattern generation
public static void GenerateTestPattern(ModbusDeviceBase device)
{
    // Generate test patterns efficiently
    Parallel.For(0, 1000, i =>
    {
        device.SetCoil(i, (i & 1) == 0);                    // Alternating pattern
        device.SetHoldingRegister(i, (ushort)(i * 10));     // Linear sequence
    });
}
Event Handling & Logging Event System

β˜‘ Event System Overview

Modbus Digital Twin Simulator provides a comprehensive event system for monitoring device activity, debugging, and integration with external systems.

Available Events

Event Event Args Trigger Usage
OnLogMessage string message All device activities General logging and debugging
OnRequestReceived ModbusRequestEventArgs Modbus request received Request monitoring and analysis

β˜‘ Event Handler Implementation

Basic Event Handling

// Set up basic event handlers
var device = ModbusDeviceFactory.CreateTcpDevice(1, 502);

// Log message handler - for general logging
device.OnLogMessage += (sender, message) => 
{
    var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
    Console.WriteLine($"[{timestamp}] {message}");
};

// Request received handler - for monitoring requests
device.OnRequestReceived += (sender, args) => 
{
    Console.WriteLine($"Request: Device={args.DeviceId}, " +
                     $"Function={args.FunctionCode}, " +
                     $"Time={DateTime.Now:HH:mm:ss.fff}");
};

Advanced Event Handling

// Advanced event handling with logging levels and filtering
public class ModbusLogger
{
    private readonly string logFile;
    private readonly object lockObject = new object();

    public ModbusLogger(string logFile)
    {
        this.logFile = logFile;
    }

    public void AttachToDevice(ModbusDeviceBase device)
    {
        device.OnLogMessage += HandleLogMessage;
        device.OnRequestReceived += HandleRequestReceived;
    }

    private void HandleLogMessage(object sender, string message)
    {
        var level = DetermineLogLevel(message);
        var formattedMessage = FormatLogMessage(level, message);
        
        // Console output with color coding
        ConsoleColor color = level switch
        {
            LogLevel.Error => ConsoleColor.Red,
            LogLevel.Warning => ConsoleColor.Yellow,
            LogLevel.Info => ConsoleColor.White,
            LogLevel.Debug => ConsoleColor.Gray,
            _ => ConsoleColor.White
        };

        lock (lockObject)
        {
            Console.ForegroundColor = color;
            Console.WriteLine(formattedMessage);
            Console.ResetColor();
            
            // Write to file
            File.AppendAllText(logFile, formattedMessage + Environment.NewLine);
        }
    }

    private void HandleRequestReceived(object sender, ModbusRequestEventArgs args)
    {
        var requestInfo = new
        {
            Timestamp = DateTime.Now,
            DeviceId = args.DeviceId,
            FunctionCode = args.FunctionCode,
            FunctionName = GetFunctionName(args.FunctionCode),
            RequestLength = args.Request.Length,
            RequestHex = BitConverter.ToString(args.Request)
        };

        var json = System.Text.Json.JsonSerializer.Serialize(requestInfo, 
            new JsonSerializerOptions { WriteIndented = true });
        
        File.AppendAllText($"{logFile}.requests", json + Environment.NewLine);
    }

    private LogLevel DetermineLogLevel(string message)
    {
        if (message.Contains("error", StringComparison.OrdinalIgnoreCase) ||
            message.Contains("exception", StringComparison.OrdinalIgnoreCase))
            return LogLevel.Error;
        
        if (message.Contains("warning", StringComparison.OrdinalIgnoreCase) ||
            message.Contains("CRC error", StringComparison.OrdinalIgnoreCase))
            return LogLevel.Warning;
        
        if (message.Contains("started", StringComparison.OrdinalIgnoreCase) ||
            message.Contains("stopped", StringComparison.OrdinalIgnoreCase) ||
            message.Contains("connected", StringComparison.OrdinalIgnoreCase))
            return LogLevel.Info;
        
        return LogLevel.Debug;
    }

    private string FormatLogMessage(LogLevel level, string message)
    {
        return $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {message}";
    }

    private string GetFunctionName(ModbusFunctionCode functionCode)
    {
        return functionCode switch
        {
            ModbusFunctionCode.ReadCoils => "Read Coils",
            ModbusFunctionCode.ReadDiscreteInputs => "Read Discrete Inputs",
            ModbusFunctionCode.ReadHoldingRegisters => "Read Holding Registers",
            ModbusFunctionCode.ReadInputRegisters => "Read Input Registers",
            ModbusFunctionCode.WriteSingleCoil => "Write Single Coil",
            ModbusFunctionCode.WriteSingleRegister => "Write Single Register",
            ModbusFunctionCode.WriteMultipleCoils => "Write Multiple Coils",
            ModbusFunctionCode.WriteMultipleRegisters => "Write Multiple Registers",
            _ => $"Unknown (0x{(byte)functionCode:X2})"
        };
    }
}

public enum LogLevel
{
    Debug,
    Info,
    Warning,
    Error
}

β˜‘ Performance Monitoring

// Performance monitoring through events
public class ModbusPerformanceMonitor
{
    private int totalRequests = 0;
    private int errorCount = 0;
    private DateTime startTime = DateTime.Now;
    private readonly Dictionary functionCounts = new();
    private readonly Timer reportTimer;

    public ModbusPerformanceMonitor(ModbusDeviceBase device)
    {
        device.OnRequestReceived += TrackRequest;
        device.OnLogMessage += TrackErrors;
        
        // Report statistics every 30 seconds
        reportTimer = new Timer(ReportStatistics, null, 
                               TimeSpan.FromSeconds(30), 
                               TimeSpan.FromSeconds(30));
    }

    private void TrackRequest(object sender, ModbusRequestEventArgs args)
    {
        Interlocked.Increment(ref totalRequests);
        
        lock (functionCounts)
        {
            if (functionCounts.ContainsKey(args.FunctionCode))
                functionCounts[args.FunctionCode]++;
            else
                functionCounts[args.FunctionCode] = 1;
        }
    }

    private void TrackErrors(object sender, string message)
    {
        if (message.Contains("error", StringComparison.OrdinalIgnoreCase) ||
            message.Contains("exception", StringComparison.OrdinalIgnoreCase))
        {
            Interlocked.Increment(ref errorCount);
        }
    }

    private void ReportStatistics(object state)
    {
        var elapsed = DateTime.Now - startTime;
        var requestsPerSecond = totalRequests / elapsed.TotalSeconds;
        var errorRate = totalRequests > 0 ? (errorCount * 100.0 / totalRequests) : 0;

        Console.WriteLine("═══ MODBUS PERFORMANCE REPORT ═══");
        Console.WriteLine($"Uptime: {elapsed:hh\\:mm\\:ss}");
        Console.WriteLine($"Total Requests: {totalRequests:N0}");
        Console.WriteLine($"Requests/Second: {requestsPerSecond:F2}");
        Console.WriteLine($"Error Rate: {errorRate:F2}%");
        Console.WriteLine($"Errors: {errorCount:N0}");
        
        Console.WriteLine("\nFunction Code Distribution:");
        lock (functionCounts)
        {
            foreach (var kvp in functionCounts.OrderByDescending(x => x.Value))
            {
                var percentage = (kvp.Value * 100.0) / totalRequests;
                Console.WriteLine($"  {kvp.Key}: {kvp.Value:N0} ({percentage:F1}%)");
            }
        }
        Console.WriteLine("════════════════════════════════");
    }

    public void Dispose()
    {
        reportTimer?.Dispose();
    }
}

β˜‘ Integration Examples

Database Integration

// Log events to database for analysis
public class DatabaseEventLogger
{
    private readonly string connectionString;

    public DatabaseEventLogger(string connectionString)
    {
        this.connectionString = connectionString;
        InitializeDatabase();
    }

    public void AttachToDevice(ModbusDeviceBase device, string deviceName)
    {
        device.OnRequestReceived += (sender, args) => 
            LogRequestAsync(deviceName, args);
    }

    private async void LogRequestAsync(string deviceName, ModbusRequestEventArgs args)
    {
        try
        {
            using var connection = new SqlConnection(connectionString);
            await connection.OpenAsync();

            var sql = @"
                INSERT INTO ModbusRequests 
                (Timestamp, DeviceName, DeviceId, FunctionCode, RequestLength) 
                VALUES (@Timestamp, @DeviceName, @DeviceId, @FunctionCode, @RequestLength)";

            using var command = new SqlCommand(sql, connection);
            command.Parameters.AddWithValue("@Timestamp", DateTime.Now);
            command.Parameters.AddWithValue("@DeviceName", deviceName);
            command.Parameters.AddWithValue("@DeviceId", args.DeviceId);
            command.Parameters.AddWithValue("@FunctionCode", (byte)args.FunctionCode);
            command.Parameters.AddWithValue("@RequestLength", args.Request.Length);

            await command.ExecuteNonQueryAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Database logging failed: {ex.Message}");
        }
    }

    private void InitializeDatabase()
    {
        // Create table if it doesn't exist
        var createTable = @"
            IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='ModbusRequests' AND xtype='U')
            CREATE TABLE ModbusRequests (
                Id bigint IDENTITY(1,1) PRIMARY KEY,
                Timestamp datetime2 NOT NULL,
                DeviceName nvarchar(100) NOT NULL,
                DeviceId tinyint NOT NULL,
                FunctionCode tinyint NOT NULL,
                RequestLength int NOT NULL
            )";

        using var connection = new SqlConnection(connectionString);
        connection.Open();
        using var command = new SqlCommand(createTable, connection);
        command.ExecuteNonQuery();
    }
}

Webhook Integration

// Send events to external systems via webhooks
public class WebhookEventForwarder
{
    private readonly HttpClient httpClient;
    private readonly string webhookUrl;
    private readonly Queue eventQueue = new();
    private readonly Timer batchTimer;

    public WebhookEventForwarder(string webhookUrl)
    {
        this.webhookUrl = webhookUrl;
        this.httpClient = new HttpClient();
        
        // Send batched events every 10 seconds
        batchTimer = new Timer(SendBatchedEvents, null, 
                              TimeSpan.FromSeconds(10), 
                              TimeSpan.FromSeconds(10));
    }

    public void AttachToDevice(ModbusDeviceBase device, string deviceName)
    {
        device.OnRequestReceived += (sender, args) =>
        {
            var eventData = new
            {
                EventType = "ModbusRequest",
                Timestamp = DateTime.UtcNow,
                DeviceName = deviceName,
                DeviceId = args.DeviceId,
                FunctionCode = (byte)args.FunctionCode,
                FunctionName = args.FunctionCode.ToString()
            };

            lock (eventQueue)
            {
                eventQueue.Enqueue(eventData);
            }
        };
    }

    private async void SendBatchedEvents(object state)
    {
        List events;
        lock (eventQueue)
        {
            if (eventQueue.Count == 0) return;
            
            events = new List();
            while (eventQueue.Count > 0)
            {
                events.Add(eventQueue.Dequeue());
            }
        }

        try
        {
            var payload = new { events = events };
            var json = System.Text.Json.JsonSerializer.Serialize(payload);
            var content = new StringContent(json, Encoding.UTF8, "application/json");

            var response = await httpClient.PostAsync(webhookUrl, content);
            response.EnsureSuccessStatusCode();

            Console.WriteLine($"Sent {events.Count} events to webhook");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Webhook delivery failed: {ex.Message}");
            
            // Re-queue events for retry
            lock (eventQueue)
            {
                foreach (var evt in events)
                {
                    eventQueue.Enqueue(evt);
                }
            }
        }
    }
}
                
            
        

        
        
Complete Code Examples Real World

β˜‘ 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"
  }
]
*/
Complete API Reference All Classes & Methods

β˜‘ Class Hierarchy

namespace ULTRAMEGA.Modbus.DTS
{
    // Enumerations
    public enum ModbusFunctionCode : byte
    public enum ModbusExceptionCode : byte

    // Core Classes
    public class ModbusMemory
    public abstract class ModbusDeviceBase
    public class ModbusTcpDevice : ModbusDeviceBase
    public class ModbusRtuDevice : ModbusDeviceBase

    // Factory and Events
    public static class ModbusDeviceFactory
    public class ModbusRequestEventArgs : EventArgs

    // Example Implementation
    public class ModbusSimulatorExample
}

β˜‘ ModbusMemory Class

Member Type Description Usage
Coils Dictionary<int, bool> Coil memory area memory.Coils[0] = true
DiscreteInputs Dictionary<int, bool> Discrete input memory area memory.DiscreteInputs[0] = false
HoldingRegisters Dictionary<int, ushort> Holding register memory area memory.HoldingRegisters[0] = 1234
InputRegisters Dictionary<int, ushort> Input register memory area memory.InputRegisters[0] = 5678
InitializeMemory Method Initialize memory areas with default values InitializeMemory(1000, 1000, 1000, 1000)

β˜‘ ModbusDeviceBase Class

Member Type Description Parameters
DeviceId byte (protected) Modbus device address 1-255
Memory ModbusMemory (protected) Device memory instance -
IsRunning bool (protected) Device running state true/false
StartAsync Task (abstract) Start the device CancellationToken (optional)
StopAsync Task (abstract) Stop the device -
SetCoil void Set coil value int address, bool value
GetCoil bool Get coil value int address
SetDiscreteInput void Set discrete input value int address, bool value
GetDiscreteInput bool Get discrete input value int address
SetHoldingRegister void Set holding register value int address, ushort value
GetHoldingRegister ushort Get holding register value int address
SetInputRegister void Set input register value int address, ushort value
GetInputRegister ushort Get input register value int address
SimulateData virtual void Override for custom data simulation -

β˜‘ ModbusTcpDevice Class

Member Type Description Parameters
Constructor - Create TCP device byte deviceId, int port = 502
StartAsync Task (override) Start TCP listener CancellationToken (optional)
StopAsync Task (override) Stop TCP listener and close connections -
SimulateData void (override) Default TCP data simulation -

β˜‘ ModbusRtuDevice Class

Member Type Description Parameters
Constructor - Create RTU device byte deviceId, string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits
StartAsync Task (override) Open serial port and start listening CancellationToken (optional)
StopAsync Task (override) Close serial port -
SimulateData void (override) Default RTU data simulation -

β˜‘ Events

Event Handler Type Description Usage
OnLogMessage EventHandler<string> Fired for all log messages device.OnLogMessage += (s, msg) => Console.WriteLine(msg)
OnRequestReceived EventHandler<ModbusRequestEventArgs> Fired when Modbus request received device.OnRequestReceived += (s, args) => ProcessRequest(args)

β˜‘ ModbusDeviceFactory Class

Method Return Type Description Parameters
CreateTcpDevice ModbusTcpDevice Create TCP device with default settings byte deviceId, int port = 502
CreateRtuDevice ModbusRtuDevice Create RTU device with custom settings byte deviceId, string portName, int baudRate = 9600, Parity parity = Parity.None, int dataBits = 8, StopBits stopBits = StopBits.One
πŸ’‘ API Usage Tips
  • Thread Safety: All public methods are thread-safe
  • Memory Access: Returns default values for non-existent addresses
  • Event Handlers: Keep event handlers lightweight to avoid blocking
  • Async Methods: Always await async methods properly
  • Resource Cleanup: Always call StopAsync() for proper cleanup

No results found

Try adjusting your search terms