Commit db96b5b0 authored by Andreas Müller's avatar Andreas Müller

Enhancements

parent 4c790e30

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2003
VisualStudioVersion = 15.0.27130.2010
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modbus.Tcp", "src\Modbus.Tcp\Modbus.Tcp.csproj", "{BF60D1D4-3767-4EC4-AC53-C1772958BE14}"
EndProject
......@@ -17,7 +17,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D4E6650D-215
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3B6130C0-9168-4208-A677-29DD20301220}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleDemo", "src\ConsoleDemo\ConsoleDemo.csproj", "{2E6A165D-23DE-40D2-8A43-FE6EFA86F076}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleDemo", "src\ConsoleDemo\ConsoleDemo.csproj", "{2E6A165D-23DE-40D2-8A43-FE6EFA86F076}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modbus.Common", "src\Modbus.Common\Modbus.Common.csproj", "{71D10922-FE10-4A77-B4AC-E947797E9CFE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
......@@ -33,6 +35,10 @@ Global
{2E6A165D-23DE-40D2-8A43-FE6EFA86F076}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E6A165D-23DE-40D2-8A43-FE6EFA86F076}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E6A165D-23DE-40D2-8A43-FE6EFA86F076}.Release|Any CPU.Build.0 = Release|Any CPU
{71D10922-FE10-4A77-B4AC-E947797E9CFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71D10922-FE10-4A77-B4AC-E947797E9CFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71D10922-FE10-4A77-B4AC-E947797E9CFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71D10922-FE10-4A77-B4AC-E947797E9CFE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......@@ -40,6 +46,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{BF60D1D4-3767-4EC4-AC53-C1772958BE14} = {D4E6650D-2156-4660-B531-0B2AAD475BA1}
{2E6A165D-23DE-40D2-8A43-FE6EFA86F076} = {D4E6650D-2156-4660-B531-0B2AAD475BA1}
{71D10922-FE10-4A77-B4AC-E947797E9CFE} = {D4E6650D-2156-4660-B531-0B2AAD475BA1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FC7F1581-0E3C-4E35-9DBC-CC7952DE19C1}
......
using Modbus.Tcp.Client;
using Modbus.Tcp.Utils;
using Modbus.Common;
using Modbus.Common.Structures;
using Modbus.Tcp.Client;
using System;
using System.Collections.Generic;
using System.Linq;
......
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Modbus.Tcp.Utils
namespace Modbus.Common
{
internal class Consts
/// <summary>
/// Contains all constants used in Modbus.
/// </summary>
public static class Consts
{
#region Error/Exception
......@@ -22,31 +25,64 @@ namespace Modbus.Tcp.Utils
{ 11, "Gateway Target Device Failed to Respond" }
};
/// <summary>
/// Known Modbus exception codes returned by the server.
/// </summary>
public static ReadOnlyDictionary<byte, string> ErrorMessages => new ReadOnlyDictionary<byte, string>(exceptions);
/// <summary>
/// The Bit-Mask to filter the error-state of a Modbus response.
/// </summary>
public const byte ErrorMask = 0x80;
#endregion Error/Exception
#region Protocol limitations
public const int MinDeviceId = 0x0000;
public const int MaxDeviceId = 0x00FF; // 255
public const int MinAddress = 0x0000;
public const int MaxAddress = 0xFFFF; // 65535
public const int MinCount = 0x01;
public const int MaxCoilCountRead = 0x7D0; // 2000
public const int MaxCoilCountWrite = 0x7B0; // 1968
public const int MaxRegisterCountRead = 0x7D; // 125
public const int MaxRegisterCountWrite = 0x7B; // 123
/// <summary>
/// The lowest accepted device id.
/// </summary>
public const byte MinDeviceId = 0x00;
/// <summary>
/// The highest accepted device id.
/// </summary>
public const byte MaxDeviceId = 0xFF; // 255
/// <summary>
/// The lowest address.
/// </summary>
public const ushort MinAddress = 0x0000;
/// <summary>
/// The highest address.
/// </summary>
public const ushort MaxAddress = 0xFFFF; // 65535
/// <summary>
/// The lowest number of requested data sets.
/// </summary>
public const ushort MinCount = 0x01;
/// <summary>
/// The highest number of requested coils to read.
/// </summary>
public const ushort MaxCoilCountRead = 0x7D0; // 2000
/// <summary>
/// The highest number of requested coils to write.
/// </summary>
public const ushort MaxCoilCountWrite = 0x7B0; // 1968
/// <summary>
/// The highest number of requested registers to read.
/// </summary>
public const ushort MaxRegisterCountRead = 0x7D; // 125
/// <summary>
/// The highest number of requested registers to write.
/// </summary>
public const ushort MaxRegisterCountWrite = 0x7B; // 123
#endregion Protocol limitations
}
......
namespace Modbus.Common
{
/// <summary>
/// Lists the Modbus request types.
/// </summary>
public enum MessageType
{
/// <summary>
/// The type is not set.
/// </summary>
Unset,
/// <summary>
/// The request reads data.
/// </summary>
Read,
/// <summary>
/// The request writes one data set.
/// </summary>
WriteSingle,
/// <summary>
/// The request writes multiple data sets.
/// </summary>
WriteMultiple
}
/// <summary>
/// Lists the Modbus function codes.
/// </summary>
public enum FunctionCode : byte
{
/// <summary>
/// Read coils (Fn 1).
/// </summary>
ReadCoils = 0x01,
/// <summary>
/// Read discrete inputs (Fn 2).
/// </summary>
ReadDiscreteInputs = 0x02,
/// <summary>
/// Reads holding registers (Fn 3).
/// </summary>
ReadHoldingRegisters = 0x03,
/// <summary>
/// Reads input registers (Fn 4).
/// </summary>
ReadInputRegisters = 0x04,
/// <summary>
/// Writes a single coil (Fn 5).
/// </summary>
WriteSingleCoil = 0x05,
/// <summary>
/// Writes a single register (Fn 6).
/// </summary>
WriteSingleRegister = 0x06,
/// <summary>
/// Writes multiple coils (Fn 15).
/// </summary>
WriteMultipleCoils = 0x0F,
/// <summary>
/// Writes multiple registers (Fn 16).
/// </summary>
WriteMultipleRegisters = 0x10
}
}
using Modbus.Common.Structures;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Modbus.Common
{
/// <summary>
/// Contains some extensions to handle some features more easily.
/// </summary>
public static class Extensions
{
#region Register handling
#region From Register
#region To unsigned data types
/// <summary>
/// Converts a register value into a byte.
/// </summary>
/// <param name="register">The register.</param>
/// <returns></returns>
public static byte GetByte(this Register register)
{
return (byte)register.Value;
}
/// <summary>
/// Converts a register into a word.
/// </summary>
/// <param name="register">The register.</param>
/// <returns></returns>
public static ushort GetUInt16(this Register register)
{
return register.Value;
}
/// <summary>
/// Converts two registers into a dword.
/// </summary>
/// <param name="list">The list of registers (min. 2).</param>
/// <param name="startIndex">The start index. Default: 0.</param>
/// <returns></returns>
public static uint GetUInt32(this IEnumerable<Register> list, int startIndex = 0)
{
var registers = list.Skip(startIndex).Take(2).ToArray();
var blob = new byte[registers.Length * 2];
for (int i = 0; i < registers.Length; i++)
{
blob[i * 2] = registers[i].HiByte;
blob[i * 2 + 1] = registers[i].LoByte;
}
if (BitConverter.IsLittleEndian)
{
Array.Reverse(blob);
}
return BitConverter.ToUInt32(blob, 0);
}
/// <summary>
/// Converts four registers into a qword.
/// </summary>
/// <param name="list">The list of registers (min. 4).</param>
/// <param name="startIndex">The start index. Default: 0.</param>
/// <returns></returns>
public static ulong GetUInt64(this IEnumerable<Register> list, int startIndex = 0)
{
var registers = list.Skip(startIndex).Take(4).ToArray();
var blob = new byte[registers.Length * 2];
for (int i = 0; i < registers.Length; i++)
{
blob[i * 2] = registers[i].HiByte;
blob[i * 2 + 1] = registers[i].LoByte;
}
if (BitConverter.IsLittleEndian)
{
Array.Reverse(blob);
}
return BitConverter.ToUInt64(blob, 0);
}
#endregion To unsigned data types
#region To signed data types
/// <summary>
/// Converts a register into a signed byte.
/// </summary>
/// <param name="register">The register.</param>
/// <returns></returns>
public static sbyte GetSByte(this Register register)
{
return (sbyte)register.Value;
}
/// <summary>
/// Converts a register into a short.
/// </summary>
/// <param name="register">The register.</param>
/// <returns></returns>
public static short GetInt16(this Register register)
{
var blob = new[] { register.HiByte, register.LoByte };
if (BitConverter.IsLittleEndian)
{
Array.Reverse(blob);
}
return BitConverter.ToInt16(blob, 0);
}
/// <summary>
/// Converts two registers into an int.
/// </summary>
/// <param name="list">A list of registers (min. 2).</param>
/// <param name="startIndex">The start index. Default: 0.</param>
/// <returns></returns>
public static int GetInt32(this IEnumerable<Register> list, int startIndex = 0)
{
var registers = list.Skip(startIndex).Take(2).ToArray();
var blob = new byte[registers.Length * 2];
for (int i = 0; i < registers.Length; i++)
{
blob[i * 2] = registers[i].HiByte;
blob[i * 2 + 1] = registers[i].LoByte;
}
if (BitConverter.IsLittleEndian)
{
Array.Reverse(blob);
}
return BitConverter.ToInt32(blob, 0);
}
/// <summary>
/// Converts four registers into a long.
/// </summary>
/// <param name="list">A list of registers (min. 4).</param>
/// <param name="startIndex">The start index. Default: 0.</param>
/// <returns></returns>
public static long GetInt64(this IEnumerable<Register> list, int startIndex = 0)
{
var registers = list.Skip(startIndex).Take(4).ToArray();
var blob = new byte[registers.Length * 2];
for (int i = 0; i < registers.Length; i++)
{
blob[i * 2] = registers[i].HiByte;
blob[i * 2 + 1] = registers[i].LoByte;
}
if (BitConverter.IsLittleEndian)
{
Array.Reverse(blob);
}
return BitConverter.ToInt64(blob, 0);
}
#endregion To signed data types
#region To floating point types
/// <summary>
/// Converts two registers into a single.
/// </summary>
/// <param name="list">A list of registers (min. 2).</param>
/// <param name="startIndex">The start index. Default: 0.</param>
/// <returns></returns>
public static float GetSingle(this IEnumerable<Register> list, int startIndex = 0)
{
var registers = list.Skip(startIndex).Take(2).ToArray();
var blob = new byte[registers.Length * 2];
for (int i = 0; i < registers.Length; i++)
{
blob[i * 2] = registers[i].HiByte;
blob[i * 2 + 1] = registers[i].LoByte;
}
if (BitConverter.IsLittleEndian)
{
Array.Reverse(blob);
}
return BitConverter.ToSingle(blob, 0);
}
/// <summary>
/// Converts four registers into a double.
/// </summary>
/// <param name="list">A list of registers (min. 4).</param>
/// <param name="startIndex">The start index. Default: 0.</param>
/// <returns></returns>
public static double GetDouble(this IEnumerable<Register> list, int startIndex = 0)
{
var registers = list.Skip(startIndex).Take(4).ToArray();
var blob = new byte[registers.Length * 2];
for (int i = 0; i < registers.Length; i++)
{
blob[i * 2] = registers[i].HiByte;
blob[i * 2 + 1] = registers[i].LoByte;
}
if (BitConverter.IsLittleEndian)
{
Array.Reverse(blob);
}
return BitConverter.ToDouble(blob, 0);
}
#endregion To floating point types
#region To string
/// <summary>
/// Converts a list of registers into a string.
/// </summary>
/// <param name="list">A list of registers.</param>
/// <param name="length">The number of registers to use.</param>
/// <param name="index">The start index. Default: 0.</param>
/// <param name="encoding">The encoding to convert the string. Default: <see cref="Encoding.UTF8"/>.</param>
/// <returns></returns>
public static string GetString(this IEnumerable<Register> list, int length, int index = 0, Encoding encoding = null)
{
if (encoding == null)
{
encoding = Encoding.UTF8;
}
var registers = list.Skip(index).Take(length).ToArray();
var blob = new byte[registers.Length * 2];
for (int i = 0; i < registers.Length; i++)
{
blob[i * 2] = registers[i].HiByte;
blob[i * 2 + 1] = registers[i].LoByte;
}
return encoding.GetString(blob).Trim(new[] { ' ', '\t', '\0', '\r', '\n' });
}
#endregion To string
#endregion From Register
#endregion Register handling
}
}
using Modbus.Common.Structures;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Modbus.Common
{
/// <summary>
/// Represents the interface for a Modbus client.
/// </summary>
public interface IModbusClient : IDisposable
{
#region Properties
/// <summary>
/// Gets a value indicating whether the connection is established.
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Gets or sets the max reconnect timespan until the reconnect is aborted.
/// </summary>
TimeSpan ReconnectTimeSpan { get; set; }
#endregion Properties
#region Read methods
/// <summary>
/// Reads one or more coils of a device. (Modbus function 1).
/// </summary>
/// <param name="deviceId">The id to address the device (slave).</param>
/// <param name="startAddress">The first coil number to read.</param>
/// <param name="count">The number of coils to read.</param>
/// <returns>A list of coils or null on error.</returns>
Task<List<Coil>> ReadCoils(byte deviceId, ushort startAddress, ushort count);
/// <summary>
/// Reads one or more discrete inputs of a device. (Modbus function 2).
/// </summary>
/// <param name="deviceId">The id to address the device (slave).</param>
/// <param name="startAddress">The first discrete input number to read.</param>
/// <param name="count">The number of discrete inputs to read.</param>
/// <returns>A list of discrete inputs or null on error.</returns>
Task<List<DiscreteInput>> ReadDiscreteInputs(byte deviceId, ushort startAddress, ushort count);
/// <summary>
/// Reads one or more holding registers of a device. (Modbus function 3).
/// </summary>
/// <param name="deviceId">The id to address the device (slave).</param>
/// <param name="startAddress">The first register number to read.</param>
/// <param name="count">The number of registers to read.</param>
/// <returns>A list of registers or null on error.</returns>
Task<List<Register>> ReadHoldingRegisters(byte deviceId, ushort startAddress, ushort count);
/// <summary>
/// Reads one or more input registers of a device. (Modbus function 4).
/// </summary>
/// <param name="deviceId">The id to address the device (slave).</param>
/// <param name="startAddress">The first register number to read.</param>
/// <param name="count">The number of registers to read.</param>
/// <returns>A list of registers or null on error.</returns>
Task<List<Register>> ReadInputRegisters(byte deviceId, ushort startAddress, ushort count);
#endregion Read methods
#region Write methods
/// <summary>
/// Writes a single coil status to the Modbus device. (Modbus function 5)
/// </summary>
/// <param name="deviceId">The id to address the device (slave).</param>
/// <param name="coil">The coil to write.</param>
/// <returns>true on success, otherwise false.</returns>
Task<bool> WriteSingleCoil(byte deviceId, Coil coil);
/// <summary>
/// Writes a single register to the Modbus device. (Modbus function 6)
/// </summary>
/// <param name="deviceId">The id to address the device (slave).</param>
/// <param name="register">The register to write.</param>
/// <returns>true on success, otherwise false.</returns>
Task<bool> WriteSingleRegister(byte deviceId, Register register);
/// <summary>
/// Writes multiple coil status to the Modbus device. (Modbus function 15)
/// </summary>
/// <param name="deviceId">The id to address the device (slave).</param>
/// <param name="coils">A list of coils to write.</param>
/// <returns>true on success, otherwise false.</returns>
Task<bool> WriteCoils(byte deviceId, IEnumerable<Coil> coils);
/// <summary>
/// Writes multiple registers to the Modbus device. (Modbus function 16)
/// </summary>
/// <param name="deviceId">The id to address the device (slave).</param>
/// <param name="registers">A list of registers to write.</param>
/// <returns>true on success, otherwise false.</returns>
Task<bool> WriteRegisters(byte deviceId, IEnumerable<Register> registers);
#endregion Write methods
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>$(SolutionDir)artifacts\bin\$(RootNamespace)\$(Configuration)</OutputPath>
<IntermediateOutputPath>$(SolutionDir)artifacts\obj\$(RootNamespace)\$(Configuration)</IntermediateOutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>$(SolutionDir)artifacts\bin\$(RootNamespace)\$(Configuration)</OutputPath>
<IntermediateOutputPath>$(SolutionDir)artifacts\obj\$(RootNamespace)\$(Configuration)</IntermediateOutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
</Project>
namespace Modbus.Tcp.Utils
namespace Modbus.Common.Structures
{
/// <summary>
/// Represents the contents of a coil on a Modbus device.
......
namespace Modbus.Tcp.Utils
namespace Modbus.Common.Structures
{
/// <summary>
/// Represents the contents of a discrete input on a Modbus device.
......
using System;
namespace Modbus.Tcp.Utils
namespace Modbus.Common.Structures
{
/// <summary>
/// Represents errors that occurr during Modbus requests.
......
using System;
using System.Collections.Generic;
using System.Text;
namespace Modbus.Common.Structures
{
/// <summary>
/// Represents a register on a Modbus device.
/// </summary>
public class Register
{
#region Creates
#region unsigned
/// <summary>
/// Initializes a new register from a byte.
/// </summary>
/// <param name="value">The byte value.</param>
/// <param name="address">The register address.</param>
/// <returns></returns>
public static Register Create(byte value, ushort address)
{
return new Register
{
Address = address,
Value = value
};
}
/// <summary>
/// Initializes a new register from a unsigned short.
/// </summary>
/// <param name="value">The uint16 value.</param>
/// <param name="address">The register address.</param>
/// <returns></returns>
public static Register Create(ushort value, ushort address)
{
return new Register
{
Address = address,
Value = value
};
}
/// <summary>
/// Initializes new registers from an unsigned int.
/// </summary>
/// <param name="value">The uint32 value.</param>
/// <param name="address">The register address.</param>
/// <returns></returns>
public static List<Register> Create(uint value, ushort address)
{
if (address + 1 > Consts.MaxAddress)
{
throw new ArgumentOutOfRangeException(nameof(address));
}
var list = new List<Register>();
var blob = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(blob);
}
for (int i = 0; i < blob.Length / 2; i++)
{
list.Add(new Register
{
Address = Convert.ToUInt16(address + i),
HiByte = blob[(i * 2)],
LoByte = blob[(i * 2) + 1]
});
}
return list;
}
/// <summary>
/// Initializes new registers from an unsigned long.
/// </summary>
/// <param name="value">The uint64 value.</param>
/// <param name="address">The register address.</param>
/// <returns></returns>
public static List<Register> Create(ulong value, ushort address)
{
if (address + 3 > Consts.MaxAddress)
{
throw new ArgumentOutOfRangeException(nameof(address));
}
var list = new List<Register>();
var blob = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(blob);
}
for (int i = 0; i < blob.Length / 2; i++)
{
list.Add(new Register
{
Address = Convert.ToUInt16(address + i),
HiByte = blob[(i * 2)],
LoByte = blob[(i * 2) + 1]
});
}