csharp/AndreasAmMueller/Modbus/src/Modbus.Tcp/Server/ModbusServer.cs

ModbusServer.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Modbus.Common;
using AMWD.Modbus.Common.Interfaces;
using AMWD.Modbus.Common.Structures;
using AMWD.Modbus.Common.Util;
using AMWD.Modbus.Tcp.Protocol;
using AMWD.Modbus.Tcp.Util;
using Microsoft.Extensions.Logging;

namespace AMWD.Modbus.Tcp.Server
{
	/// 
	/// A handler to process the modbus requests.
	/// 
	/// The request to process.
	/// The cancellation token fired on .
	/// The response.
	public delegate Response ModbusTcpRequestHandler(Request request, CancellationToken cancellationToken);

	/// 
	/// A server to communicate via Modbus TCP.
	/// 
	public clast ModbusServer : IModbusServer
	{
		#region Fields

		private readonly ILogger logger;

		private readonly CancellationTokenSource stopCts = new();
		private TcpListener tcpListener;
		private readonly ConcurrentDictionary modbusDevices = new();

		private Task clientConnect;
		private readonly ConcurrentDictionary tcpClients = new();
		private readonly List clientTasks = new();
		private readonly ModbusTcpRequestHandler requestHandler;

		#endregion Fields

		#region Constructors

		/// 
		/// Initializes a new instance of the  clast.
		/// 
		/// The port to listen. (Default: 502)
		/// The ip address to bind on. (Default: )
		///  instance to write log entries. (Default: no logger)
		/// Set this request handler to override the default implemented handling. (Default: serving the data provided by Set* methods)
		public ModbusServer(int port = 502, IPAddress listenAddress = null, ILogger logger = null, ModbusTcpRequestHandler requestHandler = null)
		{
			ListenAddress = listenAddress;
			if (ListenAddress == null)
				ListenAddress = IPAddress.IPv6Any;

			if (port < 0 || port > 65535)
				throw new ArgumentOutOfRangeException(nameof(port));

			try
			{
				var listener = new TcpListener(ListenAddress, port);
				listener.Start(10);
				Port = ((IPEndPoint)listener.LocalEndpoint).Port;
				listener.Stop();
			}
			catch (Exception ex)
			{
				throw new ArgumentException(nameof(port), ex);
			}

			this.logger = logger;
			this.requestHandler = requestHandler ?? HandleRequest;

			Initialization = Task.Run(() => Initialize());
		}

		#endregion Constructors

		#region Events

		/// 
		/// Raised when a client has connected to the server.
		/// 
		public event EventHandler ClientConnected;

		/// 
		/// Raised when a client has disconnected from the server.
		/// 
		public event EventHandler ClientDisconnected;

		/// 
		/// Raised when a coil was written.
		/// 
		public event EventHandler InputWritten;

		/// 
		/// Raised when a register was written.
		/// 
		public event EventHandler RegisterWritten;

		#endregion Events

		#region Properties

		/// 
		/// Gets the result of the asynchronous initialization of this instance.
		/// 
		public Task Initialization { get; } = Task.CompletedTask;

		/// 
		/// Gets the UTC timestamp of the server start.
		/// 
		public DateTime StartTime { get; private set; } = DateTime.MinValue;

		/// 
		/// Gets a value indicating whether the server is running.
		/// 
		public bool IsRunning { get; private set; }

		/// 
		/// Gets the binding address.
		/// 
		public IPAddress ListenAddress { get; }

		/// 
		/// Gets the port listening on.
		/// 
		public int Port { get; }

		/// 
		/// Gets or sets read/write timeout. (Default: 1 second)
		/// 
		public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(1);

		/// 
		/// Gets a list of device ids the server handles.
		/// 
		public List DeviceIds => modbusDevices.Keys.ToList();

		#endregion Properties

		#region Public methods

		#region Coils

		/// 
		/// Returns a coil of a device.
		/// 
		/// The device id.
		/// The address of the coil.
		/// The coil.
		public Coil GetCoil(byte deviceId, ushort coilNumber)
		{
			try
			{
				logger?.LogTrace("ModbusServer.GetCoil enter");
				CheckDisposed();
				if (!modbusDevices.TryGetValue(deviceId, out ModbusDevice device))
					throw new ArgumentException($"Device #{deviceId} does not exist");

				return device.GetCoil(coilNumber);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.GetCoil leave");
			}
		}

		/// 
		/// Sets the status of a coild to a device.
		/// 
		/// The device id.
		/// The address of the coil.
		/// The status of the coil.
		public void SetCoil(byte deviceId, ushort coilNumber, bool value)
		{
			try
			{
				logger?.LogTrace("ModbusServer.SetCoil(byte, ushort, bool) enter");
				CheckDisposed();
				if (!modbusDevices.TryGetValue(deviceId, out ModbusDevice device))
					throw new ArgumentException($"Device #{deviceId} does not exist");

				device.SetCoil(coilNumber, value);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.SetCoil(byte, ushort, bool) leave");
			}
		}

		/// 
		/// Sets the status of a coild to a device.
		/// 
		/// The device id.
		/// The coil.
		public void SetCoil(byte deviceId, ModbusObject coil)
		{
			try
			{
				logger?.LogTrace("ModbusServer.SetCoil(byte, ModbusObject) enter");
				CheckDisposed();
				if (coil.Type != ModbusObjectType.Coil)
					throw new ArgumentException("Invalid coil type set");

				SetCoil(deviceId, coil.Address, coil.BoolValue);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.SetCoil(byte, ModbusObject) leave");
			}
		}

		#endregion Coils

		#region Discrete Inputs

		/// 
		/// Returns a discrete input of a device.
		/// 
		/// The device id.
		/// The discrete input address.
		/// The discrete input.
		public DiscreteInput GetDiscreteInput(byte deviceId, ushort inputNumber)
		{
			try
			{
				logger?.LogTrace("ModbusServer.GetDiscreteInput enter");
				CheckDisposed();
				if (!modbusDevices.TryGetValue(deviceId, out ModbusDevice device))
					throw new ArgumentException($"Device #{deviceId} does not exist");

				return device.GetInput(inputNumber);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.GetDiscreteInput leave");
			}
		}

		/// 
		/// Sets a discrete input of a device.
		/// 
		/// The device id.
		/// The discrete input address.
		/// A value inidcating whether the input is set.
		public void SetDiscreteInput(byte deviceId, ushort inputNumber, bool value)
		{
			try
			{
				logger?.LogTrace("ModbusServer.SetDiscreteInput(byte, ushort, bool) enter");
				CheckDisposed();
				if (!modbusDevices.TryGetValue(deviceId, out ModbusDevice device))
					throw new ArgumentException($"Device #{deviceId} does not exist");

				device.SetInput(inputNumber, value);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.SetDiscreteInput(byte, ushort, bool) leave");
			}
		}

		/// 
		/// Sets a discrete input of a device.
		/// 
		/// The device id.
		/// The discrete input to set.
		public void SetDiscreteInput(byte deviceId, ModbusObject discreteInput)
		{
			try
			{
				logger?.LogTrace("ModbusServer.SetDiscreteInput(byte, ModbusObject) enter");
				CheckDisposed();
				if (discreteInput.Type != ModbusObjectType.DiscreteInput)
					throw new ArgumentException("Invalid input type set");

				SetDiscreteInput(deviceId, discreteInput.Address, discreteInput.BoolValue);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.SetDiscreteInput(byte, ModbusObject) leave");
			}
		}

		#endregion Discrete Inputs

		#region Input Registers

		/// 
		/// Returns an input register of a device.
		/// 
		/// The device id.
		/// The input register address.
		/// The input register.
		public Register GetInputRegister(byte deviceId, ushort registerNumber)
		{
			try
			{
				logger?.LogTrace("ModbusServer.GetInputRegister enter");
				CheckDisposed();
				if (!modbusDevices.TryGetValue(deviceId, out ModbusDevice device))
					throw new ArgumentException($"Device #{deviceId} does not exist");

				return device.GetInputRegister(registerNumber);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.GetInputRegister leave");
			}
		}

		/// 
		/// Sets an input register of a device.
		/// 
		/// The device id.
		/// The input register address.
		/// The register value.
		public void SetInputRegister(byte deviceId, ushort registerNumber, ushort value)
		{
			try
			{
				logger?.LogTrace("ModbusServer.SetInputRegister(byte, ushort, ushort) enter");
				CheckDisposed();
				if (!modbusDevices.TryGetValue(deviceId, out ModbusDevice device))
					throw new ArgumentException($"Device #{deviceId} does not exist");

				device.SetInputRegister(registerNumber, value);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.SetInputRegister(byte, ushort, ushort) leave");
			}
		}

		/// 
		/// Sets an input register of a device.
		/// 
		/// The device id.
		/// The input register address.
		/// The High-Byte value.
		/// The Low-Byte value.
		public void SetInputRegister(byte deviceId, ushort registerNumber, byte highByte, byte lowByte)
		{
			try
			{
				logger?.LogTrace("ModbusServer.SetInputRegister(byte, ushort, byte, byte) enter");
				CheckDisposed();
				SetInputRegister(deviceId, new Register { Address = registerNumber, HiByte = highByte, LoByte = lowByte, Type = ModbusObjectType.InputRegister });
			}
			finally
			{
				logger?.LogTrace("ModbusServer.SetInputRegister(byte, ushort, byte, byte) leave");
			}
		}

		/// 
		/// Sets an input register of a device.
		/// 
		/// The device id.
		/// The input register.
		public void SetInputRegister(byte deviceId, ModbusObject register)
		{
			try
			{
				logger?.LogTrace("ModbusServer.SetInputRegister(byte, ModbusObject) enter");
				CheckDisposed();
				if (register.Type != ModbusObjectType.InputRegister)
					throw new ArgumentException("Invalid register type set");

				SetInputRegister(deviceId, register.Address, register.RegisterValue);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.SetInputRegister(byte, ModbusObject) leave");
			}
		}

		#endregion Input Registers

		#region Holding Registers

		/// 
		/// Returns a holding register of a device.
		/// 
		/// The device id.
		/// The holding register address.
		/// The holding register.
		public Register GetHoldingRegister(byte deviceId, ushort registerNumber)
		{
			try
			{
				logger?.LogTrace("ModbusServer.GetHoldingRegister enter");
				CheckDisposed();
				if (!modbusDevices.TryGetValue(deviceId, out ModbusDevice device))
					throw new ArgumentException($"Device #{deviceId} does not exist");

				return device.GetHoldingRegister(registerNumber);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.GetHoldingRegister leave");
			}
		}

		/// 
		/// Sets a holding register of a device.
		/// 
		/// The device id.
		/// The holding register address.
		/// The register value.
		public void SetHoldingRegister(byte deviceId, ushort registerNumber, ushort value)
		{
			try
			{
				logger?.LogTrace("ModbusServer.SetHoldingRegister(byte, ushort, ushort) enter");
				CheckDisposed();
				if (!modbusDevices.TryGetValue(deviceId, out ModbusDevice device))
					throw new ArgumentException($"Device #{deviceId} does not exist");

				device.SetHoldingRegister(registerNumber, value);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.SetHoldingRegister(byte, ushort, ushort) leave");
			}
		}

		/// 
		/// Sets a holding register of a device.
		/// 
		/// The device id.
		/// The holding register address.
		/// The high byte value.
		/// The low byte value.
		public void SetHoldingRegister(byte deviceId, ushort registerNumber, byte highByte, byte lowByte)
		{
			try
			{
				logger?.LogTrace("ModbusServer.SetHoldingRegister(byte, ushort, byte, byte) enter");
				CheckDisposed();
				SetHoldingRegister(deviceId, new Register { Address = registerNumber, HiByte = highByte, LoByte = lowByte, Type = ModbusObjectType.HoldingRegister });
			}
			finally
			{
				logger?.LogTrace("ModbusServer.SetHoldingRegister(byte, ushort, byte, byte) leave");
			}
		}

		/// 
		/// Sets a holding register of a device.
		/// 
		/// The device id.
		/// The register.
		public void SetHoldingRegister(byte deviceId, ModbusObject register)
		{
			try
			{
				logger?.LogTrace("ModbusServer.SetHoldingRegister(byte, ModbusObject) enter");
				CheckDisposed();
				if (register.Type != ModbusObjectType.HoldingRegister)
					throw new ArgumentException("Invalid register type set");

				SetHoldingRegister(deviceId, register.Address, register.RegisterValue);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.SetHoldingRegister(byte, ModbusObject) leave");
			}
		}

		#endregion Holding Registers

		#region Devices

		/// 
		/// Adds a new device to the server.
		/// 
		/// The id of the new device.
		/// true on success, otherwise false.
		public bool AddDevice(byte deviceId)
		{
			try
			{
				logger?.LogTrace("ModbusServer.AddDevice enter");
				CheckDisposed();
				return modbusDevices.TryAdd(deviceId, new ModbusDevice(deviceId));
			}
			finally
			{
				logger?.LogTrace("ModbusServer.AddDevice leave");
			}
		}

		/// 
		/// Removes a device from the server.
		/// 
		/// The device id to remove.
		/// true on success, otherwise false.
		public bool RemoveDevice(byte deviceId)
		{
			try
			{
				logger?.LogTrace("ModbusServer.RemoveDevice enter");
				CheckDisposed();
				return modbusDevices.TryRemove(deviceId, out ModbusDevice _);
			}
			finally
			{
				logger?.LogTrace("ModbusServer.RemoveDevice leave");
			}
		}

		#endregion Devices

		#endregion Public methods

		#region Private methods

		#region Server

		private Task Initialize()
		{
			try
			{
				logger?.LogTrace("ModbusServer.Initialize enter");
				CheckDisposed();

				tcpListener?.Stop();
				tcpListener = null;
				tcpListener = new TcpListener(ListenAddress, Port);

				if (ListenAddress.AddressFamily == AddressFamily.InterNetworkV6)
					tcpListener.Server.DualMode = true;

				tcpListener.Start();
				StartTime = DateTime.UtcNow;
				IsRunning = true;

				clientConnect = Task.Run(async () => await WaitForClient());
				logger?.LogInformation($"Modbus server started. Listening on {ListenAddress}:{Port}/tcp.");

				return Task.CompletedTask;
			}
			finally
			{
				logger?.LogTrace("ModbusServer.Initialize leave");
			}
		}

		private async Task WaitForClient()
		{
			try
			{
				logger?.LogTrace("ModbusServer.WaitForClient enter");
				while (!stopCts.IsCancellationRequested)
				{
					try
					{
						var client = await tcpListener.AcceptTcpClientAsync();
						if (tcpClients.TryAdd(client, true))
						{
							var clientTask = Task.Run(async () => await HandleClient(client));
							clientTasks.Add(clientTask);
						}
					}
					catch
					{
						// keep things quiet
					}
				}
			}
			finally
			{
				logger?.LogTrace("ModbusServer.WaitForClient leave");
			}
		}

		private async Task HandleClient(TcpClient client)
		{
			logger?.LogTrace("ModbusServer.HandleClient enter");
			var endpoint = (IPEndPoint)client.Client.RemoteEndPoint;
			try
			{
				ClientConnected?.Invoke(this, new ClientEventArgs(endpoint));
				logger?.LogInformation($"Client connected: {endpoint.Address}.");

				var stream = client.GetStream();
				while (!stopCts.IsCancellationRequested)
				{
					using var requestStream = new MemoryStream();

					using (var cts = new CancellationTokenSource(Timeout))
					using (stopCts.Token.Register(() => cts.Cancel()))
					{
						try
						{
							byte[] header = await stream.ReadExpectedBytes(6, cts.Token);
							await requestStream.WriteAsync(header, 0, header.Length, cts.Token);

							byte[] bytes = header.Skip(4).Take(2).ToArray();
							if (BitConverter.IsLittleEndian)
								Array.Reverse(bytes);

							int following = BitConverter.ToUInt16(bytes, 0);
							byte[] payload = await stream.ReadExpectedBytes(following, cts.Token);
							await requestStream.WriteAsync(payload, 0, payload.Length, cts.Token);
						}
						catch (OperationCanceledException) when (cts.IsCancellationRequested)
						{
							continue;
						}
					}

					try
					{
						var request = new Request(requestStream.GetBuffer());
						var response = requestHandler?.Invoke(request, stopCts.Token);
						if (response != null)
						{
							using var cts = new CancellationTokenSource(Timeout);
							using var reg = stopCts.Token.Register(() => cts.Cancel());
							try
							{
								byte[] bytes = response.Serialize();
								await stream.WriteAsync(bytes, 0, bytes.Length, cts.Token);
							}
							catch (OperationCanceledException) when (cts.IsCancellationRequested)
							{
								continue;
							}
						}
					}
					catch (ArgumentException ex)
					{
						logger?.LogWarning(ex, $"Invalid data received from {endpoint.Address}: {ex.Message}");
					}
					catch (NotImplementedException ex)
					{
						logger?.LogWarning(ex, $"Invalid data received from {endpoint.Address}: {ex.Message}");
					}
				}
			}
			catch (IOException)
			{
				// client connection closed
				return;
			}
			catch (Exception ex)
			{
				logger?.LogError(ex, $"Unexpected error ({ex.GetType().Name}) occurred: {ex.GetMessage()}");
			}
			finally
			{
				ClientDisconnected?.Invoke(this, new ClientEventArgs(endpoint));
				logger?.LogInformation($"Client disconnected: {endpoint.Address}");

				client.Dispose();
				tcpClients.TryRemove(client, out _);

				logger?.LogTrace("ModbusServer.HandleClient leave");
			}
		}

		private Response HandleRequest(Request request, CancellationToken cancellationToken)
		{
			// The device is not known => no response to send.
			if (!modbusDevices.ContainsKey(request.DeviceId))
				return null;

			return request.Function switch
			{
				FunctionCode.ReadCoils => HandleReadCoils(request),
				FunctionCode.ReadDiscreteInputs => HandleReadDiscreteInputs(request),
				FunctionCode.ReadHoldingRegisters => HandleReadHoldingRegisters(request),
				FunctionCode.ReadInputRegisters => HandleReadInputRegisters(request),
				FunctionCode.WriteSingleCoil => HandleWriteSingleCoil(request),
				FunctionCode.WriteSingleRegister => HandleWritSingleRegister(request),
				FunctionCode.WriteMultipleCoils => HandleWriteMultipleCoils(request),
				FunctionCode.WriteMultipleRegisters => HandleWriteMultipleRegisters(request),
				FunctionCode.EncapsulatedInterface => HandleEncapsulatedInterface(request),
				_ => new Response(request)
				{
					ErrorCode = ErrorCode.IllegalFunction
				}
			};
		}

		#endregion Server

		#region Function implementation

		#region Read requests

		private Response HandleReadCoils(Request request)
		{
			var response = new Response(request);

			try
			{
				if (request.Count < Consts.MinCount || request.Count > Consts.MaxCoilCountRead)
				{
					response.ErrorCode = ErrorCode.IllegalDataValue;
				}
				else if (request.Address < Consts.MinAddress || request.Address + request.Count > Consts.MaxAddress)
				{
					response.ErrorCode = ErrorCode.IllegalDataAddress;
				}
				else
				{
					try
					{
						int len = (int)Math.Ceiling(request.Count / 8.0);
						response.Data = new DataBuffer(len);
						for (int i = 0; i < request.Count; i++)
						{
							ushort addr = (ushort)(request.Address + i);
							if (GetCoil(request.DeviceId, addr).BoolValue)
							{
								int posByte = i / 8;
								int posBit = i % 8;

								byte mask = (byte)Math.Pow(2, posBit);
								response.Data[posByte] = (byte)(response.Data[posByte] | mask);
							}
						}
					}
					catch
					{
						response.ErrorCode = ErrorCode.SlaveDeviceFailure;
					}
				}
			}
			catch
			{
				return null;
			}

			return response;
		}

		private Response HandleReadDiscreteInputs(Request request)
		{
			var response = new Response(request);
			try
			{
				if (request.Count < Consts.MinCount || request.Count > Consts.MaxCoilCountRead)
				{
					response.ErrorCode = ErrorCode.IllegalDataValue;
				}
				else if (request.Address < Consts.MinAddress || request.Address + request.Count > Consts.MaxAddress)
				{
					response.ErrorCode = ErrorCode.IllegalDataAddress;
				}
				else
				{
					try
					{
						int len = (int)Math.Ceiling(request.Count / 8.0);
						response.Data = new DataBuffer(len);
						for (int i = 0; i < request.Count; i++)
						{
							ushort addr = (ushort)(request.Address + i);
							if (GetDiscreteInput(request.DeviceId, addr).BoolValue)
							{
								int posByte = i / 8;
								int posBit = i % 8;

								byte mask = (byte)Math.Pow(2, posBit);
								response.Data[posByte] = (byte)(response.Data[posByte] | mask);
							}
						}
					}
					catch
					{
						response.ErrorCode = ErrorCode.SlaveDeviceFailure;
					}
				}
			}
			catch
			{
				return null;
			}

			return response;
		}

		private Response HandleReadHoldingRegisters(Request request)
		{
			var response = new Response(request);
			try
			{
				if (request.Count < Consts.MinCount || request.Count > Consts.MaxRegisterCountRead)
				{
					response.ErrorCode = ErrorCode.IllegalDataValue;
				}
				else if (request.Address < Consts.MinAddress || request.Address + request.Count > Consts.MaxAddress)
				{
					response.ErrorCode = ErrorCode.IllegalDataAddress;
				}
				else
				{
					try
					{
						response.Data = new DataBuffer(request.Count * 2);
						for (int i = 0; i < request.Count; i++)
						{
							ushort addr = (ushort)(request.Address + i);
							var reg = GetHoldingRegister(request.DeviceId, addr);
							response.Data.SetUInt16(i * 2, reg.RegisterValue);
						}
					}
					catch
					{
						response.ErrorCode = ErrorCode.SlaveDeviceFailure;
					}
				}
			}
			catch
			{
				return null;
			}

			return response;
		}

		private Response HandleReadInputRegisters(Request request)
		{
			var response = new Response(request);

			try
			{
				if (request.Count < Consts.MinCount || request.Count > Consts.MaxRegisterCountRead)
				{
					response.ErrorCode = ErrorCode.IllegalDataValue;
				}
				else if (request.Address < Consts.MinAddress || request.Address + request.Count > Consts.MaxAddress)
				{
					response.ErrorCode = ErrorCode.IllegalDataAddress;
				}
				else
				{
					try
					{
						response.Data = new DataBuffer(request.Count * 2);
						for (int i = 0; i < request.Count; i++)
						{
							ushort addr = (ushort)(request.Address + i);
							var reg = GetInputRegister(request.DeviceId, addr);
							response.Data.SetUInt16(i * 2, reg.RegisterValue);
						}
					}
					catch
					{
						response.ErrorCode = ErrorCode.SlaveDeviceFailure;
					}
				}
			}
			catch
			{
				return null;
			}

			return response;
		}

		private Response HandleEncapsulatedInterface(Request request)
		{
			var response = new Response(request);
			if (request.MEIType != MEIType.ReadDeviceInformation)
			{
				response.ErrorCode = ErrorCode.IllegalFunction;
				return response;
			}

			if ((byte)request.MEIObject < 0x00 ||
				(byte)request.MEIObject > 0xFF ||
				((byte)request.MEIObject > 0x06 && (byte)request.MEIObject < 0x80))
			{
				response.ErrorCode = ErrorCode.IllegalDataAddress;
				return response;
			}

			if (request.MEICategory < DeviceIDCategory.Basic || request.MEICategory > DeviceIDCategory.Individual)
			{
				response.ErrorCode = ErrorCode.IllegalDataValue;
				return response;
			}

			string version = astembly.Getastembly(typeof(ModbusServer))
				.GetCustomAttribute()
				.InformationalVersion;

			response.MEIType = request.MEIType;
			response.MEICategory = request.MEICategory;

			var dict = new Dictionary();
			switch (request.MEICategory)
			{
				case DeviceIDCategory.Basic:
					response.ConformityLevel = 0x01;
					dict.Add(DeviceIDObject.VendorName, "AM.WD");
					dict.Add(DeviceIDObject.ProductCode, "AM.WD-MBS-TCP");
					dict.Add(DeviceIDObject.MajorMinorRevision, version);
					break;
				case DeviceIDCategory.Regular:
					response.ConformityLevel = 0x02;
					dict.Add(DeviceIDObject.VendorName, "AM.WD");
					dict.Add(DeviceIDObject.ProductCode, "AM.WD-MBS-TCP");
					dict.Add(DeviceIDObject.MajorMinorRevision, version);
					dict.Add(DeviceIDObject.VendorUrl, "https://github.com/AndreasAmMueller/Modbus");
					dict.Add(DeviceIDObject.ProductName, "AM.WD Modbus");
					dict.Add(DeviceIDObject.ModelName, "TCP Server");
					dict.Add(DeviceIDObject.UserApplicationName, "Modbus TCP Server");
					break;
				case DeviceIDCategory.Extended:
					response.ConformityLevel = 0x03;
					dict.Add(DeviceIDObject.VendorName, "AM.WD");
					dict.Add(DeviceIDObject.ProductCode, "AM.WD-MBS-TCP");
					dict.Add(DeviceIDObject.MajorMinorRevision, version);
					dict.Add(DeviceIDObject.VendorUrl, "https://github.com/AndreasAmMueller/Modbus");
					dict.Add(DeviceIDObject.ProductName, "AM.WD Modbus");
					dict.Add(DeviceIDObject.ModelName, "TCP Server");
					dict.Add(DeviceIDObject.UserApplicationName, "Modbus TCP Server");
					break;
				case DeviceIDCategory.Individual:
					switch (request.MEIObject)
					{
						case DeviceIDObject.VendorName:
							response.ConformityLevel = 0x81;
							dict.Add(DeviceIDObject.VendorName, "AM.WD");
							break;
						case DeviceIDObject.ProductCode:
							response.ConformityLevel = 0x81;
							dict.Add(DeviceIDObject.ProductCode, "AM.WD-MBS-TCP");
							break;
						case DeviceIDObject.MajorMinorRevision:
							response.ConformityLevel = 0x81;
							dict.Add(DeviceIDObject.MajorMinorRevision, version);
							break;
						case DeviceIDObject.VendorUrl:
							response.ConformityLevel = 0x82;
							dict.Add(DeviceIDObject.VendorUrl, "https://github.com/AndreasAmMueller/Modbus");
							break;
						case DeviceIDObject.ProductName:
							response.ConformityLevel = 0x82;
							dict.Add(DeviceIDObject.ProductName, "AM.WD Modbus");
							break;
						case DeviceIDObject.ModelName:
							response.ConformityLevel = 0x82;
							dict.Add(DeviceIDObject.ModelName, "TCP Server");
							break;
						case DeviceIDObject.UserApplicationName:
							response.ConformityLevel = 0x82;
							dict.Add(DeviceIDObject.UserApplicationName, "Modbus TCP Server");
							break;
						default:
							response.ConformityLevel = 0x83;
							dict.Add(request.MEIObject, "Custom Data for " + request.MEIObject);
							break;
					}
					break;
			}

			response.MoreRequestsNeeded = false;
			response.NextObjectId = 0x00;
			response.ObjectCount = (byte)dict.Count;
			response.Data = new DataBuffer();

			foreach (var kvp in dict)
			{
				byte[] bytes = Encoding.ASCII.GetBytes(kvp.Value);

				response.Data.AddByte((byte)kvp.Key);
				response.Data.AddByte((byte)bytes.Length);
				response.Data.AddBytes(bytes);
			}

			return response;
		}

		#endregion Read requests

		#region Write requests

		private Response HandleWriteSingleCoil(Request request)
		{
			var response = new Response(request);

			try
			{
				ushort val = request.Data.GetUInt16(0);
				if (val != 0x0000 && val != 0xFF00)
				{
					response.ErrorCode = ErrorCode.IllegalDataValue;
				}
				else if (request.Address < Consts.MinAddress || request.Address > Consts.MaxAddress)
				{
					response.ErrorCode = ErrorCode.IllegalDataAddress;
				}
				else
				{
					try
					{
						var coil = new Coil { Address = request.Address, BoolValue = (val > 0) };

						SetCoil(request.DeviceId, coil);
						response.Data = request.Data;

						InputWritten?.Invoke(this, new WriteEventArgs(request.DeviceId, coil));
					}
					catch
					{
						response.ErrorCode = ErrorCode.SlaveDeviceFailure;
					}
				}
			}
			catch
			{
				return null;
			}

			return response;
		}

		private Response HandleWritSingleRegister(Request request)
		{
			var response = new Response(request);

			try
			{
				ushort val = request.Data.GetUInt16(0);

				if (request.Address < Consts.MinAddress || request.Address > Consts.MaxAddress)
				{
					response.ErrorCode = ErrorCode.IllegalDataAddress;
				}
				else
				{
					try
					{
						var register = new Register { Address = request.Address, RegisterValue = val, Type = ModbusObjectType.HoldingRegister };

						SetHoldingRegister(request.DeviceId, register);
						response.Data = request.Data;

						RegisterWritten?.Invoke(this, new WriteEventArgs(request.DeviceId, register));
					}
					catch
					{
						response.ErrorCode = ErrorCode.SlaveDeviceFailure;
					}
				}
			}
			catch
			{
				return null;
			}

			return response;
		}

		private Response HandleWriteMultipleCoils(Request request)
		{
			try
			{
				var response = new Response(request);

				int numBytes = (int)Math.Ceiling(request.Count / 8.0);
				if (request.Count < Consts.MinCount || request.Count > Consts.MaxCoilCountWrite || numBytes != request.Data.Length)
				{
					response.ErrorCode = ErrorCode.IllegalDataValue;
				}
				else if (request.Address < Consts.MinAddress || request.Address + request.Count > Consts.MaxAddress)
				{
					response.ErrorCode = ErrorCode.IllegalDataAddress;
				}
				else
				{
					try
					{
						var list = new List();
						for (int i = 0; i < request.Count; i++)
						{
							ushort addr = (ushort)(request.Address + i);

							int posByte = i / 8;
							int posBit = i % 8;

							byte mask = (byte)Math.Pow(2, posBit);
							int val = request.Data[posByte] & mask;

							var coil = new Coil { Address = addr, BoolValue = (val > 0) };
							SetCoil(request.DeviceId, coil);
							list.Add(coil);
						}
						InputWritten?.Invoke(this, new WriteEventArgs(request.DeviceId, list));
					}
					catch
					{
						response.ErrorCode = ErrorCode.SlaveDeviceFailure;
					}
				}

				return response;
			}
			catch
			{
				return null;
			}
		}

		private Response HandleWriteMultipleRegisters(Request request)
		{
			try
			{
				var response = new Response(request);

				//request.Data contains [byte count] [data]..[data]
				if (request.Count < Consts.MinCount || request.Count > Consts.MaxRegisterCountWrite || request.Count * 2 != request.Data.Length - 1)
				{
					response.ErrorCode = ErrorCode.IllegalDataValue;
				}
				else if (request.Address < Consts.MinAddress || request.Address + request.Count > Consts.MaxAddress)
				{
					response.ErrorCode = ErrorCode.IllegalDataAddress;
				}
				else
				{
					try
					{
						var list = new List();
						for (int i = 0; i < request.Count; i++)
						{
							ushort addr = (ushort)(request.Address + i);
							ushort val = request.Data.GetUInt16(i * 2 + 1);

							var register = new Register { Address = addr, RegisterValue = val, Type = ModbusObjectType.HoldingRegister };
							SetHoldingRegister(request.DeviceId, register);
							list.Add(register);
						}
						RegisterWritten?.Invoke(this, new WriteEventArgs(request.DeviceId, list));
					}
					catch
					{
						response.ErrorCode = ErrorCode.SlaveDeviceFailure;
					}
				}

				return response;
			}
			catch
			{
				return null;
			}
		}

		#endregion Write requests

		#endregion Function implementation

		#endregion Private methods

		#region IDisposable implementation

		private bool isDisposed;

		/// 
		public void Dispose()
		{
			if (isDisposed)
				return;

			isDisposed = true;
			stopCts.Cancel();

			tcpListener.Stop();
			foreach (var client in tcpClients.Keys)
				client.Dispose();

			Task.WaitAll(clientTasks.ToArray());
			Task.WaitAll(clientConnect);

			tcpClients.Clear();

			IsRunning = false;
		}

		/// 
		/// Checks whether the object is already disposed.
		/// 
		protected void CheckDisposed()
		{
			if (isDisposed)
				throw new ObjectDisposedException(GetType().FullName);
		}

		#endregion IDisposable implementation
	}

	/// 
	/// Provides connection information of a client.
	/// 
	public clast ClientEventArgs : EventArgs
	{
		/// 
		/// Initializes a new instance of the  clast.
		/// 
		/// The client end point.
		public ClientEventArgs(IPEndPoint endpoint)
		{
			EndPoint = endpoint;
		}

		/// 
		/// Gets the endpoint information of the client.
		/// 
		public IPEndPoint EndPoint { get; }
	}
}