Commit 652cc905 authored by Andreas Müller's avatar Andreas Müller

Project enhanced for Modbus Serial

parent 4b5c12ec
The MIT License
Copyright (c) 2017 Andreas Müller
Copyright (c) 2018 Andreas Müller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
......
......@@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleDemo", "src\ConsoleD
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modbus.Common", "src\Modbus.Common\Modbus.Common.csproj", "{71D10922-FE10-4A77-B4AC-E947797E9CFE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modbus.Serial", "src\Modbus.Serial\Modbus.Serial.csproj", "{30629B0B-B815-4951-AE56-9A0038210ECB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
......@@ -39,6 +41,10 @@ Global
{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
{30629B0B-B815-4951-AE56-9A0038210ECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{30629B0B-B815-4951-AE56-9A0038210ECB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30629B0B-B815-4951-AE56-9A0038210ECB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{30629B0B-B815-4951-AE56-9A0038210ECB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......@@ -47,6 +53,7 @@ Global
{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}
{30629B0B-B815-4951-AE56-9A0038210ECB} = {D4E6650D-2156-4660-B531-0B2AAD475BA1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FC7F1581-0E3C-4E35-9DBC-CC7952DE19C1}
......
using Modbus.Common;
using Modbus.Common.Structures;
using Modbus.Tcp.Client;
using AMWD.Modbus.Common.Structures;
using AMWD.Modbus.Common.Util;
using AMWD.Modbus.Tcp.Client;
using System;
using System.Collections.Generic;
using System.Linq;
......
{
"profiles": {
"ConsoleDemo": {
"commandName": "Project"
}
}
}
\ No newline at end of file
namespace Modbus.Common
namespace AMWD.Modbus.Common
{
/// <summary>
/// Contains all constants used in Modbus.
......
using System.ComponentModel;
namespace Modbus.Common
namespace AMWD.Modbus.Common
{
/// <summary>
/// Lists the Modbus request types.
......
using Modbus.Common.Structures;
using AMWD.Modbus.Common.Structures;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Modbus.Common.Interfaces
namespace AMWD.Modbus.Common.Interfaces
{
/// <summary>
/// Represents the interface for a Modbus client.
......@@ -22,6 +22,16 @@ namespace Modbus.Common.Interfaces
/// </summary>
TimeSpan ReconnectTimeSpan { get; set; }
/// <summary>
/// Gets or sets the send timeout in milliseconds. Default: 1000.
/// </summary>
int SendTimeout { get; set; }
/// <summary>
/// Gets ors sets the receive timeout in milliseconds. Default: 1000;
/// </summary>
int ReceiveTimeout { get; set; }
#endregion Properties
#region Read methods
......
using Modbus.Common.Structures;
using AMWD.Modbus.Common.Structures;
using System;
using System.Collections.Generic;
namespace Modbus.Common.Interfaces
namespace AMWD.Modbus.Common.Interfaces
{
/// <summary>
/// Represents the interface of a Modbus server.
......
......@@ -2,6 +2,21 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>0.9.0</Version>
<Authors>Andreas Müller</Authors>
<Company />
<Product>Modbus.Common</Product>
<Description>Common files for AMWD.Modbus.TCP and AMWD.Modbus.Serial packages.</Description>
<Copyright>Copyright (c) 2018 Andreas Müller</Copyright>
<PackageLicenseUrl>https://github.com/AndreasAmMueller/Modbus/blob/master/LICENSE.txt</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/AndreasAmMueller/Modbus</PackageProjectUrl>
<RepositoryUrl>https://github.com/AndreasAmMueller/Modbus.git</RepositoryUrl>
<PackageTags>Modbus</PackageTags>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageId>AMWD.Modbus.Common</PackageId>
<AssemblyName>AMWD.Modbus.Common</AssemblyName>
<RootNamespace>AMWD.Modbus.Common</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
......
namespace Modbus.Common.Structures
namespace AMWD.Modbus.Common.Structures
{
/// <summary>
/// Represents the contents of a coil on a Modbus device.
......
namespace Modbus.Common.Structures
namespace AMWD.Modbus.Common.Structures
{
/// <summary>
/// Represents the contents of a discrete input on a Modbus device.
......@@ -18,7 +18,7 @@
/// <inheritdoc/>
public override string ToString()
{
return $"Coil#{Address} | {Value}";
return $"DiscreteInput#{Address} | {Value}";
}
}
}
using System;
namespace Modbus.Common.Structures
namespace AMWD.Modbus.Common.Structures
{
/// <summary>
/// Represents errors that occurr during Modbus requests.
......
......@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text;
namespace Modbus.Common.Structures
namespace AMWD.Modbus.Common.Structures
{
/// <summary>
/// Represents a register on a Modbus device.
......@@ -397,8 +397,7 @@ namespace Modbus.Common.Structures
/// <inheritdoc/>
public override bool Equals(object obj)
{
var reg = obj as Register;
if (reg == null)
if (!(obj is Register reg))
{
return false;
}
......
using System;
namespace AMWD.Modbus.Common.Util
{
/// <summary>
/// Helper class for checksums.
/// </summary>
public class Checksum
{
/// <summary>
/// Calculates the CRC checksum with 16 bits of an array.
/// </summary>
/// <param name="array">The array with data.</param>
/// <returns>CRC16 Checksum as byte array. [0] = low byte, [1] = high byte.</returns>
public static byte[] CRC16(byte[] array)
{
return CRC16(array, 0, array.Length);
}
/// <summary>
/// Calculates the CRC checksum with 16 bits of an array.
/// </summary>
/// <param name="array">The array with data.</param>
/// <param name="start">The first byte to use.</param>
/// <param name="length">The number of bytes to use.</param>
/// <returns>CRC16 Checksum as byte array. [0] = low byte, [1] = high byte.</returns>
public static byte[] CRC16(byte[] array, int start, int length)
{
if (array == null || array.Length == 0)
{
throw new ArgumentNullException(nameof(array));
}
if (start < 0 || start >= array.Length)
{
throw new ArgumentOutOfRangeException(nameof(start));
}
if (length <= 0 || (start + length) > array.Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
ushort crc16 = 0xFFFF;
byte lsb;
for (var i = start; i < (start + length); i++)
{
crc16 = (ushort)(crc16 ^ array[i]);
for (var j = 0; j < 8; j++)
{
lsb = (byte)(crc16 & 1);
crc16 = (ushort)(crc16 >> 1);
if (lsb == 1)
{
crc16 = (ushort)(crc16 ^ 0xA001);
}
}
}
var b = new byte[2];
b[0] = (byte)crc16;
b[1] = (byte)(crc16 >> 8);
return b;
}
}
}
using System;
using System.Text;
namespace Modbus.Tcp.Utils
namespace AMWD.Modbus.Common.Util
{
/// <summary>
/// Implements a more flexible handling of a byte array.
/// </summary>
internal class DataBuffer
public class DataBuffer
{
#region Fields
......
using Modbus.Common.Structures;
using AMWD.Modbus.Common.Structures;
using System;
using System.Collections.Generic;
using System.ComponentModel;
......@@ -6,7 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Modbus.Common
namespace AMWD.Modbus.Common.Util
{
/// <summary>
/// Contains some extensions to handle some features more easily.
......@@ -271,5 +271,18 @@ namespace Modbus.Common
}
#endregion
#region Task handling
/// <summary>
/// Forgets about the result of the task. (Prevent compiler warning).
/// </summary>
/// <param name="task">The task to forget.</param>
public async static void Forget(this Task task)
{
await task;
}
#endregion Task handling
}
}
using Modbus.Common.Structures;
using System;
using AMWD.Modbus.Common.Structures;
using System.Collections.Generic;
namespace Modbus.Tcp.Utils
namespace AMWD.Modbus.Common.Util
{
internal class ModbusDevice
/// <summary>
/// Represents a Modbus device.
/// </summary>
public class ModbusDevice
{
private List<ushort> coils = new List<ushort>();
private List<ushort> discreteInputs = new List<ushort>();
private Dictionary<ushort, ushort> inputRegisters = new Dictionary<ushort, ushort>();
private Dictionary<ushort, ushort> holdingRegisters = new Dictionary<ushort, ushort>();
/// <summary>
/// Initializes a new instance of the <see cref="ModbusDevice"/> class.
/// </summary>
/// <param name="id">The device id.</param>
public ModbusDevice(byte id)
{
DeviceId = id;
}
/// <summary>
/// Gets the device id.
/// </summary>
public byte DeviceId { get; private set; }
#region Coils
/// <summary>
/// Gets a coil at an address.
/// </summary>
/// <param name="address">The address of the coil.</param>
/// <returns></returns>
public Coil GetCoil(ushort address)
{
lock (coils)
......@@ -28,6 +42,11 @@ namespace Modbus.Tcp.Utils
}
}
/// <summary>
/// Sets a coil value.
/// </summary>
/// <param name="address">The address.</param>
/// <param name="value">A value indicating whether the coil is active.</param>
public void SetCoil(ushort address, bool value)
{
lock (coils)
......@@ -47,6 +66,11 @@ namespace Modbus.Tcp.Utils
#region Discrete Input
/// <summary>
/// Gets an input.
/// </summary>
/// <param name="address">The address.</param>
/// <returns></returns>
public DiscreteInput GetInput(ushort address)
{
lock (discreteInputs)
......@@ -55,6 +79,11 @@ namespace Modbus.Tcp.Utils
}
}
/// <summary>
/// Sets an input.
/// </summary>
/// <param name="address">The address.</param>
/// <param name="value">A value indicating whether the input is active.</param>
public void SetInput(ushort address, bool value)
{
lock (discreteInputs)
......@@ -74,6 +103,11 @@ namespace Modbus.Tcp.Utils
#region Input Register
/// <summary>
/// Gets an input register.
/// </summary>
/// <param name="address">The address.</param>
/// <returns></returns>
public Register GetInputRegister(ushort address)
{
lock (inputRegisters)
......@@ -86,6 +120,11 @@ namespace Modbus.Tcp.Utils
return new Register { Address = address };
}
/// <summary>
/// Sets an input register.
/// </summary>
/// <param name="address">The address.</param>
/// <param name="value">The value.</param>
public void SetInputRegister(ushort address, ushort value)
{
lock (inputRegisters)
......@@ -105,6 +144,11 @@ namespace Modbus.Tcp.Utils
#region Holding Register
/// <summary>
/// Gets an holding register.
/// </summary>
/// <param name="address">The address.</param>
/// <returns></returns>
public Register GetHoldingRegister(ushort address)
{
lock (holdingRegisters)
......@@ -117,6 +161,11 @@ namespace Modbus.Tcp.Utils
return new Register { Address = address };
}
/// <summary>
/// Sets an holding register.
/// </summary>
/// <param name="address">The address.</param>
/// <param name="value">The value to set.</param>
public void SetHoldingRegister(ushort address, ushort value)
{
lock (holdingRegisters)
......
This diff is collapsed.
namespace AMWD.Modbus.Serial
{
/// <summary>
/// Defines the baud rates for a serial connection.
/// </summary>
public enum BaudRate : int
{
/// <summary>
/// 2400 Baud.
/// </summary>
Baud2400 = 2400,
/// <summary>
/// 4800 Baud.
/// </summary>
Baud4800 = 4800,
/// <summary>
/// 9600 Baud.
/// </summary>
Baud9600 = 9600,
/// <summary>
/// 19200 Baud.
/// </summary>
Baud19200 = 19200,
/// <summary>
/// 38400 Baud.
/// </summary>
Baud38400 = 38400,
/// <summary>
/// 57600 Baud.
/// </summary>
Baud57600 = 57600,
/// <summary>
/// 115200 Baud.
/// </summary>
Baud115200 = 115200
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>0.9.0</Version>
<Authors>Andreas Müller</Authors>
<Company />
<Product>Modbus.Serial</Product>
<Description>Small library to connect via Modbus RTU on remote devices.</Description>
<Copyright>Copyright (c) 2018 Andreas Müller</Copyright>
<PackageLicenseUrl>https://github.com/AndreasAmMueller/Modbus/blob/master/LICENSE.txt</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/AndreasAmMueller/Modbus</PackageProjectUrl>
<RepositoryUrl>https://github.com/AndreasAmMueller/Modbus.git</RepositoryUrl>
<PackageTags>Modbus, Serial, RTU</PackageTags>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageId>AMWD.Modbus.Serial</PackageId>
<AssemblyName>AMWD.Modbus.Serial</AssemblyName>
<RootNamespace>AMWD.Modbus.Serial</RootNamespace>
</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>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Modbus.Common\Modbus.Common.csproj" />
</ItemGroup>
</Project>
using AMWD.Modbus.Common;
using AMWD.Modbus.Common.Util;
using System;
using System.Linq;
namespace AMWD.Modbus.Serial.Protocol
{
/// <summary>
/// Represents the request from a client to the server.
/// </summary>
internal class Request
{
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="Request"/> class.
/// </summary>
/// <remarks>
/// The transaction id is automatically set to a unique number.
/// </remarks>
internal Request()
{ }
/// <summary>
/// Initializes a new instance of the <see cref="Request"/> class.
/// </summary>
/// <param name="bytes">The serialized request from the client.</param>
internal Request(byte[] bytes)
{
Deserialize(bytes);
}
#endregion Constructors
#region Properties
/// <summary>
/// Gets the id to identify the device.
/// </summary>
public byte DeviceId { get; set; }
/// <summary>
/// Gets or sets the function code.
/// </summary>
public FunctionCode Function { get; set; }
/// <summary>
/// Gets or sets the (first) address.
/// </summary>
public ushort Address { get; set; }
/// <summary>
/// Gets or sets the number of elements.
/// </summary>
public ushort Count { get; set; }
/// <summary>
/// Gets or sets the data bytes.
/// </summary>
public byte[] Bytes
{
get { return Data.Buffer; }
set { Data = new DataBuffer(value); }
}
/// <summary>
/// Gets or sets the data.
/// </summary>
internal DataBuffer Data { get; set; }
#endregion Properties
#region Serialization
/// <summary>
/// Serializes the request ready to send via serial.
/// </summary>
/// <returns></returns>
internal byte[] Serialize()
{
var buffer = new DataBuffer(4);
buffer.SetByte(0, DeviceId);
buffer.SetByte(1, (byte)Function);
buffer.SetUInt16(2, Address);
switch (Function)
{
case FunctionCode.ReadCoils:
case FunctionCode.ReadDiscreteInputs:
case FunctionCode.ReadHoldingRegisters:
case FunctionCode.ReadInputRegisters:
buffer.AddUInt16(Count);
break;
case FunctionCode.WriteMultipleCoils:
case FunctionCode.WriteMultipleRegisters:
buffer.AddUInt16(Count);
if (Data?.Length > 0)
{
buffer.AddBytes(Data.Buffer);
}
break;
case FunctionCode.WriteSingleCoil:
case FunctionCode.WriteSingleRegister:
if (Data?.Length > 0)
{
buffer.AddBytes(Data.Buffer);
}
break;
default:
throw new NotImplementedException();
}
var crc = Checksum.CRC16(buffer.Buffer);
buffer.AddBytes(crc);
return buffer.Buffer;
}
private void Deserialize(byte[] bytes)
{
var buffer = new DataBuffer(bytes);
DeviceId = buffer.GetByte(0);
Function = (FunctionCode)buffer.GetByte(1);
Address = buffer.GetUInt16(2);
var crcBuff = buffer.GetBytes(buffer.Length - 3, 2);
var crcCalc = Checksum.CRC16(bytes, 0, bytes.Length - 2);
if (crcBuff[0] != crcCalc[0] || crcBuff[1] != crcCalc[1])
{
throw new InvalidOperationException("Data not valid (CRC check failed).");
}
switch (Function)
{
case FunctionCode.ReadCoils:
case FunctionCode.ReadDiscreteInputs:
case FunctionCode.ReadHoldingRegisters:
case FunctionCode.ReadInputRegisters:
Count = buffer.GetUInt16(4);
break;
case FunctionCode.WriteMultipleCoils:
case FunctionCode.WriteMultipleRegisters:
Count = buffer.GetUInt16(4);
Data = new DataBuffer(buffer.GetBytes(6, buffer.Length - 8));
break;
case FunctionCode.WriteSingleCoil:
case FunctionCode.WriteSingleRegister:
Data = new DataBuffer(buffer.GetBytes(4, buffer.Length - 6));
break;
default:
throw new NotImplementedException();
}
}
#endregion Serialization
#region Overrides
/// <inheritdoc/>
public override string ToString()
{
return $"Request | Device#{DeviceId}, Fn: {Function}, Address: {Address}, Count: {Count} | {string.Join(" ", Bytes.Select(b => b.ToString("X2")).ToArray())}";
}
/// <inheritdoc/>
public override int GetHashCode()
{
return base.GetHashCode() ^
DeviceId.GetHashCode() ^
Function.GetHashCode() ^
Address.GetHashCode() ^
Count.GetHashCode() ^
Bytes.GetHashCode();
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
if (!(obj is Request req))
{
return false;
}
return req.DeviceId == DeviceId &&
req.Function == Function &&
req.Address == Address &&
req.Count == Count &&
Data.Equals(req.Data);
}
#endregion Overrides
}
}
using AMWD.Modbus.Common;
using AMWD.Modbus.Common.Util;
using System;
using System.Linq;
namespace AMWD.Modbus.Serial.Protocol
{
/// <summary>
/// Represents the response from the server to a client.
/// </summary>
internal class Response
{
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="Response"/> class.
/// </summary>
/// <param name="request">The corresponding request.</param>
public Response(Request request)
{
DeviceId = request.DeviceId;
Function = request.Function;
Address = request.Address;
Count = request.Count;